Add personal / work tabs for work profile widgets

Video: https://drive.google.com/file/d/1TORRNcvVM7fIvNd_WZaajLbI7D9z4VFA/view?usp=sharing


Test: Main profile only: run AddConfigWidgetTest.
      With work profile: manually launch the full widgets sheet.
      Go to the personal tab: only personal widgets are shown.
      Go to the work tab: only work widgets are shown
      Successfully add personal / work widgets from the full widgets
      sheet.

Bug: 179797520

Change-Id: Iad8b90c2af35e0580319d7a05510ec88e4f8b86c
This commit is contained in:
Steven Ng 2021-02-17 15:58:23 +00:00
parent dfdeddc66a
commit 391404fcb7
9 changed files with 222 additions and 75 deletions

View File

@ -27,12 +27,6 @@
android:background="?android:attr/colorPrimary"
android:elevation="4dp">
<com.android.launcher3.widget.picker.WidgetsRecyclerView
android:id="@+id/widgets_list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false" />
<!-- Fast scroller popup -->
<TextView
android:id="@+id/fast_scroller_popup"

View File

@ -0,0 +1,44 @@
<?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">
<include layout="@layout/personal_work_tabs" />
<com.android.launcher3.workprofile.PersonalWorkPagedView
android:id="@+id/widgets_view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/tabs"
android:clipToPadding="false"
android:descendantFocusability="afterDescendants"
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.workprofile.PersonalWorkPagedView>
</merge>

View File

@ -0,0 +1,21 @@
<?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.
-->
<com.android.launcher3.widget.picker.WidgetsRecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/primary_widgets_list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false" />

View File

@ -96,6 +96,8 @@ public abstract class PagedView<T extends View & PageIndicator> extends ViewGrou
private static final int MIN_FLING_VELOCITY = 250;
private boolean mFreeScroll = false;
/** If {@code false}, disable swipe gesture to switch between pages. */
private boolean mSwipeGestureEnabled = true;
protected final int mFlingThresholdVelocity;
protected final int mEasyFlingThresholdVelocity;
@ -857,6 +859,14 @@ public abstract class PagedView<T extends View & PageIndicator> extends ViewGrou
}
}
/**
* If {@code enableSwipeGesture} is {@code true}, enables swipe gesture to navigate between
* pages. Otherwise, disables the navigation gesture.
*/
public void setSwipeGestureEnabled(boolean swipeGestureEnabled) {
mSwipeGestureEnabled = swipeGestureEnabled;
}
/**
* {@inheritDoc}
*/
@ -879,6 +889,8 @@ public abstract class PagedView<T extends View & PageIndicator> extends ViewGrou
* scrolling there.
*/
if (!mSwipeGestureEnabled) return false;
// Skip touch handling if there are no pages to swipe
if (getChildCount() <= 0) return false;

View File

@ -60,6 +60,6 @@ public class PackageItemInfo extends ItemInfoWithIcon {
@Override
public int hashCode() {
return Objects.hash(packageName);
return Objects.hash(packageName, user);
}
}

View File

