Merge "Implement two pane widget picker" into tm-qpr-dev

This commit is contained in:
Federico Baron
2023-02-10 17:20:55 +00:00
committed by Android (Google) Code Review
18 changed files with 653 additions and 34 deletions
+27
View File
@@ -0,0 +1,27 @@
<!--
Copyright (C) 2023 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/widget_picker_background_selected"
android:gravity="center"
>
<path
android:fillColor="@android:color/white"
android:pathData="M16.65,13 L11,7.35 16.65,1.7 22.3,7.35ZM3,11V3H11V11ZM13,21V13H21V21ZM3,21V13H11V21ZM5,9H9V5H5ZM16.675,10.2 L19.5,7.375 16.675,4.55 13.85,7.375ZM15,19H19V15H15ZM5,19H9V15H5ZM9,9ZM13.85,7.375ZM9,15ZM15,15Z"/>
</vector>
+30
View File
@@ -0,0 +1,30 @@
<!--
Copyright (C) 2023 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.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<size
android:width="48dp"
android:height="48dp" />
<solid android:color="@color/surface"/>
</shape>
</item>
<item
android:width="24dp"
android:height="24dp"
android:drawable="@drawable/widget_suggestions"
android:gravity="center" />
</layer-list>
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2022 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.
-->
<com.android.launcher3.widget.picker.WidgetsFullSheet xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:theme="?attr/widgetsTheme">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_widgets_full_sheet"
android:focusable="true"
android:importantForAccessibility="no">
<FrameLayout
android:id="@+id/recycler_view_container"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintEnd_toStartOf="@id/right_pane"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintWidth_percent="0.33">
<TextView
android:id="@+id/no_widgets_text"
style="@style/PrimaryHeadline"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="18sp"
android:visibility="gone"
tools:text="No widgets available" />
<TextView
android:id="@+id/fast_scroller_popup"
style="@style/FastScrollerPopup"
android:layout_marginEnd="@dimen/fastscroll_popup_margin" />
<!-- Fast scroller popup -->
<com.android.launcher3.views.RecyclerViewFastScroller
android:id="@+id/fast_scroller"
android:layout_width="@dimen/fastscroll_width"
android:layout_height="match_parent"
android:layout_marginEnd="@dimen/fastscroll_end_margin" />
<com.android.launcher3.widget.picker.WidgetsRecyclerView
android:id="@+id/search_widgets_list_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingHorizontal="@dimen/widget_list_horizontal_margin_large_screen"
android:visibility="gone" />
</FrameLayout>
<ScrollView
android:id="@+id/right_pane"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/recycler_view_container"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintBottom_toBottomOf="parent"
android:paddingEnd="16dp"
android:paddingStart="8dp"
android:layout_marginTop="26dp"
app:layout_constraintWidth_percent="0.67">
<com.android.launcher3.widget.picker.WidgetsRecommendationTableLayout
android:id="@+id/recommended_widget_table"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/widgets_surface_background"
android:paddingHorizontal="@dimen/widget_list_horizontal_margin_large_screen"
android:paddingVertical="@dimen/recommended_widgets_table_vertical_padding"
android:visibility="gone" />
</ScrollView>
<View
android:id="@+id/collapse_handle"
android:layout_width="@dimen/bottom_sheet_handle_width"
android:layout_height="@dimen/bottom_sheet_handle_height"
android:layout_marginTop="@dimen/bottom_sheet_handle_margin"
android:background="@drawable/bg_rounded_corner_bottom_sheet_handle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/collapse_handle"
android:layout_marginTop="24dp"
android:gravity="center_horizontal"
android:paddingHorizontal="@dimen/widget_list_horizontal_margin_large_screen"
android:text="@string/widget_button_text"
android:textColor="?android:attr/textColorSecondary"
android:textSize="24sp" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.android.launcher3.widget.picker.WidgetsFullSheet>
@@ -0,0 +1,122 @@
<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2023 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.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:launcher="http://schemas.android.com/apk/res-auto"
xmlns:app="http://schemas.android.com/apk/res-auto">
<FrameLayout
android:id="@+id/widgets_full_sheet_paged_view_large_screen"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintEnd_toStartOf="@id/scrollView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintWidth_percent="0.33">
<com.android.launcher3.widget.picker.WidgetPagedView
android:id="@+id/widgets_view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:descendantFocusability="afterDescendants"
android:paddingHorizontal="@dimen/widget_list_horizontal_margin_large_screen"
launcher:pageIndicator="@+id/tabs" >
<com.android.launcher3.widget.picker.WidgetsRecyclerView
android:id="@+id/primary_widgets_list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false" />
<com.android.launcher3.widget.picker.WidgetsRecyclerView
android:id="@+id/work_widgets_list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false" />
</com.android.launcher3.widget.picker.WidgetPagedView>
<!-- SearchAndRecommendationsView without the tab layout as well -->
<com.android.launcher3.views.StickyHeaderLayout
android:id="@+id/search_and_recommendations_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<FrameLayout
android:id="@+id/search_bar_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/colorBackground"
android:clipToPadding="false"
android:elevation="0.1dp"
android:paddingBottom="8dp"
android:paddingHorizontal="@dimen/widget_list_horizontal_margin_large_screen"
launcher:layout_sticky="true">
<include layout="@layout/widgets_search_bar" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/suggestions_header"
android:layout_marginTop="8dp"
android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin_large_screen"
android:orientation="horizontal">
</LinearLayout>
<com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="64dp"
android:gravity="center_horizontal"
android:orientation="horizontal"
android:paddingVertical="8dp"
android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin_large_screen"
android:background="?android:attr/colorBackground"
style="@style/TextHeadline"
launcher:layout_sticky="true">
<Button
android:id="@+id/tab_personal"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginEnd="@dimen/widget_tabs_button_horizontal_padding"
android:layout_marginVertical="@dimen/widget_apps_tabs_vertical_padding"
android:layout_weight="1"
android:background="@drawable/all_apps_tabs_background"
android:text="@string/widgets_full_sheet_personal_tab"
android:textColor="@color/all_apps_tab_text"
android:textSize="14sp"
style="?android:attr/borderlessButtonStyle" />
<Button
android:id="@+id/tab_work"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginEnd="@dimen/widget_tabs_button_horizontal_padding"
android:layout_marginVertical="@dimen/widget_apps_tabs_vertical_padding"
android:layout_weight="1"
android:background="@drawable/all_apps_tabs_background"
android:text="@string/widgets_full_sheet_work_tab"
android:textColor="@color/all_apps_tab_text"
android:textSize="14sp"
style="?android:attr/borderlessButtonStyle" />
</com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip>
</com.android.launcher3.views.StickyHeaderLayout>
</FrameLayout>
</merge>
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?><!-- 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.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:launcher="http://schemas.android.com/apk/res-auto"
xmlns:app="http://schemas.android.com/apk/res-auto">
<FrameLayout
android:id="@+id/widgets_full_sheet_recyclerview_large_screen"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintEnd_toStartOf="@id/scrollView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintWidth_percent="0.33">
<com.android.launcher3.widget.picker.WidgetsRecyclerView
android:id="@+id/primary_widgets_list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin_large_screen"
android:clipToPadding="false" />
<!-- SearchAndRecommendationsView without the tab layout as well -->
<com.android.launcher3.views.StickyHeaderLayout
android:id="@+id/search_and_recommendations_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<FrameLayout
android:id="@+id/search_bar_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/colorBackground"
android:clipToPadding="false"
android:elevation="0.1dp"
android:paddingBottom="8dp"
android:paddingHorizontal="@dimen/widget_list_horizontal_margin_large_screen"
launcher:layout_sticky="true">
<include layout="@layout/widgets_search_bar" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/suggestions_header"
android:layout_marginTop="8dp"
android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin_large_screen"
android:orientation="horizontal">
</LinearLayout>
</com.android.launcher3.views.StickyHeaderLayout>
</FrameLayout>
</merge>
+1
View File
@@ -31,6 +31,7 @@
<!-- Widget picker-->
<dimen name="widget_list_horizontal_margin">49dp</dimen>
<dimen name="widget_list_horizontal_margin_large_screen">24dp</dimen>
<!-- Bottom sheet-->
<dimen name="bottom_sheet_extra_top_padding">0dp</dimen>
+2
View File
@@ -64,4 +64,6 @@
<color name="all_apps_button_color_light">@android:color/system_neutral2_700</color>
<color name="all_apps_button_color_dark">@android:color/system_neutral2_200</color>
<color name="widget_picker_background_selected">@android:color/system_accent2_100</color>
</resources>
+1
View File
@@ -192,6 +192,7 @@
<dimen name="widget_list_header_view_vertical_padding">20dp</dimen>
<dimen name="widget_list_entry_spacing">2dp</dimen>
<dimen name="widget_list_horizontal_margin">16dp</dimen>
<dimen name="widget_list_horizontal_margin_large_screen">24dp</dimen>
<dimen name="widget_preview_shadow_blur">0.5dp</dimen>
<dimen name="widget_preview_key_shadow_distance">1dp</dimen>
+3
View File
@@ -63,6 +63,9 @@
<!-- Accessibility spoken message announced when a widget gets added to the home screen using a
button in a dialog. [CHAR_LIMIT=none] -->
<string name="added_to_home_screen_accessibility_text"><xliff:g id="widget_name" example="Calendar month view">%1$s</xliff:g> widget added to home screen</string>
<!-- Widget suggestions header title in the full widgets picker for large screen devices
in landscape mode. [CHAR_LIMIT=50] -->
<string name="suggested_widgets_header_title">Suggestions</string>
<!-- Label for showing the number of widgets an app has in the full widgets picker.
[CHAR_LIMIT=25][ICU SYNTAX] -->
<string name="widgets_count">
@@ -63,6 +63,9 @@ public abstract class FastScrollRecyclerView extends RecyclerView {
public void bindFastScrollbar() {
ViewGroup parent = (ViewGroup) getParent().getParent();
if (parent.findViewById(R.id.fast_scroller) == null) {
parent = (ViewGroup) parent.getParent();
}
mScrollbar = parent.findViewById(R.id.fast_scroller);
mScrollbar.setRecyclerView(this, parent.findViewById(R.id.fast_scroller_popup));
onUpdateScrollbar(0);
@@ -238,6 +238,15 @@ public class PopupDataProvider implements NotificationListener.NotificationsChan
.collect(Collectors.toList());
}
/** Gets the WidgetsListContentEntry for the currently selected header. */
public WidgetsListContentEntry getSelectedAppWidgets(PackageUserKey packageUserKey) {
return (WidgetsListContentEntry) mAllWidgets.stream()
.filter(row -> row instanceof WidgetsListContentEntry
&& row.mPkgItem.packageName.equals(packageUserKey.mPackageName))
.findAny()
.orElse(null);
}
/**
* Returns a list of notifications that are relevant to given ItemInfo.
*/
@@ -20,6 +20,7 @@ import static android.view.View.MeasureSpec.makeMeasureSpec;
import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
import static com.android.launcher3.allapps.AllAppsTransitionController.SWIPE_ALL_APPS_TO_HOME_MIN_SCALE;
import static com.android.launcher3.config.FeatureFlags.LARGE_SCREEN_WIDGET_PICKER;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_SEARCHED;
import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
@@ -45,14 +46,18 @@ import android.view.WindowInsets;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import androidx.annotation.FloatRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.RecyclerView;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.R;
@@ -62,7 +67,9 @@ import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.compat.AccessibilityManagerCompat;
import com.android.launcher3.model.UserManagerState;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.views.ArrowTipView;
import com.android.launcher3.views.RecyclerViewFastScroller;
import com.android.launcher3.views.SpringRelativeLayout;
@@ -71,6 +78,8 @@ import com.android.launcher3.views.WidgetsEduView;
import com.android.launcher3.widget.BaseWidgetSheet;
import com.android.launcher3.widget.LauncherWidgetHolder.ProviderChangedListener;
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.search.SearchModeListener;
import com.android.launcher3.widget.picker.search.WidgetsSearchBar;
import com.android.launcher3.widget.util.WidgetsTableUtils;
@@ -78,6 +87,7 @@ import com.android.launcher3.workprofile.PersonalWorkPagedView;
import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.IntStream;
@@ -93,6 +103,9 @@ public class WidgetsFullSheet extends BaseWidgetSheet
private static final long EDUCATION_TIP_DELAY_MS = 200;
private static final long EDUCATION_DIALOG_DELAY_MS = 500;
private static final float VERTICAL_START_POSITION = 0.3f;
private static final int PERSONAL_TAB = 0;
private static final int WORK_TAB = 1;
private static final String SUGGESTIONS_PACKAGE_NAME = "widgets_list_suggestions_entry";
// The widget recommendation table can easily take over the entire screen on devices with small
// resolution or landscape on phone. This ratio defines the max percentage of content area that
// the table can display.
@@ -169,14 +182,23 @@ public class WidgetsFullSheet extends BaseWidgetSheet
private StickyHeaderLayout mSearchScrollView;
private WidgetsRecommendationTableLayout mRecommendedWidgetsTable;
private LinearLayout mSuggestedWidgetsContainer;
private WidgetsListHeader mSuggestedWidgetsHeader;
private View mTabBar;
private View mSearchBarContainer;
private WidgetsSearchBar mSearchBar;
private TextView mHeaderTitle;
private ScrollView mRightPane;
private WidgetsListTableViewHolderBinder mWidgetsListTableViewHolderBinder;
private final boolean mIsTwoPane;
private int mOrientation;
private @Nullable WidgetsRecyclerView mCurrentTouchEventRecyclerView;
public WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
DeviceProfile dp = Launcher.getLauncher(context).getDeviceProfile();
mIsTwoPane = dp.isTablet && dp.isLandscape && LARGE_SCREEN_WIDGET_PICKER.get();
mHasWorkProfile = context.getSystemService(LauncherApps.class).getProfiles().size() > 1;
mAdapters.put(AdapterHolder.PRIMARY, new AdapterHolder(AdapterHolder.PRIMARY));
mAdapters.put(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK));
@@ -205,9 +227,16 @@ public class WidgetsFullSheet extends BaseWidgetSheet
LayoutInflater layoutInflater = LayoutInflater.from(getContext());
int contentLayoutRes = mHasWorkProfile ? R.layout.widgets_full_sheet_paged_view
: R.layout.widgets_full_sheet_recyclerview;
if (mIsTwoPane) {
contentLayoutRes = mHasWorkProfile ? R.layout.widgets_full_sheet_paged_view_large_screen
: R.layout.widgets_full_sheet_recyclerview_large_screen;
}
layoutInflater.inflate(contentLayoutRes, mContent, true);
RecyclerViewFastScroller fastScroller = findViewById(R.id.fast_scroller);
if (mIsTwoPane) {
fastScroller.setVisibility(GONE);
}
mAdapters.get(AdapterHolder.PRIMARY).setup(findViewById(R.id.primary_widgets_list_view));
mAdapters.get(AdapterHolder.SEARCH).setup(findViewById(R.id.search_widgets_list_view));
if (mHasWorkProfile) {
@@ -230,15 +259,61 @@ public class WidgetsFullSheet extends BaseWidgetSheet
mSearchScrollView = findViewById(R.id.search_and_recommendations_container);
mSearchScrollView.setCurrentRecyclerView(findViewById(R.id.primary_widgets_list_view));
mRecommendedWidgetsTable = mSearchScrollView.findViewById(R.id.recommended_widget_table);
mRecommendedWidgetsTable = mIsTwoPane
? mContent.findViewById(R.id.recommended_widget_table)
: mSearchScrollView.findViewById(R.id.recommended_widget_table);
mRecommendedWidgetsTable.setWidgetCellLongClickListener(this);
mRecommendedWidgetsTable.setWidgetCellOnClickListener(this);
// Add suggested widgets.
if (mIsTwoPane) {
mSuggestedWidgetsContainer = mSearchScrollView.findViewById(R.id.suggestions_header);
// Inflate the suggestions header.
mSuggestedWidgetsHeader = (WidgetsListHeader) layoutInflater.inflate(
R.layout.widgets_list_row_header, mSuggestedWidgetsContainer, false);
mSuggestedWidgetsHeader.setExpanded(true);
mSuggestedWidgetsHeader.setBackground(
new WidgetsListDrawableFactory(getContext()).createHeaderBackgroundDrawable());
PackageItemInfo packageItemInfo = new PackageItemInfo(
/* packageName= */ SUGGESTIONS_PACKAGE_NAME,
Process.myUserHandle()) {
@Override
public boolean usingLowResIcon() {
return false;
}
};
packageItemInfo.title = getContext().getString(R.string.suggested_widgets_header_title);
WidgetsListHeaderEntry widgetsListHeaderEntry = new WidgetsListHeaderEntry(
packageItemInfo,
getContext().getString(R.string.suggested_widgets_header_title),
mActivityContext.getPopupDataProvider().getRecommendedWidgets())
.withWidgetListShown();
mSuggestedWidgetsHeader.applyFromItemInfoWithIcon(widgetsListHeaderEntry);
mSuggestedWidgetsHeader.setIcon(
getContext().getDrawable(R.drawable.widget_suggestions_icon));
mSuggestedWidgetsHeader.setOnExpandChangeListener(isExpanded -> {
mSuggestedWidgetsHeader.setExpanded(isExpanded);
resetExpandedHeaders();
mRightPane.removeAllViews();
mRightPane.addView(mRecommendedWidgetsTable);
});
mSuggestedWidgetsContainer.addView(mSuggestedWidgetsHeader);
}
mTabBar = mSearchScrollView.findViewById(R.id.tabs);
mSearchBarContainer = mSearchScrollView.findViewById(R.id.search_bar_container);
mSearchBar = mSearchScrollView.findViewById(R.id.widgets_search_bar);
mHeaderTitle = mSearchScrollView.findViewById(R.id.title);
mHeaderTitle = mIsTwoPane
? mContent.findViewById(R.id.title)
: mSearchScrollView.findViewById(R.id.title);
mRightPane = mIsTwoPane ? mContent.findViewById(R.id.right_pane) : null;
mWidgetsListTableViewHolderBinder = new WidgetsListTableViewHolderBinder(
layoutInflater, this, this,
new WidgetsListDrawableFactory(getContext()));
onRecommendedWidgetsBound();
onWidgetsBound();
@@ -260,6 +335,13 @@ public class WidgetsFullSheet extends BaseWidgetSheet
@Override
public void onActivePageChanged(int currentActivePage) {
// if the current active page changes to personal or work we set suggestions
// to be the selected widget
if (mIsTwoPane && (currentActivePage == PERSONAL_TAB || currentActivePage == WORK_TAB)) {
mSuggestedWidgetsHeader.callOnClick();
}
AdapterHolder currentAdapterHolder = mAdapters.get(currentActivePage);
WidgetsRecyclerView currentRecyclerView =
mAdapters.get(currentActivePage).mWidgetsRecyclerView;
@@ -428,6 +510,11 @@ public class WidgetsFullSheet extends BaseWidgetSheet
View content = mHasWorkProfile
? mViewPager
: mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView;
if (mIsTwoPane && mRightPane != null) {
content = mRightPane;
}
int maxHorizontalSpans = computeMaxHorizontalSpans(content,
mWidgetSheetContentHorizontalPadding);
if (mMaxSpansPerRow != maxHorizontalSpans) {
@@ -520,6 +607,9 @@ public class WidgetsFullSheet extends BaseWidgetSheet
public void onSearchResults(List<WidgetsListBaseEntry> entries) {
mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.setWidgetsOnSearch(entries);
updateRecyclerViewVisibility(mAdapters.get(AdapterHolder.SEARCH));
if (mIsTwoPane) {
mAdapters.get(AdapterHolder.SEARCH).mWidgetsListAdapter.selectFirstHeaderEntry();
}
mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.scrollToTop();
}
@@ -527,6 +617,9 @@ public class WidgetsFullSheet extends BaseWidgetSheet
mIsInSearchMode = isInSearchMode;
if (isInSearchMode) {
mRecommendedWidgetsTable.setVisibility(GONE);
if (mIsTwoPane) {
mSuggestedWidgetsContainer.setVisibility(GONE);
}
if (mHasWorkProfile) {
mViewPager.setVisibility(GONE);
mTabBar.setVisibility(GONE);
@@ -538,6 +631,10 @@ public class WidgetsFullSheet extends BaseWidgetSheet
mNoWidgetsView.setVisibility(GONE);
} else {
mAdapters.get(AdapterHolder.SEARCH).mWidgetsRecyclerView.setVisibility(GONE);
if (mIsTwoPane) {
mSuggestedWidgetsContainer.setVisibility(VISIBLE);
mSuggestedWidgetsHeader.callOnClick();
}
// Visibility of recommended widgets, recycler views and headers are handled in methods
// below.
onRecommendedWidgetsBound();
@@ -572,7 +669,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet
MeasureSpec.EXACTLY),
makeMeasureSpec(mActivityContext.getDeviceProfile().availableHeightPx,
MeasureSpec.EXACTLY));
float maxTableHeight = (mContent.getMeasuredHeight()
float maxTableHeight = mIsTwoPane ? Float.MAX_VALUE : (mContent.getMeasuredHeight()
- mTabsHeight - getHeaderViewHeight()
- noWidgetsViewHeight) * RECOMMENDATION_TABLE_HEIGHT_RATIO;
@@ -626,7 +723,7 @@ public class WidgetsFullSheet extends BaseWidgetSheet
@Override
public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
// Disable swipe down when recycler view is scrolling
// Disable swipe down when recycler view is scrolling or scroll view is scrolling
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
mNoIntercept = false;
WidgetsRecyclerView recyclerView = getRecyclerView();
@@ -636,6 +733,8 @@ public class WidgetsFullSheet extends BaseWidgetSheet
mNoIntercept = true;
} else if (getPopupContainer().isEventOverView(recyclerView, ev)) {
mNoIntercept = !recyclerView.shouldContainerScroll(ev, getPopupContainer());
} else if (mIsTwoPane && getPopupContainer().isEventOverView(mRightPane, ev)) {
mNoIntercept = mRightPane.getScrollY() > 0;
}
if (mSearchBar.isSearchBarFocused()
@@ -649,7 +748,11 @@ public class WidgetsFullSheet extends BaseWidgetSheet
/** 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);
.inflate(LARGE_SCREEN_WIDGET_PICKER.get()
&& launcher.getDeviceProfile().isTablet
&& launcher.getDeviceProfile().isLandscape
? R.layout.widgets_full_sheet_large_screen
: R.layout.widgets_full_sheet, launcher.getDragLayer(), false);
sheet.attachToContainer();
sheet.mIsOpen = true;
sheet.open(animate);
@@ -746,6 +849,13 @@ public class WidgetsFullSheet extends BaseWidgetSheet
if (mIsInSearchMode) {
mSearchBar.reset();
}
// Checks the orientation of the screen
if (LARGE_SCREEN_WIDGET_PICKER.get() && mOrientation != newConfig.orientation) {
mOrientation = newConfig.orientation;
handleClose(false);
show(Launcher.getLauncher(getContext()), false);
}
}
@Override
@@ -835,16 +945,44 @@ public class WidgetsFullSheet extends BaseWidgetSheet
AdapterHolder(int adapterType) {
mAdapterType = adapterType;
Context context = getContext();
LauncherAppState apps = LauncherAppState.getInstance(context);
HeaderChangeListener headerChangeListener = new HeaderChangeListener() {
@Override
public void onHeaderChanged(@NonNull PackageUserKey selectedHeader) {
WidgetsListContentEntry contentEntry = mActivityContext.getPopupDataProvider()
.getSelectedAppWidgets(selectedHeader);
if (contentEntry == null || mRightPane == null) {
return;
}
if (mSuggestedWidgetsHeader != null) {
mSuggestedWidgetsHeader.setExpanded(false);
}
WidgetsRowViewHolder widgetsRowViewHolder =
mWidgetsListTableViewHolderBinder.newViewHolder(mRightPane);
mWidgetsListTableViewHolderBinder.bindViewHolder(widgetsRowViewHolder,
contentEntry,
0, Collections.EMPTY_LIST);
widgetsRowViewHolder.mDataCallback = data -> {
mWidgetsListTableViewHolderBinder.bindViewHolder(widgetsRowViewHolder,
contentEntry,
0, Collections.singletonList(data));
};
mRightPane.removeAllViews();
mRightPane.addView(widgetsRowViewHolder.itemView);
}
};
mWidgetsListAdapter = new WidgetsListAdapter(
context,
LayoutInflater.from(context),
apps.getIconCache(),
this::getEmptySpaceHeight,
/* iconClickListener= */ WidgetsFullSheet.this,
/* iconLongClickListener= */ WidgetsFullSheet.this);
/* iconLongClickListener= */ WidgetsFullSheet.this,
mIsTwoPane ? headerChangeListener : null);
mWidgetsListAdapter.setHasStableIds(true);
switch (mAdapterType) {
case PRIMARY:
@@ -871,8 +1009,10 @@ public class WidgetsFullSheet extends BaseWidgetSheet
mWidgetsRecyclerView.setAdapter(mWidgetsListAdapter);
mWidgetsRecyclerView.setItemAnimator(mWidgetsListItemAnimator);
mWidgetsRecyclerView.setHeaderViewDimensionsProvider(WidgetsFullSheet.this);
mWidgetsRecyclerView.setEdgeEffectFactory(
((SpringRelativeLayout) mContent).createEdgeEffectFactory());
if (!mIsTwoPane) {
mWidgetsRecyclerView.setEdgeEffectFactory(
((SpringRelativeLayout) mContent).createEdgeEffectFactory());
}
// Recycler view binds to fast scroller when it is attached to screen. Make sure
// search recycler view is bound to fast scroller if user is in search mode at the time
// of attachment.
@@ -882,4 +1022,15 @@ public class WidgetsFullSheet extends BaseWidgetSheet
mWidgetsListAdapter.setMaxHorizontalSpansPerRow(mMaxSpansPerRow);
}
}
/**
* This is a listener for when the selected header gets changed in the left pane.
*/
public interface HeaderChangeListener {
/**
* Sets the right pane to have the widgets for the currently selected header from
* the left pane.
*/
void onHeaderChanged(@NonNull PackageUserKey selectedHeader);
}
}
@@ -88,6 +88,7 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
private final SparseArray<ViewHolderBinder> mViewHolderBinders = new SparseArray<>();
private final WidgetListBaseRowEntryComparator mRowComparator =
new WidgetListBaseRowEntryComparator();
@Nullable private final WidgetsFullSheet.HeaderChangeListener mHeaderChangeListener;
private final List<WidgetsListBaseEntry> mAllEntries = new ArrayList<>();
private ArrayList<WidgetsListBaseEntry> mVisibleEntries = new ArrayList<>();
@@ -105,7 +106,9 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
public WidgetsListAdapter(Context context, LayoutInflater layoutInflater,
IconCache iconCache, IntSupplier emptySpaceHeightProvider,
OnClickListener iconClickListener, OnLongClickListener iconLongClickListener) {
OnClickListener iconClickListener, OnLongClickListener iconLongClickListener,
WidgetsFullSheet.HeaderChangeListener headerChangeListener) {
mHeaderChangeListener = headerChangeListener;
mContext = context;
mDiffReporter = new WidgetsDiffReporter(iconCache, this);
WidgetsListDrawableFactory listDrawableFactory = new WidgetsListDrawableFactory(context);
@@ -196,9 +199,11 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
getOffsetForPosition(previousPositionForPackageUserKey);
List<WidgetsListBaseEntry> newVisibleEntries = mAllEntries.stream()
.filter(entry -> ((mFilter == null || mFilter.test(entry))
.filter(entry -> (((mFilter == null || mFilter.test(entry))
&& mHeaderAndSelectedContentFilter.test(entry))
|| entry instanceof WidgetListSpaceEntry)
&& (mHeaderChangeListener == null
|| !(entry instanceof WidgetsListContentEntry)))
.map(entry -> {
if (entry instanceof WidgetsListBaseEntry.Header<?>
&& matchesKey(entry, mWidgetsContentVisiblePackageUserKey)) {
@@ -225,7 +230,6 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
}
}
/** Returns whether {@code entry} matches {@code key}. */
private static boolean isHeaderForPackageUserKey(
@NonNull WidgetsListBaseEntry entry, @Nullable PackageUserKey key) {
@@ -258,7 +262,6 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
@Override
public void onBindViewHolder(ViewHolder holder, int pos, List<Object> payloads) {
ViewHolderBinder viewHolderBinder = mViewHolderBinders.get(getItemViewType(pos));
WidgetsListBaseEntry entry = mVisibleEntries.get(pos);
// The first entry has an empty space, count from second entries.
int listPos = (pos > 1) ? POSITION_DEFAULT : POSITION_FIRST;
@@ -268,6 +271,23 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
viewHolderBinder.bindViewHolder(holder, mVisibleEntries.get(pos), listPos, payloads);
}
/**
* Selects the first visible header. This is used in search as we want to always select the
* first header in the new list that gets generated as we search.
*/
void selectFirstHeaderEntry() {
WidgetsListSearchHeaderEntry firstEntry = null;
for (WidgetsListBaseEntry entry: mVisibleEntries) {
if (entry instanceof WidgetsListSearchHeaderEntry) {
firstEntry = (WidgetsListSearchHeaderEntry) entry;
break;
}
}
if (firstEntry != null) {
onHeaderClicked(true, PackageUserKey.fromPackageItemInfo(firstEntry.mPkgItem));
}
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (DEBUG) {
@@ -318,6 +338,9 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
// Ignore invalid clicks, such as collapsing a package that isn't currently expanded.
if (!showWidgets && !packageUserKey.equals(mWidgetsContentVisiblePackageUserKey)) return;
if (mHeaderChangeListener != null
&& packageUserKey.equals(mWidgetsContentVisiblePackageUserKey)) return;
if (showWidgets) {
mWidgetsContentVisiblePackageUserKey = packageUserKey;
ActivityContext.lookupContext(mContext)
@@ -331,6 +354,10 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
mPendingClickHeader = packageUserKey;
updateVisibleEntries();
if (mHeaderChangeListener != null && mWidgetsContentVisiblePackageUserKey != null) {
mHeaderChangeListener.onHeaderChanged(mWidgetsContentVisiblePackageUserKey);
}
}
/**
@@ -15,13 +15,17 @@
*/
package com.android.launcher3.widget.picker;
import static com.android.launcher3.config.FeatureFlags.LARGE_SCREEN_WIDGET_PICKER;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.CheckBox;
@@ -35,12 +39,14 @@ import androidx.annotation.UiThread;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.icons.IconCache.ItemInfoUpdateReceiver;
import com.android.launcher3.icons.PlaceHolderIconDrawable;
import com.android.launcher3.icons.cache.HandlerRunnable;
import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.util.PluralMessageFormat;
import com.android.launcher3.util.Themes;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;
@@ -55,19 +61,19 @@ import java.util.stream.Collectors;
*/
public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpdateReceiver {
private boolean mEnableIconUpdateAnimation = false;
private final int mIconSize;
private final boolean mIsTwoPane;
@Nullable private HandlerRunnable mIconLoadRequest;
@Nullable private Drawable mIconDrawable;
private final int mIconSize;
@Nullable private WidgetsListDrawableState mListDrawableState;
private ImageView mAppIcon;
private TextView mTitle;
private TextView mSubtitle;
private GradientDrawable mBackground;
private CheckBox mExpandToggle;
private boolean mEnableIconUpdateAnimation = false;
private boolean mIsExpanded = false;
@Nullable private WidgetsListDrawableState mListDrawableState;
public WidgetsListHeader(Context context) {
this(context, /* attrs= */ null);
@@ -86,6 +92,11 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd
R.styleable.WidgetsListRowHeader, defStyleAttr, /* defStyleRes= */ 0);
mIconSize = a.getDimensionPixelSize(R.styleable.WidgetsListRowHeader_appIconSize,
grid.iconSizePx);
mIsTwoPane = grid.isLandscape && grid.isTablet && LARGE_SCREEN_WIDGET_PICKER.get();
if (mIsTwoPane) {
setLargeScreenTheme();
}
}
@Override
@@ -95,6 +106,9 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd
mTitle = findViewById(R.id.app_title);
mSubtitle = findViewById(R.id.app_subtitle);
mExpandToggle = findViewById(R.id.toggle);
if (mIsTwoPane) {
mExpandToggle.setVisibility(GONE);
}
setAccessibilityDelegate(new AccessibilityDelegate() {
@Override
@@ -132,7 +146,7 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd
@Nullable OnExpansionChangeListener onExpandChangeListener) {
// Use the entire touch area of this view to expand / collapse an app widgets section.
setOnClickListener(view -> {
setExpanded(!mIsExpanded);
setExpanded(mIsTwoPane || !mIsExpanded);
if (onExpandChangeListener != null) {
onExpandChangeListener.onExpansionChange(mIsExpanded);
}
@@ -144,8 +158,38 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd
public void setExpanded(boolean isExpanded) {
this.mIsExpanded = isExpanded;
mExpandToggle.setChecked(isExpanded);
if (mIsTwoPane) {
if (Utilities.isDarkTheme(getContext())) {
if (mIsExpanded) {
mTitle.setTextColor(Color.BLACK);
mSubtitle.setTextColor(Color.BLACK);
} else {
mTitle.setTextColor(Color.WHITE);
mSubtitle.setTextColor(Themes.getAttrColor(getContext(),
android.R.attr.textColorSecondary));
}
}
setLargeScreenTheme();
}
}
/**
* Sets the style for the header when we are using large screens in landscape.
*/
private void setLargeScreenTheme() {
if (mBackground == null) {
mBackground = new GradientDrawable();
mBackground.setCornerRadius((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
28,
getContext().getResources().getDisplayMetrics()));
}
mBackground.setColor(mIsExpanded
? getResources().getColor(R.color.widget_picker_background_selected)
: Color.TRANSPARENT);
this.setBackground(mBackground);
}
/** Sets the {@link WidgetsListDrawableState} and refreshes the background drawable. */
@UiThread
public void setListDrawableState(WidgetsListDrawableState state) {
@@ -163,7 +207,7 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd
@UiThread
private void applyIconAndLabel(WidgetsListHeaderEntry entry) {
PackageItemInfo info = entry.mPkgItem;
setIcon(info);
setIcon(info.newIcon(getContext()));
setTitles(entry);
setExpanded(entry.isWidgetListShown());
@@ -172,9 +216,7 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd
verifyHighRes();
}
private void setIcon(PackageItemInfo info) {
Drawable icon;
icon = info.newIcon(getContext());
void setIcon(Drawable icon) {
applyDrawables(icon);
mIconDrawable = icon;
if (mIconDrawable != null) {
@@ -239,7 +281,7 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd
@UiThread
private void applyIconAndLabel(WidgetsListSearchHeaderEntry entry) {
PackageItemInfo info = entry.mPkgItem;
setIcon(info);
setIcon(info.newIcon(getContext()));
setTitles(entry);
setExpanded(entry.isWidgetListShown());
@@ -265,7 +307,7 @@ public final class WidgetsListHeader extends LinearLayout implements ItemInfoUpd
// Optimization: Starting in N, pre-uploads the bitmap to RenderThread.
info.bitmap.icon.prepareToDraw();
setIcon((PackageItemInfo) info);
setIcon(info.newIcon(getContext()));
mEnableIconUpdateAnimation = false;
}
@@ -56,6 +56,7 @@ public final class WidgetsListSearchHeaderViewHolderBinder implements
WidgetsListSearchHeaderEntry data, @ListPosition int position, List<Object> payloads) {
WidgetsListHeader widgetsListHeader = viewHolder.mWidgetsListHeader;
widgetsListHeader.applyFromItemInfoWithIcon(data);
widgetsListHeader.setSelected(data.isWidgetListShown());
widgetsListHeader.setExpanded(data.isWidgetListShown());
widgetsListHeader.setListDrawableState(
WidgetsListDrawableState.obtain(
@@ -110,13 +110,8 @@ public final class WidgetsListTableViewHolderBinder
// When preview loads, notify adapter to rebind the item and possibly animate
widget.applyFromCellItem(widgetItem, 1f,
bitmap -> {
if (holder.getBindingAdapter() != null) {
holder.getBindingAdapter().notifyItemChanged(
holder.getBindingAdapterPosition(),
Pair.create(widgetItem, bitmap));
}
}, holder.previewCache.get(widgetItem));
bitmap -> holder.onPreviewLoaded(Pair.create(widgetItem, bitmap)),
holder.previewCache.get(widgetItem));
}
}
}
@@ -16,6 +16,7 @@
package com.android.launcher3.widget.picker;
import android.graphics.Bitmap;
import android.util.Pair;
import android.view.View;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
@@ -25,16 +26,33 @@ import com.android.launcher3.model.WidgetItem;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
/** A {@link ViewHolder} for showing widgets of an app in the full widget picker. */
public final class WidgetsRowViewHolder extends ViewHolder {
public final WidgetsListTableView tableContainer;
public final Map<WidgetItem, Bitmap> previewCache = new HashMap<>();
Consumer<Pair<WidgetItem, Bitmap>> mDataCallback;
public WidgetsRowViewHolder(View v) {
super(v);
tableContainer = v.findViewById(R.id.widgets_table);
}
/**
* When the preview is loaded we callback to notify that the preview loaded and we rebind the
* view.
*
* @param data is the payload which is needed when binding the view.
*/
public void onPreviewLoaded(Pair<WidgetItem, Bitmap> data) {
if (mDataCallback != null) {
mDataCallback.accept(data);
}
if (getBindingAdapter() != null) {
getBindingAdapter().notifyItemChanged(getBindingAdapterPosition(), data);
}
}
}
@@ -86,7 +86,7 @@ public final class WidgetsListAdapterTest {
mTestProfile.numColumns = 5;
mUserHandle = Process.myUserHandle();
mAdapter = new WidgetsListAdapter(mContext, mMockLayoutInflater,
mIconCache, () -> 0, null, null);
mIconCache, () -> 0, null, null, null);
mAdapter.registerAdapterDataObserver(mListener);
doAnswer(invocation -> ((ComponentWithLabel) invocation.getArgument(0))