| /* |
| * Copyright (C) 2021 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.launcher3.widget.picker; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| |
| import static org.mockito.ArgumentMatchers.any; |
| import static org.mockito.Mockito.doAnswer; |
| import static org.mockito.Mockito.verify; |
| import static org.mockito.Mockito.verifyZeroInteractions; |
| import static org.robolectric.Shadows.shadowOf; |
| |
| import android.appwidget.AppWidgetProviderInfo; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.graphics.Bitmap; |
| import android.os.UserHandle; |
| |
| import androidx.recyclerview.widget.RecyclerView; |
| |
| import com.android.launcher3.InvariantDeviceProfile; |
| import com.android.launcher3.LauncherAppWidgetProviderInfo; |
| import com.android.launcher3.icons.BitmapInfo; |
| import com.android.launcher3.icons.ComponentWithLabel; |
| import com.android.launcher3.icons.IconCache; |
| import com.android.launcher3.model.WidgetItem; |
| import com.android.launcher3.model.data.PackageItemInfo; |
| import com.android.launcher3.widget.model.WidgetsListBaseEntry; |
| import com.android.launcher3.widget.model.WidgetsListContentEntry; |
| import com.android.launcher3.widget.model.WidgetsListHeaderEntry; |
| import com.android.launcher3.widget.picker.WidgetsListAdapter.WidgetListBaseRowEntryComparator; |
| |
| 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.shadows.ShadowPackageManager; |
| import org.robolectric.util.ReflectionHelpers; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| @RunWith(RobolectricTestRunner.class) |
| public final class WidgetsDiffReporterTest { |
| private static final String TEST_PACKAGE_PREFIX = "com.google.test"; |
| private static final WidgetListBaseRowEntryComparator COMPARATOR = |
| new WidgetListBaseRowEntryComparator(); |
| |
| @Mock private IconCache mIconCache; |
| @Mock private RecyclerView.Adapter mAdapter; |
| |
| private InvariantDeviceProfile mTestProfile; |
| private WidgetsDiffReporter mWidgetsDiffReporter; |
| private Context mContext; |
| private WidgetsListHeaderEntry mHeaderA; |
| private WidgetsListHeaderEntry mHeaderB; |
| private WidgetsListHeaderEntry mHeaderC; |
| private WidgetsListHeaderEntry mHeaderD; |
| private WidgetsListHeaderEntry mHeaderE; |
| private WidgetsListContentEntry mContentC; |
| private WidgetsListContentEntry mContentE; |
| |
| @Before |
| public void setUp() { |
| MockitoAnnotations.initMocks(this); |
| mTestProfile = new InvariantDeviceProfile(); |
| mTestProfile.numRows = 5; |
| mTestProfile.numColumns = 5; |
| |
| doAnswer(invocation -> ((ComponentWithLabel) invocation.getArgument(0)) |
| .getComponent().getPackageName()) |
| .when(mIconCache).getTitleNoCache(any()); |
| |
| mContext = RuntimeEnvironment.application; |
| mWidgetsDiffReporter = new WidgetsDiffReporter(mIconCache, mAdapter); |
| mHeaderA = createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "A", |
| /* appName= */ "A", /* numOfWidgets= */ 3); |
| mHeaderB = createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "B", |
| /* appName= */ "B", /* numOfWidgets= */ 3); |
| mHeaderC = createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "C", |
| /* appName= */ "C", /* numOfWidgets= */ 3); |
| mContentC = createWidgetsContentEntry(TEST_PACKAGE_PREFIX + "C", |
| /* appName= */ "C", /* numOfWidgets= */ 3); |
| mHeaderD = createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "D", |
| /* appName= */ "D", /* numOfWidgets= */ 3); |
| mHeaderE = createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "E", |
| /* appName= */ "E", /* numOfWidgets= */ 3); |
| mContentE = createWidgetsContentEntry(TEST_PACKAGE_PREFIX + "E", |
| /* appName= */ "E", /* numOfWidgets= */ 3); |
| } |
| |
| @Test |
| public void listNotChanged_shouldNotInvokeAnyCallbacks() { |
| // GIVEN the current list has app headers [A, B, C]. |
| ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>( |
| List.of(mHeaderA, mHeaderB, mHeaderC)); |
| |
| // WHEN computing the list difference. |
| mWidgetsDiffReporter.process(currentList, currentList, COMPARATOR); |
| |
| // THEN there is no adaptor callback. |
| verifyZeroInteractions(mAdapter); |
| // THEN the current list contains the same entries. |
| assertThat(currentList).containsExactly(mHeaderA, mHeaderB, mHeaderC); |
| } |
| |
| @Test |
| public void headersOnly_emptyListToNonEmpty_shouldInvokeNotifyDataSetChanged() { |
| // GIVEN the current list has app headers [A, B, C]. |
| ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>(); |
| |
| List<WidgetsListBaseEntry> newList = List.of( |
| createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "A", "A", 3), |
| createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "B", "B", 3), |
| createWidgetsHeaderEntry(TEST_PACKAGE_PREFIX + "C", "C", 3)); |
| |
| // WHEN computing the list difference. |
| mWidgetsDiffReporter.process(currentList, newList, COMPARATOR); |
| |
| // THEN notifyDataSetChanged is called |
| verify(mAdapter).notifyDataSetChanged(); |
| // THEN the current list contains all elements from the new list. |
| assertThat(currentList).containsExactlyElementsIn(newList); |
| } |
| |
| @Test |
| public void headersOnly_nonEmptyToEmptyList_shouldInvokeNotifyDataSetChanged() { |
| // GIVEN the current list has app headers [A, B, C]. |
| ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>( |
| List.of(mHeaderA, mHeaderB, mHeaderC)); |
| // GIVEN the new list is empty. |
| List<WidgetsListBaseEntry> newList = List.of(); |
| |
| // WHEN computing the list difference. |
| mWidgetsDiffReporter.process(currentList, newList, COMPARATOR); |
| |
| // THEN notifyDataSetChanged is called. |
| verify(mAdapter).notifyDataSetChanged(); |
| // THEN the current list isEmpty. |
| assertThat(currentList).isEmpty(); |
| } |
| |
| @Test |
| public void headersOnly_itemAddedAndRemovedInTheNewList_shouldInvokeCorrectCallbacks() { |
| // GIVEN the current list has app headers [A, B, D]. |
| ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>( |
| List.of(mHeaderA, mHeaderB, mHeaderD)); |
| // GIVEN the new list has app headers [A, C, E]. |
| List<WidgetsListBaseEntry> newList = List.of(mHeaderA, mHeaderC, mHeaderE); |
| |
| // WHEN computing the list difference. |
| mWidgetsDiffReporter.process(currentList, newList, COMPARATOR); |
| |
| // THEN "B" is removed from position 1. |
| verify(mAdapter).notifyItemRemoved(/* position= */ 1); |
| // THEN "D" is removed from position 2. |
| verify(mAdapter).notifyItemRemoved(/* position= */ 2); |
| // THEN "C" is inserted at position 1. |
| verify(mAdapter).notifyItemInserted(/* position= */ 1); |
| // THEN "E" is inserted at position 2. |
| verify(mAdapter).notifyItemInserted(/* position= */ 2); |
| // THEN the current list contains all elements from the new list. |
| assertThat(currentList).containsExactlyElementsIn(newList); |
| } |
| |
| @Test |
| public void headersContentsMix_itemAddedAndRemovedInTheNewList_shouldInvokeCorrectCallbacks() { |
| // GIVEN the current list has app headers [A, B, E content]. |
| ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>( |
| List.of(mHeaderA, mHeaderB, mContentE)); |
| // GIVEN the new list has app headers [A, C content, D]. |
| List<WidgetsListBaseEntry> newList = List.of(mHeaderA, mContentC, mHeaderD); |
| |
| // WHEN computing the list difference. |
| mWidgetsDiffReporter.process(currentList, newList, COMPARATOR); |
| |
| // THEN "B" is removed from position 1. |
| verify(mAdapter).notifyItemRemoved(/* position= */ 1); |
| // THEN "C content" is inserted at position 1. |
| verify(mAdapter).notifyItemInserted(/* position= */ 1); |
| // THEN "D" is inserted at position 2. |
| verify(mAdapter).notifyItemInserted(/* position= */ 2); |
| // THEN "E content" is removed from position 3. |
| verify(mAdapter).notifyItemRemoved(/* position= */ 3); |
| // THEN the current list contains all elements from the new list. |
| assertThat(currentList).containsExactlyElementsIn(newList); |
| } |
| |
| @Test |
| public void headersContentsMix_userInteractWithHeader_shouldInvokeCorrectCallbacks() { |
| // GIVEN the current list has app headers [A, B, E content]. |
| ArrayList<WidgetsListBaseEntry> currentList = new ArrayList<>( |
| List.of(mHeaderA, mHeaderB, mContentE)); |
| // GIVEN the new list has app headers [A, B, E content]. |
| List<WidgetsListBaseEntry> newList = List.of(mHeaderA, mHeaderB, mContentE); |
| // GIVEN the user has interacted with B. |
| mHeaderB.setIsWidgetListShown(true); |
| |
| // WHEN computing the list difference. |
| mWidgetsDiffReporter.process(currentList, newList, COMPARATOR); |
| |
| // THEN notify "B" has been changed. |
| verify(mAdapter).notifyItemChanged(/* position= */ 1); |
| // THEN the current list contains all elements from the new list. |
| assertThat(currentList).containsExactlyElementsIn(newList); |
| } |
| |
| |
| private WidgetsListHeaderEntry createWidgetsHeaderEntry(String packageName, String appName, |
| int numOfWidgets) { |
| List<WidgetItem> widgetItems = generateWidgetItems(packageName, numOfWidgets); |
| PackageItemInfo pInfo = createPackageItemInfo(packageName, appName, |
| widgetItems.get(0).user); |
| |
| return new WidgetsListHeaderEntry(pInfo, /* titleSectionName= */ "", widgetItems); |
| } |
| |
| private WidgetsListContentEntry createWidgetsContentEntry(String packageName, String appName, |
| int numOfWidgets) { |
| List<WidgetItem> widgetItems = generateWidgetItems(packageName, numOfWidgets); |
| PackageItemInfo pInfo = createPackageItemInfo(packageName, appName, |
| widgetItems.get(0).user); |
| |
| return new WidgetsListContentEntry(pInfo, /* titleSectionName= */ "", widgetItems); |
| } |
| |
| private PackageItemInfo createPackageItemInfo(String packageName, String appName, |
| UserHandle userHandle) { |
| PackageItemInfo pInfo = new PackageItemInfo(packageName); |
| pInfo.title = appName; |
| pInfo.user = userHandle; |
| pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0); |
| return pInfo; |
| } |
| |
| private List<WidgetItem> generateWidgetItems(String packageName, int numOfWidgets) { |
| ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager()); |
| ArrayList<WidgetItem> widgetItems = new ArrayList<>(); |
| for (int i = 0; i < numOfWidgets; i++) { |
| ComponentName cn = ComponentName.createRelative(packageName, ".SampleWidget" + i); |
| AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo(); |
| widgetInfo.provider = cn; |
| ReflectionHelpers.setField(widgetInfo, "providerInfo", |
| packageManager.addReceiverIfNotPresent(cn)); |
| |
| WidgetItem widgetItem = new WidgetItem( |
| LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo), |
| mTestProfile, mIconCache); |
| widgetItems.add(widgetItem); |
| } |
| return widgetItems; |
| } |
| } |