Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2f5648a9 authored by Steven Ng's avatar Steven Ng
Browse files

Refactoring before adding a new view type in the WidgetsListAdapter

Changes made:
1. Model: added an abstract class for storing common information for
   entries shown in the full page widgets picker.
2. Introduced a ViewHolderBinder interface to split the logic of binding
   data to ViewHolder into separate classes.
3. Move the view holder binding of WidgetsListRow from WidgetListAdapter
   to its new class.
4. Move some widgets picker classes into a new picker package.

Test: Auto: Run WidgetsListAdapterTest, WidgetsListRowEntryTest and
      WidgetsListRowViewHolderBinderTest.
      Manual: open the all apps widgets tray and navigate the list.

Bug: 179797520
Change-Id: Iab29557842bb79156cad84d00a4c5d0db0c5aa06
parent 823c5f8b
Loading
Loading
Loading
Loading
+8 −8
Original line number Diff line number Diff line
@@ -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<WidgetListRowEntry> EMPTY_WIDGET_LIST = new ArrayList<>();
    private static final ArrayList<WidgetsListBaseEntry> 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<WidgetListRowEntry> getWidgetsList(Context context) {
    public synchronized ArrayList<WidgetsListBaseEntry> getWidgetsList(Context context) {
        return EMPTY_WIDGET_LIST;
    }

+3 −3
Original line number Diff line number Diff line
@@ -13,7 +13,7 @@
     See the License for the specific language governing permissions and
     limitations under the License.
-->
<com.android.launcher3.widget.WidgetsFullSheet
<com.android.launcher3.widget.picker.WidgetsFullSheet
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
@@ -27,7 +27,7 @@
        android:background="?android:attr/colorPrimary"
        android:elevation="4dp">

        <com.android.launcher3.widget.WidgetsRecyclerView
        <com.android.launcher3.widget.picker.WidgetsRecyclerView
            android:id="@+id/widgets_list_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
@@ -49,4 +49,4 @@
            android:layout_alignParentTop="true"
            android:layout_marginEnd="@dimen/fastscroll_end_margin" />
    </com.android.launcher3.views.TopRoundedCornerView>
</com.android.launcher3.widget.WidgetsFullSheet>
 No newline at end of file
</com.android.launcher3.widget.picker.WidgetsFullSheet>
 No newline at end of file
+41 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -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;
+215 −0
Original line number Diff line number Diff line
@@ -13,11 +13,12 @@
 * 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 org.mockito.ArgumentMatchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isNull;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
import static org.robolectric.Shadows.shadowOf;

@@ -33,9 +34,12 @@ 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;
@@ -48,10 +52,13 @@ import org.robolectric.shadows.ShadowPackageManager;
import org.robolectric.util.ReflectionHelpers;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@RunWith(RobolectricTestRunner.class)
public class WidgetsListAdapterTest {
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;
@@ -72,80 +79,137 @@ public class WidgetsListAdapterTest {
        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 test_notifyDataSetChanged() throws Exception {
    public void setWidgets_shouldNotifyDataSetChanged() {
        mAdapter.setWidgets(generateSampleMap(1));
        verify(mListener, times(1)).onChanged();

        verify(mListener).onChanged();
    }

    @Test
    public void test_notifyItemInserted() throws Exception {
    public void setWidgets_withItemInserted_shouldNotifyItemInserted() {
        mAdapter.setWidgets(generateSampleMap(1));
        mAdapter.setWidgets(generateSampleMap(2));
        verify(mListener, times(1)).onChanged();
        verify(mListener, times(1)).onItemRangeInserted(eq(1), eq(1));

        verify(mListener).onItemRangeInserted(eq(1), eq(1));
    }

    @Test
    public void test_notifyItemRemoved() throws Exception {
    public void setWidgets_withItemRemoved_shouldNotifyItemRemoved() {
        mAdapter.setWidgets(generateSampleMap(2));
        mAdapter.setWidgets(generateSampleMap(1));
        verify(mListener, times(1)).onChanged();
        verify(mListener, times(1)).onItemRangeRemoved(eq(1), eq(1));

        verify(mListener).onItemRangeRemoved(eq(1), eq(1));
    }

    @Test
    public void testNotifyItemChanged_PackageIconDiff() throws Exception {
    public void setWidgets_appIconChanged_shouldNotifyItemChanged() {
        mAdapter.setWidgets(generateSampleMap(1));
        mAdapter.setWidgets(generateSampleMap(1));
        verify(mListener, times(1)).onChanged();
        verify(mListener, times(1)).onItemRangeChanged(eq(0), eq(1), isNull());

        verify(mListener).onItemRangeChanged(eq(0), eq(1), isNull());
    }

    @Test
    public void testNotifyItemChanged_widgetItemInfoDiff() throws Exception {
        // TODO: same package name but item number changed
    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 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]
    public void setWidgets_hodgepodge_shouldInvokeExpectedDataObserverCallbacks() {
        List<WidgetsListBaseEntry> allAppsWithWidgets = generateSampleMap(5);
        // GIVEN the current widgets list consist of [A, B, E].
        List<WidgetsListBaseEntry> 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<WidgetsListBaseEntry> 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<WidgetListRowEntry> generateSampleMap(int num) {
        ArrayList<WidgetListRowEntry> result = new ArrayList<>();
    private ArrayList<WidgetsListBaseEntry> generateSampleMap(int num) {
        ArrayList<WidgetsListBaseEntry> 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));
            String packageName = "com.placeholder.apk" + i;

            WidgetItem wi = new WidgetItem(LauncherAppWidgetProviderInfo
                    .fromProviderInfo(mContext, widgetInfo), mTestProfile, mIconCache);
            List<WidgetItem> widgetItems = generateWidgetItems(packageName, /* numOfWidgets= */ 1);

            PackageItemInfo pInfo = new PackageItemInfo(wi.componentName.getPackageName());
            PackageItemInfo pInfo = new PackageItemInfo(packageName);
            pInfo.title = pInfo.packageName;
            pInfo.user = wi.user;
            pInfo.user = widgetItems.get(0).user;
            pInfo.bitmap = BitmapInfo.of(Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8), 0);

            result.add(new WidgetListRowEntry(pInfo, new ArrayList<>(Collections.singleton(wi))));
            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<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));

            widgetItems.add(new WidgetItem(
                    LauncherAppWidgetProviderInfo.fromProviderInfo(mContext, widgetInfo),
                    mTestProfile, mIconCache));
        }
        return widgetItems;
    }
}
Loading