@ -22,16 +22,22 @@ import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.PropertyValuesHolder;
import android.content.Context;
import android.content.pm.LauncherApps;
import android.graphics.Rect;
import android.os.Process;
import android.os.UserHandle;
import android.util.AttributeSet;
import android.util.Pair;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.recyclerview.widget.RecyclerView;
import com.android.launcher3.Insettable;
import com.android.launcher3.Launcher;
@ -43,30 +49,39 @@ import com.android.launcher3.compat.AccessibilityManagerCompat;
import com.android.launcher3.views.RecyclerViewFastScroller;
import com.android.launcher3.views.TopRoundedCornerView;
import com.android.launcher3.widget.BaseWidgetSheet;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import com.android.launcher3.workprofile.PersonalWorkPagedView;
import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener;
import java.util.List;
import java.util.function.Predicate;
/**
* Popup for showing the full list of available widgets
*/
public class WidgetsFullSheet extends BaseWidgetSheet
implements Insettable, ProviderChangedListener {
implements Insettable, ProviderChangedListener, OnActivePageChangedListener {
private static final long DEFAULT_OPEN_DURATION = 267;
private static final long FADE_IN_DURATION = 150;
private static final float VERTICAL_START_POSITION = 0.3f;
private final Rect mInsets = new Rect();
private final boolean mHasWorkProfile;
private final SparseArray<AdapterHolder> mAdapters = new SparseArray();
private final UserHandle mCurrentUser = Process.myUserHandle();
private final Predicate<WidgetsListBaseEntry> mPrimaryWidgetsFilter = entry ->
mCurrentUser.equals(entry.mPkgItem.user);
private final Predicate<WidgetsListBaseEntry> mWorkWidgetsFilter =
mPrimaryWidgetsFilter.negate();
private final WidgetsListAdapter mAdapter;
private WidgetsRecyclerView mRecyclerView;
@Nullable private PersonalWorkPagedView mViewPager;
public WidgetsFullSheet(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LauncherAppState apps = LauncherAppState.getInstance(context);
mAdapter = new WidgetsListAdapter(context,
LayoutInflater.from(context), apps.getWidgetCache(), apps.getIconCache(),
this, this);
mHasWorkProfile = context.getSystemService(LauncherApps.class).getProfiles().size() > 1;
mAdapters.put(AdapterHolder.PRIMARY, new AdapterHolder(AdapterHolder.PRIMARY));
mAdapters.put(AdapterHolder.WORK, new AdapterHolder(AdapterHolder.WORK));
}
public WidgetsFullSheet(Context context, AttributeSet attrs) {
@ -77,25 +92,51 @@ public class WidgetsFullSheet extends BaseWidgetSheet
protected void onFinishInflate() {
super.onFinishInflate();
mContent = findViewById(R.id.container);
mRecyclerView = findViewById(R.id.widgets_list_view);
mRecyclerView.setAdapter(mAdapter);
mAdapter.setApplyBitmapDeferred(true, mRecyclerView);
TopRoundedCornerView springLayout = (TopRoundedCornerView) mContent;
springLayout.addSpringView(R.id.widgets_list_view);
mRecyclerView.setEdgeEffectFactory(springLayout.createEdgeEffectFactory());
LayoutInflater layoutInflater = LayoutInflater.from(getContext());
int contentLayoutRes = mHasWorkProfile ? R.layout.widgets_full_sheet_paged_view
: R.layout.widgets_full_sheet_recyclerview;
layoutInflater.inflate(contentLayoutRes, springLayout, true);
if (mHasWorkProfile) {
mViewPager = findViewById(R.id.widgets_view_pager);
// Temporarily disable swipe gesture until widgets list horizontal scrollviews per
// app are replaced by gird views.
mViewPager.setSwipeGestureEnabled(false);
mViewPager.initParentViews(this);
mViewPager.getPageIndicator().setOnActivePageChangedListener(this);
mViewPager.getPageIndicator().setActiveMarker(AdapterHolder.PRIMARY);
findViewById(R.id.tab_personal)
.setOnClickListener((View view) -> mViewPager.snapToPage(0));
findViewById(R.id.tab_work)
.setOnClickListener((View view) -> mViewPager.snapToPage(1));
springLayout.addSpringView(R.id.primary_widgets_list_view);
springLayout.addSpringView(R.id.work_widgets_list_view);
} else {
mViewPager = null;
springLayout.addSpringView(R.id.primary_widgets_list_view);
}
onWidgetsBound();
}
@Override
public void onActivePageChanged(int currentActivePage) {
mAdapters.get(currentActivePage).mWidgetsRecyclerView.bindFastScrollbar();
}
@VisibleForTesting
public WidgetsRecyclerView getRecyclerView() {
return mRecyclerView;
if (!mHasWorkProfile || mViewPager.getCurrentPage() == AdapterHolder.PRIMARY) {
return mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView;
}
return mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView;
}
@Override
protected Pair<View, String> getAccessibilityTarget() {
return Pair.create(mRecyclerView, getContext().getString(
return Pair.create(getRecyclerView(), getContext().getString(
mIsOpen ? R.string.widgets_list : R.string.widgets_list_closed));
}
@ -116,9 +157,10 @@ public class WidgetsFullSheet extends BaseWidgetSheet
public void setInsets(Rect insets) {
mInsets.set(insets);
mRecyclerView.setPadding(
mRecyclerView.getPaddingLeft(), mRecyclerView.getPaddingTop(),
mRecyclerView.getPaddingRight(), insets.bottom);
setBottomPadding(mAdapters.get(AdapterHolder.PRIMARY).mWidgetsRecyclerView, insets.bottom);
if (mHasWorkProfile) {
setBottomPadding(mAdapters.get(AdapterHolder.WORK).mWidgetsRecyclerView, insets.bottom);
}
if (insets.bottom > 0) {
setupNavBarColor();
} else {
@ -129,6 +171,14 @@ public class WidgetsFullSheet extends BaseWidgetSheet
requestLayout();
}
private void setBottomPadding(RecyclerView recyclerView, int bottomPadding) {
recyclerView.setPadding(
recyclerView.getPaddingLeft(),
recyclerView.getPaddingTop(),
recyclerView.getPaddingRight(),
bottomPadding);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthUsed;
@ -168,7 +218,17 @@ public class WidgetsFullSheet extends BaseWidgetSheet
@Override
public void onWidgetsBound() {
mAdapter.setWidgets(mLauncher.getPopupDataProvider().getAllWidgets());
List<WidgetsListBaseEntry> allWidgets = mLauncher.getPopupDataProvider().getAllWidgets();
AdapterHolder primaryUserAdapterHolder = mAdapters.get(AdapterHolder.PRIMARY);
primaryUserAdapterHolder.setup(findViewById(R.id.primary_widgets_list_view));
primaryUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets);
if (mHasWorkProfile) {
AdapterHolder workUserAdapterHolder = mAdapters.get(AdapterHolder.WORK);
workUserAdapterHolder.setup(findViewById(R.id.work_widgets_list_view));
workUserAdapterHolder.mWidgetsListAdapter.setWidgets(allWidgets);
onActivePageChanged(mViewPager.getCurrentPage());
}
}
private void open(boolean animate) {
@ -183,12 +243,9 @@ public class WidgetsFullSheet extends BaseWidgetSheet
.setDuration(DEFAULT_OPEN_DURATION)
.setInterpolator(AnimationUtils.loadInterpolator(
getContext(), android.R.interpolator.linear_out_slow_in));
mRecyclerView.setLayoutFrozen(true);
mOpenCloseAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mRecyclerView.setLayoutFrozen(false);
mAdapter.setApplyBitmapDeferred(false, mRecyclerView);
mOpenCloseAnimator.removeListener(this);
}
});
@ -198,7 +255,6 @@ public class WidgetsFullSheet extends BaseWidgetSheet
});
} else {
setTranslationShift(TRANSLATION_SHIFT_OPENED);
mAdapter.setApplyBitmapDeferred(false, mRecyclerView);
post(this::announceAccessibilityChanges);
}
}
@ -218,12 +274,12 @@ public class WidgetsFullSheet extends BaseWidgetSheet
// Disable swipe down when recycler view is scrolling
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
mNoIntercept = false;
RecyclerViewFastScroller scroller = mRecyclerView.getScrollbar();
RecyclerViewFastScroller scroller = getRecyclerView().getScrollbar();
if (scroller.getThumbOffsetY() >= 0
&& getPopupContainer().isEventOverView(scroller, ev)) {
mNoIntercept = true;
} else if (getPopupContainer().isEventOverView(mContent, ev)) {
mNoIntercept = !mRecyclerView.shouldContainerScroll(ev, getPopupContainer());
mNoIntercept = !getRecyclerView().shouldContainerScroll(ev, getPopupContainer());
}
}
return super.onControllerInterceptTouchEvent(ev);
@ -242,14 +298,14 @@ public class WidgetsFullSheet extends BaseWidgetSheet
/** Gets the {@link WidgetsRecyclerView} which shows all widgets in {@link WidgetsFullSheet}. */
@VisibleForTesting
public static WidgetsRecyclerView getWidgetsView(Launcher launcher) {
return launcher.findViewById(R.id.widgets_list_view);
return launcher.findViewById(R.id.primary_widgets_list_view);
}
@Override
public void addHintCloseAnim(
float distanceToMove, Interpolator interpolator, PendingAnimation target) {
target.setFloat(mRecyclerView, VIEW_TRANSLATE_Y, -distanceToMove, interpolator);
target.setViewAlpha(mRecyclerView, 0.5f, interpolator);
target.setFloat(getRecyclerView(), VIEW_TRANSLATE_Y, -distanceToMove, interpolator);
target.setViewAlpha(getRecyclerView(), 0.5f, interpolator);
}
@Override
@ -257,4 +313,39 @@ public class WidgetsFullSheet extends BaseWidgetSheet
super.onCloseComplete();
AccessibilityManagerCompat.sendStateEventToTest(getContext(), NORMAL_STATE_ORDINAL);
}
/** A holder class for holding adapters & their corresponding recycler view. */
private final class AdapterHolder {
static final int PRIMARY = 0;
static final int WORK = 1;
private final int mAdapterType;
private final WidgetsListAdapter mWidgetsListAdapter;
private WidgetsRecyclerView mWidgetsRecyclerView;
AdapterHolder(int adapterType) {
mAdapterType = adapterType;
Context context = getContext();
LauncherAppState apps = LauncherAppState.getInstance(context);
mWidgetsListAdapter = new WidgetsListAdapter(
context,
LayoutInflater.from(context),
apps.getWidgetCache(),
apps.getIconCache(),
/* iconClickListener= */ WidgetsFullSheet.this,
/* iconLongClickListener= */ WidgetsFullSheet.this);
mWidgetsListAdapter.setFilter(
mAdapterType == PRIMARY ? mPrimaryWidgetsFilter : mWorkWidgetsFilter);
}
void setup(WidgetsRecyclerView recyclerView) {
mWidgetsRecyclerView = recyclerView;
mWidgetsRecyclerView.setAdapter(mWidgetsListAdapter);
mWidgetsRecyclerView.setEdgeEffectFactory(
((TopRoundedCornerView) mContent).createEdgeEffectFactory());
mWidgetsListAdapter.setApplyBitmapDeferred(false, mWidgetsRecyclerView);
}
}
}

