diff --git a/res/layout/widgets_full_sheet.xml b/res/layout/widgets_full_sheet.xml index 6c18d7ac75..226c4f713a 100644 --- a/res/layout/widgets_full_sheet.xml +++ b/res/layout/widgets_full_sheet.xml @@ -51,5 +51,13 @@ android:layout_alignParentEnd="true" android:layout_alignParentTop="true" android:layout_marginEnd="@dimen/fastscroll_end_margin" /> + + + \ No newline at end of file diff --git a/res/layout/widgets_list_row_header.xml b/res/layout/widgets_list_row_header.xml index 041e0073fd..21920a29f3 100644 --- a/res/layout/widgets_list_row_header.xml +++ b/res/layout/widgets_list_row_header.xml @@ -52,6 +52,8 @@ android:id="@+id/app_subtitle" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:ellipsize="end" + android:maxLines="1" tools:text="m widgets, n shortcuts" /> diff --git a/res/values/id.xml b/res/values/id.xml new file mode 100644 index 0000000000..39c49bd5e1 --- /dev/null +++ b/res/values/id.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java index b972c6ff0e..cc36f630c9 100644 --- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java +++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsDiffReporterTest.java @@ -221,6 +221,27 @@ public final class WidgetsDiffReporterTest { assertThat(currentList).containsExactlyElementsIn(newList); } + @Test + public void headersContentsMix_headerWidgetsModified_shouldInvokeCorrectCallbacks() { + // GIVEN the current list has app headers [A, B, E content]. + ArrayList currentList = new ArrayList<>( + List.of(mHeaderA, mHeaderB, mContentE)); + // GIVEN the new list has one of the headers widgets list modified. + List newList = List.of( + new WidgetsListHeaderEntry( + mHeaderA.mPkgItem, mHeaderA.mTitleSectionName, + mHeaderA.mWidgets.subList(0, 1)), + mHeaderB, mContentE); + + // WHEN computing the list difference. + mWidgetsDiffReporter.process(currentList, newList, COMPARATOR); + + // THEN notify "A" has been changed. + verify(mAdapter).notifyItemChanged(/* position= */ 0); + // THEN the current list contains all elements from the new list. + assertThat(currentList).containsExactlyElementsIn(newList); + } + private WidgetsListHeaderEntry createWidgetsHeaderEntry(String packageName, String appName, int numOfWidgets) { diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java index a7c8d92f79..e1214ff39f 100644 --- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java +++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListAdapterTest.java @@ -26,6 +26,8 @@ import android.appwidget.AppWidgetProviderInfo; import android.content.ComponentName; import android.content.Context; import android.graphics.Bitmap; +import android.os.Process; +import android.os.UserHandle; import android.view.LayoutInflater; import androidx.recyclerview.widget.RecyclerView; @@ -37,6 +39,7 @@ 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.util.PackageUserKey; import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; import com.android.launcher3.widget.model.WidgetsListBaseEntry; import com.android.launcher3.widget.model.WidgetsListContentEntry; @@ -67,6 +70,7 @@ public final class WidgetsListAdapterTest { private WidgetsListAdapter mAdapter; private InvariantDeviceProfile mTestProfile; + private UserHandle mUserHandle; private Context mContext; @Before @@ -76,6 +80,7 @@ public final class WidgetsListAdapterTest { mTestProfile = new InvariantDeviceProfile(); mTestProfile.numRows = 5; mTestProfile.numColumns = 5; + mUserHandle = Process.myUserHandle(); mAdapter = new WidgetsListAdapter(mContext, mMockLayoutInflater, mMockWidgetCache, mIconCache, null, null); mAdapter.registerAdapterDataObserver(mListener); @@ -126,7 +131,8 @@ public final class WidgetsListAdapterTest { mAdapter.setWidgets(generateSampleMap(3)); // WHEN com.google.test.1 header is expanded. - mAdapter.onHeaderClicked(/* isExpanded= */ true, TEST_PACKAGE_PLACEHOLDER + 1); + mAdapter.onHeaderClicked(/* showWidgets= */ true, + new PackageUserKey(TEST_PACKAGE_PLACEHOLDER + 1, mUserHandle)); // THEN the visible entries list becomes: // [com.google.test0, com.google.test1, com.google.test1 content, com.google.test2] @@ -143,7 +149,8 @@ public final class WidgetsListAdapterTest { // GIVEN test com.google.test1 is expanded. // Visible entries in the adapter are: // [com.google.test0, com.google.test1, com.google.test1 content] - mAdapter.onHeaderClicked(/* isExpanded= */ true, TEST_PACKAGE_PLACEHOLDER + 1); + mAdapter.onHeaderClicked(/* showWidgets= */ true, + new PackageUserKey(TEST_PACKAGE_PLACEHOLDER + 1, mUserHandle)); Mockito.reset(mListener); // WHEN the adapter is updated with the same list of apps but com.google.test1 has 2 widgets @@ -200,6 +207,30 @@ public final class WidgetsListAdapterTest { verify(mListener).onItemRangeRemoved(/* positionStart= */ 3, /* itemCount= */ 1); } + @Test + public void setWidgetsOnSearch_expandedApp_shouldResetExpandedApp() { + // GIVEN a list of widgets entries: + // [com.google.test0, com.google.test0 content, + // com.google.test1, com.google.test1 content, + // com.google.test2, com.google.test2 content] + // The visible widgets entries: [com.google.test0, com.google.test1, com.google.test2]. + ArrayList allEntries = generateSampleMap(2); + mAdapter.setWidgetsOnSearch(allEntries); + // GIVEN com.google.test.1 header is expanded. The visible entries list becomes: + // [com.google.test0, com.google.test1, com.google.test1 content, com.google.test2] + mAdapter.onHeaderClicked(/* showWidgets= */ true, + new PackageUserKey(TEST_PACKAGE_PLACEHOLDER + 1, mUserHandle)); + Mockito.reset(mListener); + + // WHEN same widget entries are set again. + mAdapter.setWidgetsOnSearch(allEntries); + + // THEN expanded app is reset and the visible entries list becomes: + // [com.google.test0, com.google.test1, com.google.test2] + verify(mListener).onItemRangeChanged(eq(1), eq(1), isNull()); + verify(mListener).onItemRangeRemoved(/* positionStart= */ 2, /* itemCount= */ 1); + } + /** * Generates a list of sample widget entries. * diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java index 848630ef5a..e8c11da2e7 100644 --- a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java +++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinderTest.java @@ -18,7 +18,9 @@ package com.android.launcher3.widget.picker; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.verify; import static org.robolectric.Shadows.shadowOf; import android.appwidget.AppWidgetProviderInfo; @@ -26,12 +28,9 @@ import android.content.ComponentName; import android.content.Context; import android.graphics.Bitmap; import android.view.LayoutInflater; -import android.view.View; import android.widget.FrameLayout; import android.widget.TextView; -import androidx.annotation.Nullable; - import com.android.launcher3.DeviceProfile; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.R; @@ -41,10 +40,9 @@ 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.util.PackageUserKey; import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; -import com.android.launcher3.widget.WidgetCell; import com.android.launcher3.widget.model.WidgetsListHeaderEntry; -import com.android.launcher3.widget.picker.WidgetsListHeaderViewHolderBinder.OnHeaderClickListener; import org.junit.After; import org.junit.Before; @@ -74,12 +72,13 @@ public final class WidgetsListHeaderViewHolderBinderTest { // testing. private ActivityController mActivityController; private TestActivity mTestActivity; - private FakeOnHeaderClickListener mFakeOnHeaderClickListener = new FakeOnHeaderClickListener(); @Mock private IconCache mIconCache; @Mock private DeviceProfile mDeviceProfile; + @Mock + private OnHeaderClickListener mOnHeaderClickListener; @Before public void setUp() { @@ -99,8 +98,7 @@ public final class WidgetsListHeaderViewHolderBinderTest { }).when(mIconCache).getTitleNoCache(any()); mViewHolderBinder = new WidgetsListHeaderViewHolderBinder( - LayoutInflater.from(mTestActivity), - mFakeOnHeaderClickListener); + LayoutInflater.from(mTestActivity), mOnHeaderClickListener); } @After @@ -125,6 +123,23 @@ public final class WidgetsListHeaderViewHolderBinderTest { assertThat(appSubtitle.getText()).isEqualTo("3 widgets"); } + @Test + public void bindViewHolder_shouldAttachOnHeaderClickListener() { + WidgetsListHeaderHolder viewHolder = mViewHolderBinder.newViewHolder( + new FrameLayout(mTestActivity)); + WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader; + WidgetsListHeaderEntry entry = generateSampleAppHeader( + APP_NAME, + TEST_PACKAGE, + /* numOfWidgets= */ 3); + + mViewHolderBinder.bindViewHolder(viewHolder, entry); + widgetsListHeader.callOnClick(); + + verify(mOnHeaderClickListener).onHeaderClicked(eq(true), + eq(new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user))); + } + private WidgetsListHeaderEntry generateSampleAppHeader(String appName, String packageName, int numOfWidgets) { PackageItemInfo appInfo = new PackageItemInfo(packageName); @@ -152,22 +167,4 @@ public final class WidgetsListHeaderViewHolderBinderTest { } 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); - } - - private final class FakeOnHeaderClickListener implements OnHeaderClickListener { - - boolean mShowWidgets = false; - @Nullable String mHeaderClickedPackage = null; - - @Override - public void onHeaderClicked(boolean showWidgets, String packageName) { - mShowWidgets = showWidgets; - mHeaderClickedPackage = packageName; - } - } } diff --git a/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinderTest.java b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinderTest.java new file mode 100644 index 0000000000..07fbfd2a3c --- /dev/null +++ b/robolectric_tests/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinderTest.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.widget.picker; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +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 android.widget.FrameLayout; +import android.widget.TextView; + +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.InvariantDeviceProfile; +import com.android.launcher3.R; +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.util.PackageUserKey; +import com.android.launcher3.widget.LauncherAppWidgetProviderInfo; +import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry; + +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 WidgetsListSearchHeaderViewHolderBinderTest { + private static final String TEST_PACKAGE = "com.google.test"; + private static final String APP_NAME = "Test app"; + + private Context mContext; + private WidgetsListSearchHeaderViewHolderBinder mViewHolderBinder; + private InvariantDeviceProfile mTestProfile; + // Replace ActivityController with ActivityScenario, which is the recommended way for activity + // testing. + private ActivityController mActivityController; + private TestActivity mTestActivity; + + @Mock + private IconCache mIconCache; + @Mock + private DeviceProfile mDeviceProfile; + @Mock + private OnHeaderClickListener mOnHeaderClickListener; + + @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 WidgetsListSearchHeaderViewHolderBinder( + LayoutInflater.from(mTestActivity), mOnHeaderClickListener); + } + + @After + public void tearDown() { + mActivityController.destroy(); + } + + @Test + public void bindViewHolder_appWith3Widgets_shouldShowTheCorrectAppNameAndSubtitle() { + WidgetsListSearchHeaderHolder viewHolder = mViewHolderBinder.newViewHolder( + new FrameLayout(mTestActivity)); + WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader; + WidgetsListSearchHeaderEntry entry = generateSampleSearchHeader( + APP_NAME, + TEST_PACKAGE, + /* numOfWidgets= */ 3); + mViewHolderBinder.bindViewHolder(viewHolder, entry); + + TextView appTitle = widgetsListHeader.findViewById(R.id.app_title); + TextView appSubtitle = widgetsListHeader.findViewById(R.id.app_subtitle); + assertThat(appTitle.getText()).isEqualTo(APP_NAME); + assertThat(appSubtitle.getText()) + .isEqualTo(".SampleWidget0, .SampleWidget1, .SampleWidget2"); + } + + @Test + public void bindViewHolder_shouldAttachOnHeaderClickListener() { + WidgetsListSearchHeaderHolder viewHolder = mViewHolderBinder.newViewHolder( + new FrameLayout(mTestActivity)); + WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader; + WidgetsListSearchHeaderEntry entry = generateSampleSearchHeader( + APP_NAME, + TEST_PACKAGE, + /* numOfWidgets= */ 3); + + mViewHolderBinder.bindViewHolder(viewHolder, entry); + widgetsListHeader.callOnClick(); + + verify(mOnHeaderClickListener).onHeaderClicked(eq(true), + eq(new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user))); + } + + private WidgetsListSearchHeaderEntry generateSampleSearchHeader(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 WidgetsListSearchHeaderEntry(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/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java index 09517e1cfb..d09fd49e6e 100644 --- a/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java +++ b/src/com/android/launcher3/widget/model/WidgetsListBaseEntry.java @@ -51,10 +51,11 @@ public abstract class WidgetsListBaseEntry { public abstract int getRank(); @Retention(SOURCE) - @IntDef({RANK_WIDGETS_LIST_HEADER, RANK_WIDGETS_LIST_CONTENT}) + @IntDef({RANK_WIDGETS_LIST_HEADER, RANK_WIDGETS_LIST_SEARCH_HEADER, RANK_WIDGETS_LIST_CONTENT}) public @interface Rank { } public static final int RANK_WIDGETS_LIST_HEADER = 1; - public static final int RANK_WIDGETS_LIST_CONTENT = 2; + public static final int RANK_WIDGETS_LIST_SEARCH_HEADER = 2; + public static final int RANK_WIDGETS_LIST_CONTENT = 3; } diff --git a/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java b/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java index b0cb8c7455..afc0f443bd 100644 --- a/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java +++ b/src/com/android/launcher3/widget/model/WidgetsListContentEntry.java @@ -39,7 +39,7 @@ public final class WidgetsListContentEntry extends WidgetsListBaseEntry { @Override public String toString() { - return mPkgItem.packageName + ":" + mWidgets.size(); + return "Content:" + mPkgItem.packageName + ":" + mWidgets.size(); } @Override diff --git a/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java b/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java index 6899647764..309b678409 100644 --- a/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java +++ b/src/com/android/launcher3/widget/model/WidgetsListHeaderEntry.java @@ -17,21 +17,25 @@ 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.Collection; +import java.util.List; +import java.util.stream.Collectors; /** An information holder for an app which has widgets or/and shortcuts. */ public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry { public final int widgetsCount; public final int shortcutsCount; + public final List mWidgets; private boolean mIsWidgetListShown = false; private boolean mHasEntryUpdated = false; public WidgetsListHeaderEntry(PackageItemInfo pkgItem, String titleSectionName, - Collection items) { + List items) { super(pkgItem, titleSectionName); + mWidgets = items.stream().sorted(new WidgetItemComparator()).collect(Collectors.toList()); widgetsCount = (int) items.stream().filter(item -> item.widgetInfo != null).count(); shortcutsCount = Math.max(0, items.size() - widgetsCount); } @@ -56,9 +60,22 @@ public final class WidgetsListHeaderEntry extends WidgetsListBaseEntry { return mHasEntryUpdated; } + @Override + public String toString() { + return "Header:" + mPkgItem.packageName + ":" + mWidgets.size(); + } + @Override @Rank public int getRank() { return RANK_WIDGETS_LIST_HEADER; } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof WidgetsListHeaderEntry)) return false; + WidgetsListHeaderEntry otherEntry = (WidgetsListHeaderEntry) obj; + return mWidgets.equals(otherEntry.mWidgets) && mPkgItem.equals(otherEntry.mPkgItem) + && mTitleSectionName.equals(otherEntry.mTitleSectionName); + } } diff --git a/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java b/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java new file mode 100644 index 0000000000..a8b887bb9e --- /dev/null +++ b/src/com/android/launcher3/widget/model/WidgetsListSearchHeaderEntry.java @@ -0,0 +1,77 @@ +/* + * 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.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; + +/** An information holder for an app which has widgets or/and shortcuts, to be shown in search. */ +public final class WidgetsListSearchHeaderEntry extends WidgetsListBaseEntry { + + public final List mWidgets; + + private boolean mIsWidgetListShown = false; + private boolean mHasEntryUpdated = false; + + public WidgetsListSearchHeaderEntry(PackageItemInfo pkgItem, String titleSectionName, + List items) { + super(pkgItem, titleSectionName); + mWidgets = items.stream().sorted(new WidgetItemComparator()).collect(Collectors.toList()); + } + + /** Sets if the widgets list associated with this header is shown. */ + public void setIsWidgetListShown(boolean isWidgetListShown) { + if (mIsWidgetListShown != isWidgetListShown) { + this.mIsWidgetListShown = isWidgetListShown; + mHasEntryUpdated = true; + } else { + mHasEntryUpdated = false; + } + } + + /** Returns {@code true} if the widgets list associated with this header is shown. */ + public boolean isWidgetListShown() { + return mIsWidgetListShown; + } + + /** Returns {@code true} if this entry has been updated due to user interactions. */ + public boolean hasEntryUpdated() { + return mHasEntryUpdated; + } + + @Override + public String toString() { + return "SearchHeader:" + mPkgItem.packageName + ":" + mWidgets.size(); + } + + @Override + @Rank + public int getRank() { + return RANK_WIDGETS_LIST_SEARCH_HEADER; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof WidgetsListSearchHeaderEntry)) return false; + WidgetsListSearchHeaderEntry otherEntry = (WidgetsListSearchHeaderEntry) obj; + return mWidgets.equals(otherEntry.mWidgets) && mPkgItem.equals(otherEntry.mPkgItem) + && mTitleSectionName.equals(otherEntry.mTitleSectionName); + } +} diff --git a/src/com/android/launcher3/widget/picker/OnHeaderClickListener.java b/src/com/android/launcher3/widget/picker/OnHeaderClickListener.java new file mode 100644 index 0000000000..73727515c2 --- /dev/null +++ b/src/com/android/launcher3/widget/picker/OnHeaderClickListener.java @@ -0,0 +1,28 @@ +/* + * 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 com.android.launcher3.util.PackageUserKey; + +/** + * A listener to be invoked when a header is clicked. + */ +public interface OnHeaderClickListener { + /** + * Calls when a header is clicked to show / hide widgets for a package. + */ + void onHeaderClicked(boolean showWidgets, PackageUserKey packageUserKey); +} diff --git a/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java b/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java index 95fa05f19e..7eb5b834fd 100644 --- a/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java +++ b/src/com/android/launcher3/widget/picker/SearchAndRecommendationsScrollController.java @@ -34,6 +34,7 @@ final class SearchAndRecommendationsScrollController implements private final boolean mHasWorkProfile; private final SearchAndRecommendationViewHolder mViewHolder; private final WidgetsRecyclerView mPrimaryRecyclerView; + private final WidgetsRecyclerView mSearchRecyclerView; // The following are only non null if mHasWorkProfile is true. @Nullable private final WidgetsRecyclerView mWorkRecyclerView; @@ -48,12 +49,14 @@ final class SearchAndRecommendationsScrollController implements SearchAndRecommendationViewHolder viewHolder, WidgetsRecyclerView primaryRecyclerView, @Nullable WidgetsRecyclerView workRecyclerView, + WidgetsRecyclerView searchRecyclerView, @Nullable View personalWorkTabsView, @Nullable PersonalWorkPagedView primaryWorkViewPager) { mHasWorkProfile = hasWorkProfile; mViewHolder = viewHolder; mPrimaryRecyclerView = primaryRecyclerView; mWorkRecyclerView = workRecyclerView; + mSearchRecyclerView = searchRecyclerView; mPrimaryWorkTabsView = personalWorkTabsView; mPrimaryWorkViewPager = primaryWorkViewPager; mCurrentRecyclerView = mPrimaryRecyclerView; @@ -149,6 +152,11 @@ final class SearchAndRecommendationsScrollController implements mPrimaryRecyclerView.getPaddingRight(), mPrimaryRecyclerView.getPaddingBottom()); } + mSearchRecyclerView.setPadding( + mSearchRecyclerView.getPaddingLeft(), + topContainerHeight, + mSearchRecyclerView.getPaddingRight(), + mSearchRecyclerView.getPaddingBottom()); } /** diff --git a/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java b/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java index dbd1bdf523..2366609b3f 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java +++ b/src/com/android/launcher3/widget/picker/WidgetsDiffReporter.java @@ -25,6 +25,7 @@ import com.android.launcher3.model.data.PackageItemInfo; import com.android.launcher3.widget.model.WidgetsListBaseEntry; import com.android.launcher3.widget.model.WidgetsListContentEntry; import com.android.launcher3.widget.model.WidgetsListHeaderEntry; +import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry; import com.android.launcher3.widget.picker.WidgetsListAdapter.WidgetListBaseRowEntryComparator; import java.util.ArrayList; @@ -113,7 +114,7 @@ public class WidgetsDiffReporter { // or did the header view changed due to user interactions? // or did the widget size and desc, span, etc change? if (!isSamePackageItemInfo(orgRowEntry.mPkgItem, newRowEntry.mPkgItem) - || hasHeaderUpdated(newRowEntry) + || hasHeaderUpdated(orgRowEntry, newRowEntry) || hasWidgetsListChanged(orgRowEntry, newRowEntry)) { index = currentEntries.indexOf(orgRowEntry); currentEntries.set(index, newRowEntry); @@ -174,12 +175,16 @@ public class WidgetsDiffReporter { * Returns {@code true} if {@code newRow} is {@link WidgetsListHeaderEntry} and its content has * been changed due to user interactions. */ - private boolean hasHeaderUpdated(WidgetsListBaseEntry newRow) { - if (!(newRow instanceof WidgetsListHeaderEntry)) { - return false; + private boolean hasHeaderUpdated(WidgetsListBaseEntry curRow, WidgetsListBaseEntry newRow) { + if (newRow instanceof WidgetsListHeaderEntry && curRow instanceof WidgetsListHeaderEntry) { + return ((WidgetsListHeaderEntry) newRow).hasEntryUpdated() || !curRow.equals(newRow); } - WidgetsListHeaderEntry newRowEntry = (WidgetsListHeaderEntry) newRow; - return newRowEntry.hasEntryUpdated(); + if (newRow instanceof WidgetsListSearchHeaderEntry + && curRow instanceof WidgetsListSearchHeaderEntry) { + return ((WidgetsListSearchHeaderEntry) newRow).hasEntryUpdated() + || !curRow.equals(newRow); + } + return false; } private boolean isSamePackageItemInfo(PackageItemInfo curInfo, PackageItemInfo newInfo) { diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java index 330175f653..946a456e29 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java +++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java @@ -34,7 +34,6 @@ import android.view.MotionEvent; import android.view.View; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; -import android.widget.EditText; import android.widget.TextView; import androidx.annotation.Nullable; @@ -53,6 +52,8 @@ import com.android.launcher3.views.TopRoundedCornerView; import com.android.launcher3.widget.BaseWidgetSheet; import com.android.launcher3.widget.LauncherAppWidgetHost.ProviderChangedListener; import com.android.launcher3.widget.model.WidgetsListBaseEntry; +import com.android.launcher3.widget.picker.search.SearchModeListener; +import com.android.launcher3.widget.picker.search.WidgetsSearchBar; import com.android.launcher3.workprofile.PersonalWorkPagedView; import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener; @@ -64,7 +65,7 @@ import java.util.function.Predicate; */ public class WidgetsFullSheet extends BaseWidgetSheet implements Insettable, ProviderChangedListener, OnActivePageChangedListener, - WidgetsRecyclerView.HeaderViewDimensionsProvider { + WidgetsRecyclerView.HeaderViewDimensionsProvider, SearchModeListener { private static final long DEFAULT_OPEN_DURATION = 267; private static final long FADE_IN_DURATION = 150; @@ -81,6 +82,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet @Nullable private PersonalWorkPagedView mViewPager; private int mInitialTabsHeight = 0; + private boolean mIsInSearchMode; private View mTabsView; private TextView mNoWidgetsView; private SearchAndRecommendationViewHolder mSearchAndRecommendationViewHolder; @@ -91,6 +93,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet mHasWorkProfile = context.getSystemService(LauncherApps.class).getProfiles().size() > 1; mAdapters.put(AdapterHolder.PRIMARY, new AdapterHolder(AdapterHolder.PRIMARY)); mAdapters.put(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK)); + mAdapters.put(AdapterHolder.SEARCH, new AdapterHolder(AdapterHolder.SEARCH)); } public WidgetsFullSheet(Context context, AttributeSet attrs) { @@ -138,6 +141,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet mSearchAndRecommendationViewHolder, findViewById(R.id.primary_widgets_list_view), mHasWorkProfile ? findViewById(R.id.work_widgets_list_view) : null, + findViewById(R.id.search_widgets_list_view), mTabsView, mViewPager); fastScroller.setOnFastScrollChangeListener(mSearchAndRecommendationsScrollController); @@ -145,17 +149,25 @@ public class WidgetsFullSheet extends BaseWidgetSheet mNoWidgetsView = findViewById(R.id.no_widgets_text); onWidgetsBound(); + + mSearchAndRecommendationViewHolder.mSearchBar.initialize( + mLauncher.getPopupDataProvider().getAllWidgets(), /* searchModeListener= */ this); } @Override public void onActivePageChanged(int currentActivePage) { AdapterHolder currentAdapterHolder = mAdapters.get(currentActivePage); - WidgetsRecyclerView currentRecyclerView = currentAdapterHolder.mWidgetsRecyclerView; - currentRecyclerView.bindFastScrollbar(); - mSearchAndRecommendationsScrollController.setCurrentRecyclerView(currentRecyclerView); + WidgetsRecyclerView currentRecyclerView = + mAdapters.get(currentActivePage).mWidgetsRecyclerView; updateNoWidgetsView(currentAdapterHolder); + attachScrollbarToRecyclerView(currentRecyclerView); + } + + private void attachScrollbarToRecyclerView(WidgetsRecyclerView recyclerView) { + recyclerView.bindFastScrollbar(); + mSearchAndRecommendationsScrollController.setCurrentRecyclerView(recyclerView); reset(); } @@ -173,11 +185,15 @@ public class WidgetsFullSheet extends BaseWidgetSheet if (mHasWorkProfile) { mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView.scrollToTop(); } + mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.scrollToTop(); mSearchAndRecommendationsScrollController.reset(); } @VisibleForTesting public WidgetsRecyclerView getRecyclerView() { + if (mIsInSearchMode) { + return mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView; + } if (!mHasWorkProfile || mViewPager.getCurrentPage() == AdapterHolder.PRIMARY) { return mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView; } @@ -289,6 +305,8 @@ public class WidgetsFullSheet extends BaseWidgetSheet AdapterHolder primaryUserAdapterHolder = mAdapters.get(AdapterHolder.PRIMARY); primaryUserAdapterHolder.setup(findViewById(R.id.primary_widgets_list_view)); + AdapterHolder searchAdapterHolder = mAdapters.get(AdapterHolder.SEARCH); + searchAdapterHolder.setup(findViewById(R.id.search_widgets_list_view)); primaryUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets); updateNoWidgetsView(primaryUserAdapterHolder); @@ -300,6 +318,40 @@ public class WidgetsFullSheet extends BaseWidgetSheet } } + @Override + public void enterSearchMode() { + if (mIsInSearchMode) return; + setViewVisibilityBasedOnSearch(/*isInSearchMode= */ true); + attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView); + } + + @Override + public void exitSearchMode() { + setViewVisibilityBasedOnSearch(/*isInSearchMode=*/ false); + if (mHasWorkProfile) { + mViewPager.snapToPage(AdapterHolder.PRIMARY); + } + attachScrollbarToRecyclerView(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView); + } + + @Override + public void onSearchResults(List entries) { + mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setWidgetsOnSearch(entries); + } + + private void setViewVisibilityBasedOnSearch(boolean isInSearchMode) { + mIsInSearchMode = isInSearchMode; + if (mHasWorkProfile) { + mViewPager.setVisibility(isInSearchMode ? GONE : VISIBLE); + mTabsView.setVisibility(isInSearchMode ? GONE : VISIBLE); + } else { + mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView + .setVisibility(isInSearchMode ? GONE : VISIBLE); + } + mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView + .setVisibility(mIsInSearchMode ? VISIBLE : GONE); + } + private void open(boolean animate) { if (animate) { if (getPopupContainer().getInsets().bottom > 0) { @@ -402,6 +454,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet private final class AdapterHolder { static final int PRIMARY = 0; static final int WORK = 1; + static final int SEARCH = 2; private final int mAdapterType; private final WidgetsListAdapter mWidgetsListAdapter; @@ -420,8 +473,16 @@ public class WidgetsFullSheet extends BaseWidgetSheet apps.getIconCache(), /* iconClickListener= */ WidgetsFullSheet.this, /* iconLongClickListener= */ WidgetsFullSheet.this); - mWidgetsListAdapter.setFilter( - mAdapterType == PRIMARY ? mPrimaryWidgetsFilter : mWorkWidgetsFilter); + switch (mAdapterType) { + case PRIMARY: + mWidgetsListAdapter.setFilter(mPrimaryWidgetsFilter); + break; + case WORK: + mWidgetsListAdapter.setFilter(mWorkWidgetsFilter); + break; + default: + break; + } } void setup(WidgetsRecyclerView recyclerView) { @@ -437,7 +498,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet final class SearchAndRecommendationViewHolder { final View mContainer; final View mCollapseHandle; - final EditText mSearchBar; + final WidgetsSearchBar mSearchBar; final TextView mHeaderTitle; SearchAndRecommendationViewHolder(View searchAndRecommendationContainer) { diff --git a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java index 8b49d1ef15..f6ed1eafb1 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java +++ b/src/com/android/launcher3/widget/picker/WidgetsListAdapter.java @@ -34,11 +34,12 @@ 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.util.PackageUserKey; import com.android.launcher3.widget.WidgetCell; import com.android.launcher3.widget.model.WidgetsListBaseEntry; import com.android.launcher3.widget.model.WidgetsListContentEntry; import com.android.launcher3.widget.model.WidgetsListHeaderEntry; -import com.android.launcher3.widget.picker.WidgetsListHeaderViewHolderBinder.OnHeaderClickListener; +import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry; import java.util.ArrayList; import java.util.Comparator; @@ -62,8 +63,9 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC 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 static final int VIEW_TYPE_WIDGETS_HEADER = R.layout.widgets_list_row_header; + private static final int VIEW_TYPE_WIDGETS_LIST = R.id.view_type_widgets_list; + private static final int VIEW_TYPE_WIDGETS_HEADER = R.id.view_type_widgets_header; + private static final int VIEW_TYPE_WIDGETS_SEARCH_HEADER = R.id.view_type_widgets_search_header; private final WidgetsDiffReporter mDiffReporter; private final SparseArray mViewHolderBinders = new SparseArray<>(); @@ -73,11 +75,13 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC private List mAllEntries = new ArrayList<>(); private ArrayList mVisibleEntries = new ArrayList<>(); - @Nullable private String mWidgetsContentVisiblePackage = null; + @Nullable private PackageUserKey mWidgetsContentVisiblePackageUserKey = null; private Predicate mHeaderAndSelectedContentFilter = entry -> entry instanceof WidgetsListHeaderEntry - || entry.mPkgItem.packageName.equals(mWidgetsContentVisiblePackage); + || entry instanceof WidgetsListSearchHeaderEntry + || new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user) + .equals(mWidgetsContentVisiblePackageUserKey); @Nullable private Predicate mFilter = null; public WidgetsListAdapter(Context context, LayoutInflater layoutInflater, @@ -87,8 +91,14 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC mWidgetsListTableViewHolderBinder = new WidgetsListTableViewHolderBinder(context, layoutInflater, iconClickListener, iconLongClickListener, widgetPreviewLoader); mViewHolderBinders.put(VIEW_TYPE_WIDGETS_LIST, mWidgetsListTableViewHolderBinder); - mViewHolderBinders.put(VIEW_TYPE_WIDGETS_HEADER, - new WidgetsListHeaderViewHolderBinder(layoutInflater, this::onHeaderClicked)); + mViewHolderBinders.put( + VIEW_TYPE_WIDGETS_HEADER, + new WidgetsListHeaderViewHolderBinder( + layoutInflater, /*onHeaderClickListener=*/this)); + mViewHolderBinders.put( + VIEW_TYPE_WIDGETS_SEARCH_HEADER, + new WidgetsListSearchHeaderViewHolderBinder( + layoutInflater, /*onHeaderClickListener=*/ this)); } public void setFilter(Predicate filter) { @@ -127,18 +137,30 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC return mVisibleEntries.get(pos).mTitleSectionName; } - /** Updates the widget list. */ + /** Updates the widget list based on {@code tempEntries}. */ public void setWidgets(List tempEntries) { mAllEntries = tempEntries.stream().sorted(mRowComparator) .collect(Collectors.toList()); updateVisibleEntries(); } + /** Updates the widget list based on {@code searchResults}. */ + public void setWidgetsOnSearch(List searchResults) { + // Forget the expanded package every time widget list is refreshed in search mode. + mWidgetsContentVisiblePackageUserKey = null; + setWidgets(searchResults); + } + private void updateVisibleEntries() { mAllEntries.forEach(entry -> { if (entry instanceof WidgetsListHeaderEntry) { ((WidgetsListHeaderEntry) entry).setIsWidgetListShown( - entry.mPkgItem.packageName.equals(mWidgetsContentVisiblePackage)); + new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user) + .equals(mWidgetsContentVisiblePackageUserKey)); + } else if (entry instanceof WidgetsListSearchHeaderEntry) { + ((WidgetsListSearchHeaderEntry) entry).setIsWidgetListShown( + new PackageUserKey(entry.mPkgItem.packageName, entry.mPkgItem.user) + .equals(mWidgetsContentVisiblePackageUserKey)); } }); List newVisibleEntries = mAllEntries.stream() @@ -189,17 +211,19 @@ public class WidgetsListAdapter extends Adapter implements OnHeaderC return VIEW_TYPE_WIDGETS_LIST; } else if (entry instanceof WidgetsListHeaderEntry) { return VIEW_TYPE_WIDGETS_HEADER; + } else if (entry instanceof WidgetsListSearchHeaderEntry) { + return VIEW_TYPE_WIDGETS_SEARCH_HEADER; } throw new UnsupportedOperationException("ViewHolderBinder not found for " + entry); } @Override - public void onHeaderClicked(boolean showWidgets, String expandedPackage) { + public void onHeaderClicked(boolean showWidgets, PackageUserKey packageUserKey) { if (showWidgets) { - mWidgetsContentVisiblePackage = expandedPackage; + mWidgetsContentVisiblePackageUserKey = packageUserKey; updateVisibleEntries(); - } else if (expandedPackage.equals(mWidgetsContentVisiblePackage)) { - mWidgetsContentVisiblePackage = null; + } else if (packageUserKey.equals(mWidgetsContentVisiblePackageUserKey)) { + mWidgetsContentVisiblePackageUserKey = null; updateVisibleEntries(); } } diff --git a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java index 070a9aa2c7..119d094be5 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsListHeader.java +++ b/src/com/android/launcher3/widget/picker/WidgetsListHeader.java @@ -41,6 +41,9 @@ import com.android.launcher3.model.data.ItemInfoWithIcon; import com.android.launcher3.model.data.PackageItemInfo; import com.android.launcher3.views.ActivityContext; import com.android.launcher3.widget.model.WidgetsListHeaderEntry; +import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry; + +import java.util.stream.Collectors; /** * A UI represents a header of an app shown in the full widgets tray. @@ -173,7 +176,7 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd shortcutsCount); } else if (entry.widgetsCount > 0) { subtitle = resources.getQuantityString(R.plurals.widgets_count, - entry.widgetsCount, entry.widgetsCount); + entry.widgetsCount, entry.widgetsCount); } else { subtitle = resources.getQuantityString(R.plurals.shortcuts_count, entry.shortcutsCount, entry.shortcutsCount); @@ -182,6 +185,32 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd mSubtitle.setVisibility(VISIBLE); } + /** Apply app icon, labels and tag using a generic {@link WidgetsListSearchHeaderEntry}. */ + @UiThread + public void applyFromItemInfoWithIcon(WidgetsListSearchHeaderEntry entry) { + applyIconAndLabel(entry); + } + + @UiThread + private void applyIconAndLabel(WidgetsListSearchHeaderEntry entry) { + PackageItemInfo info = entry.mPkgItem; + setIcon(info); + setTitles(entry); + setExpanded(entry.isWidgetListShown()); + + super.setTag(info); + + verifyHighRes(); + } + + private void setTitles(WidgetsListSearchHeaderEntry entry) { + mTitle.setText(entry.mPkgItem.title); + + mSubtitle.setText(entry.mWidgets.stream() + .map(item -> item.label).sorted().collect(Collectors.joining(", "))); + mSubtitle.setVisibility(VISIBLE); + } + @Override public void reapplyItemInfo(ItemInfoWithIcon info) { if (getTag() == info) { diff --git a/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinder.java index ed53e6fbc9..fcefe3a5ed 100644 --- a/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinder.java +++ b/src/com/android/launcher3/widget/picker/WidgetsListHeaderViewHolderBinder.java @@ -20,6 +20,7 @@ import android.view.ViewGroup; import com.android.launcher3.R; import com.android.launcher3.recyclerview.ViewHolderBinder; +import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.widget.model.WidgetsListHeaderEntry; /** @@ -50,12 +51,9 @@ public final class WidgetsListHeaderViewHolderBinder implements widgetsListHeader.applyFromItemInfoWithIcon(data); widgetsListHeader.setExpanded(data.isWidgetListShown()); widgetsListHeader.setOnExpandChangeListener(isExpanded -> - mOnHeaderClickListener.onHeaderClicked(isExpanded, data.mPkgItem.packageName)); - } - - /** A listener to be invoked when {@link WidgetsListHeader} is clicked. */ - public interface OnHeaderClickListener { - /** Calls when {@link WidgetsListHeader} is clicked to show / hide widgets for a package. */ - void onHeaderClicked(boolean showWidgets, String packageName); + mOnHeaderClickListener.onHeaderClicked( + isExpanded, + new PackageUserKey(data.mPkgItem.packageName, data.mPkgItem.user) + )); } } diff --git a/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderHolder.java b/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderHolder.java new file mode 100644 index 0000000000..9562af3d50 --- /dev/null +++ b/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderHolder.java @@ -0,0 +1,32 @@ +/* + * 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 androidx.recyclerview.widget.RecyclerView.ViewHolder; + +/** + * A {@link ViewHolder} for {@link WidgetsListHeader} of an app, which renders the app icon, the app + * name, label and a button for showing / hiding widgets. + */ +public final class WidgetsListSearchHeaderHolder extends ViewHolder { + final WidgetsListHeader mWidgetsListHeader; + + public WidgetsListSearchHeaderHolder(WidgetsListHeader view) { + super(view); + + mWidgetsListHeader = view; + } +} diff --git a/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinder.java new file mode 100644 index 0000000000..83c7948f95 --- /dev/null +++ b/src/com/android/launcher3/widget/picker/WidgetsListSearchHeaderViewHolderBinder.java @@ -0,0 +1,59 @@ +/* + * 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 android.view.LayoutInflater; +import android.view.ViewGroup; + +import com.android.launcher3.R; +import com.android.launcher3.recyclerview.ViewHolderBinder; +import com.android.launcher3.util.PackageUserKey; +import com.android.launcher3.widget.model.WidgetsListHeaderEntry; +import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry; + +/** + * Binds data from {@link WidgetsListHeaderEntry} to UI elements in {@link WidgetsListHeaderHolder}. + */ +public final class WidgetsListSearchHeaderViewHolderBinder implements + ViewHolderBinder { + private final LayoutInflater mLayoutInflater; + private final OnHeaderClickListener mOnHeaderClickListener; + + public WidgetsListSearchHeaderViewHolderBinder(LayoutInflater layoutInflater, + OnHeaderClickListener onHeaderClickListener) { + mLayoutInflater = layoutInflater; + mOnHeaderClickListener = onHeaderClickListener; + } + + @Override + public WidgetsListSearchHeaderHolder newViewHolder(ViewGroup parent) { + WidgetsListHeader header = (WidgetsListHeader) mLayoutInflater.inflate( + R.layout.widgets_list_row_header, parent, false); + + return new WidgetsListSearchHeaderHolder(header); + } + + @Override + public void bindViewHolder(WidgetsListSearchHeaderHolder viewHolder, + WidgetsListSearchHeaderEntry data) { + WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader; + widgetsListHeader.applyFromItemInfoWithIcon(data); + widgetsListHeader.setExpanded(data.isWidgetListShown()); + widgetsListHeader.setOnExpandChangeListener(isExpanded -> + mOnHeaderClickListener.onHeaderClicked(isExpanded, + new PackageUserKey(data.mPkgItem.packageName, data.mPkgItem.user))); + } +} diff --git a/src/com/android/launcher3/widget/picker/search/SearchModeListener.java b/src/com/android/launcher3/widget/picker/search/SearchModeListener.java new file mode 100644 index 0000000000..cee7d6735a --- /dev/null +++ b/src/com/android/launcher3/widget/picker/search/SearchModeListener.java @@ -0,0 +1,40 @@ +/* + * 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.search; + +import com.android.launcher3.widget.model.WidgetsListBaseEntry; + +import java.util.List; + +/** + * A listener to help with widgets picker search. + */ +public interface SearchModeListener { + /** + * Notifies the subscriber when user enters widget picker search mode. + */ + void enterSearchMode(); + + /** + * Notifies the subscriber when user exits widget picker search mode. + */ + void exitSearchMode(); + + /** + * Notifies the subscriber with search results. + */ + void onSearchResults(List entries); +}