OSDN Git Service

Reload homepage cards when necessary
authorJason Chiu <chiujason@google.com>
Fri, 17 Apr 2020 11:05:08 +0000 (19:05 +0800)
committerJason Chiu <chiujason@google.com>
Tue, 5 May 2020 10:03:49 +0000 (10:03 +0000)
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

src/com/android/settings/homepage/contextualcards/ContextualCardManager.java
src/com/android/settings/homepage/contextualcards/ContextualCardsAdapter.java
src/com/android/settings/homepage/contextualcards/ContextualCardsDiffCallback.java
src/com/android/settings/homepage/contextualcards/ContextualCardsFragment.java
src/com/android/settings/homepage/contextualcards/slices/DarkThemeSlice.java
src/com/android/settings/homepage/contextualcards/slices/SliceContextualCardRenderer.java
tests/robotests/src/com/android/settings/homepage/contextualcards/ContextualCardManagerTest.java
tests/robotests/src/com/android/settings/homepage/contextualcards/ContextualCardsDiffCallbackTest.java
tests/robotests/src/com/android/settings/homepage/contextualcards/ContextualCardsFragmentTest.java [new file with mode: 0644]

index 0931a80..fc666ec 100644 (file)
@@ -117,13 +117,13 @@ public class ContextualCardManager implements ContextualCardLoader.CardContentLo
         } else {
             mSavedCards = savedInstanceState.getStringArrayList(KEY_CONTEXTUAL_CARDS);
         }
-        //for data provided by Settings
+        // for data provided by Settings
         for (@ContextualCard.CardType int cardType : getSettingsCards()) {
             setupController(cardType);
         }
     }
 
-    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);
-        // Use the cached data when navigating back to the first page and upon screen rotation.
-        loaderManager.initLoader(CARD_CONTENT_LOADER_ID, null /* bundle */,
-                cardContentLoaderCallbacks);
+        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() {
@@ -146,7 +154,7 @@ public class ContextualCardManager implements ContextualCardLoader.CardContentLo
     @VisibleForTesting
     int[] getSettingsCards() {
         if (!FeatureFlagUtils.isEnabled(mContext, FeatureFlags.CONDITIONAL_CARDS)) {
-            return new int[]{ContextualCard.CardType.LEGACY_SUGGESTION};
+            return new int[] {ContextualCard.CardType.LEGACY_SUGGESTION};
         }
         return new int[]
                 {ContextualCard.CardType.CONDITIONAL, ContextualCard.CardType.LEGACY_SUGGESTION};
@@ -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);
index 4e010fd..b9bc43b 100644 (file)
@@ -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
index 58d6a41..e2cd00c 100644 (file)
@@ -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);
     }
 }
\ No newline at end of file
index 92892b3..c0527c9 100644 (file)
@@ -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);
+        }
+    }
 }
index 3c88eb2..1c1bedc 100644 (file)
 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)
@@ -169,7 +163,7 @@ public class DarkThemeSlice implements CustomSliceable {
         // checking the current battery level
         final BatteryManager batteryManager = context.getSystemService(BatteryManager.class);
         final int level = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
-        Log.d(TAG, "battery level=" + level);
+        Log.d(TAG, "battery level = " + level);
         return level <= BATTERY_LEVEL_THRESHOLD;
     }
 
@@ -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() {
         }
     }
 }
index 740c328..6538cac 100644 (file)
@@ -149,11 +149,11 @@ public class SliceContextualCardRenderer implements ContextualCardRenderer, Life
 
         if (holder.getItemViewType() != VIEW_TYPE_STICKY) {
             initDismissalActions(holder, card);
-        }
 
-        if (card.isPendingDismiss()) {
-            showDismissalView(holder);
-            mFlippedCardSet.add(holder);
+            if (card.isPendingDismiss()) {
+                showDismissalView(holder);
+                mFlippedCardSet.add(holder);
+            }
         }
     }
 
index 8958d6e..127b9e2 100644 (file)
@@ -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;
@@ -147,6 +152,27 @@ public class ContextualCardManagerTest {
     }
 
     @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);
         final int[] expected = {ContextualCard.CardType.CONDITIONAL,
@@ -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);
 
@@ -206,6 +232,24 @@ public class ContextualCardManagerTest {
     }
 
     @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());
         mManager.mContextualCards.add(
@@ -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<>();
index eb95f71..1e17a0b 100644 (file)
@@ -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)
diff --git a/tests/robotests/src/com/android/settings/homepage/contextualcards/ContextualCardsFragmentTest.java b/tests/robotests/src/com/android/settings/homepage/contextualcards/ContextualCardsFragmentTest.java
new file mode 100644 (file)
index 0000000..d127791
--- /dev/null
@@ -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
+        }
+    }
+}