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