View File

@ -43,6 +43,7 @@ import com.android.launcher3.widget.picker.WidgetsListHeaderViewHolderBinder.OnH
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
@ -74,6 +75,11 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
private ArrayList<WidgetsListBaseEntry> mVisibleEntries = new ArrayList<>();
@Nullable private String mWidgetsContentVisiblePackage = null;
private Predicate<WidgetsListBaseEntry> mHeaderAndSelectedContentFilter = entry ->
entry instanceof WidgetsListHeaderEntry
|| entry.mPkgItem.packageName.equals(mWidgetsContentVisiblePackage);
@Nullable private Predicate<WidgetsListBaseEntry> mFilter = null;
public WidgetsListAdapter(Context context, LayoutInflater layoutInflater,
WidgetPreviewLoader widgetPreviewLoader, IconCache iconCache,
OnClickListener iconClickListener, OnLongClickListener iconLongClickListener) {
@ -85,6 +91,10 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
new WidgetsListHeaderViewHolderBinder(layoutInflater, this::onHeaderClicked));
}
public void setFilter(Predicate<WidgetsListBaseEntry> filter) {
mFilter = filter;
}
/**
* Defers applying bitmap on all the {@link WidgetCell} in the {@param rv}.
*
@ -132,8 +142,8 @@ public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderC
}
});
List<WidgetsListBaseEntry> newVisibleEntries = mAllEntries.stream()
.filter(entry -> entry instanceof WidgetsListHeaderEntry
|| entry.mPkgItem.packageName.equals(mWidgetsContentVisiblePackage))
.filter(entry -> (mFilter == null || mFilter.test(entry))
&& mHeaderAndSelectedContentFilter.test(entry))
.collect(Collectors.toList());
mDiffReporter.process(mVisibleEntries, newVisibleEntries, mRowComparator);
}

View File

@ -9,7 +9,6 @@ import android.appwidget.AppWidgetProviderInfo;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Process;
import android.os.UserHandle;
import android.util.Log;
@ -115,7 +114,7 @@ public class WidgetsModel {
widgetsAndShortcuts.add(new WidgetItem(info, app.getIconCache(), pm));
updatedItems.add(info);
}
setWidgetsAndShortcuts(widgetsAndShortcuts, app, packageUser);
setWidgetsAndShortcuts(widgetsAndShortcuts, app);
} catch (Exception e) {
if (!FeatureFlags.IS_STUDIO_BUILD && Utilities.isBinderSizeError(e)) {
// the returned value may be incomplete and will not be refreshed until the next
@ -132,52 +131,28 @@ public class WidgetsModel {
}
private synchronized void setWidgetsAndShortcuts(ArrayList<WidgetItem> rawWidgetsShortcuts,
LauncherAppState app, @Nullable PackageUserKey packageUser) {
LauncherAppState app) {
if (DEBUG) {
Log.d(TAG, "addWidgetsAndShortcuts, widgetsShortcuts#=" + rawWidgetsShortcuts.size());
}
// Temporary list for {@link PackageItemInfos} to avoid having to go through
// {@link mPackageItemInfos} to locate the key to be used for {@link #mWidgetsList}
HashMap<String, PackageItemInfo> tmpPackageItemInfos = new HashMap<>();
HashMap<PackageUserKey, PackageItemInfo> tmpPackageItemInfos = new HashMap<>();
// clear the lists.
if (packageUser == null) {
mWidgetsList.clear();
} else {
PackageItemInfo packageItem = mWidgetsList.keySet()
.stream()
.filter(item -> item.packageName.equals(packageUser.mPackageName))
.findFirst()
.orElse(null);
if (packageItem != null) {
// We want to preserve the user that was on the packageItem previously,
// so add it to tmpPackageItemInfos here to avoid creating a new entry.
tmpPackageItemInfos.put(packageItem.packageName, packageItem);
// Add the widgets for other users in the rawList as it only contains widgets for
// packageUser
List<WidgetItem> otherUserItems = mWidgetsList.remove(packageItem);
otherUserItems.removeIf(w -> w.user.equals(packageUser.mUser));
rawWidgetsShortcuts.addAll(otherUserItems);
}
}
UserHandle myUser = Process.myUserHandle();
mWidgetsList.clear();
// add and update.
mWidgetsList.putAll(rawWidgetsShortcuts.stream()
.filter(new WidgetValidityCheck(app))
.collect(Collectors.groupingBy(item -> {
String packageName = item.componentName.getPackageName();
PackageItemInfo pInfo = tmpPackageItemInfos.get(packageName);
PackageUserKey packageUserKey = new PackageUserKey(
item.componentName.getPackageName(), item.user);
PackageItemInfo pInfo = tmpPackageItemInfos.get(packageUserKey);
if (pInfo == null) {
pInfo = new PackageItemInfo(packageName);
pInfo.user = item.user;
tmpPackageItemInfos.put(packageName, pInfo);
} else if (!myUser.equals(pInfo.user)) {
// Keep updating the user, until we get the primary user.
pInfo = new PackageItemInfo(packageUserKey.mPackageName);
pInfo.user = item.user;
tmpPackageItemInfos.put(packageUserKey, pInfo);
}
return pInfo;
})));

View File

@ -153,7 +153,7 @@ public final class LauncherInstrumentation {
private static final String WORKSPACE_RES_ID = "workspace";
private static final String APPS_RES_ID = "apps_view";
private static final String OVERVIEW_RES_ID = "overview_panel";
private static final String WIDGETS_RES_ID = "widgets_list_view";
private static final String WIDGETS_RES_ID = "primary_widgets_list_view";
private static final String CONTEXT_MENU_RES_ID = "deep_shortcuts_container";
public static final int WAIT_TIME_MS = 10000;
public static final int LONG_WAIT_TIME_MS = 60000;