Merge "Add CUJ Jank interactions for App Pair saving and launching" into main
diff --git a/quickstep/Android.bp b/quickstep/Android.bp
index a290e84..f14cebd 100644
--- a/quickstep/Android.bp
+++ b/quickstep/Android.bp
@@ -52,5 +52,6 @@
"tests/src/com/android/quickstep/TaplOverviewIconTest.java",
"tests/src/com/android/quickstep/TaplTestsQuickstep.java",
"tests/src/com/android/quickstep/TaplTestsSplitscreen.java",
+ "tests/src/com/android/launcher3/testcomponent/ExcludeFromRecentsTestActivity.java"
],
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
index 36ce049..2710bd9 100644
--- a/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/LauncherTaskbarUIController.java
@@ -414,6 +414,13 @@
}
@Override
+ protected boolean canToggleHomeAllApps() {
+ return mLauncher.isResumed()
+ && !mTaskbarLauncherStateController.isInOverview()
+ && !mLauncher.areFreeformTasksVisible();
+ }
+
+ @Override
public RecentsView getRecentsView() {
return mLauncher.getOverviewPanel();
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 37f1363..aedbe6c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -17,6 +17,7 @@
import static android.content.pm.PackageManager.FEATURE_PC;
import static android.os.Trace.TRACE_TAG_APP;
+import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
@@ -1214,7 +1215,7 @@
.handleAppPairLaunchInApp((AppPairIcon) launchingIconView, itemInfos);
} else {
// Tapped a single app, nothing complicated here.
- startItemInfoActivity(itemInfos.get(0));
+ startItemInfoActivity(itemInfos.get(0), null /*foundTask*/);
}
}
@@ -1255,19 +1256,37 @@
recents.getSplitSelectController().getAppPairsController().launchAppPair(
(AppPairIcon) launchingIconView, -1 /*cuj*/);
} else {
- startItemInfoActivity(itemInfos.get(0));
+ startItemInfoActivity(itemInfos.get(0), foundTask);
}
}
);
}
- private void startItemInfoActivity(ItemInfo info) {
+ /**
+ * Starts an activity with the information provided by the "info" param. However, if
+ * taskInRecents is present, it will prioritize re-launching an existing instance via
+ * {@link ActivityManagerWrapper#startActivityFromRecents(int, ActivityOptions)}
+ */
+ private void startItemInfoActivity(ItemInfo info, @Nullable Task taskInRecents) {
Intent intent = new Intent(info.getIntent())
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try {
TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "start: taskbarAppIcon");
if (info.user.equals(Process.myUserHandle())) {
// TODO(b/216683257): Use startActivityForResult for search results that require it.
+ if (taskInRecents != null) {
+ // Re launch instance from recents
+ ActivityOptionsWrapper opts = getActivityLaunchOptions(null, info);
+ opts.options.setLaunchDisplayId(
+ getDisplay() == null ? DEFAULT_DISPLAY : getDisplay().getDisplayId());
+ if (ActivityManagerWrapper.getInstance()
+ .startActivityFromRecents(taskInRecents.key, opts.options)) {
+ mControllers.uiController.getRecentsView()
+ .addSideTaskLaunchCallback(opts.onEndCallback);
+ return;
+ }
+ }
+
startActivity(intent);
} else {
getSystemService(LauncherApps.class).startMainActivity(
@@ -1566,4 +1585,8 @@
public void closeKeyboardQuickSwitchView() {
mControllers.keyboardQuickSwitchController.closeQuickSwitchView(false);
}
+
+ boolean canToggleHomeAllApps() {
+ return mControllers.uiController.canToggleHomeAllApps();
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
index a14e3fd..8d48154 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarLauncherStateController.java
@@ -205,13 +205,7 @@
mLauncherState = finalState;
updateStateForFlag(FLAG_LAUNCHER_IN_STATE_TRANSITION, false);
applyState();
- boolean disallowLongClick =
- FeatureFlags.enableSplitContextually()
- ? mLauncher.isSplitSelectionActive()
- : finalState == LauncherState.OVERVIEW_SPLIT_SELECT;
- com.android.launcher3.taskbar.Utilities.setOverviewDragState(
- mControllers, finalState.disallowTaskbarGlobalDrag(),
- disallowLongClick, finalState.allowTaskbarInitialSplitSelection());
+ updateOverviewDragState(finalState);
}
};
@@ -256,6 +250,7 @@
mCanSyncViews = true;
mLauncher.addOnDeviceProfileChangeListener(mOnDeviceProfileChangeListener);
+ updateOverviewDragState(mLauncherState);
}
public void onDestroy() {
@@ -328,7 +323,7 @@
updateStateForSysuiFlags(systemUiStateFlags, /* applyState */ true);
}
- private void updateStateForSysuiFlags(int systemUiStateFlags, boolean applyState) {
+ private void updateStateForSysuiFlags(int systemUiStateFlags, boolean applyState) {
final boolean prevIsAwake = hasAnyFlag(FLAG_AWAKE);
final boolean currIsAwake = hasAnyFlag(systemUiStateFlags, SYSUI_STATE_AWAKE);
@@ -358,6 +353,21 @@
}
/**
+ * Updates overview drag state on various controllers based on {@link #mLauncherState}.
+ *
+ * @param launcherState The current state launcher is in
+ */
+ private void updateOverviewDragState(LauncherState launcherState) {
+ boolean disallowLongClick =
+ FeatureFlags.enableSplitContextually()
+ ? mLauncher.isSplitSelectionActive()
+ : launcherState == LauncherState.OVERVIEW_SPLIT_SELECT;
+ com.android.launcher3.taskbar.Utilities.setOverviewDragState(
+ mControllers, launcherState.disallowTaskbarGlobalDrag(),
+ disallowLongClick, launcherState.allowTaskbarInitialSplitSelection());
+ }
+
+ /**
* Updates the proper flag to change the state of the task bar.
*
* Note that this only updates the flag. {@link #applyState()} needs to be called separately.
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index ff33ca9..ecbc7e7 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -23,7 +23,6 @@
import static com.android.launcher3.BaseActivity.EVENT_DESTROYED;
import static com.android.launcher3.Flags.enableUnfoldStateAnimation;
-import static com.android.launcher3.LauncherState.OVERVIEW;
import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
import static com.android.launcher3.config.FeatureFlags.enableTaskbarNoRecreate;
import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
@@ -69,6 +68,7 @@
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.SettingsCache;
import com.android.launcher3.util.SimpleBroadcastReceiver;
+import com.android.quickstep.AllAppsActionManager;
import com.android.quickstep.RecentsActivity;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.TouchInteractionService;
@@ -158,6 +158,8 @@
private final SimpleBroadcastReceiver mTaskbarBroadcastReceiver =
new SimpleBroadcastReceiver(this::showTaskbarFromBroadcast);
+ private final AllAppsActionManager mAllAppsActionManager;
+
private final Runnable mActivityOnDestroyCallback = new Runnable() {
@Override
public void run() {
@@ -212,12 +214,14 @@
private Boolean mFolded;
@SuppressLint("WrongConstant")
- public TaskbarManager(TouchInteractionService service) {
+ public TaskbarManager(
+ TouchInteractionService service, AllAppsActionManager allAppsActionManager) {
Display display =
service.getSystemService(DisplayManager.class).getDisplay(DEFAULT_DISPLAY);
mContext = service.createWindowContext(display,
ENABLE_TASKBAR_NAVBAR_UNIFICATION ? TYPE_NAVIGATION_BAR : TYPE_NAVIGATION_BAR_PANEL,
null);
+ mAllAppsActionManager = allAppsActionManager;
mNavigationBarPanelContext = ENABLE_TASKBAR_NAVBAR_UNIFICATION
? service.createWindowContext(display, TYPE_NAVIGATION_BAR_PANEL, null)
: null;
@@ -291,10 +295,10 @@
recreateTaskbar();
} else {
// Config change might be handled without re-creating the taskbar
- if (dp != null && !isTaskbarPresent(dp)) {
+ if (dp != null && !isTaskbarEnabled(dp)) {
destroyExistingTaskbar();
} else {
- if (dp != null && isTaskbarPresent(dp)) {
+ if (dp != null && isTaskbarEnabled(dp)) {
if (ENABLE_TASKBAR_NAVBAR_UNIFICATION) {
// Re-initialize for screen size change? Should this be done
// by looking at screen-size change flag in configDiff in the
@@ -349,7 +353,7 @@
}
DeviceProfile dp = mUserUnlocked ?
LauncherAppState.getIDP(mContext).getDeviceProfile(mContext) : null;
- if (dp == null || !isTaskbarPresent(dp)) {
+ if (dp == null || !isTaskbarEnabled(dp)) {
removeTaskbarRootViewFromWindow();
}
}
@@ -369,20 +373,11 @@
* @param homeAllAppsIntent Intent used if Taskbar is not enabled or Launcher is resumed.
*/
public void toggleAllApps(Intent homeAllAppsIntent) {
- if (mTaskbarActivityContext == null) {
+ if (mTaskbarActivityContext == null || mTaskbarActivityContext.canToggleHomeAllApps()) {
mContext.startActivity(homeAllAppsIntent);
- return;
+ } else {
+ mTaskbarActivityContext.toggleAllAppsSearch();
}
-
- if (mActivity != null
- && mActivity.isResumed()
- && !mActivity.isInState(OVERVIEW)
- && !(mActivity instanceof QuickstepLauncher l && l.areFreeformTasksVisible())) {
- mContext.startActivity(homeAllAppsIntent);
- return;
- }
-
- mTaskbarActivityContext.toggleAllAppsSearch();
}
/**
@@ -477,9 +472,12 @@
DeviceProfile dp = mUserUnlocked ?
LauncherAppState.getIDP(mContext).getDeviceProfile(mContext) : null;
+ // All Apps action is unrelated to navbar unification, so we only need to check DP.
+ mAllAppsActionManager.setTaskbarPresent(dp != null && dp.isTaskbarPresent);
+
destroyExistingTaskbar();
- boolean isTaskbarEnabled = dp != null && isTaskbarPresent(dp);
+ boolean isTaskbarEnabled = dp != null && isTaskbarEnabled(dp);
debugWhyTaskbarNotDestroyed("recreateTaskbar: isTaskbarEnabled=" + isTaskbarEnabled
+ " [dp != null (i.e. mUserUnlocked)]=" + (dp != null)
+ " FLAG_HIDE_NAVBAR_WINDOW=" + ENABLE_TASKBAR_NAVBAR_UNIFICATION
@@ -544,7 +542,7 @@
}
}
- private static boolean isTaskbarPresent(DeviceProfile deviceProfile) {
+ private static boolean isTaskbarEnabled(DeviceProfile deviceProfile) {
return ENABLE_TASKBAR_NAVBAR_UNIFICATION || deviceProfile.isTaskbarPresent;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index efe1e39..109400e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -197,6 +197,11 @@
return false;
}
+ /** Returns {@code true} if Home All Apps available instead of Taskbar All Apps. */
+ protected boolean canToggleHomeAllApps() {
+ return false;
+ }
+
@CallSuper
protected void dumpLogs(String prefix, PrintWriter pw) {
pw.println(String.format(
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
index a92e77a..8ef35c0 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -49,7 +49,6 @@
import static com.android.launcher3.util.NavigationMode.THREE_BUTTONS;
import static com.android.launcher3.util.VibratorWrapper.OVERVIEW_HAPTIC;
import static com.android.launcher3.util.window.RefreshRateTracker.getSingleFrameMs;
-import static com.android.quickstep.views.DesktopTaskView.isDesktopModeSupported;
import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
import static com.android.quickstep.views.RecentsView.CONTENT_ALPHA;
import static com.android.quickstep.views.RecentsView.FULLSCREEN_PROGRESS;
@@ -178,10 +177,6 @@
if ((stateFlags & SYSUI_STATE_OVERVIEW_DISABLED) != 0) {
return false;
}
- if (isDesktopModeSupported()) {
- // TODO(b/268075592): add support for quickswitch to/from desktop
- return false;
- }
if (isTrackpadMultiFingerSwipe(ev)) {
return isTrackpadFourFingerSwipe(ev);
}
@@ -324,7 +319,6 @@
@Override
public void onDragEnd(PointF velocity) {
- cancelAnimations();
boolean horizontalFling = mSwipeDetector.isFling(velocity.x);
boolean verticalFling = mSwipeDetector.isFling(velocity.y);
boolean noFling = !horizontalFling && !verticalFling;
@@ -353,6 +347,7 @@
return;
}
InteractionJankMonitorWrapper.cancel(Cuj.CUJ_LAUNCHER_APP_SWIPE_TO_RECENTS);
+ cancelAnimations();
final LauncherState targetState;
if (horizontalFling && verticalFling) {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
index ff142fe..de73630 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java
@@ -30,7 +30,6 @@
import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_FADE;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_WORKSPACE_TRANSLATE;
import static com.android.launcher3.util.SystemUiController.UI_STATE_FULLSCREEN_TASK;
-import static com.android.quickstep.views.DesktopTaskView.isDesktopModeSupported;
import static com.android.quickstep.views.RecentsView.ADJACENT_PAGE_HORIZONTAL_OFFSET;
import static com.android.quickstep.views.RecentsView.RECENTS_SCALE_PROPERTY;
import static com.android.quickstep.views.RecentsView.UPDATE_SYSUI_FLAGS_THRESHOLD;
@@ -79,10 +78,6 @@
if ((ev.getEdgeFlags() & Utilities.EDGE_NAV_BAR) == 0) {
return false;
}
- if (isDesktopModeSupported()) {
- // TODO(b/268075592): add support for quickswitch to/from desktop
- return false;
- }
return true;
}
diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
index 4752225..181bda9 100644
--- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
+++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java
@@ -1255,7 +1255,11 @@
return LAST_TASK;
}
- if (isDesktopModeSupported() && endTarget == NEW_TASK) {
+ if (((mRecentsView.getNextPageTaskView() != null
+ && mRecentsView.getNextPageTaskView().isDesktopTask())
+ || (mRecentsView.getCurrentPageTaskView() != null
+ && mRecentsView.getCurrentPageTaskView().isDesktopTask()))
+ && endTarget == NEW_TASK) {
// TODO(b/268075592): add support for quickswitch to/from desktop
return LAST_TASK;
}
@@ -1416,9 +1420,11 @@
mGestureState.setState(STATE_RECENTS_SCROLLING_FINISHED);
setClampScrollOffset(false);
};
- if (mRecentsView != null) {
+ if (mRecentsView != null && (mRecentsView.getCurrentPageTaskView() != null
+ && !mRecentsView.getCurrentPageTaskView().isDesktopTask())) {
ActiveGestureLog.INSTANCE.trackEvent(ActiveGestureErrorDetector.GestureEvent
.SET_ON_PAGE_TRANSITION_END_CALLBACK);
+ // TODO(b/268075592): add support for quickswitch to/from desktop
mRecentsView.setOnPageTransitionEndCallback(onPageTransitionEnd);
} else {
onPageTransitionEnd.run();
@@ -2232,6 +2238,15 @@
mRecentsAnimationController, mRecentsAnimationTargets);
});
+ if ((mRecentsView.getNextPageTaskView() != null
+ && mRecentsView.getNextPageTaskView().isDesktopTask())
+ || (mRecentsView.getCurrentPageTaskView() != null
+ && mRecentsView.getCurrentPageTaskView().isDesktopTask())) {
+ // TODO(b/268075592): add support for quickswitch to/from desktop
+ mRecentsViewScrollLinked = false;
+ return;
+ }
+
// Disable scrolling in RecentsView for trackpad 3-finger swipe up gesture.
if (!mGestureState.isThreeFingerTrackpadGesture()) {
mRecentsViewScrollLinked = true;
diff --git a/quickstep/src/com/android/quickstep/AllAppsActionManager.kt b/quickstep/src/com/android/quickstep/AllAppsActionManager.kt
new file mode 100644
index 0000000..fd2ed3a
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/AllAppsActionManager.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2024 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.quickstep
+
+import android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS
+import android.app.PendingIntent
+import android.app.RemoteAction
+import android.content.Context
+import android.graphics.drawable.Icon
+import android.view.accessibility.AccessibilityManager
+import com.android.launcher3.R
+import java.util.concurrent.Executor
+
+/**
+ * Registers a [RemoteAction] for toggling All Apps if needed.
+ *
+ * We need this action when either [isHomeAndOverviewSame] or [isTaskbarPresent] is `true`. When
+ * home and overview are the same, we can control Launcher's or Taskbar's All Apps tray. If they are
+ * not the same, but Taskbar is present, we can only control Taskbar's tray.
+ */
+class AllAppsActionManager(
+ private val context: Context,
+ private val bgExecutor: Executor,
+ private val createAllAppsPendingIntent: () -> PendingIntent,
+) {
+
+ /** `true` if home and overview are the same Activity. */
+ var isHomeAndOverviewSame = false
+ set(value) {
+ field = value
+ updateSystemAction()
+ }
+
+ /** `true` if Taskbar is enabled. */
+ var isTaskbarPresent = false
+ set(value) {
+ field = value
+ updateSystemAction()
+ }
+
+ /** `true` if the action should be registered. */
+ var isActionRegistered = false
+ private set
+
+ private fun updateSystemAction() {
+ val shouldRegisterAction = isHomeAndOverviewSame || isTaskbarPresent
+ if (isActionRegistered == shouldRegisterAction) return
+ isActionRegistered = shouldRegisterAction
+
+ bgExecutor.execute {
+ val accessibilityManager =
+ context.getSystemService(AccessibilityManager::class.java) ?: return@execute
+ if (shouldRegisterAction) {
+ accessibilityManager.registerSystemAction(
+ RemoteAction(
+ Icon.createWithResource(context, R.drawable.ic_apps),
+ context.getString(R.string.all_apps_label),
+ context.getString(R.string.all_apps_label),
+ createAllAppsPendingIntent(),
+ ),
+ GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS,
+ )
+ } else {
+ accessibilityManager.unregisterSystemAction(GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS)
+ }
+ }
+ }
+
+ fun onDestroy() {
+ context
+ .getSystemService(AccessibilityManager::class.java)
+ ?.unregisterSystemAction(
+ GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS,
+ )
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 719c4f7..b43c520 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -31,6 +31,7 @@
import static com.android.launcher3.MotionEventsUtils.isTrackpadMultiFingerSwipe;
import static com.android.launcher3.config.FeatureFlags.ENABLE_TRACKPAD_GESTURE;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static com.android.launcher3.util.OnboardingPrefs.HOME_BOUNCE_SEEN;
import static com.android.launcher3.util.window.WindowManagerProxy.MIN_TABLET_WIDTH;
import static com.android.quickstep.GestureState.DEFAULT_STATE;
@@ -59,14 +60,12 @@
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_STARTING_WINDOW;
import android.app.PendingIntent;
-import android.app.RemoteAction;
import android.app.Service;
import android.content.IIntentReceiver;
import android.content.IIntentSender;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Region;
-import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Looper;
@@ -77,7 +76,6 @@
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.MotionEvent;
-import android.view.accessibility.AccessibilityManager;
import androidx.annotation.BinderThread;
import androidx.annotation.NonNull;
@@ -88,7 +86,6 @@
import com.android.launcher3.ConstantItem;
import com.android.launcher3.EncryptionType;
import com.android.launcher3.LauncherPrefs;
-import com.android.launcher3.R;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.provider.RestoreDbTask;
@@ -101,7 +98,6 @@
import com.android.launcher3.uioverrides.flags.FlagsFactory;
import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
import com.android.launcher3.util.DisplayController;
-import com.android.launcher3.util.Executors;
import com.android.launcher3.util.LockedUserState;
import com.android.launcher3.util.SafeCloseable;
import com.android.launcher3.util.ScreenOnTracker;
@@ -488,6 +484,7 @@
private TaskbarManager mTaskbarManager;
private Function<GestureState, AnimatedFloat> mSwipeUpProxyProvider = i -> null;
+ private AllAppsActionManager mAllAppsActionManager;
@Override
public void onCreate() {
@@ -497,7 +494,9 @@
mMainChoreographer = Choreographer.getInstance();
mAM = ActivityManagerWrapper.getInstance();
mDeviceState = new RecentsAnimationDeviceState(this, true);
- mTaskbarManager = new TaskbarManager(this);
+ mAllAppsActionManager = new AllAppsActionManager(
+ this, UI_HELPER_EXECUTOR, this::createAllAppsPendingIntent);
+ mTaskbarManager = new TaskbarManager(this, mAllAppsActionManager);
mRotationTouchHelper = mDeviceState.getRotationTouchHelper();
mInputConsumer = InputConsumerController.getRecentsAnimationInputConsumer();
BootAwarePreloader.start(this);
@@ -590,16 +589,7 @@
}
private void onOverviewTargetChange(boolean isHomeAndOverviewSame) {
- Executors.UI_HELPER_EXECUTOR.execute(() -> {
- AccessibilityManager am = getSystemService(AccessibilityManager.class);
-
- if (isHomeAndOverviewSame) {
- am.registerSystemAction(
- createAllAppsAction(), GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS);
- } else {
- am.unregisterSystemAction(GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS);
- }
- });
+ mAllAppsActionManager.setHomeAndOverviewSame(isHomeAndOverviewSame);
StatefulActivity newOverviewActivity = mOverviewComponentObserver.getActivityInterface()
.getCreatedActivity();
@@ -609,13 +599,12 @@
mTISBinder.onOverviewTargetChange();
}
- private RemoteAction createAllAppsAction() {
+ private PendingIntent createAllAppsPendingIntent() {
final Intent homeIntent = new Intent(mOverviewComponentObserver.getHomeIntent())
.setAction(INTENT_ACTION_ALL_APPS_TOGGLE);
- final PendingIntent actionPendingIntent;
if (FeatureFlags.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get()) {
- actionPendingIntent = new PendingIntent(new IIntentSender.Stub() {
+ return new PendingIntent(new IIntentSender.Stub() {
@Override
public void send(int code, Intent intent, String resolvedType,
IBinder allowlistToken, IIntentReceiver finishedReceiver,
@@ -624,18 +613,12 @@
}
});
} else {
- actionPendingIntent = PendingIntent.getActivity(
+ return PendingIntent.getActivity(
this,
GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS,
homeIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
}
-
- return new RemoteAction(
- Icon.createWithResource(this, R.drawable.ic_apps),
- getString(R.string.all_apps_label),
- getString(R.string.all_apps_label),
- actionPendingIntent);
}
@UiThread
@@ -678,8 +661,7 @@
mDeviceState.destroy();
SystemUiProxy.INSTANCE.get(this).clearProxy();
- getSystemService(AccessibilityManager.class)
- .unregisterSystemAction(GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS);
+ mAllAppsActionManager.onDestroy();
mTaskbarManager.destroy();
sConnected = false;
diff --git a/quickstep/src/com/android/quickstep/util/AssistStateManager.java b/quickstep/src/com/android/quickstep/util/AssistStateManager.java
index 660fc22..a854656 100644
--- a/quickstep/src/com/android/quickstep/util/AssistStateManager.java
+++ b/quickstep/src/com/android/quickstep/util/AssistStateManager.java
@@ -53,12 +53,7 @@
}
/** Return {@code true} if the Settings toggle is enabled. */
- public boolean isSettingsNavHandleEnabled() {
- return false;
- }
-
- /** Return {@code true} if the Settings toggle is enabled. */
- public boolean isSettingsHomeButtonEnabled() {
+ public boolean isSettingsAllEntrypointsEnabled() {
return false;
}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 2ead7a6..af1dd3a 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -1270,6 +1270,8 @@
final SurfaceTransaction showTransaction = new SurfaceTransaction();
for (int i = apps.length - 1; i >= 0; --i) {
showTransaction.getTransaction().show(apps[i].leash);
+ showTransaction.forSurface(apps[i].leash).setLayer(
+ Integer.MAX_VALUE - 1000 + apps[i].prefixOrderIndex);
}
surfaceApplier.scheduleApply(showTransaction);
}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/AllAppsActionManagerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/AllAppsActionManagerTest.kt
new file mode 100644
index 0000000..73b35e8
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/AllAppsActionManagerTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 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.quickstep
+
+import android.app.PendingIntent
+import android.content.IIntentSender
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR
+import com.android.launcher3.util.TestUtil
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Semaphore
+import java.util.concurrent.TimeUnit.SECONDS
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val TIMEOUT = 5L
+
+@RunWith(AndroidJUnit4::class)
+class AllAppsActionManagerTest {
+ private val callbackSemaphore = Semaphore(0)
+ private val bgExecutor = UI_HELPER_EXECUTOR
+
+ private val allAppsActionManager =
+ AllAppsActionManager(
+ InstrumentationRegistry.getInstrumentation().targetContext,
+ bgExecutor,
+ ) {
+ callbackSemaphore.release()
+ PendingIntent(IIntentSender.Default())
+ }
+
+ @Test
+ fun taskbarPresent_actionRegistered() {
+ allAppsActionManager.isTaskbarPresent = true
+ assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue()
+ assertThat(allAppsActionManager.isActionRegistered).isTrue()
+ }
+
+ @Test
+ fun homeAndOverviewSame_actionRegistered() {
+ allAppsActionManager.isHomeAndOverviewSame = true
+ assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue()
+ assertThat(allAppsActionManager.isActionRegistered).isTrue()
+ }
+
+ @Test
+ fun toggleTaskbar_destroyedAfterActionRegistered_actionUnregistered() {
+ allAppsActionManager.isTaskbarPresent = true
+ assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue()
+
+ allAppsActionManager.isTaskbarPresent = false
+ TestUtil.runOnExecutorSync(bgExecutor) {} // Force system action to unregister.
+ assertThat(allAppsActionManager.isActionRegistered).isFalse()
+ }
+
+ @Test
+ fun toggleTaskbar_destroyedBeforeActionRegistered_pendingActionUnregistered() {
+ allAppsActionManager.isTaskbarPresent = true
+ allAppsActionManager.isTaskbarPresent = false
+
+ TestUtil.runOnExecutorSync(bgExecutor) {} // Force system action to unregister.
+ assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue()
+ assertThat(allAppsActionManager.isActionRegistered).isFalse()
+ }
+
+ @Test
+ fun changeHome_sameAsOverviewBeforeActionUnregistered_actionRegisteredAgain() {
+ allAppsActionManager.isHomeAndOverviewSame = true // Initialize to same.
+ assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue()
+
+ allAppsActionManager.isHomeAndOverviewSame = false
+ allAppsActionManager.isHomeAndOverviewSame = true
+ assertThat(callbackSemaphore.tryAcquire(TIMEOUT, SECONDS)).isTrue()
+ assertThat(allAppsActionManager.isActionRegistered).isTrue()
+ }
+}
diff --git a/quickstep/tests/src/com/android/launcher3/testcomponent/ExcludeFromRecentsTestActivity.java b/quickstep/tests/src/com/android/launcher3/testcomponent/ExcludeFromRecentsTestActivity.java
new file mode 100644
index 0000000..68ac3d5
--- /dev/null
+++ b/quickstep/tests/src/com/android/launcher3/testcomponent/ExcludeFromRecentsTestActivity.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 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.testcomponent;
+
+/**
+ * Extension of BaseTestingActivity to help test excludeFromRecents="true".
+ */
+public class ExcludeFromRecentsTestActivity extends BaseTestingActivity {}
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index a53bb4e..e37e5cc 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -21,6 +21,7 @@
import static com.android.quickstep.TaskbarModeSwitchRule.Mode.TRANSIENT;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeFalse;
@@ -37,6 +38,7 @@
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
+import com.android.launcher3.tapl.BaseOverview;
import com.android.launcher3.tapl.LaunchedAppState;
import com.android.launcher3.tapl.LauncherInstrumentation.NavigationModel;
import com.android.launcher3.tapl.Overview;
@@ -54,6 +56,7 @@
import org.junit.After;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -432,6 +435,7 @@
@PortraitLandscape
@TaskbarModeSwitch()
@TestStabilityRule.Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/309820115
+ @Ignore("b/315376057")
@ScreenRecord // b/309820115
public void testOverviewForTablet() throws Exception {
assumeTrue(mLauncher.isTablet());
@@ -581,4 +585,25 @@
mLauncher.getDevice().setOrientationNatural();
}
}
+
+ @Test
+ public void testExcludeFromRecents() throws Exception {
+ startExcludeFromRecentsTestActivity();
+ OverviewTask currentTask = getAndAssertLaunchedApp().switchToOverview().getCurrentTask();
+ // TODO(b/326565120): the expected content description shouldn't be null but for now there
+ // is a bug that causes it to sometimes be for excludeForRecents tasks.
+ assertTrue("Can't find ExcludeFromRecentsTestActivity after entering Overview from it",
+ currentTask.containsContentDescription("ExcludeFromRecents")
+ || currentTask.containsContentDescription(null));
+ // Going home should clear out the excludeFromRecents task.
+ BaseOverview overview = mLauncher.goHome().switchToOverview();
+ if (overview.hasTasks()) {
+ currentTask = overview.getCurrentTask();
+ assertFalse("Found ExcludeFromRecentsTestActivity after entering Overview from Home",
+ currentTask.containsContentDescription("ExcludeFromRecents")
+ || currentTask.containsContentDescription(null));
+ } else {
+ // Presumably the test started with 0 tasks and remains that way after going home.
+ }
+ }
}
diff --git a/res/layout/widget_recommendations.xml b/res/layout/widget_recommendations.xml
index 89821ac..531db2e 100644
--- a/res/layout/widget_recommendations.xml
+++ b/res/layout/widget_recommendations.xml
@@ -28,6 +28,7 @@
android:layout_marginTop="16dp"
android:accessibilityLiveRegion="polite"
android:gravity="center_horizontal"
+ android:layout_gravity="top"
android:lineHeight="20sp"
android:textColor="?attr/widgetPickerTitleColor"
android:textFontWeight="500"
@@ -38,7 +39,7 @@
android:id="@+id/widget_recommendations_page_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
+ android:layout_gravity="center_horizontal|top"
android:elevation="1dp"
android:visibility="gone" />
<!--
@@ -50,8 +51,9 @@
<com.android.launcher3.widget.picker.WidgetRecommendationsView
android:id="@+id/widget_recommendations_view"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
+ android:layout_height="0dp"
android:layout_gravity="center"
+ android:layout_weight="1"
android:background="@drawable/widgets_surface_background"
android:importantForAccessibility="yes"
launcher:pageIndicator="@+id/widget_recommendations_page_indicator" />
diff --git a/res/layout/widgets_two_pane_sheet.xml b/res/layout/widgets_two_pane_sheet.xml
index 8e45740f..6c4810c 100644
--- a/res/layout/widgets_two_pane_sheet.xml
+++ b/res/layout/widgets_two_pane_sheet.xml
@@ -122,7 +122,7 @@
<LinearLayout
android:id="@+id/widget_recommendations_container"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
+ android:layout_height="match_parent"
android:background="@drawable/widgets_surface_background"
android:orientation="vertical"
android:visibility="gone">
diff --git a/res/values/id.xml b/res/values/id.xml
index 198496f..59813ad 100644
--- a/res/values/id.xml
+++ b/res/values/id.xml
@@ -40,6 +40,7 @@
<item type="id" name="cache_entry_tag_id" />
<item type="id" name="saved_clip_children_tag_id" />
+ <item type="id" name="saved_clip_to_padding_tag_id" />
<item type="id" name="saved_floating_widget_foreground" />
<item type="id" name="saved_floating_widget_background" />
diff --git a/src/com/android/launcher3/UtilitiesKt.kt b/src/com/android/launcher3/UtilitiesKt.kt
new file mode 100644
index 0000000..a207d57
--- /dev/null
+++ b/src/com/android/launcher3/UtilitiesKt.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2024 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
+
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewParent
+
+object UtilitiesKt {
+
+ /**
+ * Modify [ViewGroup]'s attribute with type [T]. The overridden attribute is saved by calling
+ * [View.setTag] and can be later restored by [View.getTag].
+ *
+ * @param <T> type of [ViewGroup] attribute. For example, [T] is [Boolean] if modifying
+ * [ViewGroup.setClipChildren]
+ */
+ abstract class ViewGroupAttrModifier<T>(
+ private val targetAttrValue: T,
+ private val tagKey: Int
+ ) {
+ /**
+ * If [targetAttrValue] is different from existing view attribute returned from
+ * [getAttribute], this method will save existing attribute by calling [ViewGroup.setTag].
+ * Then call [setAttribute] to set attribute with [targetAttrValue].
+ */
+ fun saveAndChangeAttribute(viewGroup: ViewGroup) {
+ val oldAttrValue = getAttribute(viewGroup)
+ if (oldAttrValue !== targetAttrValue) {
+ viewGroup.setTag(tagKey, oldAttrValue)
+ setAttribute(viewGroup, targetAttrValue)
+ }
+ }
+
+ /** Restore saved attribute in [saveAndChangeAttribute] by calling [ViewGroup.getTag]. */
+ @Suppress("UNCHECKED_CAST")
+ fun restoreAttribute(viewGroup: ViewGroup) {
+ val oldAttrValue: T = viewGroup.getTag(tagKey) as T ?: return
+ setAttribute(viewGroup, oldAttrValue)
+ viewGroup.setTag(tagKey, null)
+ }
+
+ /** Subclass will override this method to decide how to get [ViewGroup] attribute. */
+ abstract fun getAttribute(viewGroup: ViewGroup): T
+
+ /** Subclass will override this method to decide how to set [ViewGroup] attribute. */
+ abstract fun setAttribute(viewGroup: ViewGroup, attr: T)
+ }
+
+ /** [ViewGroupAttrModifier] to call [ViewGroup.setClipChildren] to false. */
+ @JvmField
+ val CLIP_CHILDREN_FALSE_MODIFIER: ViewGroupAttrModifier<Boolean> =
+ object : ViewGroupAttrModifier<Boolean>(false, R.id.saved_clip_children_tag_id) {
+ override fun getAttribute(viewGroup: ViewGroup): Boolean {
+ return viewGroup.clipChildren
+ }
+
+ override fun setAttribute(viewGroup: ViewGroup, clipChildren: Boolean) {
+ viewGroup.clipChildren = clipChildren
+ }
+ }
+
+ /** [ViewGroupAttrModifier] to call [ViewGroup.setClipToPadding] to false. */
+ @JvmField
+ val CLIP_TO_PADDING_FALSE_MODIFIER: ViewGroupAttrModifier<Boolean> =
+ object : ViewGroupAttrModifier<Boolean>(false, R.id.saved_clip_to_padding_tag_id) {
+ override fun getAttribute(viewGroup: ViewGroup): Boolean {
+ return viewGroup.clipToPadding
+ }
+
+ override fun setAttribute(viewGroup: ViewGroup, clipToPadding: Boolean) {
+ viewGroup.clipToPadding = clipToPadding
+ }
+ }
+
+ /**
+ * Recursively call [ViewGroupAttrModifier.saveAndChangeAttribute] from [View] to its parent
+ * (direct or indirect) inclusive.
+ *
+ * [ViewGroupAttrModifier.saveAndChangeAttribute] will save the existing attribute value on each
+ * view with [View.setTag], which can be restored in [restoreAttributesOnViewTree].
+ *
+ * Note that if parent is null or not a parent of the view, this method will be applied all the
+ * way to root view.
+ *
+ * @param v child view
+ * @param parent direct or indirect parent of child view
+ * @param modifiers list of [ViewGroupAttrModifier] to modify view attribute
+ */
+ @JvmStatic
+ fun modifyAttributesOnViewTree(
+ v: View?,
+ parent: ViewParent?,
+ vararg modifiers: ViewGroupAttrModifier<*>
+ ) {
+ if (v == null) {
+ return
+ }
+ if (v is ViewGroup) {
+ for (modifier in modifiers) {
+ modifier.saveAndChangeAttribute(v)
+ }
+ }
+ if (v === parent) {
+ return
+ }
+ if (v.parent is View) {
+ modifyAttributesOnViewTree(v.parent as View, parent, *modifiers)
+ }
+ }
+
+ /**
+ * Recursively call [ViewGroupAttrModifier.restoreAttribute]} to restore view attributes
+ * previously saved in [ViewGroupAttrModifier.saveAndChangeAttribute] on view to its parent
+ * (direct or indirect) inclusive.
+ *
+ * Note that if parent is null or not a parent of the view, this method will be applied all the
+ * way to root view.
+ *
+ * @param v child view
+ * @param parent direct or indirect parent of child view
+ * @param modifiers list of [ViewGroupAttrModifier] to restore view attributes
+ */
+ @JvmStatic
+ fun restoreAttributesOnViewTree(
+ v: View?,
+ parent: ViewParent?,
+ vararg modifiers: ViewGroupAttrModifier<*>
+ ) {
+ if (v == null) {
+ return
+ }
+ if (v is ViewGroup) {
+ for (modifier in modifiers) {
+ modifier.restoreAttribute(v)
+ }
+ }
+ if (v === parent) {
+ return
+ }
+ if (v.parent is View) {
+ restoreAttributesOnViewTree(v.parent as View, parent, *modifiers)
+ }
+ }
+}
diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
index 31bbe01..66b8216 100644
--- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
@@ -55,6 +55,7 @@
import com.android.launcher3.views.OptionsPopupView;
import com.android.launcher3.views.OptionsPopupView.OptionItem;
import com.android.launcher3.widget.LauncherAppWidgetHostView;
+import com.android.launcher3.widget.PendingAddWidgetInfo;
import com.android.launcher3.widget.util.WidgetSizes;
import java.util.ArrayList;
@@ -413,6 +414,10 @@
announceConfirmation(R.string.item_added_to_workspace);
} else if (item instanceof PendingAddItemInfo) {
PendingAddItemInfo info = (PendingAddItemInfo) item;
+ if (info instanceof PendingAddWidgetInfo widgetInfo
+ && widgetInfo.bindOptions == null) {
+ widgetInfo.bindOptions = widgetInfo.getDefaultSizeOptions(mContext);
+ }
Workspace<?> workspace = mContext.getWorkspace();
workspace.post(
() -> workspace.snapToPage(workspace.getPageIndexForScreenId(screenId))
diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
index e2c5795..3678109 100644
--- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java
+++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java
@@ -24,6 +24,9 @@
import static com.android.launcher3.LauncherState.ALL_APPS_CONTENT;
import static com.android.launcher3.LauncherState.BACKGROUND_APP;
import static com.android.launcher3.LauncherState.NORMAL;
+import static com.android.launcher3.UtilitiesKt.CLIP_CHILDREN_FALSE_MODIFIER;
+import static com.android.launcher3.UtilitiesKt.modifyAttributesOnViewTree;
+import static com.android.launcher3.UtilitiesKt.restoreAttributesOnViewTree;
import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_BOTTOM_SHEET_FADE;
import static com.android.launcher3.states.StateAnimationConfig.ANIM_ALL_APPS_FADE;
@@ -38,12 +41,9 @@
import android.util.FloatProperty;
import android.view.HapticFeedbackConstants;
import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewParent;
import android.view.animation.Interpolator;
import androidx.annotation.FloatRange;
-import androidx.annotation.Nullable;
import com.android.app.animation.Interpolators;
import com.android.launcher3.DeviceProfile;
@@ -306,9 +306,11 @@
if (hasScaleEffect != mHasScaleEffect) {
mHasScaleEffect = hasScaleEffect;
if (mHasScaleEffect) {
- setClipChildrenOnViewTree(rv, mLauncher.getAppsView(), false);
+ modifyAttributesOnViewTree(rv, mLauncher.getAppsView(),
+ CLIP_CHILDREN_FALSE_MODIFIER);
} else {
- restoreClipChildrenOnViewTree(rv, mLauncher.getAppsView());
+ restoreAttributesOnViewTree(rv, mLauncher.getAppsView(),
+ CLIP_CHILDREN_FALSE_MODIFIER);
}
}
}
@@ -423,79 +425,6 @@
}
/**
- * Recursively call {@link ViewGroup#setClipChildren(boolean)} from {@link View} to ts parent
- * (direct or indirect) inclusive. This method will also save the old clipChildren value on each
- * view with {@link View#setTag(int, Object)}, which can be restored in
- * {@link #restoreClipChildrenOnViewTree(View, ViewParent)}.
- *
- * Note that if parent is null or not a parent of the view, this method will be applied all the
- * way to root view.
- *
- * @param v child view
- * @param parent direct or indirect parent of child view
- * @param clipChildren whether we should clip children
- */
- private static void setClipChildrenOnViewTree(
- @Nullable View v,
- @Nullable ViewParent parent,
- boolean clipChildren) {
- if (v == null) {
- return;
- }
-
- if (v instanceof ViewGroup) {
- ViewGroup viewGroup = (ViewGroup) v;
- boolean oldClipChildren = viewGroup.getClipChildren();
- if (oldClipChildren != clipChildren) {
- v.setTag(R.id.saved_clip_children_tag_id, oldClipChildren);
- viewGroup.setClipChildren(clipChildren);
- }
- }
-
- if (v == parent) {
- return;
- }
-
- if (v.getParent() instanceof View) {
- setClipChildrenOnViewTree((View) v.getParent(), parent, clipChildren);
- }
- }
-
- /**
- * Recursively call {@link ViewGroup#setClipChildren(boolean)} to restore clip children value
- * set in {@link #setClipChildrenOnViewTree(View, ViewParent, boolean)} on view to its parent
- * (direct or indirect) inclusive.
- *
- * Note that if parent is null or not a parent of the view, this method will be applied all the
- * way to root view.
- *
- * @param v child view
- * @param parent direct or indirect parent of child view
- */
- private static void restoreClipChildrenOnViewTree(
- @Nullable View v, @Nullable ViewParent parent) {
- if (v == null) {
- return;
- }
- if (v instanceof ViewGroup) {
- ViewGroup viewGroup = (ViewGroup) v;
- Object viewTag = viewGroup.getTag(R.id.saved_clip_children_tag_id);
- if (viewTag instanceof Boolean) {
- viewGroup.setClipChildren((boolean) viewTag);
- viewGroup.setTag(R.id.saved_clip_children_tag_id, null);
- }
- }
-
- if (v == parent) {
- return;
- }
-
- if (v.getParent() instanceof View) {
- restoreClipChildrenOnViewTree((View) v.getParent(), parent);
- }
- }
-
- /**
* Updates the total scroll range but does not update the UI.
*/
public void setShiftRange(float shiftRange) {
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index fba7537..e07408a 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -18,6 +18,8 @@
import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_BOTTOM_LEFT;
import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_BOTTOM_RIGHT;
import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_NOTHING;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_PREINSTALLED_APPS_COUNT;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_USER_INSTALLED_APPS_COUNT;
import android.content.Context;
@@ -342,6 +344,20 @@
Map<Boolean, List<AppInfo>> split = mPrivateApps.stream()
.collect(Collectors.partitioningBy(mPrivateProviderManager
.splitIntoUserInstalledAndSystemApps()));
+
+ // TODO(b/329688630): switch to the pulled LayoutStaticSnapshot atom
+ mActivityContext
+ .getStatsLogManager()
+ .logger()
+ .withCardinality(split.get(true).size())
+ .log(LAUNCHER_PRIVATE_SPACE_USER_INSTALLED_APPS_COUNT);
+
+ mActivityContext
+ .getStatsLogManager()
+ .logger()
+ .withCardinality(split.get(false).size())
+ .log(LAUNCHER_PRIVATE_SPACE_PREINSTALLED_APPS_COUNT);
+
// Add user installed apps
position = addAppsWithSections(split.get(true), position);
// Add system apps separator.
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 3ede267..52fb122 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -754,6 +754,12 @@
@UiEvent(doc = "User tapped add widget button in widget sheet.")
LAUNCHER_WIDGET_ADD_BUTTON_TAP(1622),
+ @UiEvent(doc = "Number of user installed Private profile apps, shown above separator line")
+ LAUNCHER_PRIVATE_SPACE_USER_INSTALLED_APPS_COUNT(1672),
+
+ @UiEvent(doc = "Number of preinstalled Private profile apps, shown under separator line")
+ LAUNCHER_PRIVATE_SPACE_PREINSTALLED_APPS_COUNT(1673)
+
// ADD MORE
;
diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java
index fa2a1b0..966b6a6 100644
--- a/src/com/android/launcher3/model/BaseLauncherBinder.java
+++ b/src/com/android/launcher3/model/BaseLauncherBinder.java
@@ -29,6 +29,8 @@
import android.util.Pair;
import android.view.View;
+import androidx.annotation.NonNull;
+
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel.CallbackTask;
@@ -309,9 +311,16 @@
// Bind workspace screens
executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor);
+ ItemInflater inflater = mCallbacks.getItemInflater();
+
// Load items on the current page.
- bindItemsInChunks(currentWorkspaceItems, ITEMS_CHUNK, mUiExecutor);
- bindItemsInChunks(currentAppWidgets, 1, mUiExecutor);
+ if (enableWorkspaceInflation() && inflater != null) {
+ inflateAsyncAndBind(currentWorkspaceItems, inflater, mUiExecutor);
+ inflateAsyncAndBind(currentAppWidgets, inflater, mUiExecutor);
+ } else {
+ bindItemsInChunks(currentWorkspaceItems, ITEMS_CHUNK, mUiExecutor);
+ bindItemsInChunks(currentAppWidgets, 1, mUiExecutor);
+ }
if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
mExtraItems.forEach(item ->
executeCallbacksTask(c -> c.bindExtraContainerItems(item), mUiExecutor));
@@ -322,18 +331,20 @@
RunnableList onCompleteSignal = new RunnableList();
- if (enableWorkspaceInflation()) {
+ if (enableWorkspaceInflation() && inflater != null) {
MODEL_EXECUTOR.execute(() -> {
- setupPendingBind(otherWorkspaceItems, otherAppWidgets, currentScreenIds,
- pendingExecutor);
+ inflateAsyncAndBind(otherWorkspaceItems, inflater, pendingExecutor);
+ inflateAsyncAndBind(otherAppWidgets, inflater, pendingExecutor);
+ setupPendingBind(currentScreenIds, pendingExecutor);
// Wait for the async inflation to complete and then notify the completion
// signal on UI thread.
MAIN_EXECUTOR.execute(onCompleteSignal::executeAllAndDestroy);
});
} else {
- setupPendingBind(
- otherWorkspaceItems, otherAppWidgets, currentScreenIds, pendingExecutor);
+ bindItemsInChunks(otherWorkspaceItems, ITEMS_CHUNK, pendingExecutor);
+ bindItemsInChunks(otherAppWidgets, 1, pendingExecutor);
+ setupPendingBind(currentScreenIds, pendingExecutor);
onCompleteSignal.executeAllAndDestroy();
}
@@ -348,13 +359,8 @@
}
private void setupPendingBind(
- List<ItemInfo> otherWorkspaceItems,
- List<ItemInfo> otherAppWidgets,
IntSet currentScreenIds,
Executor pendingExecutor) {
- bindItemsInChunks(otherWorkspaceItems, ITEMS_CHUNK, pendingExecutor);
- bindItemsInChunks(otherAppWidgets, 1, pendingExecutor);
-
StringCache cacheClone = mBgDataModel.stringCache.clone();
executeCallbacksTask(c -> c.bindStringCache(cacheClone), pendingExecutor);
@@ -371,18 +377,11 @@
* Tries to inflate the items asynchronously and bind. Returns true on success or false if
* async-binding is not supported in this case.
*/
- private boolean inflateAsyncAndBind(List<ItemInfo> items, Executor executor) {
- if (!enableWorkspaceInflation()) {
- return false;
- }
- ItemInflater inflater = mCallbacks.getItemInflater();
- if (inflater == null) {
- return false;
- }
-
+ private void inflateAsyncAndBind(
+ List<ItemInfo> items, @NonNull ItemInflater inflater, Executor executor) {
if (mMyBindingId != mBgDataModel.lastBindId) {
Log.d(TAG, "Too many consecutive reloads, skipping obsolete view inflation");
- return true;
+ return;
}
ModelWriter writer = mApp.getModel()
@@ -390,15 +389,10 @@
List<Pair<ItemInfo, View>> bindItems = items.stream().map(i ->
Pair.create(i, inflater.inflateItem(i, writer, null))).toList();
executeCallbacksTask(c -> c.bindInflatedItems(bindItems), executor);
- return true;
}
- private void bindItemsInChunks(List<ItemInfo> workspaceItems, int chunkCount,
- Executor executor) {
- if (inflateAsyncAndBind(workspaceItems, executor)) {
- return;
- }
-
+ private void bindItemsInChunks(
+ List<ItemInfo> workspaceItems, int chunkCount, Executor executor) {
// Bind the workspace items
int count = workspaceItems.size();
for (int i = 0; i < count; i += chunkCount) {
diff --git a/src/com/android/launcher3/pm/InstallSessionHelper.java b/src/com/android/launcher3/pm/InstallSessionHelper.java
index 2ec994e..f3769d5 100644
--- a/src/com/android/launcher3/pm/InstallSessionHelper.java
+++ b/src/com/android/launcher3/pm/InstallSessionHelper.java
@@ -215,11 +215,15 @@
&& SessionCommitReceiver.isEnabled(mAppContext, getUserHandle(sessionInfo))
&& verifySessionInfo(sessionInfo)
&& !promiseIconAddedForId(sessionInfo.getSessionId())) {
- FileLog.d(LOG, "Adding package name to install queue: "
- + sessionInfo.getAppPackageName());
+ // In case of unarchival, we do not want to add a workspace promise icon if one is
+ // not already present. For general app installations however, we do support it.
+ if (!Utilities.enableSupportForArchiving() || !sessionInfo.isUnarchival()) {
+ FileLog.d(LOG, "Adding package name to install queue: "
+ + sessionInfo.getAppPackageName());
- ItemInstallQueue.INSTANCE.get(mAppContext)
- .queueItem(sessionInfo.getAppPackageName(), getUserHandle(sessionInfo));
+ ItemInstallQueue.INSTANCE.get(mAppContext)
+ .queueItem(sessionInfo.getAppPackageName(), getUserHandle(sessionInfo));
+ }
getPromiseIconIds().add(sessionInfo.getSessionId());
updatePromiseIconPrefs();
diff --git a/src/com/android/launcher3/views/AbstractSlideInView.java b/src/com/android/launcher3/views/AbstractSlideInView.java
index ddc3cbb..4c9371d 100644
--- a/src/com/android/launcher3/views/AbstractSlideInView.java
+++ b/src/com/android/launcher3/views/AbstractSlideInView.java
@@ -347,8 +347,7 @@
/** Return extra space revealed during predictive back animation. */
@Px
protected int getBottomOffsetPx() {
- final int height = getMeasuredHeight();
- return (int) ((height / PREDICTIVE_BACK_MIN_SCALE - height) / 2);
+ return (int) (getMeasuredHeight() * (1 - PREDICTIVE_BACK_MIN_SCALE));
}
/**
diff --git a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
index 3b661d0..255a6d2 100644
--- a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
+++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
@@ -38,6 +38,7 @@
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
+import java.util.function.Consumer;
/**
* A {@link PagedView} that displays widget recommendations in categories with dots as paged
@@ -45,11 +46,13 @@
*/
public final class WidgetRecommendationsView extends PagedView<PageIndicatorDots> {
private @Px float mAvailableHeight = Float.MAX_VALUE;
-
private static final int MAX_CATEGORIES = 3;
private TextView mRecommendationPageTitle;
private final List<String> mCategoryTitles = new ArrayList<>();
+ /** Callbacks to run when page changes */
+ private final List<Consumer<Integer>> mPageSwitchListeners = new ArrayList<>();
+
@Nullable
private OnLongClickListener mWidgetCellOnLongClickListener;
@Nullable
@@ -84,6 +87,13 @@
}
/**
+ * Add a callback to run when the current displayed page changes.
+ */
+ public void addPageSwitchListener(Consumer<Integer> pageChangeListener) {
+ mPageSwitchListeners.add(pageChangeListener);
+ }
+
+ /**
* Displays all the provided recommendations in a single table if they fit.
*
* @param recommendedWidgets list of widgets to be displayed in recommendation section.
@@ -104,7 +114,7 @@
int displayedWidgets = maybeDisplayInTable(recommendedWidgets, deviceProfile,
availableWidth, cellPadding);
- updateTitleAndIndicator();
+ updateTitleAndIndicator(/* requestedPage= */ 0);
return displayedWidgets;
}
@@ -119,16 +129,18 @@
* @param availableWidth width in px that the recommendations should display in
* @param cellPadding padding in px that should be applied to each widget in the
* recommendations
+ * @param requestedPage page number to display initially.
* @return number of recommendations that could fit in the available space.
*/
public int setRecommendations(
Map<WidgetRecommendationCategory, List<WidgetItem>> recommendations,
- DeviceProfile deviceProfile,
- final @Px float availableHeight, final @Px int availableWidth,
- final @Px int cellPadding) {
+ DeviceProfile deviceProfile, final @Px float availableHeight,
+ final @Px int availableWidth, final @Px int cellPadding, final int requestedPage) {
this.mAvailableHeight = availableHeight;
Context context = getContext();
- mPageIndicator.setPauseScroll(true, deviceProfile.isTwoPanels);
+ // For purpose of recommendations section, we don't want paging dots to be halved in two
+ // pane display, so, we always provide isTwoPanels = "false".
+ mPageIndicator.setPauseScroll(/*pause=*/true, /*isTwoPanels=*/ false);
clear();
int displayedCategories = 0;
@@ -153,26 +165,33 @@
}
}
- updateTitleAndIndicator();
- mPageIndicator.setPauseScroll(false, deviceProfile.isTwoPanels);
+ updateTitleAndIndicator(requestedPage);
+ // For purpose of recommendations section, we don't want paging dots to be halved in two
+ // pane display, so, we always provide isTwoPanels = "false".
+ mPageIndicator.setPauseScroll(/*pause=*/false, /*isTwoPanels=*/false);
return totalDisplayedWidgets;
}
private void clear() {
mCategoryTitles.clear();
removeAllViews();
+ setCurrentPage(0);
+ mPageIndicator.setActiveMarker(0);
}
/** Displays the page title and paging indicator if there are multiple pages. */
- private void updateTitleAndIndicator() {
+ private void updateTitleAndIndicator(int requestedPage) {
boolean showPaginatedView = getPageCount() > 1;
int titleAndIndicatorVisibility = showPaginatedView ? View.VISIBLE : View.GONE;
mRecommendationPageTitle.setVisibility(titleAndIndicatorVisibility);
mPageIndicator.setVisibility(titleAndIndicatorVisibility);
if (showPaginatedView) {
- mPageIndicator.setActiveMarker(0);
- setCurrentPage(0);
- mRecommendationPageTitle.setText(mCategoryTitles.get(0));
+ if (requestedPage <= 0 || requestedPage >= getPageCount()) {
+ requestedPage = 0;
+ }
+ setCurrentPage(requestedPage);
+ mPageIndicator.setActiveMarker(requestedPage);
+ mRecommendationPageTitle.setText(mCategoryTitles.get(requestedPage));
}
}
@@ -180,7 +199,9 @@
protected void notifyPageSwitchListener(int prevPage) {
if (getPageCount() > 1) {
// Since the title is outside the paging scroll, we update the title on page switch.
- mRecommendationPageTitle.setText(mCategoryTitles.get(getNextPage()));
+ int nextPage = getNextPage();
+ mRecommendationPageTitle.setText(mCategoryTitles.get(nextPage));
+ mPageSwitchListeners.forEach(listener -> listener.accept(nextPage));
super.notifyPageSwitchListener(prevPage);
}
}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
index 848f6fa..1629ca7 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsFullSheet.java
@@ -15,8 +15,6 @@
*/
package com.android.launcher3.widget.picker;
-import static android.view.View.MeasureSpec.makeMeasureSpec;
-
import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker;
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
@@ -30,6 +28,7 @@
import android.content.res.Resources;
import android.graphics.Rect;
import android.os.Build;
+import android.os.Bundle;
import android.os.Parcelable;
import android.os.Process;
import android.os.UserHandle;
@@ -67,6 +66,7 @@
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.pm.UserCache;
import com.android.launcher3.views.ArrowTipView;
import com.android.launcher3.views.RecyclerViewFastScroller;
@@ -81,7 +81,9 @@
import com.android.launcher3.workprofile.PersonalWorkSlidingTabStrip.OnActivePageChangedListener;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.IntStream;
@@ -98,8 +100,11 @@
// 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.
- private static final float RECOMMENDATION_TABLE_HEIGHT_RATIO = 0.75f;
+ // the table can display with respect to bottom sheet's height.
+ private static final float RECOMMENDATION_TABLE_HEIGHT_RATIO = 0.45f;
+ private static final String RECOMMENDATIONS_SAVED_STATE_KEY =
+ "widgetsFullSheet:mRecommendationsCurrentPage";
+ private static final String SUPER_SAVED_STATE_KEY = "widgetsFullSheet:superHierarchyState";
private final UserCache mUserCache;
private final UserManagerState mUserManagerState = new UserManagerState();
private final UserHandle mCurrentUser = Process.myUserHandle();
@@ -109,8 +114,13 @@
protected final boolean mHasWorkProfile;
// Number of recommendations displayed
protected int mRecommendedWidgetsCount;
+ private List<WidgetItem> mRecommendedWidgets = new ArrayList<>();
+ private Map<WidgetRecommendationCategory, List<WidgetItem>> mRecommendedWidgetsMap =
+ new HashMap<>();
+ protected int mRecommendationsCurrentPage = 0;
protected final SparseArray<AdapterHolder> mAdapters = new SparseArray();
- @Nullable private ArrowTipView mLatestEducationalTip;
+ @Nullable
+ private ArrowTipView mLatestEducationalTip;
private final OnLayoutChangeListener mLayoutChangeListenerToShowTips =
new OnLayoutChangeListener() {
@Override
@@ -156,14 +166,19 @@
}
};
- @Px private final int mTabsHeight;
+ @Px
+ private final int mTabsHeight;
- @Nullable private WidgetsRecyclerView mCurrentWidgetsRecyclerView;
- @Nullable private WidgetsRecyclerView mCurrentTouchEventRecyclerView;
- @Nullable PersonalWorkPagedView mViewPager;
+ @Nullable
+ private WidgetsRecyclerView mCurrentWidgetsRecyclerView;
+ @Nullable
+ private WidgetsRecyclerView mCurrentTouchEventRecyclerView;
+ @Nullable
+ PersonalWorkPagedView mViewPager;
private boolean mIsInSearchMode;
private boolean mIsNoWidgetsViewNeeded;
- @Px protected int mMaxSpanPerRow;
+ @Px
+ protected int mMaxSpanPerRow;
protected DeviceProfile mDeviceProfile;
protected TextView mNoWidgetsView;
@@ -227,13 +242,16 @@
R.id.widget_recommendations_container);
mWidgetRecommendationsView = mSearchScrollView.findViewById(
R.id.widget_recommendations_view);
+ // To save the currently displayed page, so that, it can be requested when rebinding
+ // recommendations with different size constraints.
+ mWidgetRecommendationsView.addPageSwitchListener(
+ newPage -> mRecommendationsCurrentPage = newPage);
mWidgetRecommendationsView.initParentViews(mWidgetRecommendationsContainer);
mWidgetRecommendationsView.setWidgetCellLongClickListener(this);
mWidgetRecommendationsView.setWidgetCellOnClickListener(this);
mHeaderTitle = mSearchScrollView.findViewById(R.id.title);
- onRecommendedWidgetsBound();
onWidgetsBound();
setUpEducationViewsIfNeeded();
}
@@ -354,7 +372,6 @@
super.onAttachedToWindow();
LauncherAppState.getInstance(mActivityContext).getModel()
.refreshAndBindWidgetsAndShortcuts(null);
- onRecommendedWidgetsBound();
}
@Override
@@ -582,16 +599,26 @@
}
if (enableCategorizedWidgetSuggestions()) {
+ // We avoid applying new recommendations when some are already displayed.
+ if (mRecommendedWidgetsMap.isEmpty()) {
+ mRecommendedWidgetsMap =
+ mActivityContext.getPopupDataProvider().getCategorizedRecommendedWidgets();
+ }
mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations(
- mActivityContext.getPopupDataProvider().getCategorizedRecommendedWidgets(),
+ mRecommendedWidgetsMap,
mDeviceProfile,
/* availableHeight= */ getMaxAvailableHeightForRecommendations(),
/* availableWidth= */ mMaxSpanPerRow,
- /* cellPadding= */ mWidgetCellHorizontalPadding
+ /* cellPadding= */ mWidgetCellHorizontalPadding,
+ /* requestedPage= */ mRecommendationsCurrentPage
);
} else {
+ if (mRecommendedWidgets.isEmpty()) {
+ mRecommendedWidgets =
+ mActivityContext.getPopupDataProvider().getRecommendedWidgets();
+ }
mRecommendedWidgetsCount = mWidgetRecommendationsView.setRecommendations(
- mActivityContext.getPopupDataProvider().getRecommendedWidgets(),
+ mRecommendedWidgets,
mDeviceProfile,
/* availableHeight= */ getMaxAvailableHeightForRecommendations(),
/* availableWidth= */ mMaxSpanPerRow,
@@ -604,23 +631,8 @@
@Px
private float getMaxAvailableHeightForRecommendations() {
- float noWidgetsViewHeight = 0;
- if (mIsNoWidgetsViewNeeded) {
- // Make sure recommended section leaves enough space for noWidgetsView.
- Rect noWidgetsViewTextBounds = new Rect();
- mNoWidgetsView.getPaint()
- .getTextBounds(mNoWidgetsView.getText().toString(), /* start= */ 0,
- mNoWidgetsView.getText().length(), noWidgetsViewTextBounds);
- noWidgetsViewHeight = noWidgetsViewTextBounds.height();
- }
- if (!isTwoPane()) {
- doMeasure(
- makeMeasureSpec(mActivityContext.getDeviceProfile().availableWidthPx,
- MeasureSpec.EXACTLY),
- makeMeasureSpec(mActivityContext.getDeviceProfile().availableHeightPx,
- MeasureSpec.EXACTLY));
- }
- return getMaxTableHeight(noWidgetsViewHeight);
+ return (mDeviceProfile.heightPx - mDeviceProfile.bottomSheetTopPadding)
+ * getRecommendationSectionHeightRatio();
}
/** b/209579563: "Widgets" header should be focused first. */
@@ -629,12 +641,12 @@
return mHeaderTitle;
}
+ /**
+ * Ratio of recommendations section with respect to bottom sheet's height on scale of 0 to 1.
+ */
@Px
- protected float getMaxTableHeight(@Px float noWidgetsViewHeight) {
- return (mContent.getMeasuredHeight()
- - mTabsHeight - getHeaderViewHeight()
- - noWidgetsViewHeight)
- * RECOMMENDATION_TABLE_HEIGHT_RATIO;
+ protected float getRecommendationSectionHeightRatio() {
+ return RECOMMENDATION_TABLE_HEIGHT_RATIO;
}
private void open(boolean animate) {
@@ -705,6 +717,27 @@
return sheet;
}
+ @Override
+ public void saveHierarchyState(SparseArray<Parcelable> sparseArray) {
+ Bundle bundle = new Bundle();
+ // With widget picker open, when we open shade to switch theme, Launcher re-creates the
+ // picker and calls save/restore hierarchy state. We save the state of recommendations
+ // across those updates.
+ bundle.putInt(RECOMMENDATIONS_SAVED_STATE_KEY, mRecommendationsCurrentPage);
+ SparseArray<Parcelable> superState = new SparseArray<>();
+ super.saveHierarchyState(superState);
+ bundle.putSparseParcelableArray(SUPER_SAVED_STATE_KEY, superState);
+ sparseArray.put(0, bundle);
+ }
+
+ @Override
+ public void restoreHierarchyState(SparseArray<Parcelable> sparseArray) {
+ Bundle state = (Bundle) sparseArray.get(0);
+ mRecommendationsCurrentPage = state.getInt(
+ RECOMMENDATIONS_SAVED_STATE_KEY, /*defaultValue=*/0);
+ super.restoreHierarchyState(state.getSparseParcelableArray(SUPER_SAVED_STATE_KEY));
+ }
+
private static int getWidgetSheetId(BaseActivity activity) {
boolean isTwoPane = (activity.getDeviceProfile().isTablet
// Enables two pane picker for tablets in all orientations when the
@@ -793,7 +826,7 @@
/** private the height, in pixel, + the vertical margins of a given view. */
protected static int measureHeightWithVerticalMargins(View view) {
- if (view.getVisibility() != VISIBLE) {
+ if (view == null || view.getVisibility() != VISIBLE) {
return 0;
}
MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.getLayoutParams();
@@ -828,6 +861,7 @@
saveHierarchyState(widgetsState);
handleClose(false);
WidgetsFullSheet sheet = show(BaseActivity.fromContext(getContext()), false);
+ sheet.restoreRecommendations(mRecommendedWidgets, mRecommendedWidgetsMap);
sheet.restoreHierarchyState(widgetsState);
sheet.restorePreviousAdapterHolderType(getCurrentAdapterHolderType());
} else if (!isTwoPane()) {
@@ -838,6 +872,12 @@
mDeviceProfile = dp;
}
+ private void restoreRecommendations(List<WidgetItem> recommendedWidgets,
+ Map<WidgetRecommendationCategory, List<WidgetItem>> recommendedWidgetsMap) {
+ mRecommendedWidgets = recommendedWidgets;
+ mRecommendedWidgetsMap = recommendedWidgetsMap;
+ }
+
/**
* Indicates if layout should be re-created on device profile change - so that a different
* layout can be displayed.
@@ -887,7 +927,8 @@
}
}
- @Nullable private View getViewToShowEducationTip() {
+ @Nullable
+ private View getViewToShowEducationTip() {
if (mWidgetRecommendationsContainer.getVisibility() == VISIBLE) {
return mWidgetRecommendationsView.getViewForEducationTip();
}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
index c3bb993..c60bca0 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
@@ -17,6 +17,10 @@
import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker;
+import static com.android.launcher3.UtilitiesKt.CLIP_CHILDREN_FALSE_MODIFIER;
+import static com.android.launcher3.UtilitiesKt.CLIP_TO_PADDING_FALSE_MODIFIER;
+import static com.android.launcher3.UtilitiesKt.modifyAttributesOnViewTree;
+import static com.android.launcher3.UtilitiesKt.restoreAttributesOnViewTree;
import android.content.Context;
import android.graphics.Outline;
@@ -27,6 +31,7 @@
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewOutlineProvider;
+import android.view.ViewParent;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ScrollView;
@@ -57,13 +62,19 @@
private static final int MAXIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP = 395;
private static final String SUGGESTIONS_PACKAGE_NAME = "widgets_list_suggestions_entry";
+ // This ratio defines the max percentage of content area that the recommendations can display
+ // with respect to the bottom sheet's height.
+ private static final float RECOMMENDATION_SECTION_HEIGHT_RATIO_TWO_PANE = 0.75f;
private FrameLayout mSuggestedWidgetsContainer;
private WidgetsListHeader mSuggestedWidgetsHeader;
private PackageUserKey mSuggestedWidgetsPackageUserKey;
+ private View mPrimaryWidgetListView;
private LinearLayout mRightPane;
private ScrollView mRightPaneScrollView;
private WidgetsListTableViewHolderBinder mWidgetsListTableViewHolderBinder;
+
+ private boolean mOldIsBackSwipeProgressing;
private int mActivePage = -1;
private PackageUserKey mSelectedHeader;
@@ -118,14 +129,23 @@
mWidgetRecommendationsView.initParentViews(mWidgetRecommendationsContainer);
mWidgetRecommendationsView.setWidgetCellLongClickListener(this);
mWidgetRecommendationsView.setWidgetCellOnClickListener(this);
+ // To save the currently displayed page, so that, it can be requested when rebinding
+ // recommendations with different size constraints.
+ mWidgetRecommendationsView.addPageSwitchListener(
+ newPage -> mRecommendationsCurrentPage = newPage);
mHeaderTitle = mContent.findViewById(R.id.title);
mRightPane = mContent.findViewById(R.id.right_pane);
mRightPane.setOutlineProvider(mViewOutlineProviderRightPane);
mRightPaneScrollView = mContent.findViewById(R.id.right_pane_scroll_view);
mRightPaneScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
+ mRightPaneScrollView.setOutlineProvider(mViewOutlineProvider);
+ mRightPaneScrollView.setClipToOutline(true);
- onRecommendedWidgetsBound();
+ mPrimaryWidgetListView = findViewById(R.id.primary_widgets_list_view);
+ mPrimaryWidgetListView.setOutlineProvider(mViewOutlineProvider);
+ mPrimaryWidgetListView.setClipToOutline(true);
+
onWidgetsBound();
setUpEducationViewsIfNeeded();
@@ -134,6 +154,27 @@
}
@Override
+ protected void onScaleProgressChanged() {
+ super.onScaleProgressChanged();
+ boolean isBackSwipeProgressing = mSlideInViewScale.value > 0;
+ if (isBackSwipeProgressing == mOldIsBackSwipeProgressing) {
+ return;
+ }
+ mOldIsBackSwipeProgressing = isBackSwipeProgressing;
+ if (isBackSwipeProgressing) {
+ modifyAttributesOnViewTree(mPrimaryWidgetListView, (ViewParent) mContent,
+ CLIP_CHILDREN_FALSE_MODIFIER);
+ modifyAttributesOnViewTree(mRightPaneScrollView, (ViewParent) mContent,
+ CLIP_CHILDREN_FALSE_MODIFIER, CLIP_TO_PADDING_FALSE_MODIFIER);
+ } else {
+ restoreAttributesOnViewTree(mPrimaryWidgetListView, mContent,
+ CLIP_CHILDREN_FALSE_MODIFIER);
+ restoreAttributesOnViewTree(mRightPaneScrollView, mContent,
+ CLIP_CHILDREN_FALSE_MODIFIER, CLIP_TO_PADDING_FALSE_MODIFIER);
+ }
+ }
+
+ @Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (changed && mDeviceProfile.isTwoPanels && enableUnfoldedTwoPanePicker()) {
@@ -207,11 +248,13 @@
String suggestionsRightPaneTitle = getContext().getString(
R.string.widget_picker_right_pane_accessibility_title, suggestionsHeaderTitle);
packageItemInfo.title = suggestionsHeaderTitle;
+ // Suggestions may update at run time. The widgets count on suggestions doesn't add any
+ // value, so, we don't show the count.
WidgetsListHeaderEntry widgetsListHeaderEntry = WidgetsListHeaderEntry.create(
packageItemInfo,
/*titleSectionName=*/ suggestionsHeaderTitle,
/*items=*/ mActivityContext.getPopupDataProvider().getRecommendedWidgets(),
- /*visibleWidgetsCount=*/ mRecommendedWidgetsCount)
+ /*visibleWidgetsCount=*/ 0)
.withWidgetListShown();
mSuggestedWidgetsHeader.applyFromItemInfoWithIcon(widgetsListHeaderEntry);
@@ -233,8 +276,8 @@
@Override
@Px
- protected float getMaxTableHeight(@Px float noWidgetsViewHeight) {
- return mContent.getMeasuredHeight() - measureHeightWithVerticalMargins(mHeaderTitle);
+ protected float getRecommendationSectionHeightRatio() {
+ return RECOMMENDATION_SECTION_HEIGHT_RATIO_TWO_PANE;
}
@Override
diff --git a/tests/Android.bp b/tests/Android.bp
index 24ae158..12cea1f 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -23,14 +23,23 @@
srcs: [
"src/**/*.java",
"src/**/*.kt",
- "multivalentTests/src/**/*.java",
- "multivalentTests/src/**/*.kt",
+ ":launcher3-robo-src",
],
exclude_srcs: [
":launcher-non-quickstep-tests-src",
],
}
+filegroup {
+ name: "launcher3-robo-src",
+ // multivalentTests directory is a shared folder for not only robolectric converted test
+ // classes but also shared helper classes.
+ srcs: [
+ "multivalentTests/src/**/*.java",
+ "multivalentTests/src/**/*.kt",
+ ],
+}
+
// Source code used for screenshot tests
filegroup {
name: "launcher-image-tests-helpers",
@@ -222,11 +231,8 @@
android_robolectric_test {
enabled: true,
name: "Launcher3RoboTests",
- // multivalentTests directory is a shared folder for not only robolectric converted test
- // classes but also shared helper classes.
srcs: [
- "multivalentTests/src/**/*.java",
- "multivalentTests/src/**/*.kt",
+ ":launcher3-robo-src",
// Test util classes
":launcher-testing-helpers",
diff --git a/tests/AndroidManifest-common.xml b/tests/AndroidManifest-common.xml
index 7cb7964..27dd2a9 100644
--- a/tests/AndroidManifest-common.xml
+++ b/tests/AndroidManifest-common.xml
@@ -388,6 +388,15 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity-alias>
+ <activity android:name="com.android.launcher3.testcomponent.ExcludeFromRecentsTestActivity"
+ android:label="ExcludeFromRecentsTestActivity"
+ android:exported="true"
+ android:excludeFromRecents="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
<!-- [b/197780098] Disable eager initialization of Jetpack libraries. -->
<provider
diff --git a/tests/src/com/android/launcher3/UtilitiesKtTest.kt b/tests/src/com/android/launcher3/UtilitiesKtTest.kt
new file mode 100644
index 0000000..dd83871
--- /dev/null
+++ b/tests/src/com/android/launcher3/UtilitiesKtTest.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2024 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
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.UtilitiesKt.CLIP_CHILDREN_FALSE_MODIFIER
+import com.android.launcher3.UtilitiesKt.CLIP_TO_PADDING_FALSE_MODIFIER
+import com.android.launcher3.UtilitiesKt.modifyAttributesOnViewTree
+import com.android.launcher3.UtilitiesKt.restoreAttributesOnViewTree
+import com.android.launcher3.util.ActivityContextWrapper
+import com.android.launcher3.views.WidgetsEduView
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class UtilitiesKtTest {
+
+ val context: Context = ActivityContextWrapper(ApplicationProvider.getApplicationContext())
+
+ private lateinit var rootView: WidgetsEduView
+ private lateinit var midView: ViewGroup
+ private lateinit var childView: View
+ @Before
+ fun setup() {
+ rootView =
+ LayoutInflater.from(context).inflate(R.layout.widgets_edu, null) as WidgetsEduView
+ midView = rootView.requireViewById(R.id.edu_view)
+ childView = rootView.requireViewById(R.id.edu_header)
+ }
+
+ @Test
+ fun set_clipChildren_false() {
+ assertThat(rootView.clipChildren).isTrue()
+ assertThat(midView.clipChildren).isTrue()
+
+ modifyAttributesOnViewTree(childView, rootView, CLIP_CHILDREN_FALSE_MODIFIER)
+
+ assertThat(rootView.clipChildren).isFalse()
+ assertThat(midView.clipChildren).isFalse()
+ }
+
+ @Test
+ fun restore_clipChildren_true() {
+ assertThat(rootView.clipChildren).isTrue()
+ assertThat(midView.clipChildren).isTrue()
+ modifyAttributesOnViewTree(childView, rootView, CLIP_CHILDREN_FALSE_MODIFIER)
+ assertThat(rootView.clipChildren).isFalse()
+ assertThat(midView.clipChildren).isFalse()
+
+ restoreAttributesOnViewTree(childView, rootView, CLIP_CHILDREN_FALSE_MODIFIER)
+
+ assertThat(rootView.clipChildren).isTrue()
+ assertThat(midView.clipChildren).isTrue()
+ }
+
+ @Test
+ fun restore_clipChildren_skipRestoreMidView() {
+ assertThat(rootView.clipChildren).isTrue()
+ assertThat(midView.clipChildren).isTrue()
+ rootView.clipChildren = false
+ modifyAttributesOnViewTree(childView, rootView, CLIP_CHILDREN_FALSE_MODIFIER)
+ assertThat(rootView.clipChildren).isFalse()
+ assertThat(midView.clipChildren).isFalse()
+
+ restoreAttributesOnViewTree(childView, rootView, CLIP_CHILDREN_FALSE_MODIFIER)
+
+ assertThat(rootView.clipChildren).isFalse()
+ assertThat(midView.clipChildren).isTrue()
+ }
+
+ @Test
+ fun set_clipToPadding_false() {
+ assertThat(rootView.clipToPadding).isTrue()
+ assertThat(midView.clipToPadding).isTrue()
+
+ modifyAttributesOnViewTree(childView, rootView, CLIP_TO_PADDING_FALSE_MODIFIER)
+
+ assertThat(rootView.clipToPadding).isFalse()
+ assertThat(midView.clipToPadding).isFalse()
+ }
+
+ @Test
+ fun restore_clipToPadding_true() {
+ assertThat(rootView.clipToPadding).isTrue()
+ assertThat(midView.clipToPadding).isTrue()
+ modifyAttributesOnViewTree(childView, rootView, CLIP_TO_PADDING_FALSE_MODIFIER)
+ assertThat(rootView.clipToPadding).isFalse()
+ assertThat(midView.clipToPadding).isFalse()
+
+ restoreAttributesOnViewTree(childView, rootView, CLIP_TO_PADDING_FALSE_MODIFIER)
+
+ assertThat(rootView.clipToPadding).isTrue()
+ assertThat(midView.clipToPadding).isTrue()
+ }
+}
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index a1ff346..4e38847 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -589,6 +589,17 @@
false /* newTask */);
}
+ /** Starts ExcludeFromRecentsTestActivity, which has excludeFromRecents="true". */
+ public static void startExcludeFromRecentsTestActivity() {
+ final String packageName = getAppPackageName();
+ final Intent intent = getInstrumentation().getContext().getPackageManager()
+ .getLaunchIntentForPackage(packageName);
+ intent.setComponent(new ComponentName(packageName,
+ "com.android.launcher3.testcomponent.ExcludeFromRecentsTestActivity"));
+ startIntent(intent, By.pkg(packageName).text("ExcludeFromRecentsTestActivity"),
+ false /* newTask */);
+ }
+
private static void startIntent(Intent intent, BySelector selector, boolean newTask) {
intent.addCategory(Intent.CATEGORY_LAUNCHER);
if (newTask) {
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index bfff541..0e523c3 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -2452,7 +2452,7 @@
int bottomBound = Math.min(
containerBounds.bottom,
getRealDisplaySize().y - systemGestureRegion.bottom);
- int y = (bottomBound - containerBounds.top) / 2;
+ int y = (bottomBound + containerBounds.top) / 2;
// Do not tap in the status bar.
y = Math.max(y, systemGestureRegion.top);
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 1cfbf09..99da5c3 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -25,6 +25,7 @@
import android.util.Log;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.BySelector;
import androidx.test.uiautomator.UiObject2;
@@ -259,6 +260,23 @@
}
/**
+ * Returns whether the given String is contained in this Task's contentDescription. Also returns
+ * true if both Strings are null.
+ *
+ * TODO(b/326565120): remove Nullable support once the bug causing it to be null is fixed.
+ */
+ public boolean containsContentDescription(@Nullable String expected) {
+ String actual = mTask.getContentDescription();
+ if (actual == null && expected == null) {
+ return true;
+ }
+ if (actual == null || expected == null) {
+ return false;
+ }
+ return actual.contains(expected);
+ }
+
+ /**
* Enum used to specify which task is retrieved when it is a split task.
*/
public enum OverviewSplitTask {