Add dismissal mechanism for contextual cards.
In the homepage, we should allow cards to be dismissed. - Implement card flipping upon card long pressing. Bug: 113783548 Test: robotests, visual Change-Id: I2ddb498321ba5c5078d6944aa2ef32f1386bdb10
This commit is contained in:
@@ -21,6 +21,11 @@
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/ContextualCardStyle">
|
||||
|
||||
<ViewFlipper
|
||||
android:id="@+id/viewFlipper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.slice.widget.SliceView
|
||||
android:id="@+id/slice_view"
|
||||
android:layout_width="match_parent"
|
||||
@@ -30,4 +35,41 @@
|
||||
android:paddingStart="@dimen/homepage_card_padding_start"
|
||||
android:paddingEnd="@dimen/homepage_card_padding_end"/>
|
||||
|
||||
<!--dismissal view-->
|
||||
<LinearLayout
|
||||
android:id="@+id/dismissal_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/homepage_card_padding_start"
|
||||
android:layout_marginTop="@dimen/homepage_card_padding_start"
|
||||
android:text="@string/contextual_card_dismiss_confirm_message"
|
||||
style="@style/TextAppearance.ContextualCardDismissalText"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="bottom|end">
|
||||
|
||||
<Button
|
||||
android:id="@+id/keep"
|
||||
style="@style/ContextualCardDismissalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/contextual_card_dismiss_keep"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/remove"
|
||||
style="@style/ContextualCardDismissalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/contextual_card_dismiss_remove"/>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</ViewFlipper>
|
||||
</androidx.cardview.widget.CardView>
|
@@ -10286,4 +10286,10 @@
|
||||
<!-- Summary for the top level Privacy Settings [CHAR LIMIT=NONE]-->
|
||||
<string name="privacy_dashboard_summary">Permission, permission usage</string>
|
||||
|
||||
<!-- Label for button in contextual card for users to remove the card [CHAR LIMIT=30] -->
|
||||
<string name="contextual_card_dismiss_remove">Remove</string>
|
||||
<!-- Label for button in contextual card for users to keep the card [CHAR LIMIT=30] -->
|
||||
<string name="contextual_card_dismiss_keep">Keep</string>
|
||||
<!-- String for contextual card dismissal [CHAR LIMIT=NONE] -->
|
||||
<string name="contextual_card_dismiss_confirm_message">Remove this suggestion?</string>
|
||||
</resources>
|
@@ -16,7 +16,8 @@
|
||||
|
||||
<resources>
|
||||
|
||||
<style name="Theme.ActionBar" parent="@android:style/Widget.DeviceDefault.Light.ActionBar.Solid">
|
||||
<style name="Theme.ActionBar"
|
||||
parent="@android:style/Widget.DeviceDefault.Light.ActionBar.Solid">
|
||||
<item name="android:contentInsetStart">@dimen/actionbar_contentInsetStart</item>
|
||||
</style>
|
||||
|
||||
@@ -78,8 +79,11 @@
|
||||
<item name="android:scrollbarStyle">outsideOverlay</item>
|
||||
</style>
|
||||
|
||||
<style name="TrimmedHorizontalProgressBar" parent="android:Widget.Material.ProgressBar.Horizontal">
|
||||
<item name="android:indeterminateDrawable">@drawable/progress_indeterminate_horizontal_material_trimmed</item>
|
||||
<style name="TrimmedHorizontalProgressBar"
|
||||
parent="android:Widget.Material.ProgressBar.Horizontal">
|
||||
<item name="android:indeterminateDrawable">
|
||||
@drawable/progress_indeterminate_horizontal_material_trimmed
|
||||
</item>
|
||||
<item name="android:minHeight">3dip</item>
|
||||
<item name="android:maxHeight">3dip</item>
|
||||
</style>
|
||||
@@ -162,7 +166,8 @@
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
</style>
|
||||
|
||||
<style name="ConfirmDeviceCredentialsAnimationStyle" parent="@*android:style/Animation.Material.Activity">
|
||||
<style name="ConfirmDeviceCredentialsAnimationStyle"
|
||||
parent="@*android:style/Animation.Material.Activity">
|
||||
<item name="android:activityOpenEnterAnimation">@anim/confirm_credential_open_enter</item>
|
||||
<item name="android:activityOpenExitAnimation">@anim/confirm_credential_open_exit</item>
|
||||
</style>
|
||||
@@ -171,14 +176,16 @@
|
||||
<item name="android:background">#ff000000</item>
|
||||
</style>
|
||||
|
||||
<style name="SecurityPreferenceButtonContainer" parent="@android:style/Widget.Material.Light.SegmentedButton">
|
||||
<style name="SecurityPreferenceButtonContainer"
|
||||
parent="@android:style/Widget.Material.Light.SegmentedButton">
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:weightSum">2</item>
|
||||
<item name="android:dividerPadding">8dip</item>
|
||||
</style>
|
||||
|
||||
<style name="SecurityPreferenceButton" parent="@android:style/Widget.Material.Light.Button.Borderless">
|
||||
<style name="SecurityPreferenceButton"
|
||||
parent="@android:style/Widget.Material.Light.Button.Borderless">
|
||||
<item name="android:layout_width">0dip</item>
|
||||
<item name="android:layout_weight">1</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
@@ -192,7 +199,7 @@
|
||||
<item name="android:layout_marginStart">-16dp</item>
|
||||
</style>
|
||||
|
||||
<style name="SetupWizardButton.Positive" parent="@style/SuwGlifButton.Primary" />
|
||||
<style name="SetupWizardButton.Positive" parent="@style/SuwGlifButton.Primary"/>
|
||||
|
||||
<style name="AccentColorHighlightBorderlessButton">
|
||||
<item name="android:colorControlHighlight">?android:attr/colorAccent</item>
|
||||
@@ -252,17 +259,18 @@
|
||||
<item name="android:imeOptions">flagForceAscii|actionDone</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Medium" parent="@android:style/TextAppearance.Material.Medium" />
|
||||
<style name="TextAppearance.Small" parent="@android:style/TextAppearance.Material.Small" />
|
||||
<style name="TextAppearance.Medium" parent="@android:style/TextAppearance.Material.Medium"/>
|
||||
<style name="TextAppearance.Small" parent="@android:style/TextAppearance.Material.Small"/>
|
||||
<style name="TextAppearance.Switch" parent="@android:style/TextAppearance.Material.Title">
|
||||
<item name="android:textSize">18sp</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.CategoryTitle" parent="@android:style/TextAppearance.Material.Body2">
|
||||
<style name="TextAppearance.CategoryTitle"
|
||||
parent="@android:style/TextAppearance.Material.Body2">
|
||||
<item name="android:textColor">?android:attr/textColorSecondary</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.TileTitle" parent="@android:style/TextAppearance.Material.Subhead" />
|
||||
<style name="TextAppearance.TileTitle" parent="@android:style/TextAppearance.Material.Subhead"/>
|
||||
|
||||
<style name="TextAppearance.SuggestionTitle"
|
||||
parent="@android:style/TextAppearance.Material.Subhead">
|
||||
@@ -293,12 +301,14 @@
|
||||
<item name="android:textStyle">normal</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.RemoveDialogContent" parent="@android:style/TextAppearance.Material">
|
||||
<style name="TextAppearance.RemoveDialogContent"
|
||||
parent="@android:style/TextAppearance.Material">
|
||||
<item name="android:textSize">16sp</item>
|
||||
<item name="android:textColor">?android:attr/textColorPrimary</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.SearchBar" parent="@android:style/TextAppearance.Material.Widget.Toolbar.Subtitle">
|
||||
<style name="TextAppearance.SearchBar"
|
||||
parent="@android:style/TextAppearance.Material.Widget.Toolbar.Subtitle">
|
||||
<item name="android:textSize">@dimen/search_bar_text_size</item>
|
||||
</style>
|
||||
|
||||
@@ -345,6 +355,12 @@
|
||||
<item name="android:padding">8dp</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.ContextualCardDismissalText"
|
||||
parent="@android:style/TextAppearance.Material.Body1">
|
||||
<item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item>
|
||||
<item name="android:textSize">16sp</item>
|
||||
</style>
|
||||
|
||||
<style name="SuggestionCardText">
|
||||
<item name="android:textAlignment">viewStart</item>
|
||||
</style>
|
||||
@@ -391,7 +407,8 @@
|
||||
<item name="android:lineSpacingMultiplier">1.2</item>
|
||||
</style>
|
||||
|
||||
<style name="RingProgressBarStyle" parent="android:style/Widget.Material.ProgressBar.Horizontal">
|
||||
<style name="RingProgressBarStyle"
|
||||
parent="android:style/Widget.Material.ProgressBar.Horizontal">
|
||||
<item name="android:indeterminate">false</item>
|
||||
<item name="android:max">10000</item>
|
||||
<item name="android:mirrorForRtl">false</item>
|
||||
@@ -470,7 +487,6 @@
|
||||
<item name="android:textColor">?android:attr/colorAccent</item>
|
||||
<item name="android:textSize">14sp</item>
|
||||
<item name="android:textAllCaps">false</item>
|
||||
<item name="android:fontFamily">sans-serif-medium</item>
|
||||
</style>
|
||||
|
||||
<style name="ConditionHalfCardBorderlessButton"
|
||||
@@ -483,4 +499,9 @@
|
||||
<item name="android:textAlignment">viewEnd</item>
|
||||
</style>
|
||||
|
||||
<style name="ContextualCardDismissalButton"
|
||||
parent="android:Widget.DeviceDefault.Button.Borderless.Colored">
|
||||
<item name="android:textAllCaps">false</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
@@ -28,5 +28,7 @@ public interface ContextualCardController {
|
||||
|
||||
void onActionClick(ContextualCard card);
|
||||
|
||||
void onDismissed(ContextualCard card);
|
||||
|
||||
void setCardUpdateListener(ContextualCardUpdateListener listener);
|
||||
}
|
||||
|
@@ -146,14 +146,14 @@ public class ContextualCardManager implements ContextualCardLoader.CardContentLo
|
||||
onContextualCardUpdated(cards.stream().collect(groupingBy(ContextualCard::getCardType)));
|
||||
}
|
||||
|
||||
void setListener(ContextualCardUpdateListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public ControllerRendererPool getControllerRendererPool() {
|
||||
return mControllerRendererPool;
|
||||
}
|
||||
|
||||
void setListener(ContextualCardUpdateListener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
static class CardContentLoaderCallbacks implements
|
||||
LoaderManager.LoaderCallbacks<List<ContextualCard>> {
|
||||
|
||||
|
@@ -126,7 +126,8 @@ public class ControllerRendererPool {
|
||||
if (ConditionContextualCardRenderer.class == clz) {
|
||||
return new ConditionContextualCardRenderer(context, this /* controllerRendererPool */);
|
||||
} else if (SliceContextualCardRenderer.class == clz) {
|
||||
return new SliceContextualCardRenderer(context, lifecycleOwner);
|
||||
return new SliceContextualCardRenderer(context, lifecycleOwner,
|
||||
this /* controllerRendererPool */);
|
||||
} else if (LegacySuggestionContextualCardRenderer.class == clz) {
|
||||
return new LegacySuggestionContextualCardRenderer(context,
|
||||
this /* controllerRendererPool */);
|
||||
|
@@ -77,6 +77,11 @@ public class ConditionContextualCardController implements ContextualCardControll
|
||||
|
||||
@Override
|
||||
public void onActionClick(ContextualCard contextualCard) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismissed(ContextualCard contextualCard) {
|
||||
final ConditionalContextualCard card = (ConditionalContextualCard) contextualCard;
|
||||
mConditionManager.onActionClick(card.getConditionId());
|
||||
}
|
||||
|
@@ -112,8 +112,8 @@ public class ConditionContextualCardRenderer implements ContextualCardRenderer {
|
||||
metricsFeatureProvider.action(
|
||||
viewContext, MetricsProto.MetricsEvent.ACTION_SETTINGS_CONDITION_BUTTON,
|
||||
card.getMetricsConstant());
|
||||
mControllerRendererPool.getController(mContext, card.getCardType()).onActionClick(
|
||||
card);
|
||||
mControllerRendererPool.getController(mContext, card.getCardType())
|
||||
.onDismissed(card);
|
||||
});
|
||||
} else {
|
||||
button.setVisibility(View.GONE);
|
||||
|
@@ -86,6 +86,11 @@ public class LegacySuggestionContextualCardController implements ContextualCardC
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismissed(ContextualCard card) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCardUpdateListener(ContextualCardUpdateListener listener) {
|
||||
mCardUpdateListener = listener;
|
||||
|
@@ -25,6 +25,10 @@ import com.android.settings.homepage.contextualcards.ContextualCardUpdateListene
|
||||
*/
|
||||
public class SliceContextualCardController implements ContextualCardController {
|
||||
|
||||
private static final String TAG = "SliceCardController";
|
||||
|
||||
private ContextualCardUpdateListener mCardUpdateListener;
|
||||
|
||||
@Override
|
||||
public int getCardType() {
|
||||
return ContextualCard.CardType.SLICE;
|
||||
@@ -37,11 +41,16 @@ public class SliceContextualCardController implements ContextualCardController {
|
||||
|
||||
@Override
|
||||
public void onActionClick(ContextualCard card) {
|
||||
//TODO(b/113783548): Implement feedback mechanism
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismissed(ContextualCard card) {
|
||||
//TODO(b/113783548): Mark this card as dismissed in db and reload loader.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCardUpdateListener(ContextualCardUpdateListener listener) {
|
||||
|
||||
mCardUpdateListener = listener;
|
||||
}
|
||||
}
|
||||
|
@@ -22,6 +22,8 @@ import android.net.Uri;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ViewFlipper;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
@@ -37,6 +39,7 @@ import androidx.slice.widget.SliceView;
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.homepage.contextualcards.ContextualCard;
|
||||
import com.android.settings.homepage.contextualcards.ContextualCardRenderer;
|
||||
import com.android.settings.homepage.contextualcards.ControllerRendererPool;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@@ -54,11 +57,14 @@ public class SliceContextualCardRenderer implements ContextualCardRenderer,
|
||||
|
||||
private final Context mContext;
|
||||
private final LifecycleOwner mLifecycleOwner;
|
||||
private final ControllerRendererPool mControllerRendererPool;
|
||||
|
||||
public SliceContextualCardRenderer(Context context, LifecycleOwner lifecycleOwner) {
|
||||
public SliceContextualCardRenderer(Context context, LifecycleOwner lifecycleOwner,
|
||||
ControllerRendererPool controllerRendererPool) {
|
||||
mContext = context;
|
||||
mLifecycleOwner = lifecycleOwner;
|
||||
mSliceLiveDataMap = new ArrayMap<>();
|
||||
mControllerRendererPool = controllerRendererPool;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -104,6 +110,27 @@ public class SliceContextualCardRenderer implements ContextualCardRenderer,
|
||||
|
||||
// Set this listener so we can log the interaction users make on the slice
|
||||
cardHolder.sliceView.setOnSliceActionListener(this);
|
||||
|
||||
initDismissalActions(cardHolder, card);
|
||||
}
|
||||
|
||||
private void initDismissalActions(SliceViewHolder cardHolder, ContextualCard card) {
|
||||
final ViewFlipper viewFlipper = cardHolder.itemView.findViewById(R.id.viewFlipper);
|
||||
cardHolder.sliceView.setOnLongClickListener(v -> {
|
||||
viewFlipper.showNext();
|
||||
return true;
|
||||
});
|
||||
|
||||
final Button btnKeep = cardHolder.itemView.findViewById(R.id.keep);
|
||||
btnKeep.setOnClickListener(v -> {
|
||||
viewFlipper.showPrevious();
|
||||
});
|
||||
|
||||
final Button btnRemove = cardHolder.itemView.findViewById(R.id.remove);
|
||||
btnRemove.setOnClickListener(v -> {
|
||||
mControllerRendererPool.getController(mContext, card.getCardType()).onDismissed(
|
||||
card);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -24,15 +24,20 @@ import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ViewFlipper;
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.slice.Slice;
|
||||
import androidx.slice.widget.SliceView;
|
||||
|
||||
import com.android.settings.R;
|
||||
import com.android.settings.homepage.contextualcards.ContextualCard;
|
||||
import com.android.settings.homepage.contextualcards.ContextualCardsFragment;
|
||||
import com.android.settings.homepage.contextualcards.ControllerRendererPool;
|
||||
import com.android.settings.testutils.SettingsRobolectricTestRunner;
|
||||
|
||||
import org.junit.Before;
|
||||
@@ -47,6 +52,10 @@ public class SliceContextualCardRendererTest {
|
||||
|
||||
@Mock
|
||||
private LiveData<Slice> mSliceLiveData;
|
||||
@Mock
|
||||
private ControllerRendererPool mControllerRendererPool;
|
||||
@Mock
|
||||
private SliceContextualCardController mController;
|
||||
|
||||
private Context mContext;
|
||||
private SliceContextualCardRenderer mRenderer;
|
||||
@@ -57,7 +66,8 @@ public class SliceContextualCardRendererTest {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mContext = RuntimeEnvironment.application;
|
||||
mLifecycleOwner = new ContextualCardsFragment();
|
||||
mRenderer = new SliceContextualCardRenderer(mContext, mLifecycleOwner);
|
||||
mRenderer = new SliceContextualCardRenderer(mContext, mLifecycleOwner,
|
||||
mControllerRendererPool);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -103,7 +113,7 @@ public class SliceContextualCardRendererTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bindview_sliceLiveDataShouldRemoveObservers() {
|
||||
public void bindView_sliceLiveDataShouldRemoveObservers() {
|
||||
final String sliceUri = "content://com.android.settings.slices/action/flashlight";
|
||||
mRenderer.mSliceLiveDataMap.put(sliceUri, mSliceLiveData);
|
||||
|
||||
@@ -112,14 +122,45 @@ public class SliceContextualCardRendererTest {
|
||||
verify(mSliceLiveData).removeObservers(mLifecycleOwner);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void longClick_shouldFlipCard() {
|
||||
final String sliceUri = "content://com.android.settings.slices/action/flashlight";
|
||||
final RecyclerView.ViewHolder viewHolder = getSliceViewHolder();
|
||||
final View card = viewHolder.itemView.findViewById(R.id.slice_view);
|
||||
final ViewFlipper viewFlipper = viewHolder.itemView.findViewById(R.id.viewFlipper);
|
||||
final View dismissalView = viewHolder.itemView.findViewById(R.id.dismissal_view);
|
||||
mRenderer.bindView(viewHolder, buildContextualCard(sliceUri));
|
||||
|
||||
assertThat(card).isNotNull();
|
||||
card.performLongClick();
|
||||
|
||||
assertThat(viewFlipper.getCurrentView()).isEqualTo(dismissalView);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void viewClick_keepCard_shouldFlipBackToSlice() {
|
||||
final String sliceUri = "content://com.android.settings.slices/action/flashlight";
|
||||
final RecyclerView.ViewHolder viewHolder = getSliceViewHolder();
|
||||
final View card = viewHolder.itemView.findViewById(R.id.slice_view);
|
||||
final Button btnKeep = viewHolder.itemView.findViewById(R.id.keep);
|
||||
final ViewFlipper viewFlipper = viewHolder.itemView.findViewById(R.id.viewFlipper);
|
||||
mRenderer.bindView(viewHolder, buildContextualCard(sliceUri));
|
||||
|
||||
assertThat(card).isNotNull();
|
||||
card.performLongClick();
|
||||
assertThat(btnKeep).isNotNull();
|
||||
btnKeep.performClick();
|
||||
|
||||
assertThat(viewFlipper.getCurrentView()).isInstanceOf(SliceView.class);
|
||||
}
|
||||
|
||||
private RecyclerView.ViewHolder getSliceViewHolder() {
|
||||
final int viewType = mRenderer.getViewType(false /* isHalfWidth */);
|
||||
final RecyclerView recyclerView = new RecyclerView(mContext);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(mContext));
|
||||
final View view = LayoutInflater.from(mContext).inflate(viewType, recyclerView, false);
|
||||
final RecyclerView.ViewHolder viewHolder = mRenderer.createViewHolder(view);
|
||||
|
||||
return viewHolder;
|
||||
return mRenderer.createViewHolder(view);
|
||||
}
|
||||
|
||||
private ContextualCard buildContextualCard(String sliceUri) {
|
||||
|
Reference in New Issue
Block a user