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:
Jason Chiu
2020-04-17 19:05:08 +08:00
parent 07431066c4
commit e0327ee583
9 changed files with 419 additions and 77 deletions

View 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;
@@ -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<>();

View 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)

View File

@@ -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
}
}
}