Reload homepage cards when necessary
Many users leave Settings app by pressing Home key, but Settings remains in the same card status and doesn't update when users come back, which may lead to a bad UX. This change reloads cards and resets the UI session for some events, including home key, recent app key, and screen off. Fixes: 151789260 Test: robotest Change-Id: Idb575cef4a58894984cb42238d7b3b43c49389a3
This commit is contained in:
@@ -123,7 +123,7 @@ public class ContextualCardManager implements ContextualCardLoader.CardContentLo
|
||||
}
|
||||
}
|
||||
|
||||
void loadContextualCards(LoaderManager loaderManager) {
|
||||
void loadContextualCards(LoaderManager loaderManager, boolean restartLoaderNeeded) {
|
||||
if (mContext.getResources().getBoolean(R.bool.config_use_legacy_suggestion)) {
|
||||
Log.w(TAG, "Legacy suggestion contextual card enabled, skipping contextual cards.");
|
||||
return;
|
||||
@@ -132,9 +132,17 @@ public class ContextualCardManager implements ContextualCardLoader.CardContentLo
|
||||
final CardContentLoaderCallbacks cardContentLoaderCallbacks =
|
||||
new CardContentLoaderCallbacks(mContext);
|
||||
cardContentLoaderCallbacks.setListener(this);
|
||||
if (!restartLoaderNeeded) {
|
||||
// Use the cached data when navigating back to the first page and upon screen rotation.
|
||||
loaderManager.initLoader(CARD_CONTENT_LOADER_ID, null /* bundle */,
|
||||
cardContentLoaderCallbacks);
|
||||
} else {
|
||||
// Reload all cards when navigating back after pressing home key, recent app key, or
|
||||
// turn off screen.
|
||||
mIsFirstLaunch = true;
|
||||
loaderManager.restartLoader(CARD_CONTENT_LOADER_ID, null /* bundle */,
|
||||
cardContentLoaderCallbacks);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadCardControllers() {
|
||||
@@ -194,7 +202,7 @@ public class ContextualCardManager implements ContextualCardLoader.CardContentLo
|
||||
// except Conditional cards, all other cards are from the database. So when the map sent
|
||||
// here is empty, we only keep Conditional cards.
|
||||
if (cardTypes.isEmpty()) {
|
||||
final Set<Integer> conditionalCardTypes = new TreeSet() {{
|
||||
final Set<Integer> conditionalCardTypes = new TreeSet<Integer>() {{
|
||||
add(ContextualCard.CardType.CONDITIONAL);
|
||||
add(ContextualCard.CardType.CONDITIONAL_HEADER);
|
||||
add(ContextualCard.CardType.CONDITIONAL_FOOTER);
|
||||
|
@@ -135,8 +135,6 @@ public class ContextualCardsAdapter extends RecyclerView.Adapter<RecyclerView.Vi
|
||||
// Adding items to empty list, should animate.
|
||||
mRecyclerView.scheduleLayoutAnimation();
|
||||
}
|
||||
|
||||
//TODO(b/119465242): flickering conditional cards after collapsing/expanding
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -16,6 +16,9 @@
|
||||
|
||||
package com.android.settings.homepage.contextualcards;
|
||||
|
||||
import static com.android.settings.intelligence.ContextualCardProto.ContextualCard.Category.IMPORTANT_VALUE;
|
||||
import static com.android.settings.intelligence.ContextualCardProto.ContextualCard.Category.STICKY_VALUE;
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
|
||||
import java.util.List;
|
||||
@@ -52,11 +55,14 @@ public class ContextualCardsDiffCallback extends DiffUtil.Callback {
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(int oldCardPosition, int newCardPosition) {
|
||||
// Slices with toggles needs to be updated continuously, which means their contents may
|
||||
// change. So here we assume the content will always be different to force view rebinding.
|
||||
if (mNewCards.get(newCardPosition).hasInlineAction()) {
|
||||
final ContextualCard newCard = mNewCards.get(newCardPosition);
|
||||
// Sticky, important, or toggleable slices need to be updated continuously, which means
|
||||
// their contents may change. So here we assume the content will always be different to
|
||||
// force view rebinding.
|
||||
if (newCard.getCategory() == STICKY_VALUE || newCard.getCategory() == IMPORTANT_VALUE
|
||||
|| newCard.hasInlineAction()) {
|
||||
return false;
|
||||
}
|
||||
return mOldCards.get(oldCardPosition).equals(mNewCards.get(newCardPosition));
|
||||
return mOldCards.get(oldCardPosition).equals(newCard);
|
||||
}
|
||||
}
|
@@ -19,12 +19,18 @@ package com.android.settings.homepage.contextualcards;
|
||||
import static com.android.settings.homepage.contextualcards.ContextualCardsAdapter.SPAN_COUNT;
|
||||
|
||||
import android.app.settings.SettingsEnums;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
@@ -38,6 +44,17 @@ import com.android.settings.wifi.slice.ContextualWifiScanWorker;
|
||||
public class ContextualCardsFragment extends InstrumentedFragment implements
|
||||
FocusRecyclerView.FocusListener {
|
||||
|
||||
private static final String TAG = "ContextualCardsFragment";
|
||||
private static final boolean DEBUG = Build.IS_DEBUGGABLE;
|
||||
|
||||
@VisibleForTesting
|
||||
static boolean sRestartLoaderNeeded;
|
||||
|
||||
@VisibleForTesting
|
||||
BroadcastReceiver mKeyEventReceiver;
|
||||
@VisibleForTesting
|
||||
BroadcastReceiver mScreenOffReceiver;
|
||||
|
||||
private FocusRecyclerView mCardsContainer;
|
||||
private GridLayoutManager mLayoutManager;
|
||||
private ContextualCardsAdapter mContextualCardsAdapter;
|
||||
@@ -53,14 +70,30 @@ public class ContextualCardsFragment extends InstrumentedFragment implements
|
||||
}
|
||||
mContextualCardManager = new ContextualCardManager(context, getSettingsLifecycle(),
|
||||
savedInstanceState);
|
||||
|
||||
mKeyEventReceiver = new KeyEventReceiver();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
registerScreenOffReceiver();
|
||||
registerKeyEventReceiver();
|
||||
ContextualWifiScanWorker.newVisibleUiSession();
|
||||
mContextualCardManager.loadContextualCards(LoaderManager.getInstance(this));
|
||||
mContextualCardManager.loadContextualCards(LoaderManager.getInstance(this),
|
||||
sRestartLoaderNeeded);
|
||||
sRestartLoaderNeeded = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
unregisterKeyEventReceiver();
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
unregisterScreenOffReceiver();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -92,4 +125,82 @@ public class ContextualCardsFragment extends InstrumentedFragment implements
|
||||
public int getMetricsCategory() {
|
||||
return SettingsEnums.SETTINGS_HOMEPAGE;
|
||||
}
|
||||
|
||||
private void registerKeyEventReceiver() {
|
||||
getActivity().registerReceiver(mKeyEventReceiver,
|
||||
new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
|
||||
}
|
||||
|
||||
private void unregisterKeyEventReceiver() {
|
||||
getActivity().unregisterReceiver(mKeyEventReceiver);
|
||||
}
|
||||
|
||||
private void registerScreenOffReceiver() {
|
||||
if (mScreenOffReceiver == null) {
|
||||
mScreenOffReceiver = new ScreenOffReceiver();
|
||||
getActivity().registerReceiver(mScreenOffReceiver,
|
||||
new IntentFilter(Intent.ACTION_SCREEN_OFF));
|
||||
}
|
||||
}
|
||||
|
||||
private void unregisterScreenOffReceiver() {
|
||||
if (mScreenOffReceiver != null) {
|
||||
getActivity().unregisterReceiver(mScreenOffReceiver);
|
||||
mScreenOffReceiver = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void resetSession(Context context) {
|
||||
sRestartLoaderNeeded = true;
|
||||
unregisterScreenOffReceiver();
|
||||
FeatureFactory.getFactory(context).getSlicesFeatureProvider().newUiSession();
|
||||
}
|
||||
|
||||
/**
|
||||
* Receiver for updating UI session when home key or recent app key is pressed.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
class KeyEventReceiver extends BroadcastReceiver {
|
||||
|
||||
private static final String KEY_REASON = "reason";
|
||||
private static final String SYSTEM_DIALOG_REASON_HOME_KEY = "homekey";
|
||||
private static final String SYSTEM_DIALOG_REASON_RECENT_APPS = "recentapps";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent == null || !Intent.ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
|
||||
return;
|
||||
}
|
||||
|
||||
final String reason = intent.getStringExtra(KEY_REASON);
|
||||
if (!SYSTEM_DIALOG_REASON_RECENT_APPS.equals(reason)
|
||||
&& !SYSTEM_DIALOG_REASON_HOME_KEY.equals(reason)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "key pressed = " + reason);
|
||||
}
|
||||
resetSession(context);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receiver for updating UI session when screen is turned off.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
class ScreenOffReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent == null || !Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "screen off");
|
||||
}
|
||||
resetSession(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -16,17 +16,14 @@
|
||||
package com.android.settings.homepage.contextualcards.slices;
|
||||
|
||||
import static android.provider.Settings.Global.LOW_POWER_MODE;
|
||||
import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL;
|
||||
|
||||
import static androidx.slice.builders.ListBuilder.ICON_IMAGE;
|
||||
|
||||
import android.annotation.ColorInt;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.UiModeManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.res.Configuration;
|
||||
import android.database.ContentObserver;
|
||||
import android.net.Uri;
|
||||
@@ -36,7 +33,6 @@ import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.PowerManager;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
@@ -52,8 +48,6 @@ import com.android.settings.slices.CustomSliceRegistry;
|
||||
import com.android.settings.slices.CustomSliceable;
|
||||
import com.android.settings.slices.SliceBackgroundWorker;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class DarkThemeSlice implements CustomSliceable {
|
||||
private static final String TAG = "DarkThemeSlice";
|
||||
private static final boolean DEBUG = Build.IS_DEBUGGABLE;
|
||||
@@ -97,11 +91,11 @@ public class DarkThemeSlice implements CustomSliceable {
|
||||
// Next time the Settings displays on screen again this card should no longer persist.
|
||||
if (DEBUG) {
|
||||
Log.d(TAG,
|
||||
"!sKeepSliceShow = " + !sKeepSliceShow + " !sSliceClicked = "
|
||||
+ !sSliceClicked + " !isAvailable = " + !isAvailable(mContext));
|
||||
"sKeepSliceShow = " + sKeepSliceShow + ", sSliceClicked = " + sSliceClicked
|
||||
+ ", isAvailable = " + isAvailable(mContext));
|
||||
}
|
||||
if (mPowerManager.isPowerSaveMode() || ((!sKeepSliceShow || !sSliceClicked)
|
||||
&& !isAvailable(mContext))) {
|
||||
if (mPowerManager.isPowerSaveMode()
|
||||
|| ((!sKeepSliceShow || !sSliceClicked) && !isAvailable(mContext))) {
|
||||
return new ListBuilder(mContext, CustomSliceRegistry.DARK_THEME_SLICE_URI,
|
||||
ListBuilder.INFINITY)
|
||||
.setIsError(true)
|
||||
@@ -188,7 +182,7 @@ public class DarkThemeSlice implements CustomSliceable {
|
||||
private boolean isNightModeScheduled() {
|
||||
final int mode = mUiModeManager.getNightMode();
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "night mode : " + mode);
|
||||
Log.d(TAG, "night mode = " + mode);
|
||||
}
|
||||
// Turn on from sunset to sunrise or turn on at custom time
|
||||
if (mode == UiModeManager.MODE_NIGHT_AUTO || mode == UiModeManager.MODE_NIGHT_CUSTOM) {
|
||||
@@ -208,12 +202,10 @@ public class DarkThemeSlice implements CustomSliceable {
|
||||
}
|
||||
}
|
||||
};
|
||||
private final HomeKeyReceiver mHomeKeyReceiver;
|
||||
|
||||
public DarkThemeWorker(Context context, Uri uri) {
|
||||
super(context, uri);
|
||||
mContext = context;
|
||||
mHomeKeyReceiver = new HomeKeyReceiver();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -221,55 +213,15 @@ public class DarkThemeSlice implements CustomSliceable {
|
||||
mContext.getContentResolver().registerContentObserver(
|
||||
Settings.Global.getUriFor(LOW_POWER_MODE), false /* notifyForDescendants */,
|
||||
mContentObserver);
|
||||
final IntentFilter intentFilter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
|
||||
mContext.registerReceiver(mHomeKeyReceiver, intentFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSliceUnpinned() {
|
||||
mContext.getContentResolver().unregisterContentObserver(mContentObserver);
|
||||
mContext.unregisterReceiver(mHomeKeyReceiver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A receiver for Home key and recent app key.
|
||||
*/
|
||||
public static class HomeKeyReceiver extends BroadcastReceiver {
|
||||
private static final String SYSTEM_DIALOG_REASON_HOME_KEY = "homekey";
|
||||
private static final String SYSTEM_DIALOG_REASON_RECENT_APPS = "recentapps";
|
||||
private static final String SYSTEM_DIALOG_REASON_KEY = "reason";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (TextUtils.equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS, intent.getAction())) {
|
||||
if (TextUtils.equals(getTargetKey(context),
|
||||
intent.getStringExtra(SYSTEM_DIALOG_REASON_KEY))) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "HomeKeyReceiver : target key = " + getTargetKey(context));
|
||||
}
|
||||
if (DarkThemeSlice.isDarkThemeMode(context)) {
|
||||
FeatureFactory.getFactory(
|
||||
context).getSlicesFeatureProvider().newUiSession();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getTargetKey(Context context) {
|
||||
if (isGestureNavigationEnabled(context)) {
|
||||
return SYSTEM_DIALOG_REASON_RECENT_APPS;
|
||||
}
|
||||
return SYSTEM_DIALOG_REASON_HOME_KEY;
|
||||
}
|
||||
|
||||
private boolean isGestureNavigationEnabled(Context context) {
|
||||
return NAV_BAR_MODE_GESTURAL == context.getResources().getInteger(
|
||||
com.android.internal.R.integer.config_navBarInteractionMode);
|
||||
public void close() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -149,13 +149,13 @@ public class SliceContextualCardRenderer implements ContextualCardRenderer, Life
|
||||
|
||||
if (holder.getItemViewType() != VIEW_TYPE_STICKY) {
|
||||
initDismissalActions(holder, card);
|
||||
}
|
||||
|
||||
if (card.isPendingDismiss()) {
|
||||
showDismissalView(holder);
|
||||
mFlippedCardSet.add(holder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initDismissalActions(RecyclerView.ViewHolder holder, ContextualCard card) {
|
||||
final Button btnKeep = holder.itemView.findViewById(R.id.keep);
|
||||
|
@@ -24,6 +24,7 @@ import static com.android.settings.homepage.contextualcards.slices.SliceContextu
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.ArgumentMatchers.nullable;
|
||||
import static org.mockito.Mockito.atLeast;
|
||||
@@ -44,6 +45,7 @@ import android.telephony.TelephonyManager;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.FeatureFlagUtils;
|
||||
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import com.android.settings.core.FeatureFlags;
|
||||
@@ -64,6 +66,7 @@ import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.shadows.ShadowSubscriptionManager;
|
||||
import org.robolectric.shadows.ShadowTelephonyManager;
|
||||
|
||||
@@ -77,13 +80,15 @@ import java.util.stream.Collectors;
|
||||
public class ContextualCardManagerTest {
|
||||
private static final int SUB_ID = 2;
|
||||
|
||||
private static final String TEST_SLICE_URI = "context://test/test";
|
||||
private static final String TEST_SLICE_URI = "content://test/test";
|
||||
private static final String TEST_SLICE_NAME = "test_name";
|
||||
|
||||
@Mock
|
||||
ContextualCardUpdateListener mListener;
|
||||
@Mock
|
||||
Lifecycle mLifecycle;
|
||||
@Mock
|
||||
LoaderManager mLoaderManager;
|
||||
|
||||
private Context mContext;
|
||||
private ShadowSubscriptionManager mShadowSubscriptionManager;
|
||||
@@ -146,6 +151,27 @@ public class ContextualCardManagerTest {
|
||||
assertThat(actual).containsExactlyElementsIn(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = "mcc999")
|
||||
public void loadContextualCards_restartLoaderNotNeeded_shouldInitLoader() {
|
||||
mManager.loadContextualCards(mLoaderManager, false /* restartLoaderNeeded */);
|
||||
|
||||
verify(mLoaderManager).initLoader(anyInt(), nullable(Bundle.class),
|
||||
any(ContextualCardManager.CardContentLoaderCallbacks.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(qualifiers = "mcc999")
|
||||
public void loadContextualCards_restartLoaderNeeded_shouldRestartLoaderAndSetIsFirstLaunch() {
|
||||
mManager.mIsFirstLaunch = false;
|
||||
|
||||
mManager.loadContextualCards(mLoaderManager, true /* restartLoaderNeeded */);
|
||||
|
||||
verify(mLoaderManager).restartLoader(anyInt(), nullable(Bundle.class),
|
||||
any(ContextualCardManager.CardContentLoaderCallbacks.class));
|
||||
assertThat(mManager.mIsFirstLaunch).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getSettingsCards_conditionalsEnabled_shouldContainLegacyAndConditionals() {
|
||||
FeatureFlagUtils.setEnabled(mContext, FeatureFlags.CONDITIONAL_CARDS, true);
|
||||
@@ -184,7 +210,7 @@ public class ContextualCardManagerTest {
|
||||
final ContextualCard card1 =
|
||||
buildContextualCard(TEST_SLICE_URI).mutate().setRankingScore(99.0).build();
|
||||
final ContextualCard card2 =
|
||||
buildContextualCard("context://test/test2").mutate().setRankingScore(88.0).build();
|
||||
buildContextualCard("content://test/test2").mutate().setRankingScore(88.0).build();
|
||||
cards.add(card1);
|
||||
cards.add(card2);
|
||||
|
||||
@@ -205,6 +231,24 @@ public class ContextualCardManagerTest {
|
||||
.isEqualTo(ContextualCard.CardType.CONDITIONAL);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sortCards_hasStickyCards_stickyShouldAlwaysBeTheLast() {
|
||||
final List<ContextualCard> cards = new ArrayList<>();
|
||||
cards.add(buildContextualCard(CustomSliceRegistry.CONTEXTUAL_WIFI_SLICE_URI,
|
||||
ContextualCardProto.ContextualCard.Category.STICKY_VALUE, 1.02f));
|
||||
cards.add(buildContextualCard(CustomSliceRegistry.BLUETOOTH_DEVICES_SLICE_URI,
|
||||
ContextualCardProto.ContextualCard.Category.STICKY_VALUE, 1.01f));
|
||||
cards.add(buildContextualCard(CustomSliceRegistry.LOW_STORAGE_SLICE_URI,
|
||||
ContextualCardProto.ContextualCard.Category.SUGGESTION_VALUE, 0.01f));
|
||||
|
||||
final List<ContextualCard> sortedCards = mManager.sortCards(cards);
|
||||
|
||||
assertThat(sortedCards.get(cards.size() - 1).getSliceUri())
|
||||
.isEqualTo(CustomSliceRegistry.BLUETOOTH_DEVICES_SLICE_URI);
|
||||
assertThat(sortedCards.get(cards.size() - 2).getSliceUri())
|
||||
.isEqualTo(CustomSliceRegistry.CONTEXTUAL_WIFI_SLICE_URI);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onContextualCardUpdated_emptyMapWithExistingCards_shouldOnlyKeepConditionalCard() {
|
||||
mManager.mContextualCards.add(new ConditionalContextualCard.Builder().build());
|
||||
@@ -686,6 +730,17 @@ public class ContextualCardManagerTest {
|
||||
.build();
|
||||
}
|
||||
|
||||
private ContextualCard buildContextualCard(Uri uri, int category, double rankingScore) {
|
||||
return new ContextualCard.Builder()
|
||||
.setName(uri.toString())
|
||||
.setCardType(ContextualCard.CardType.SLICE)
|
||||
.setSliceUri(uri)
|
||||
.setViewType(VIEW_TYPE_FULL_WIDTH)
|
||||
.setCategory(category)
|
||||
.setRankingScore(rankingScore)
|
||||
.build();
|
||||
}
|
||||
|
||||
private List<ContextualCard> buildCategoriedCards(List<ContextualCard> cards,
|
||||
List<Integer> categories) {
|
||||
final List<ContextualCard> result = new ArrayList<>();
|
||||
|
@@ -16,6 +16,9 @@
|
||||
|
||||
package com.android.settings.homepage.contextualcards;
|
||||
|
||||
import static com.android.settings.intelligence.ContextualCardProto.ContextualCard.Category.IMPORTANT_VALUE;
|
||||
import static com.android.settings.intelligence.ContextualCardProto.ContextualCard.Category.STICKY_VALUE;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.net.Uri;
|
||||
@@ -24,7 +27,6 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -84,6 +86,24 @@ public class ContextualCardsDiffCallbackTest {
|
||||
assertThat(mDiffCallback.areContentsTheSame(0, 0)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void areContentsTheSame_stickySlice_returnFalse() {
|
||||
final ContextualCard card = getContextualCard("test1").mutate()
|
||||
.setCategory(STICKY_VALUE).build();
|
||||
mNewCards.add(0, card);
|
||||
|
||||
assertThat(mDiffCallback.areContentsTheSame(0, 0)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void areContentsTheSame_importantSlice_returnFalse() {
|
||||
final ContextualCard card = getContextualCard("test1").mutate()
|
||||
.setCategory(IMPORTANT_VALUE).build();
|
||||
mNewCards.add(0, card);
|
||||
|
||||
assertThat(mDiffCallback.areContentsTheSame(0, 0)).isFalse();
|
||||
}
|
||||
|
||||
private ContextualCard getContextualCard(String name) {
|
||||
return new ContextualCard.Builder()
|
||||
.setName(name)
|
||||
|
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* Copyright (C) 2020 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.settings.homepage.contextualcards;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ViewModelStoreOwner;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
|
||||
import com.android.settings.homepage.contextualcards.ContextualCardsFragment.ScreenOffReceiver;
|
||||
import com.android.settings.slices.SlicesFeatureProvider;
|
||||
import com.android.settings.testutils.FakeFeatureFactory;
|
||||
import com.android.settings.testutils.shadow.ShadowFragment;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.annotation.Implementation;
|
||||
import org.robolectric.annotation.Implements;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(shadows = {ShadowFragment.class, ContextualCardsFragmentTest.ShadowLoaderManager.class,
|
||||
ContextualCardsFragmentTest.ShadowContextualCardManager.class})
|
||||
public class ContextualCardsFragmentTest {
|
||||
|
||||
@Mock
|
||||
private FragmentActivity mActivity;
|
||||
private Context mContext;
|
||||
private ContextualCardsFragment mFragment;
|
||||
private SlicesFeatureProvider mSlicesFeatureProvider;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mContext = RuntimeEnvironment.application;
|
||||
mSlicesFeatureProvider = FakeFeatureFactory.setupForTest().slicesFeatureProvider;
|
||||
|
||||
mFragment = spy(new ContextualCardsFragment());
|
||||
doReturn(mActivity).when(mFragment).getActivity();
|
||||
mFragment.onCreate(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onStart_shouldRegisterBothReceivers() {
|
||||
mFragment.onStart();
|
||||
|
||||
verify(mActivity).registerReceiver(eq(mFragment.mKeyEventReceiver),
|
||||
any(IntentFilter.class));
|
||||
verify(mActivity).registerReceiver(eq(mFragment.mScreenOffReceiver),
|
||||
any(IntentFilter.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onStop_shouldUnregisterKeyEventReceiver() {
|
||||
mFragment.onStart();
|
||||
mFragment.onStop();
|
||||
|
||||
verify(mActivity).unregisterReceiver(eq(mFragment.mKeyEventReceiver));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onDestroy_shouldUnregisterScreenOffReceiver() {
|
||||
mFragment.onStart();
|
||||
mFragment.onDestroy();
|
||||
|
||||
verify(mActivity).unregisterReceiver(any(ScreenOffReceiver.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onStart_needRestartLoader_shouldClearRestartLoaderNeeded() {
|
||||
mFragment.sRestartLoaderNeeded = true;
|
||||
|
||||
mFragment.onStart();
|
||||
|
||||
assertThat(mFragment.sRestartLoaderNeeded).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onReceive_homeKey_shouldResetSession() {
|
||||
final Intent intent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
|
||||
intent.putExtra("reason", "homekey");
|
||||
mFragment.onStart();
|
||||
|
||||
mFragment.mKeyEventReceiver.onReceive(mContext, intent);
|
||||
|
||||
assertThat(mFragment.sRestartLoaderNeeded).isTrue();
|
||||
verify(mSlicesFeatureProvider, times(2)).newUiSession();
|
||||
verify(mActivity).unregisterReceiver(any(ScreenOffReceiver.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onReceive_recentApps_shouldResetSession() {
|
||||
final Intent intent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
|
||||
intent.putExtra("reason", "recentapps");
|
||||
mFragment.onStart();
|
||||
|
||||
mFragment.mKeyEventReceiver.onReceive(mContext, intent);
|
||||
|
||||
assertThat(mFragment.sRestartLoaderNeeded).isTrue();
|
||||
verify(mSlicesFeatureProvider, times(2)).newUiSession();
|
||||
verify(mActivity).unregisterReceiver(any(ScreenOffReceiver.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onReceive_otherKey_shouldNotResetSession() {
|
||||
final Intent intent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
|
||||
intent.putExtra("reason", "other");
|
||||
mFragment.onStart();
|
||||
|
||||
mFragment.mKeyEventReceiver.onReceive(mContext, intent);
|
||||
|
||||
assertThat(mFragment.sRestartLoaderNeeded).isFalse();
|
||||
verify(mSlicesFeatureProvider).newUiSession();
|
||||
verify(mActivity, never()).unregisterReceiver(any(ScreenOffReceiver.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void onReceive_screenOff_shouldResetSession() {
|
||||
final Intent intent = new Intent(Intent.ACTION_SCREEN_OFF);
|
||||
mFragment.onStart();
|
||||
|
||||
mFragment.mScreenOffReceiver.onReceive(mContext, intent);
|
||||
|
||||
assertThat(mFragment.sRestartLoaderNeeded).isTrue();
|
||||
verify(mSlicesFeatureProvider, times(2)).newUiSession();
|
||||
verify(mActivity).unregisterReceiver(any(ScreenOffReceiver.class));
|
||||
}
|
||||
|
||||
@Implements(value = LoaderManager.class)
|
||||
static class ShadowLoaderManager {
|
||||
|
||||
@Mock
|
||||
private static LoaderManager sLoaderManager;
|
||||
|
||||
@Implementation
|
||||
public static <T extends LifecycleOwner & ViewModelStoreOwner> LoaderManager getInstance(
|
||||
T owner) {
|
||||
return sLoaderManager;
|
||||
}
|
||||
}
|
||||
|
||||
@Implements(value = ContextualCardManager.class)
|
||||
public static class ShadowContextualCardManager {
|
||||
|
||||
public ShadowContextualCardManager() {
|
||||
}
|
||||
|
||||
@Implementation
|
||||
protected void setupController(int cardType) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@Implementation
|
||||
protected void loadContextualCards(LoaderManager loaderManager,
|
||||
boolean restartLoaderNeeded) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user