diff --git a/go/src/com/android/launcher3/model/WidgetsModel.java b/go/src/com/android/launcher3/model/WidgetsModel.java index 89b3831564..a202095c47 100644 --- a/go/src/com/android/launcher3/model/WidgetsModel.java +++ b/go/src/com/android/launcher3/model/WidgetsModel.java @@ -25,7 +25,7 @@ import androidx.annotation.Nullable; import com.android.launcher3.LauncherAppState; import com.android.launcher3.icons.ComponentWithLabelAndIcon; import com.android.launcher3.util.PackageUserKey; -import com.android.launcher3.widget.WidgetListRowEntry; +import com.android.launcher3.widget.model.WidgetsListBaseEntry; import java.util.ArrayList; import java.util.Collections; @@ -43,17 +43,17 @@ public class WidgetsModel { public static final boolean GO_DISABLE_WIDGETS = true; public static final boolean GO_DISABLE_NOTIFICATION_DOTS = true; - private static final ArrayList EMPTY_WIDGET_LIST = new ArrayList<>(); + private static final ArrayList EMPTY_WIDGET_LIST = new ArrayList<>(); /** - * Returns a list of {@link WidgetListRowEntry}. All {@link WidgetItem} in a single row - * are sorted (based on label and user), but the overall list of {@link WidgetListRowEntry}s - * is not sorted. This list is sorted at the UI when using - * {@link com.android.launcher3.widget.WidgetsDiffReporter} + * Returns a list of {@link WidgetsListBaseEntry}. All {@link WidgetItem} in a single row are + * sorted (based on label and user), but the overall list of {@link WidgetsListBaseEntry}s is + * not sorted. This list is sorted at the UI when using + * {@link com.android.launcher3.widget.picker.WidgetsDiffReporter} * - * @see com.android.launcher3.widget.WidgetsListAdapter#setWidgets(ArrayList) + * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List) */ - public synchronized ArrayList getWidgetsList(Context context) { + public synchronized ArrayList getWidgetsList(Context context) { return EMPTY_WIDGET_LIST; } diff --git a/res/layout/widgets_full_sheet.xml b/res/layout/widgets_full_sheet.xml index f507a88f1a..6e7cf0fee9 100644 --- a/res/layout/widgets_full_sheet.xml +++ b/res/layout/widgets_full_sheet.xml @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. --> - - - \ No newline at end of file + \ No newline at end of file diff --git a/robolectric_tests/src/com/android/launcher3/testing/TestActivity.java b/robolectric_tests/src/com/android/launcher3/testing/TestActivity.java new file mode 100644 index 0000000000..dbf4b3e6c0 --- /dev/null +++ b/robolectric_tests/src/com/android/launcher3/testing/TestActivity.java @@ -0,0 +1,41 @@ +/* + * 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.testing; + +import com.android.launcher3.BaseActivity; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.views.ActivityContext; +import com.android.launcher3.views.BaseDragLayer; + +/** An empty activity for {@link android.app.Fragment}s, {@link android.view.View}s testing. */ +public class TestActivity extends BaseActivity implements ActivityContext { + + private DeviceProfile mDeviceProfile; + + @Override + public BaseDragLayer getDragLayer() { + return null; + } + + @Override + public DeviceProfile getDeviceProfile() { + return mDeviceProfile; + } + + public void setDeviceProfile(DeviceProfile deviceProfile) { + mDeviceProfile = deviceProfile; + } +} diff --git a/robolectric_tests/src/com/android/launcher3/ui/LauncherUIScrollTest.java b/robolectric_tests/src/com/android/launcher3/ui/LauncherUIScrollTest.java index 957ae77eee..34cb2ad763 100644 --- a/robolectric_tests/src/com/android/launcher3/ui/LauncherUIScrollTest.java +++ b/robolectric_tests/src/com/android/launcher3/ui/LauncherUIScrollTest.java @@ -38,7 +38,7 @@ import com.android.launcher3.folder.FolderPagedView; import com.android.launcher3.util.LauncherLayoutBuilder; import com.android.launcher3.util.LauncherLayoutBuilder.FolderBuilder; import com.android.launcher3.util.LauncherModelHelper; -import com.android.launcher3.widget.WidgetsFullSheet; +import com.android.launcher3.widget.picker.WidgetsFullSheet; import org.junit.Before; import org.junit.Test; diff --git a/robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java b/robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java deleted file mode 100644 index 5ab3106bd9..0000000000 --- a/robolectric_tests/src/com/android/launcher3/widget/WidgetsListAdapterTest.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (C) 2017 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; - -import static org.mockito.Matchers.eq; -import static org.mockito.Matchers.isNull; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.robolectric.Shadows.shadowOf; - -import android.appwidget.AppWidgetProviderInfo; -import android.content.ComponentName; -import android.content.Context; -import android.graphics.Bitmap; -import android.view.LayoutInflater; - -import androidx.recyclerview.widget.RecyclerView; - -import com.android.launcher3.InvariantDeviceProfile; -import com.android.launcher3.LauncherAppWidgetProviderInfo; -import com.android.launcher3.WidgetPreviewLoader; -import com.android.launcher3.icons.BitmapInfo; -import com.android.launcher3.icons.IconCache; -import com.android.launcher3.model.WidgetItem; -import com.android.launcher3.model.data.PackageItemInfo; - -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.Collections; - -@RunWith(RobolectricTestRunner.class) -public class WidgetsListAdapterTest { - - @Mock private LayoutInflater mMockLayoutInflater; - @Mock private WidgetPreviewLoader mMockWidgetCache; - @Mock private RecyclerView.AdapterDataObserver mListener; - @Mock private IconCache mIconCache; - - private WidgetsListAdapter mAdapter; - private InvariantDeviceProfile mTestProfile; - private Context mContext; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - mContext = RuntimeEnvironment.application; - mTestProfile = new InvariantDeviceProfile(); - mTestProfile.numRows = 5; - mTestProfile.numColumns = 5; - mAdapter = new WidgetsListAdapter(mContext, mMockLayoutInflater, mMockWidgetCache, - mIconCache, null, null); - mAdapter.registerAdapterDataObserver(mListener); - } - - @Test - public void test_notifyDataSetChanged() throws Exception { - mAdapter.setWidgets(generateSampleMap(1)); - verify(mListener, times(1)).onChanged(); - } - - @Test - public void test_notifyItemInserted() throws Exception { - mAdapter.setWidgets(generateSampleMap(1)); - mAdapter.setWidgets(generateSampleMap(2)); - verify(mListener, times(1)).onChanged(); - verify(mListener, times(1)).onItemRangeInserted(eq(1), eq(1)); - } - - @Test - public void test_notifyItemRemoved() throws Exception { - mAdapter.setWidgets(generateSampleMap(2)); - mAdapter.setWidgets(generateSampleMap(1)); - verify(mListener, times(1)).onChanged(); - verify(mListener, times(1)).onItemRangeRemoved(eq(1), eq(1)); - } - - @Test - public void testNotifyItemChanged_PackageIconDiff() throws Exception { - mAdapter.setWidgets(generateSampleMap(1)); - mAdapter.setWidgets(generateSampleMap(1)); - verify(mListener, times(1)).onChanged(); - verify(mListener, times(1)).onItemRangeChanged(eq(0), eq(1), isNull()); - } - - @Test - public void testNotifyItemChanged_widgetItemInfoDiff() throws Exception { - // TODO: same package name but item number changed - } - - @Test - public void testNotifyItemInsertedRemoved_hodgepodge() throws Exception { - // TODO: insert and remove combined. curMap - // newMap [A, C, D] [A, B, E] - // B - C < 0, removed B from index 1 [A, E] - // E - C > 0, C inserted to index 1 [A, C, E] - // E - D > 0, D inserted to index 2 [A, C, D, E] - // E - null = -1, E deleted from index 3 [A, C, D] - } - - /** - * Helper method to generate the sample widget model map that can be used for the tests - * @param num the number of WidgetItem the map should contain - */ - private ArrayList generateSampleMap(int num) { - ArrayList result = new ArrayList<>(); - if (num <= 0) return result; - ShadowPackageManager spm = shadowOf(mContext.getPackageManager()); - - for (int i = 0; i < num; i++) { - ComponentName cn = new ComponentName("com.placeholder.apk" + i, "PlaceholderWidet"); - - AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo(); - widgetInfo.provider = cn; - ReflectionHelpers.setField(widgetInfo, "providerInfo", spm.addReceiverIfNotPresent(cn)); - - WidgetItem wi = new WidgetItem(LauncherAppWidgetProviderInfo - .fromProviderInfo(mContext, widgetInfo), mTestProfile, mIconCache); - - PackageItemInfo pInfo = new PackageItemInfo(wi.componentName.getPackageName()); - pInfo.title = pInfo.packageName; - pInfo.user = wi.user; - pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0); - - result.add(new WidgetListRowEntry(pInfo, new ArrayList<>(Collections.singleton(wi)))); - } - - return result; - } -} diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java new file mode 100644 index 0000000000..9bea2fb406 --- /dev/null +++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2017 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 org.mockito.ArgumentMatchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isNull; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.verify; +import static org.robolectric.Shadows.shadowOf; + +import android.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; +import android.content.Context; +import android.graphics.Bitmap; +import android.view.LayoutInflater; + +import androidx.recyclerview.widget.RecyclerView; + +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.WidgetPreviewLoader; +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 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 WidgetsListAdapterTest { + + private static final String TEST_PACKAGE_1 = "com.google.test.1"; + private static final String TEST_PACKAGE_2 = "com.google.test.2"; + + @Mock private LayoutInflater mMockLayoutInflater; + @Mock private WidgetPreviewLoader mMockWidgetCache; + @Mock private RecyclerView.AdapterDataObserver mListener; + @Mock private IconCache mIconCache; + + private WidgetsListAdapter mAdapter; + private InvariantDeviceProfile mTestProfile; + private Context mContext; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + mTestProfile = new InvariantDeviceProfile(); + mTestProfile.numRows = 5; + mTestProfile.numColumns = 5; + mAdapter = new WidgetsListAdapter(mContext, mMockLayoutInflater, mMockWidgetCache, + mIconCache, null, null); + mAdapter.registerAdapterDataObserver(mListener); + + doAnswer(invocation -> ((ComponentWithLabel) invocation.getArgument(0)) + .getComponent().getPackageName()) + .when(mIconCache).getTitleNoCache(any()); + } + + @Test + public void setWidgets_shouldNotifyDataSetChanged() { + mAdapter.setWidgets(generateSampleMap(1)); + + verify(mListener).onChanged(); + } + + @Test + public void setWidgets_withItemInserted_shouldNotifyItemInserted() { + mAdapter.setWidgets(generateSampleMap(1)); + mAdapter.setWidgets(generateSampleMap(2)); + + verify(mListener).onItemRangeInserted(eq(1), eq(1)); + } + + @Test + public void setWidgets_withItemRemoved_shouldNotifyItemRemoved() { + mAdapter.setWidgets(generateSampleMap(2)); + mAdapter.setWidgets(generateSampleMap(1)); + + verify(mListener).onItemRangeRemoved(eq(1), eq(1)); + } + + @Test + public void setWidgets_appIconChanged_shouldNotifyItemChanged() { + mAdapter.setWidgets(generateSampleMap(1)); + mAdapter.setWidgets(generateSampleMap(1)); + + verify(mListener).onItemRangeChanged(eq(0), eq(1), isNull()); + } + + @Test + public void setWidgets_sameApp_moreWidgets_shouldNotifyItemChangedWithWidgetItemInfoDiff() { + // GIVEN the adapter was first populated with test package 1 & test package 2. + WidgetsListBaseEntry testPackage1With2WidgetsListEntry = + generateSampleAppWithWidgets(TEST_PACKAGE_1, /* numOfWidgets= */ 2); + WidgetsListBaseEntry testPackage2With2WidgetsListEntry = + generateSampleAppWithWidgets(TEST_PACKAGE_2, /* numOfWidgets= */ 2); + mAdapter.setWidgets( + List.of(testPackage1With2WidgetsListEntry, testPackage2With2WidgetsListEntry)); + + // WHEN the adapter is updated with the same list of apps but test package 2 has 3 widgets + // now. + WidgetsListBaseEntry testPackage1With3WidgetsListEntry = + generateSampleAppWithWidgets(TEST_PACKAGE_2, /* numOfWidgets= */ 2); + mAdapter.setWidgets( + List.of(testPackage1With2WidgetsListEntry, testPackage1With3WidgetsListEntry)); + + // THEN the onItemRangeChanged is invoked. + verify(mListener).onItemRangeChanged(eq(1), eq(1), isNull()); + } + + @Test + public void setWidgets_hodgepodge_shouldInvokeExpectedDataObserverCallbacks() { + List allAppsWithWidgets = generateSampleMap(5); + // GIVEN the current widgets list consist of [A, B, E]. + List currentList = List.of( + allAppsWithWidgets.get(0), allAppsWithWidgets.get(1), allAppsWithWidgets.get(4)); + mAdapter.setWidgets(currentList); + + // WHEN the widgets list is updated to [A, C, D]. + List newList = List.of( + allAppsWithWidgets.get(0), allAppsWithWidgets.get(2), allAppsWithWidgets.get(3)); + mAdapter.setWidgets(newList); + + // Computation logic | [Intermediate list during computation] + // THEN B <> C < 0, removed B from index 1 | [A, E] + verify(mListener).onItemRangeRemoved(/* positionStart= */ 1, /* itemCount= */ 1); + // THEN E <> C > 0, C inserted to index 1 | [A, C, E] + verify(mListener).onItemRangeInserted(/* positionStart= */ 1, /* itemCount= */ 1); + // THEN E <> D > 0, D inserted to index 2 | [A, C, D, E] + verify(mListener).onItemRangeInserted(/* positionStart= */ 2, /* itemCount= */ 1); + // THEN E <> null = -1, E deleted from index 3 | [A, C, D] + verify(mListener).onItemRangeRemoved(/* positionStart= */ 3, /* itemCount= */ 1); + } + + /** + * Helper method to generate the sample widget model map that can be used for the tests + * @param num the number of WidgetItem the map should contain + */ + private ArrayList generateSampleMap(int num) { + ArrayList result = new ArrayList<>(); + if (num <= 0) return result; + + for (int i = 0; i < num; i++) { + String packageName = "com.placeholder.apk" + i; + + List widgetItems = generateWidgetItems(packageName, /* numOfWidgets= */ 1); + + PackageItemInfo pInfo = new PackageItemInfo(packageName); + pInfo.title = pInfo.packageName; + pInfo.user = widgetItems.get(0).user; + pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0); + + result.add(new WidgetsListContentEntry(pInfo, /* titleSectionName= */ "", widgetItems)); + } + + return result; + } + + private WidgetsListBaseEntry generateSampleAppWithWidgets(String packageName, + int numOfWidgets) { + PackageItemInfo appInfo = new PackageItemInfo(packageName); + appInfo.title = appInfo.packageName; + appInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0); + + return new WidgetsListContentEntry(appInfo, + /* titleSectionName= */ "", + generateWidgetItems(packageName, numOfWidgets)); + } + + private List generateWidgetItems(String packageName, int numOfWidgets) { + ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager()); + ArrayList 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)); + + widgetItems.add(new WidgetItem( + LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo), + mTestProfile, mIconCache)); + } + return widgetItems; + } +} diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListRowViewHolderBinderTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListRowViewHolderBinderTest.java new file mode 100644 index 0000000000..4e9e227a52 --- /dev/null +++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListRowViewHolderBinderTest.java @@ -0,0 +1,190 @@ +/* + * 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 android.os.Looper.getMainLooper; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.robolectric.Shadows.shadowOf; + +import android.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; +import android.content.Context; +import android.graphics.Bitmap; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; +import android.widget.FrameLayout; +import android.widget.TextView; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.R; +import com.android.launcher3.WidgetPreviewLoader; +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.testing.TestActivity; +import com.android.launcher3.widget.WidgetCell; +import com.android.launcher3.widget.model.WidgetsListContentEntry; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.android.controller.ActivityController; +import org.robolectric.shadows.ShadowPackageManager; +import org.robolectric.util.ReflectionHelpers; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +public final class WidgetsListRowViewHolderBinderTest { + private static final String TEST_PACKAGE = "com.google.test"; + private static final String APP_NAME = "Test app"; + + private Context mContext; + private WidgetsListRowViewHolderBinder mViewHolderBinder; + private InvariantDeviceProfile mTestProfile; + // Replace ActivityController with ActivityScenario, which is the recommended way for activity + // testing. + private ActivityController mActivityController; + private TestActivity mTestActivity; + + @Mock + private OnLongClickListener mOnLongClickListener; + @Mock + private OnClickListener mOnIconClickListener; + @Mock + private IconCache mIconCache; + @Mock + private WidgetPreviewLoader mWidgetPreviewLoader; + @Mock + private DeviceProfile mDeviceProfile; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + mTestProfile = new InvariantDeviceProfile(); + mTestProfile.numRows = 5; + mTestProfile.numColumns = 5; + + mActivityController = Robolectric.buildActivity(TestActivity.class); + mTestActivity = mActivityController.setup().get(); + mTestActivity.setDeviceProfile(mDeviceProfile); + + doAnswer(invocation -> { + ComponentWithLabel componentWithLabel = (ComponentWithLabel) invocation.getArgument(0); + return componentWithLabel.getComponent().getShortClassName(); + }).when(mIconCache).getTitleNoCache(any()); + + mViewHolderBinder = new WidgetsListRowViewHolderBinder( + mContext, + LayoutInflater.from(mTestActivity), + mOnIconClickListener, + mOnLongClickListener, + mWidgetPreviewLoader); + } + + @After + public void tearDown() { + mActivityController.destroy(); + } + + @Test + public void bindViewHolder_appWith3Widgets_shouldMatchAppTitle() { + WidgetsRowViewHolder viewHolder = mViewHolderBinder.newViewHolder( + new FrameLayout(mTestActivity)); + WidgetsListContentEntry entry = generateSampleAppWithWidgets( + APP_NAME, + TEST_PACKAGE, + /* numOfWidgets= */ 3); + mViewHolderBinder.bindViewHolder(viewHolder, entry); + + assertThat(viewHolder.title.getText()).isEqualTo(APP_NAME); + } + + @Test + public void bindViewHolder_appWith3Widgets_shouldHave3Widgets() { + WidgetsRowViewHolder viewHolder = mViewHolderBinder.newViewHolder( + new FrameLayout(mTestActivity)); + WidgetsListContentEntry entry = generateSampleAppWithWidgets( + APP_NAME, + TEST_PACKAGE, + /* numOfWidgets= */ 3); + mViewHolderBinder.bindViewHolder(viewHolder, entry); + shadowOf(getMainLooper()).idle(); + + // THEN the cell container has 5 children: 3 widgets + 2 separators + // Index: 0 1 2 3 4 + // View: .SampleWidget0 | .SampleWidget1 | .SampleWidget2 + assertThat(viewHolder.cellContainer.getChildCount()).isEqualTo(5); + // Widget 0 label is .SampleWidget0. + assertWidgetCellWithLabel(viewHolder.cellContainer.getChildAt(0), ".SampleWidget0"); + // Widget 1 label is .SampleWidget1. + assertWidgetCellWithLabel(viewHolder.cellContainer.getChildAt(2), ".SampleWidget1"); + // Widget 2 label is .SampleWidget2. + assertWidgetCellWithLabel(viewHolder.cellContainer.getChildAt(4), ".SampleWidget2"); + } + + private WidgetsListContentEntry generateSampleAppWithWidgets(String appName, String packageName, + int numOfWidgets) { + PackageItemInfo appInfo = new PackageItemInfo(packageName); + appInfo.title = appName; + appInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0); + + return new WidgetsListContentEntry(appInfo, + /* titleSectionName= */ "", + generateWidgetItems(packageName, numOfWidgets)); + } + + private List generateWidgetItems(String packageName, int numOfWidgets) { + ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager()); + ArrayList 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)); + + widgetItems.add(new WidgetItem( + LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo), + mTestProfile, mIconCache)); + } + return widgetItems; + } + + private void assertWidgetCellWithLabel(View view, String label) { + assertThat(view).isInstanceOf(WidgetCell.class); + TextView widgetLabel = (TextView) view.findViewById(R.id.widget_name); + assertThat(widgetLabel.getText()).isEqualTo(label); + } +} diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java new file mode 100644 index 0000000000..a11d0c9505 --- /dev/null +++ b/robolectric_tests/src/com/android/launcher3/widget/picker/model/WidgetsListContentEntryTest.java @@ -0,0 +1,177 @@ +/* + * 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.model; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.robolectric.Shadows.shadowOf; + +import android.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; +import android.content.Context; + +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.LauncherAppWidgetProviderInfo; +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.WidgetsListContentEntry; + +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.HashMap; +import java.util.List; +import java.util.Map; + +@RunWith(RobolectricTestRunner.class) +public final class WidgetsListContentEntryTest { + private static final String PACKAGE_NAME = "com.google.test"; + private static final PackageItemInfo PACKAGE_ITEM_INFO = new PackageItemInfo(PACKAGE_NAME); + private static final ComponentName WIDGET_1 = ComponentName.createRelative(PACKAGE_NAME, + ".widget1"); + private static final ComponentName WIDGET_2 = ComponentName.createRelative(PACKAGE_NAME, + ".widget2"); + private static final ComponentName WIDGET_3 = ComponentName.createRelative(PACKAGE_NAME, + ".widget3"); + private static final Map WIDGETS_TO_LABELS = new HashMap(); + + static { + WIDGETS_TO_LABELS.put(WIDGET_1, "Cat"); + WIDGETS_TO_LABELS.put(WIDGET_2, "Dog"); + WIDGETS_TO_LABELS.put(WIDGET_3, "Bird"); + } + + @Mock private IconCache mIconCache; + + private Context mContext; + private InvariantDeviceProfile mTestProfile; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + mTestProfile = new InvariantDeviceProfile(); + mTestProfile.numRows = 5; + mTestProfile.numColumns = 5; + + doAnswer(invocation -> { + ComponentWithLabel componentWithLabel = (ComponentWithLabel) invocation.getArgument(0); + return WIDGETS_TO_LABELS.get(componentWithLabel.getComponent()); + }).when(mIconCache).getTitleNoCache(any()); + } + + @Test + public void unsortedWidgets_diffLabels_shouldSortWidgetItems() { + // GIVEN a list of widgets in unsorted order. + // Cat 2x3 + WidgetItem widgetItem1 = createWidgetItem(WIDGET_1, /* spanX= */ 2, /* spanY= */ 3); + // Dog 2x3 + WidgetItem widgetItem2 = createWidgetItem(WIDGET_2, /* spanX= */ 2, /* spanY= */ 3); + // Bird 2x3 + WidgetItem widgetItem3 = createWidgetItem(WIDGET_3, /* spanX= */ 2, /* spanY= */ 3); + + // WHEN creates a WidgetsListRowEntry with the unsorted widgets. + WidgetsListContentEntry widgetsListRowEntry = new WidgetsListContentEntry(PACKAGE_ITEM_INFO, + /* titleSectionName= */ "T", + List.of(widgetItem1, widgetItem2, widgetItem3)); + + // THEN the widgets list is sorted by their labels alphabetically: [Bird, Cat, Dog]. + assertThat(widgetsListRowEntry.mWidgets) + .containsExactly(widgetItem3, widgetItem1, widgetItem2) + .inOrder(); + assertThat(widgetsListRowEntry.mTitleSectionName).isEqualTo("T"); + assertThat(widgetsListRowEntry.mPkgItem).isEqualTo(PACKAGE_ITEM_INFO); + } + + @Test + public void unsortedWidgets_sameLabels_differentSize_shouldSortWidgetItems() { + // GIVEN a list of widgets in unsorted order. + // Cat 3x3 + WidgetItem widgetItem1 = createWidgetItem(WIDGET_1, /* spanX= */ 3, /* spanY= */ 3); + // Cat 1x2 + WidgetItem widgetItem2 = createWidgetItem(WIDGET_1, /* spanX= */ 1, /* spanY= */ 2); + // Cat 2x2 + WidgetItem widgetItem3 = createWidgetItem(WIDGET_1, /* spanX= */ 2, /* spanY= */ 2); + + // WHEN creates a WidgetsListRowEntry with the unsorted widgets. + WidgetsListContentEntry widgetsListRowEntry = new WidgetsListContentEntry(PACKAGE_ITEM_INFO, + /* titleSectionName= */ "T", + List.of(widgetItem1, widgetItem2, widgetItem3)); + + // THEN the widgets list is sorted by their gird sizes in an ascending order: + // [1x2, 2x2, 3x3]. + assertThat(widgetsListRowEntry.mWidgets) + .containsExactly(widgetItem2, widgetItem3, widgetItem1) + .inOrder(); + assertThat(widgetsListRowEntry.mTitleSectionName).isEqualTo("T"); + assertThat(widgetsListRowEntry.mPkgItem).isEqualTo(PACKAGE_ITEM_INFO); + } + + @Test + public void unsortedWidgets_hodgepodge_shouldSortWidgetItems() { + // GIVEN a list of widgets in unsorted order. + // Cat 3x3 + WidgetItem widgetItem1 = createWidgetItem(WIDGET_1, /* spanX= */ 3, /* spanY= */ 3); + // Cat 1x2 + WidgetItem widgetItem2 = createWidgetItem(WIDGET_1, /* spanX= */ 1, /* spanY= */ 2); + // Dog 2x2 + WidgetItem widgetItem3 = createWidgetItem(WIDGET_2, /* spanX= */ 2, /* spanY= */ 2); + // Bird 2x2 + WidgetItem widgetItem4 = createWidgetItem(WIDGET_3, /* spanX= */ 2, /* spanY= */ 2); + + // WHEN creates a WidgetsListRowEntry with the unsorted widgets. + WidgetsListContentEntry widgetsListRowEntry = new WidgetsListContentEntry(PACKAGE_ITEM_INFO, + /* titleSectionName= */ "T", + List.of(widgetItem1, widgetItem2, widgetItem3, widgetItem4)); + + // THEN the widgets list is first sorted by labels alphabetically. Then, for widgets with + // same labels, they are sorted by their gird sizes in an ascending order: + // [Bird 2x2, Cat 1x2, Cat 3x3, Dog 2x2] + assertThat(widgetsListRowEntry.mWidgets) + .containsExactly(widgetItem4, widgetItem2, widgetItem1, widgetItem3) + .inOrder(); + assertThat(widgetsListRowEntry.mTitleSectionName).isEqualTo("T"); + assertThat(widgetsListRowEntry.mPkgItem).isEqualTo(PACKAGE_ITEM_INFO); + } + + private WidgetItem createWidgetItem(ComponentName componentName, int spanX, int spanY) { + String label = WIDGETS_TO_LABELS.get(componentName); + ShadowPackageManager packageManager = shadowOf(mContext.getPackageManager()); + AppWidgetProviderInfo widgetInfo = new AppWidgetProviderInfo(); + widgetInfo.provider = componentName; + ReflectionHelpers.setField(widgetInfo, "providerInfo", + packageManager.addReceiverIfNotPresent(componentName)); + + LauncherAppWidgetProviderInfo launcherAppWidgetProviderInfo = + LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo); + launcherAppWidgetProviderInfo.spanX = spanX; + launcherAppWidgetProviderInfo.spanY = spanY; + launcherAppWidgetProviderInfo.label = label; + + return new WidgetItem(launcherAppWidgetProviderInfo, mTestProfile, mIconCache); + } +} diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 344ae0a699..2df9cbed54 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -181,10 +181,10 @@ import com.android.launcher3.widget.PendingAddWidgetInfo; import com.android.launcher3.widget.PendingAppWidgetHostView; import com.android.launcher3.widget.WidgetAddFlowHandler; import com.android.launcher3.widget.WidgetHostViewLoader; -import com.android.launcher3.widget.WidgetListRowEntry; import com.android.launcher3.widget.WidgetManagerHelper; -import com.android.launcher3.widget.WidgetsFullSheet; import com.android.launcher3.widget.custom.CustomWidgetManager; +import com.android.launcher3.widget.model.WidgetsListBaseEntry; +import com.android.launcher3.widget.picker.WidgetsFullSheet; import com.android.systemui.plugins.OverlayPlugin; import com.android.systemui.plugins.PluginListener; import com.android.systemui.plugins.shared.LauncherExterns; @@ -2552,7 +2552,7 @@ public class Launcher extends StatefulActivity implements Launche } @Override - public void bindAllWidgets(final ArrayList allWidgets) { + public void bindAllWidgets(final List allWidgets) { mPopupDataProvider.setAllWidgets(allWidgets); } diff --git a/src/com/android/launcher3/model/BaseModelUpdateTask.java b/src/com/android/launcher3/model/BaseModelUpdateTask.java index d1e501788e..543ee2adf7 100644 --- a/src/com/android/launcher3/model/BaseModelUpdateTask.java +++ b/src/com/android/launcher3/model/BaseModelUpdateTask.java @@ -28,7 +28,7 @@ import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.ItemInfoMatcher; -import com.android.launcher3.widget.WidgetListRowEntry; +import com.android.launcher3.widget.model.WidgetsListBaseEntry; import java.util.ArrayList; import java.util.HashMap; @@ -123,7 +123,7 @@ public abstract class BaseModelUpdateTask implements ModelUpdateTask { } public void bindUpdatedWidgets(BgDataModel dataModel) { - final ArrayList widgets = + final ArrayList widgets = dataModel.widgetsModel.getWidgetsList(mApp.getContext()); scheduleCallbackTask(c -> c.bindAllWidgets(widgets)); } diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java index 2d860a41d3..1d7d1a2ba7 100644 --- a/src/com/android/launcher3/model/BgDataModel.java +++ b/src/com/android/launcher3/model/BgDataModel.java @@ -50,7 +50,7 @@ import com.android.launcher3.util.IntSet; import com.android.launcher3.util.IntSparseArrayMap; import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.util.ViewOnDrawExecutor; -import com.android.launcher3.widget.WidgetListRowEntry; +import com.android.launcher3.widget.model.WidgetsListBaseEntry; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -467,7 +467,7 @@ public class BgDataModel { void bindWidgetsRestored(ArrayList widgets); void bindRestoreItemsChange(HashSet updates); void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher); - void bindAllWidgets(ArrayList widgets); + void bindAllWidgets(List widgets); void onPageBoundSynchronously(int page); void executeOnNextDraw(ViewOnDrawExecutor executor); void bindDeepShortcutMap(HashMap deepShortcutMap); diff --git a/src/com/android/launcher3/popup/PopupDataProvider.java b/src/com/android/launcher3/popup/PopupDataProvider.java index 76048ba610..18536325eb 100644 --- a/src/com/android/launcher3/popup/PopupDataProvider.java +++ b/src/com/android/launcher3/popup/PopupDataProvider.java @@ -31,10 +31,10 @@ import com.android.launcher3.notification.NotificationListener; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.ShortcutUtil; -import com.android.launcher3.widget.WidgetListRowEntry; +import com.android.launcher3.widget.model.WidgetsListBaseEntry; +import com.android.launcher3.widget.model.WidgetsListContentEntry; import java.io.PrintWriter; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -59,7 +59,7 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan /** Maps packages to their DotInfo's . */ private Map mPackageUserToDotInfos = new HashMap<>(); /** Maps packages to their Widgets */ - private ArrayList mAllWidgets = new ArrayList<>(); + private List mAllWidgets = List.of(); private PopupDataChangeListener mChangeListener = PopupDataChangeListener.INSTANCE; @@ -187,7 +187,7 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan notificationListener.cancelNotificationFromLauncher(notificationKey); } - public void setAllWidgets(ArrayList allWidgets) { + public void setAllWidgets(List allWidgets) { mAllWidgets = allWidgets; mChangeListener.onWidgetsBound(); } @@ -196,14 +196,15 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan mChangeListener = listener == null ? PopupDataChangeListener.INSTANCE : listener; } - public ArrayList getAllWidgets() { + public List getAllWidgets() { return mAllWidgets; } public List getWidgetsForPackageUser(PackageUserKey packageUserKey) { return mAllWidgets.stream() - .filter(row -> row.pkgItem.packageName.equals(packageUserKey.mPackageName)) - .flatMap(row -> row.widgets.stream()) + .filter(row -> row instanceof WidgetsListContentEntry + && row.mPkgItem.packageName.equals(packageUserKey.mPackageName)) + .flatMap(row -> ((WidgetsListContentEntry) row).mWidgets.stream()) .filter(widget -> packageUserKey.mUser.equals(widget.user)) .collect(Collectors.toList()); } diff --git a/src/com/android/launcher3/recyclerview/ViewHolderBinder.java b/src/com/android/launcher3/recyclerview/ViewHolderBinder.java new file mode 100644 index 0000000000..4653774b39 --- /dev/null +++ b/src/com/android/launcher3/recyclerview/ViewHolderBinder.java @@ -0,0 +1,44 @@ +/* + * 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.recyclerview; + +import android.view.ViewGroup; + +import androidx.recyclerview.widget.RecyclerView.ViewHolder; + +/** + * Creates and populates views with data + * + * @param A data model which is used to populate the {@link ViewHolder}. + * @param A subclass of {@link ViewHolder} which holds references to views. + */ +public interface ViewHolderBinder { + /** + * Creates a new view, and attach it to the parent {@link ViewGroup}. Then, populates UI + * references in a {@link ViewHolder}. + */ + V newViewHolder(ViewGroup parent); + + /** Populate UI references in {@link ViewHolder} with data. */ + void bindViewHolder(V viewHolder, T data); + + /** + * Called when the view is recycled. Views are recycled in batches once they are sufficiently + * far off screen that it is unlikely the user will scroll back to them soon. Optionally + * override this to free expensive resources. + */ + default void unbindViewHolder(V viewHolder) {} +} diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java index 026634529f..0e8d4aea87 100644 --- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java +++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java @@ -46,7 +46,7 @@ import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.util.Themes; import com.android.launcher3.util.ViewOnDrawExecutor; import com.android.launcher3.views.BaseDragLayer; -import com.android.launcher3.widget.WidgetListRowEntry; +import com.android.launcher3.widget.model.WidgetsListBaseEntry; import java.util.ArrayList; import java.util.HashMap; @@ -232,7 +232,7 @@ public class SecondaryDisplayLauncher extends BaseDraggingActivity public void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher) { } @Override - public void bindAllWidgets(ArrayList widgets) { } + public void bindAllWidgets(List widgets) { } @Override public void onPageBoundSynchronously(int page) { } diff --git a/src/com/android/launcher3/testing/TestInformationHandler.java b/src/com/android/launcher3/testing/TestInformationHandler.java index d4a132e5ba..4261d080d9 100644 --- a/src/com/android/launcher3/testing/TestInformationHandler.java +++ b/src/com/android/launcher3/testing/TestInformationHandler.java @@ -33,7 +33,7 @@ import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherState; import com.android.launcher3.R; import com.android.launcher3.util.ResourceBasedOverride; -import com.android.launcher3.widget.WidgetsFullSheet; +import com.android.launcher3.widget.picker.WidgetsFullSheet; import java.util.concurrent.ExecutionException; import java.util.function.Function; diff --git a/src/com/android/launcher3/views/OptionsPopupView.java b/src/com/android/launcher3/views/OptionsPopupView.java index 80f0981af5..addaf9cb82 100644 --- a/src/com/android/launcher3/views/OptionsPopupView.java +++ b/src/com/android/launcher3/views/OptionsPopupView.java @@ -49,7 +49,7 @@ import com.android.launcher3.popup.ArrowPopup; import com.android.launcher3.shortcuts.DeepShortcutView; import com.android.launcher3.testing.TestLogging; import com.android.launcher3.testing.TestProtocol; -import com.android.launcher3.widget.WidgetsFullSheet; +import com.android.launcher3.widget.picker.WidgetsFullSheet; import java.util.ArrayList; import java.util.List; diff --git a/src/com/android/launcher3/widget/WidgetListRowEntry.java b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java similarity index 55% rename from src/com/android/launcher3/widget/WidgetListRowEntry.java rename to src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java index 17e4673c44..10ea7dbc5d 100644 --- a/src/com/android/launcher3/widget/WidgetListRowEntry.java +++ b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * 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. @@ -13,36 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.launcher3.widget; -import com.android.launcher3.model.WidgetItem; +package com.android.launcher3.widget.model; + import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.PackageItemInfo; -import java.util.ArrayList; - -/** - * Holder class to store all the information related to a single row in the widget list - */ -public class WidgetListRowEntry { - - public final PackageItemInfo pkgItem; - - public final ArrayList widgets; +/** Holder class to store the package information of an entry shown in the widgets list. */ +public abstract class WidgetsListBaseEntry { + public final PackageItemInfo mPkgItem; /** * Character that is used as a section name for the {@link ItemInfo#title}. * (e.g., "G" will be stored if title is "Google") */ - public String titleSectionName; + public final String mTitleSectionName; - public WidgetListRowEntry(PackageItemInfo pkgItem, ArrayList items) { - this.pkgItem = pkgItem; - this.widgets = items; - } - - @Override - public String toString() { - return pkgItem.packageName + ":" + widgets.size(); + public WidgetsListBaseEntry(PackageItemInfo pkgItem, String titleSectionName) { + mPkgItem = pkgItem; + mTitleSectionName = titleSectionName; } } diff --git a/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java b/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java new file mode 100644 index 0000000000..407f194cc3 --- /dev/null +++ b/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2016 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.model; + +import com.android.launcher3.model.WidgetItem; +import com.android.launcher3.model.data.PackageItemInfo; +import com.android.launcher3.widget.WidgetItemComparator; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Holder class to store all the information related to a list of widgets from the same app which is + * shown in the {@link com.android.launcher3.widget.picker.WidgetsFullSheet}. + */ +public final class WidgetsListContentEntry extends WidgetsListBaseEntry { + + public final List mWidgets; + + public WidgetsListContentEntry(PackageItemInfo pkgItem, String titleSectionName, + List items) { + super(pkgItem, titleSectionName); + this.mWidgets = + items.stream().sorted(new WidgetItemComparator()).collect(Collectors.toList()); + } + + @Override + public String toString() { + return mPkgItem.packageName + ":" + mWidgets.size(); + } +} diff --git a/src/com/android/launcher3/widget/WidgetsDiffReporter.java b/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java similarity index 65% rename from src/com/android/launcher3/widget/WidgetsDiffReporter.java rename to src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java index df6e2c3023..398d9ba448 100644 --- a/src/com/android/launcher3/widget/WidgetsDiffReporter.java +++ b/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.launcher3.widget; +package com.android.launcher3.widget.picker; import android.util.Log; @@ -22,7 +22,9 @@ import androidx.recyclerview.widget.RecyclerView; import com.android.launcher3.icons.IconCache; import com.android.launcher3.model.data.PackageItemInfo; -import com.android.launcher3.widget.WidgetsListAdapter.WidgetListRowEntryComparator; +import com.android.launcher3.widget.model.WidgetsListBaseEntry; +import com.android.launcher3.widget.model.WidgetsListContentEntry; +import com.android.launcher3.widget.picker.WidgetsListAdapter.WidgetListBaseRowEntryComparator; import java.util.ArrayList; import java.util.Iterator; @@ -43,8 +45,13 @@ public class WidgetsDiffReporter { mListener = listener; } - public void process(ArrayList currentEntries, - ArrayList newEntries, WidgetListRowEntryComparator comparator) { + /** + * Notifies the difference between {@code currentEntries} & {@code newEntries} by calling the + * relevant {@link androidx.recyclerview.widget.RecyclerView.RecyclerViewDataObserver} methods. + */ + public void process(ArrayList currentEntries, + ArrayList newEntries, + WidgetListBaseRowEntryComparator comparator) { if (DEBUG) { Log.d(TAG, "process oldEntries#=" + currentEntries.size() + " newEntries#=" + newEntries.size()); @@ -62,20 +69,20 @@ public class WidgetsDiffReporter { } return; } - ArrayList orgEntries = - (ArrayList) currentEntries.clone(); - Iterator orgIter = orgEntries.iterator(); - Iterator newIter = newEntries.iterator(); + ArrayList orgEntries = + (ArrayList) currentEntries.clone(); + Iterator orgIter = orgEntries.iterator(); + Iterator newIter = newEntries.iterator(); - WidgetListRowEntry orgRowEntry = orgIter.next(); - WidgetListRowEntry newRowEntry = newIter.next(); + WidgetsListBaseEntry orgRowEntry = orgIter.next(); + WidgetsListBaseEntry newRowEntry = newIter.next(); do { int diff = comparePackageName(orgRowEntry, newRowEntry, comparator); if (DEBUG) { Log.d(TAG, String.format("diff=%d orgRowEntry (%s) newRowEntry (%s)", - diff, orgRowEntry != null? orgRowEntry.toString() : null, - newRowEntry != null? newRowEntry.toString() : null)); + diff, orgRowEntry != null ? orgRowEntry.toString() : null, + newRowEntry != null ? newRowEntry.toString() : null)); } int index = -1; if (diff < 0) { @@ -83,17 +90,17 @@ public class WidgetsDiffReporter { mListener.notifyItemRemoved(index); if (DEBUG) { Log.d(TAG, String.format("notifyItemRemoved called (%d)%s", index, - orgRowEntry.titleSectionName)); + orgRowEntry.mTitleSectionName)); } currentEntries.remove(index); orgRowEntry = orgIter.hasNext() ? orgIter.next() : null; } else if (diff > 0) { - index = orgRowEntry != null? currentEntries.indexOf(orgRowEntry): - currentEntries.size(); + index = orgRowEntry != null ? currentEntries.indexOf(orgRowEntry) + : currentEntries.size(); currentEntries.add(index, newRowEntry); if (DEBUG) { Log.d(TAG, String.format("notifyItemInserted called (%d)%s", index, - newRowEntry.titleSectionName)); + newRowEntry.mTitleSectionName)); } newRowEntry = newIter.hasNext() ? newIter.next() : null; mListener.notifyItemInserted(index); @@ -102,14 +109,14 @@ public class WidgetsDiffReporter { // same package name but, // did the icon, title, etc, change? // or did the widget size and desc, span, etc change? - if (!isSamePackageItemInfo(orgRowEntry.pkgItem, newRowEntry.pkgItem) || - !orgRowEntry.widgets.equals(newRowEntry.widgets)) { + if (!isSamePackageItemInfo(orgRowEntry.mPkgItem, newRowEntry.mPkgItem) + || !areWidgetsEqual(orgRowEntry, newRowEntry)) { index = currentEntries.indexOf(orgRowEntry); currentEntries.set(index, newRowEntry); mListener.notifyItemChanged(index); if (DEBUG) { Log.d(TAG, String.format("notifyItemChanged called (%d)%s", index, - newRowEntry.titleSectionName)); + newRowEntry.mTitleSectionName)); } } orgRowEntry = orgIter.hasNext() ? orgIter.next() : null; @@ -122,10 +129,11 @@ public class WidgetsDiffReporter { * Compare package name using the same comparator as in {@link WidgetsListAdapter}. * Also handle null row pointers. */ - private int comparePackageName(WidgetListRowEntry curRow, WidgetListRowEntry newRow, - WidgetListRowEntryComparator comparator) { + private int comparePackageName(WidgetsListBaseEntry curRow, WidgetsListBaseEntry newRow, + WidgetListBaseRowEntryComparator comparator) { if (curRow == null && newRow == null) { - throw new IllegalStateException("Cannot compare PackageItemInfo if both rows are null."); + throw new IllegalStateException( + "Cannot compare PackageItemInfo if both rows are null."); } if (curRow == null && newRow != null) { @@ -136,6 +144,17 @@ public class WidgetsDiffReporter { return comparator.compare(curRow, newRow); } + private boolean areWidgetsEqual(WidgetsListBaseEntry curRow, + WidgetsListBaseEntry newRow) { + if (!(curRow instanceof WidgetsListContentEntry) + || !(newRow instanceof WidgetsListContentEntry)) { + return false; + } + WidgetsListContentEntry orgRowEntry = (WidgetsListContentEntry) curRow; + WidgetsListContentEntry newRowEntry = (WidgetsListContentEntry) newRow; + return orgRowEntry.mWidgets.equals(newRowEntry.mWidgets); + } + private boolean isSamePackageItemInfo(PackageItemInfo curInfo, PackageItemInfo newInfo) { return curInfo.bitmap.icon.equals(newInfo.bitmap.icon) && !mIconCache.isDefaultIcon(curInfo.bitmap, curInfo.user); diff --git a/src/com/android/launcher3/widget/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java similarity index 96% rename from src/com/android/launcher3/widget/WidgetsFullSheet.java rename to src/com/android/launcher3/widget/picker/WidgetsFullSheet.java index 4c8c339998..03623d5183 100644 --- a/src/com/android/launcher3/widget/WidgetsFullSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.launcher3.widget; +package com.android.launcher3.widget.picker; import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y; import static com.android.launcher3.testing.TestProtocol.NORMAL_STATE_ORDINAL; @@ -42,6 +42,7 @@ import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.compat.AccessibilityManagerCompat; import com.android.launcher3.views.RecyclerViewFastScroller; import com.android.launcher3.views.TopRoundedCornerView; +import com.android.launcher3.widget.BaseWidgetSheet; /** * Popup for showing the full list of available widgets @@ -218,8 +219,8 @@ public class WidgetsFullSheet extends BaseWidgetSheet if (ev.getAction() == MotionEvent.ACTION_DOWN) { mNoIntercept = false; RecyclerViewFastScroller scroller = mRecyclerView.getScrollbar(); - if (scroller.getThumbOffsetY() >= 0 && - getPopupContainer().isEventOverView(scroller, ev)) { + if (scroller.getThumbOffsetY() >= 0 + && getPopupContainer().isEventOverView(scroller, ev)) { mNoIntercept = true; } else if (getPopupContainer().isEventOverView(mContent, ev)) { mNoIntercept = !mRecyclerView.shouldContainerScroll(ev, getPopupContainer()); @@ -228,6 +229,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet return super.onControllerInterceptTouchEvent(ev); } + /** Shows the {@link WidgetsFullSheet} on the launcher. */ public static WidgetsFullSheet show(Launcher launcher, boolean animate) { WidgetsFullSheet sheet = (WidgetsFullSheet) launcher.getLayoutInflater() .inflate(R.layout.widgets_full_sheet, launcher.getDragLayer(), false); @@ -237,6 +239,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet return sheet; } + /** Gets the {@link WidgetsRecyclerView} which shows all widgets in {@link WidgetsFullSheet}. */ @VisibleForTesting public static WidgetsRecyclerView getWidgetsView(Launcher launcher) { return launcher.findViewById(R.id.widgets_list_view); diff --git a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java new file mode 100644 index 0000000000..9d308423e6 --- /dev/null +++ b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2015 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 android.content.Context; +import android.util.Log; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; +import android.view.ViewGroup; + +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerView.Adapter; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; + +import com.android.launcher3.R; +import com.android.launcher3.WidgetPreviewLoader; +import com.android.launcher3.icons.IconCache; +import com.android.launcher3.recyclerview.ViewHolderBinder; +import com.android.launcher3.util.LabelComparator; +import com.android.launcher3.widget.WidgetCell; +import com.android.launcher3.widget.model.WidgetsListBaseEntry; +import com.android.launcher3.widget.model.WidgetsListContentEntry; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * List view adapter for the widget tray. + * + *

Memory vs. Performance: + * The less number of types of views are inserted into a {@link RecyclerView}, the more recycling + * happens and less memory is consumed. + */ +public class WidgetsListAdapter extends Adapter { + + private static final String TAG = "WidgetsListAdapter"; + private static final boolean DEBUG = false; + + /** Uniquely identifies widgets list view type within the app. */ + private static final int VIEW_TYPE_WIDGETS_LIST = R.layout.widgets_list_row_view; + + private final WidgetsDiffReporter mDiffReporter; + private final SparseArray mViewHolderBinders = new SparseArray<>(); + private final WidgetsListRowViewHolderBinder mWidgetsListRowViewHolderBinder; + + private ArrayList mEntries = new ArrayList<>(); + + public WidgetsListAdapter(Context context, LayoutInflater layoutInflater, + WidgetPreviewLoader widgetPreviewLoader, IconCache iconCache, + OnClickListener iconClickListener, OnLongClickListener iconLongClickListener) { + mDiffReporter = new WidgetsDiffReporter(iconCache, this); + mWidgetsListRowViewHolderBinder = new WidgetsListRowViewHolderBinder(context, + layoutInflater, iconClickListener, iconLongClickListener, widgetPreviewLoader); + mViewHolderBinders.put(VIEW_TYPE_WIDGETS_LIST, mWidgetsListRowViewHolderBinder); + } + + /** + * Defers applying bitmap on all the {@link WidgetCell} in the {@param rv}. + * + * @see WidgetCell#setApplyBitmapDeferred(boolean) + */ + public void setApplyBitmapDeferred(boolean isDeferred, RecyclerView rv) { + mWidgetsListRowViewHolderBinder.setApplyBitmapDeferred(isDeferred); + + for (int i = rv.getChildCount() - 1; i >= 0; i--) { + ViewHolder viewHolder = rv.getChildViewHolder(rv.getChildAt(i)); + if (viewHolder.getItemViewType() == VIEW_TYPE_WIDGETS_LIST) { + WidgetsRowViewHolder holder = (WidgetsRowViewHolder) viewHolder; + for (int j = holder.cellContainer.getChildCount() - 1; j >= 0; j--) { + View v = holder.cellContainer.getChildAt(j); + if (v instanceof WidgetCell) { + ((WidgetCell) v).setApplyBitmapDeferred(isDeferred); + } + } + } + } + } + + @Override + public int getItemCount() { + return mEntries.size(); + } + + /** Gets the section name for {@link com.android.launcher3.views.RecyclerViewFastScroller}. */ + public String getSectionName(int pos) { + return mEntries.get(pos).mTitleSectionName; + } + + /** Updates the widget list. */ + public void setWidgets(List tempEntries) { + ArrayList newEntries = new ArrayList<>(tempEntries); + WidgetListBaseRowEntryComparator rowComparator = new WidgetListBaseRowEntryComparator(); + Collections.sort(newEntries, rowComparator); + mDiffReporter.process(mEntries, newEntries, rowComparator); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int pos) { + ViewHolderBinder viewHolderBinder = mViewHolderBinders.get(getItemViewType(pos)); + viewHolderBinder.bindViewHolder(holder, mEntries.get(pos)); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if (DEBUG) { + Log.v(TAG, "\nonCreateViewHolder"); + } + + return mViewHolderBinders.get(viewType).newViewHolder(parent); + } + + @Override + public void onViewRecycled(ViewHolder holder) { + mViewHolderBinders.get(holder.getItemViewType()).unbindViewHolder(holder); + } + + @Override + public boolean onFailedToRecycleView(ViewHolder holder) { + // If child views are animating, then the RecyclerView may choose not to recycle the view, + // causing extraneous onCreateViewHolder() calls. It is safe in this case to continue + // recycling this view, and take care in onViewRecycled() to cancel any existing + // animations. + return true; + } + + @Override + public long getItemId(int pos) { + return pos; + } + + @Override + public int getItemViewType(int pos) { + WidgetsListBaseEntry entry = mEntries.get(pos); + if (entry instanceof WidgetsListContentEntry) { + return VIEW_TYPE_WIDGETS_LIST; + } + throw new UnsupportedOperationException("ViewHolderBinder not found for " + entry); + } + + /** Comparator for sorting WidgetListRowEntry based on package title. */ + public static class WidgetListBaseRowEntryComparator implements + Comparator { + + private final LabelComparator mComparator = new LabelComparator(); + + @Override + public int compare(WidgetsListBaseEntry a, WidgetsListBaseEntry b) { + return mComparator.compare(a.mPkgItem.title.toString(), b.mPkgItem.title.toString()); + } + } +} diff --git a/src/com/android/launcher3/widget/WidgetsListAdapter.java b/src/com/android/launcher3/widget/picker/WidgetsListRowViewHolderBinder.java similarity index 50% rename from src/com/android/launcher3/widget/WidgetsListAdapter.java rename to src/com/android/launcher3/widget/picker/WidgetsListRowViewHolderBinder.java index 5bf96906be..22a8d0071c 100644 --- a/src/com/android/launcher3/widget/WidgetsListAdapter.java +++ b/src/com/android/launcher3/widget/picker/WidgetsListRowViewHolderBinder.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 The Android Open Source Project + * 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.launcher3.widget; +package com.android.launcher3.widget.picker; import android.content.Context; import android.util.Log; @@ -23,103 +23,76 @@ import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; import android.view.ViewGroup; -import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.RecyclerView.Adapter; - import com.android.launcher3.R; import com.android.launcher3.WidgetPreviewLoader; -import com.android.launcher3.icons.IconCache; import com.android.launcher3.model.WidgetItem; -import com.android.launcher3.util.LabelComparator; +import com.android.launcher3.recyclerview.ViewHolderBinder; +import com.android.launcher3.widget.WidgetCell; +import com.android.launcher3.widget.model.WidgetsListContentEntry; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; import java.util.List; /** - * List view adapter for the widget tray. - * - *

Memory vs. Performance: - * The less number of types of views are inserted into a {@link RecyclerView}, the more recycling - * happens and less memory is consumed. {@link #getItemViewType} was not overridden as there is - * only a single type of view. + * Binds data from {@link WidgetsListContentEntry} to UI elements in {@link WidgetsRowViewHolder}. */ -public class WidgetsListAdapter extends Adapter { - - private static final String TAG = "WidgetsListAdapter"; +public class WidgetsListRowViewHolderBinder + implements ViewHolderBinder { private static final boolean DEBUG = false; + private static final String TAG = "WidgetsListRowViewHolderBinder"; - private final WidgetPreviewLoader mWidgetPreviewLoader; private final LayoutInflater mLayoutInflater; - + private final int mIndent; private final OnClickListener mIconClickListener; private final OnLongClickListener mIconLongClickListener; - private final int mIndent; - private ArrayList mEntries = new ArrayList<>(); - private final WidgetsDiffReporter mDiffReporter; + private final WidgetPreviewLoader mWidgetPreviewLoader; + private boolean mApplyBitmapDeferred = false; - private boolean mApplyBitmapDeferred; - - public WidgetsListAdapter(Context context, LayoutInflater layoutInflater, - WidgetPreviewLoader widgetPreviewLoader, IconCache iconCache, - OnClickListener iconClickListener, OnLongClickListener iconLongClickListener) { + public WidgetsListRowViewHolderBinder( + Context context, + LayoutInflater layoutInflater, + OnClickListener iconClickListener, + OnLongClickListener iconLongClickListener, + WidgetPreviewLoader widgetPreviewLoader) { mLayoutInflater = layoutInflater; - mWidgetPreviewLoader = widgetPreviewLoader; + mIndent = context.getResources().getDimensionPixelSize(R.dimen.widget_section_indent); mIconClickListener = iconClickListener; mIconLongClickListener = iconLongClickListener; - mIndent = context.getResources().getDimensionPixelSize(R.dimen.widget_section_indent); - mDiffReporter = new WidgetsDiffReporter(iconCache, this); + mWidgetPreviewLoader = widgetPreviewLoader; } /** - * Defers applying bitmap on all the {@link WidgetCell} in the {@param rv} - * - * @see WidgetCell#setApplyBitmapDeferred(boolean) + * Defers applying bitmap on all the {@link WidgetCell} at + * {@link #bindViewHolder(WidgetsRowViewHolder, WidgetsListContentEntry)} if + * {@code applyBitmapDeferred} is {@code true}. */ - public void setApplyBitmapDeferred(boolean isDeferred, RecyclerView rv) { - mApplyBitmapDeferred = isDeferred; + public void setApplyBitmapDeferred(boolean applyBitmapDeferred) { + mApplyBitmapDeferred = applyBitmapDeferred; + } - for (int i = rv.getChildCount() - 1; i >= 0; i--) { - WidgetsRowViewHolder holder = (WidgetsRowViewHolder) - rv.getChildViewHolder(rv.getChildAt(i)); - for (int j = holder.cellContainer.getChildCount() - 1; j >= 0; j--) { - View v = holder.cellContainer.getChildAt(j); - if (v instanceof WidgetCell) { - ((WidgetCell) v).setApplyBitmapDeferred(mApplyBitmapDeferred); - } - } + @Override + public WidgetsRowViewHolder newViewHolder(ViewGroup parent) { + if (DEBUG) { + Log.v(TAG, "\nonCreateViewHolder"); } - } - /** - * Update the widget list. - */ - public void setWidgets(ArrayList tempEntries) { - WidgetListRowEntryComparator rowComparator = new WidgetListRowEntryComparator(); - Collections.sort(tempEntries, rowComparator); - mDiffReporter.process(mEntries, tempEntries, rowComparator); + ViewGroup container = (ViewGroup) mLayoutInflater.inflate( + R.layout.widgets_list_row_view, parent, false); + + // if the end padding is 0, then container view (horizontal scroll view) doesn't respect + // the end of the linear layout width + the start padding and doesn't allow scrolling. + container.findViewById(R.id.widgets_cell_list).setPaddingRelative(mIndent, 0, 1, 0); + + return new WidgetsRowViewHolder(container); } @Override - public int getItemCount() { - return mEntries.size(); - } - - public String getSectionName(int pos) { - return mEntries.get(pos).titleSectionName; - } - - @Override - public void onBindViewHolder(WidgetsRowViewHolder holder, int pos) { - WidgetListRowEntry entry = mEntries.get(pos); - List infoList = entry.widgets; + public void bindViewHolder(WidgetsRowViewHolder holder, WidgetsListContentEntry entry) { + List infoList = entry.mWidgets; ViewGroup row = holder.cellContainer; if (DEBUG) { - Log.d(TAG, String.format( - "onBindViewHolder [pos=%d, widget#=%d, row.getChildCount=%d]", - pos, infoList.size(), row.getChildCount())); + Log.d(TAG, String.format("onBindViewHolder [widget#=%d, row.getChildCount=%d]", + infoList.size(), row.getChildCount())); } // Add more views. @@ -150,7 +123,7 @@ public class WidgetsListAdapter extends Adapter { } // Bind the views in the application info section. - holder.title.applyFromItemInfoWithIcon(entry.pkgItem); + holder.title.applyFromItemInfoWithIcon(entry.mPkgItem); // Bind the view in the widget horizontal tray region. for (int i = 0; i < infoList.size(); i++) { @@ -167,53 +140,11 @@ public class WidgetsListAdapter extends Adapter { } @Override - public WidgetsRowViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - if (DEBUG) { - Log.v(TAG, "\nonCreateViewHolder"); - } - - ViewGroup container = (ViewGroup) mLayoutInflater.inflate( - R.layout.widgets_list_row_view, parent, false); - - // if the end padding is 0, then container view (horizontal scroll view) doesn't respect - // the end of the linear layout width + the start padding and doesn't allow scrolling. - container.findViewById(R.id.widgets_cell_list).setPaddingRelative(mIndent, 0, 1, 0); - - return new WidgetsRowViewHolder(container); - } - - @Override - public void onViewRecycled(WidgetsRowViewHolder holder) { + public void unbindViewHolder(WidgetsRowViewHolder holder) { int total = holder.cellContainer.getChildCount(); for (int i = 0; i < total; i += 2) { WidgetCell widget = (WidgetCell) holder.cellContainer.getChildAt(i); widget.clear(); } } - - public boolean onFailedToRecycleView(WidgetsRowViewHolder holder) { - // If child views are animating, then the RecyclerView may choose not to recycle the view, - // causing extraneous onCreateViewHolder() calls. It is safe in this case to continue - // recycling this view, and take care in onViewRecycled() to cancel any existing - // animations. - return true; - } - - @Override - public long getItemId(int pos) { - return pos; - } - - /** - * Comparator for sorting WidgetListRowEntry based on package title - */ - public static class WidgetListRowEntryComparator implements Comparator { - - private final LabelComparator mComparator = new LabelComparator(); - - @Override - public int compare(WidgetListRowEntry a, WidgetListRowEntry b) { - return mComparator.compare(a.pkgItem.title.toString(), b.pkgItem.title.toString()); - } - } } diff --git a/src/com/android/launcher3/widget/WidgetsRecyclerView.java b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java similarity index 97% rename from src/com/android/launcher3/widget/WidgetsRecyclerView.java rename to src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java index 69de12b9d5..52e94969c6 100644 --- a/src/com/android/launcher3/widget/WidgetsRecyclerView.java +++ b/src/com/android/launcher3/widget/picker/WidgetsRecyclerView.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.launcher3.widget; +package com.android.launcher3.widget.picker; import android.content.Context; import android.graphics.Point; @@ -22,13 +22,13 @@ import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; -import com.android.launcher3.BaseRecyclerView; -import com.android.launcher3.R; - import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener; +import com.android.launcher3.BaseRecyclerView; +import com.android.launcher3.R; + /** * The widgets recycler view. */ @@ -89,7 +89,7 @@ public class WidgetsRecyclerView extends BaseRecyclerView implements OnItemTouch LinearLayoutManager layoutManager = ((LinearLayoutManager) getLayoutManager()); layoutManager.scrollToPositionWithOffset(0, (int) -(availableScrollHeight * touchFraction)); - int posInt = (int) ((touchFraction == 1)? pos -1 : pos); + int posInt = (int) ((touchFraction == 1) ? pos - 1 : pos); return mAdapter.getSectionName(posInt); } @@ -171,4 +171,4 @@ public class WidgetsRecyclerView extends BaseRecyclerView implements OnItemTouch @Override public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { } -} \ No newline at end of file +} diff --git a/src/com/android/launcher3/widget/WidgetsRowViewHolder.java b/src/com/android/launcher3/widget/picker/WidgetsRowViewHolder.java similarity index 86% rename from src/com/android/launcher3/widget/WidgetsRowViewHolder.java rename to src/com/android/launcher3/widget/picker/WidgetsRowViewHolder.java index d26edb6134..9be079e826 100644 --- a/src/com/android/launcher3/widget/WidgetsRowViewHolder.java +++ b/src/com/android/launcher3/widget/picker/WidgetsRowViewHolder.java @@ -13,16 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.launcher3.widget; +package com.android.launcher3.widget.picker; import android.view.ViewGroup; +import androidx.recyclerview.widget.RecyclerView.ViewHolder; + import com.android.launcher3.BubbleTextView; import com.android.launcher3.R; -import androidx.recyclerview.widget.RecyclerView.ViewHolder; - -public class WidgetsRowViewHolder extends ViewHolder { +/** A {@link ViewHolder} for a row in the full widget picker. */ +public final class WidgetsRowViewHolder extends ViewHolder { public final ViewGroup cellContainer; public final BubbleTextView title; diff --git a/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java b/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java index 269af7bcc5..73b1601c34 100644 --- a/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java +++ b/src_shortcuts_overrides/com/android/launcher3/model/LoaderResults.java @@ -21,10 +21,10 @@ import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import com.android.launcher3.LauncherAppState; import com.android.launcher3.model.BgDataModel.Callbacks; import com.android.launcher3.util.ComponentKey; -import com.android.launcher3.widget.WidgetListRowEntry; +import com.android.launcher3.widget.model.WidgetsListBaseEntry; -import java.util.ArrayList; import java.util.HashMap; +import java.util.List; /** * Helper class to handle results of {@link com.android.launcher3.model.LoaderTask}. @@ -47,7 +47,7 @@ public class LoaderResults extends BaseLoaderResults { @Override public void bindWidgets() { - final ArrayList widgets = + final List widgets = mBgDataModel.widgetsModel.getWidgetsList(mApp.getContext()); executeCallbacksTask(c -> c.bindAllWidgets(widgets), mUiExecutor); } diff --git a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java index b4e45f835a..f27922b757 100644 --- a/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java +++ b/src_shortcuts_overrides/com/android/launcher3/model/WidgetsModel.java @@ -28,12 +28,12 @@ import com.android.launcher3.model.data.PackageItemInfo; import com.android.launcher3.pm.ShortcutConfigActivityInfo; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.Preconditions; -import com.android.launcher3.widget.WidgetItemComparator; -import com.android.launcher3.widget.WidgetListRowEntry; import com.android.launcher3.widget.WidgetManagerHelper; +import com.android.launcher3.widget.model.WidgetsListBaseEntry; +import com.android.launcher3.widget.model.WidgetsListContentEntry; +import com.android.launcher3.widget.picker.WidgetsDiffReporter; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -60,24 +60,23 @@ public class WidgetsModel { private final Map> mWidgetsList = new HashMap<>(); /** - * Returns a list of {@link WidgetListRowEntry}. All {@link WidgetItem} in a single row - * are sorted (based on label and user), but the overall list of {@link WidgetListRowEntry}s - * is not sorted. This list is sorted at the UI when using - * {@link com.android.launcher3.widget.WidgetsDiffReporter} + * Returns a list of {@link WidgetsListBaseEntry}. All {@link WidgetItem} in a single row + * are sorted (based on label and user), but the overall list of + * {@link WidgetsListBaseEntry}s is not sorted. This list is sorted at the UI when using + * {@link WidgetsDiffReporter} * - * @see com.android.launcher3.widget.WidgetsListAdapter#setWidgets(ArrayList) + * @see com.android.launcher3.widget.picker.WidgetsListAdapter#setWidgets(List) */ - public synchronized ArrayList getWidgetsList(Context context) { - ArrayList result = new ArrayList<>(); + public synchronized ArrayList getWidgetsList(Context context) { + ArrayList result = new ArrayList<>(); AlphabeticIndexCompat indexer = new AlphabeticIndexCompat(context); - WidgetItemComparator widgetComparator = new WidgetItemComparator(); for (Map.Entry> entry : mWidgetsList.entrySet()) { - WidgetListRowEntry row = new WidgetListRowEntry( - entry.getKey(), new ArrayList<>(entry.getValue())); - row.titleSectionName = (row.pkgItem.title == null) ? "" : - indexer.computeSectionName(row.pkgItem.title); - Collections.sort(row.widgets, widgetComparator); + PackageItemInfo pkgItem = entry.getKey(); + String sectionName = (pkgItem.title == null) ? "" : + indexer.computeSectionName(pkgItem.title); + WidgetsListContentEntry row = + new WidgetsListContentEntry(pkgItem, sectionName, entry.getValue()); result.add(row); } return result; diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java index 1648bddcd8..4c9a8e7c3d 100644 --- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java +++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java @@ -37,8 +37,8 @@ import com.android.launcher3.tapl.AppIconMenuItem; import com.android.launcher3.tapl.Widgets; import com.android.launcher3.tapl.Workspace; import com.android.launcher3.views.OptionsPopupView; -import com.android.launcher3.widget.WidgetsFullSheet; -import com.android.launcher3.widget.WidgetsRecyclerView; +import com.android.launcher3.widget.picker.WidgetsFullSheet; +import com.android.launcher3.widget.picker.WidgetsRecyclerView; import org.junit.Before; import org.junit.Ignore;