Merge "[Predictive Back] Support WidgetsTwoPaneSheet" into main
diff --git a/Android.bp b/Android.bp
index 78db013..cdada0a 100644
--- a/Android.bp
+++ b/Android.bp
@@ -201,6 +201,8 @@
"Launcher3ResLib",
"launcher-testing-shared",
"animationlib",
+ "kotlinx_coroutines_android",
+ "kotlinx_coroutines",
"com_android_launcher3_flags_lib",
"com_android_wm_shell_flags_lib",
"android.appwidget.flags-aconfig-java",
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 4de1c96..634caa7 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -174,6 +174,13 @@
}
flag {
+ name: "force_monochrome_app_icons"
+ namespace: "launcher"
+ description: "Enable the ability to generate monochromatic icons, if it is not provided by the app"
+ bug: "270396209"
+}
+
+flag {
name: "enable_add_app_widget_via_config_activity_v2"
namespace: "launcher"
description: "When adding app widget through config activity, directly add it to workspace to reduce flicker"
@@ -215,3 +222,10 @@
description: "Enables the Home gesture animation"
bug: "308801666"
}
+
+flag {
+ name: "enable_widget_tap_to_add"
+ namespace: "launcher"
+ description: "Enables an add button in the widget picker"
+ bug: "323886237"
+}
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 5f69a9c..9625789 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);
} 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/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..3a1c42d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java
@@ -324,7 +324,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 +352,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/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/BaseActivityInterface.java b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
index 2341e4c..59302b7 100644
--- a/quickstep/src/com/android/quickstep/BaseActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/BaseActivityInterface.java
@@ -380,13 +380,6 @@
}
/**
- * Calculates the task size for the desktop task
- */
- public final void calculateDesktopTaskSize(Context context, DeviceProfile dp, Rect outRect) {
- calculateFocusTaskSize(context, dp, outRect);
- }
-
- /**
* Calculates the modal taskView size for the provided device configuration
*/
public final void calculateModalTaskSize(Context context, DeviceProfile dp, Rect outRect,
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/orientation/LandscapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
index 39fb158..1640104 100644
--- a/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/LandscapePagedViewHandler.kt
@@ -15,6 +15,7 @@
*/
package com.android.quickstep.orientation
+import android.annotation.SuppressLint
import android.content.res.Resources
import android.graphics.Point
import android.graphics.PointF
@@ -33,6 +34,7 @@
import android.view.accessibility.AccessibilityEvent
import android.widget.FrameLayout
import android.widget.LinearLayout
+import androidx.annotation.VisibleForTesting
import androidx.core.util.component1
import androidx.core.util.component2
import com.android.launcher3.DeviceProfile
@@ -44,7 +46,11 @@
import com.android.launcher3.touch.PagedOrientationHandler.Float2DAction
import com.android.launcher3.touch.PagedOrientationHandler.Int2DAction
import com.android.launcher3.touch.SingleAxisSwipeDetector
-import com.android.launcher3.util.SplitConfigurationOptions.*
+import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT
+import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT
+import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
+import com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN
+import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds
import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption
import com.android.launcher3.util.SplitConfigurationOptions.StagePosition
import com.android.launcher3.views.BaseDragLayer
@@ -451,13 +457,8 @@
// (portrait bottom) and secondary is on the right (portrait top)
val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx
val totalThumbnailHeight = parentHeight - spaceAboveSnapshot
- val dividerBar =
- Math.round(
- totalThumbnailHeight *
- if (splitBoundsConfig.appsStackedVertically)
- splitBoundsConfig.dividerHeightPercent
- else splitBoundsConfig.dividerWidthPercent
- )
+ val dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig)
+
val (taskViewFirst, taskViewSecond) =
getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight)
@@ -482,13 +483,8 @@
): Pair<Point, Point> {
val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx
val totalThumbnailHeight = parentHeight - spaceAboveSnapshot
- val dividerBar =
- Math.round(
- totalThumbnailHeight *
- if (splitBoundsConfig.appsStackedVertically)
- splitBoundsConfig.dividerHeightPercent
- else splitBoundsConfig.dividerWidthPercent
- )
+ val dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig)
+
val taskPercent =
if (splitBoundsConfig.appsStackedVertically) {
splitBoundsConfig.topTaskPercent
@@ -569,64 +565,22 @@
deviceProfile: DeviceProfile,
splitConfig: SplitBounds
) {
- val primaryIconParams = primaryIconView.layoutParams as FrameLayout.LayoutParams
- val secondaryIconParams =
- if (Flags.enableOverviewIconMenu())
- secondaryIconView.layoutParams as FrameLayout.LayoutParams
- else FrameLayout.LayoutParams(primaryIconParams)
+ val spaceAboveSnapshot = deviceProfile.overviewTaskThumbnailTopMarginPx
+ val totalThumbnailHeight = groupedTaskViewHeight - spaceAboveSnapshot
+ val dividerBar: Int = getDividerBarSize(totalThumbnailHeight, splitConfig)
- // We calculate the "midpoint" of the thumbnail area, and place the icons there.
- // This is the place where the thumbnail area splits by default, in a near-50/50 split.
- // It is usually not exactly 50/50, due to insets/screen cutouts.
- val fullscreenInsetThickness = (deviceProfile.insets.top - deviceProfile.insets.bottom)
- val fullscreenMidpointFromBottom = ((deviceProfile.heightPx - fullscreenInsetThickness) / 2)
- val midpointFromBottomPct = fullscreenMidpointFromBottom.toFloat() / deviceProfile.heightPx
- val insetPct = fullscreenInsetThickness.toFloat() / deviceProfile.heightPx
- val spaceAboveSnapshots = deviceProfile.overviewTaskThumbnailTopMarginPx
- val overviewThumbnailAreaThickness = groupedTaskViewHeight - spaceAboveSnapshots
- val bottomToMidpointOffset =
- (overviewThumbnailAreaThickness * midpointFromBottomPct).toInt()
- val insetOffset = (overviewThumbnailAreaThickness * insetPct).toInt()
- if (Flags.enableOverviewIconMenu()) {
- val gravity = if (isRtl) Gravity.BOTTOM or Gravity.START else Gravity.TOP or Gravity.END
- primaryIconParams.gravity = gravity
- secondaryIconParams.gravity = gravity
- } else {
- primaryIconParams.gravity = Gravity.BOTTOM or if (isRtl) Gravity.START else Gravity.END
- secondaryIconParams.gravity =
- Gravity.BOTTOM or if (isRtl) Gravity.START else Gravity.END
- }
- primaryIconView.translationX = 0f
- secondaryIconView.translationX = 0f
- when {
- Flags.enableOverviewIconMenu() -> {
- val primaryAppChipView = primaryIconView as IconAppChipView
- val secondaryAppChipView = secondaryIconView as IconAppChipView
- if (primaryIconView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
- secondaryAppChipView.setSplitTranslationY(-primarySnapshotHeight.toFloat())
- primaryAppChipView.setSplitTranslationY(0f)
- } else {
- val secondarySnapshotHeight = groupedTaskViewHeight - primarySnapshotHeight
- primaryAppChipView.setSplitTranslationY(secondarySnapshotHeight.toFloat())
- }
- }
- splitConfig.initiatedFromSeascape -> {
- // if the split was initiated from seascape,
- // the task on the right (secondary) is slightly larger
- primaryIconView.translationY = (-bottomToMidpointOffset - insetOffset).toFloat()
- secondaryIconView.translationY =
- (-bottomToMidpointOffset - insetOffset + taskIconHeight).toFloat()
- }
- else -> {
- // if not,
- // the task on the left (primary) is slightly larger
- primaryIconView.translationY = -bottomToMidpointOffset.toFloat()
- secondaryIconView.translationY =
- (-bottomToMidpointOffset + taskIconHeight).toFloat()
- }
- }
- primaryIconView.layoutParams = primaryIconParams
- secondaryIconView.layoutParams = secondaryIconParams
+ val (topLeftY, bottomRightY) =
+ getSplitIconsPosition(
+ taskIconHeight,
+ primarySnapshotHeight,
+ totalThumbnailHeight,
+ isRtl,
+ deviceProfile.overviewTaskMarginPx,
+ dividerBar
+ )
+
+ updateSplitIconsPosition(primaryIconView, topLeftY, isRtl)
+ updateSplitIconsPosition(secondaryIconView, bottomRightY, isRtl)
}
override fun getDefaultSplitPosition(deviceProfile: DeviceProfile): Int {
@@ -656,4 +610,91 @@
override fun getFloatingTaskPrimaryTranslation(floatingTask: View, dp: DeviceProfile): Float =
floatingTask.translationY
+
+ /**
+ * Retrieves split icons position
+ *
+ * @param taskIconHeight The height of the task icon.
+ * @param primarySnapshotHeight The height for the primary snapshot (i.e., top-left snapshot).
+ * @param totalThumbnailHeight The total height for the group task view.
+ * @param isRtl Whether the layout direction is RTL (or false for LTR).
+ * @param overviewTaskMarginPx The space under the focused task icon provided by Device Profile.
+ * @param dividerSize The size of the divider for the group task view.
+ * @return The top-left and right-bottom positions for the icon views.
+ */
+ @VisibleForTesting
+ open fun getSplitIconsPosition(
+ taskIconHeight: Int,
+ primarySnapshotHeight: Int,
+ totalThumbnailHeight: Int,
+ isRtl: Boolean,
+ overviewTaskMarginPx: Int,
+ dividerSize: Int,
+ ): SplitIconPositions {
+ return if (Flags.enableOverviewIconMenu()) {
+ if (isRtl) {
+ SplitIconPositions(0, -(totalThumbnailHeight - primarySnapshotHeight))
+ } else {
+ SplitIconPositions(0, primarySnapshotHeight + dividerSize)
+ }
+ } else {
+ val topLeftY = primarySnapshotHeight + overviewTaskMarginPx
+ SplitIconPositions(
+ topLeftY = topLeftY,
+ bottomRightY = topLeftY + dividerSize + taskIconHeight
+ )
+ }
+ }
+
+ /**
+ * Updates icon view gravity and translation for split tasks
+ *
+ * @param iconView View to be updated
+ * @param translationY the translationY that should be applied
+ * @param isRtl Whether the layout direction is RTL (or false for LTR).
+ */
+ @SuppressLint("RtlHardcoded")
+ @VisibleForTesting
+ open fun updateSplitIconsPosition(iconView: View, translationY: Int, isRtl: Boolean) {
+ val layoutParams = iconView.layoutParams as FrameLayout.LayoutParams
+
+ if (Flags.enableOverviewIconMenu()) {
+ val appChipView = iconView as IconAppChipView
+ layoutParams.gravity =
+ if (isRtl) Gravity.BOTTOM or Gravity.START else Gravity.TOP or Gravity.END
+ appChipView.layoutParams = layoutParams
+ appChipView.setSplitTranslationX(0f)
+ appChipView.setSplitTranslationY(translationY.toFloat())
+ } else {
+ layoutParams.gravity = Gravity.TOP or Gravity.RIGHT
+ layoutParams.topMargin = translationY
+ iconView.translationX = 0f
+ iconView.translationY = 0f
+ iconView.layoutParams = layoutParams
+ }
+ }
+
+ /**
+ * It calculates the divider's size in the group task view.
+ *
+ * @param totalThumbnailHeight The total height for the group task view
+ * @param splitConfig Contains information about sizes and proportions for split task.
+ * @return The divider size for the group task view.
+ */
+ protected fun getDividerBarSize(totalThumbnailHeight: Int, splitConfig: SplitBounds): Int {
+ return Math.round(
+ totalThumbnailHeight *
+ if (splitConfig.appsStackedVertically) splitConfig.dividerHeightPercent
+ else splitConfig.dividerWidthPercent
+ )
+ }
+
+ /**
+ * Data structure to keep the y position to be used for the split task icon views translation.
+ *
+ * @param topLeftY The y-axis position for the task view position on the Top or Left side.
+ * @param bottomRightY The y-axis position for the task view position on the Bottom or Right
+ * side.
+ */
+ data class SplitIconPositions(val topLeftY: Int, val bottomRightY: Int)
}
diff --git a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
index 62dfd82..0476fe8 100644
--- a/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
+++ b/quickstep/src/com/android/quickstep/orientation/PortraitPagedViewHandler.java
@@ -75,6 +75,7 @@
public <T> T getSecondaryValue(T x, T y) {
return y;
}
+
@Override
public boolean isLayoutNaturalToLauncher() {
return true;
diff --git a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
index 0c78b8f..5bebf8c 100644
--- a/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
+++ b/quickstep/src/com/android/quickstep/orientation/SeascapePagedViewHandler.kt
@@ -15,6 +15,7 @@
*/
package com.android.quickstep.orientation
+import android.annotation.SuppressLint
import android.content.res.Resources
import android.graphics.Point
import android.graphics.PointF
@@ -32,7 +33,12 @@
import com.android.launcher3.R
import com.android.launcher3.Utilities
import com.android.launcher3.touch.SingleAxisSwipeDetector
-import com.android.launcher3.util.SplitConfigurationOptions.*
+import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT
+import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_TOP_OR_LEFT
+import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
+import com.android.launcher3.util.SplitConfigurationOptions.STAGE_TYPE_MAIN
+import com.android.launcher3.util.SplitConfigurationOptions.SplitBounds
+import com.android.launcher3.util.SplitConfigurationOptions.SplitPositionOption
import com.android.launcher3.views.BaseDragLayer
import com.android.quickstep.views.IconAppChipView
@@ -255,97 +261,6 @@
iconAppChipView.setRotation(degreesRotated)
}
- override fun setSplitIconParams(
- primaryIconView: View,
- secondaryIconView: View,
- taskIconHeight: Int,
- primarySnapshotWidth: Int,
- primarySnapshotHeight: Int,
- groupedTaskViewHeight: Int,
- groupedTaskViewWidth: Int,
- isRtl: Boolean,
- deviceProfile: DeviceProfile,
- splitConfig: SplitBounds
- ) {
- super.setSplitIconParams(
- primaryIconView,
- secondaryIconView,
- taskIconHeight,
- primarySnapshotWidth,
- primarySnapshotHeight,
- groupedTaskViewHeight,
- groupedTaskViewWidth,
- isRtl,
- deviceProfile,
- splitConfig
- )
- val primaryIconParams = primaryIconView.layoutParams as FrameLayout.LayoutParams
- val secondaryIconParams = secondaryIconView.layoutParams as FrameLayout.LayoutParams
-
- // We calculate the "midpoint" of the thumbnail area, and place the icons there.
- // This is the place where the thumbnail area splits by default, in a near-50/50 split.
- // It is usually not exactly 50/50, due to insets/screen cutouts.
- val fullscreenInsetThickness = (deviceProfile.insets.top - deviceProfile.insets.bottom)
- val fullscreenMidpointFromBottom = (deviceProfile.heightPx - fullscreenInsetThickness) / 2
- val midpointFromBottomPct = fullscreenMidpointFromBottom.toFloat() / deviceProfile.heightPx
- val insetPct = fullscreenInsetThickness.toFloat() / deviceProfile.heightPx
- val spaceAboveSnapshots = deviceProfile.overviewTaskThumbnailTopMarginPx
- val overviewThumbnailAreaThickness = groupedTaskViewHeight - spaceAboveSnapshots
- val bottomToMidpointOffset =
- (overviewThumbnailAreaThickness * midpointFromBottomPct).toInt()
- val insetOffset = (overviewThumbnailAreaThickness * insetPct).toInt()
- val gravity = if (isRtl) Gravity.TOP or Gravity.END else Gravity.BOTTOM or Gravity.START
- primaryIconParams.gravity = gravity
- secondaryIconParams.gravity = gravity
- primaryIconView.translationX = 0f
- secondaryIconView.translationX = 0f
- when {
- Flags.enableOverviewIconMenu() -> {
- val primaryAppChipView = primaryIconView as IconAppChipView
- val secondaryAppChipView = secondaryIconView as IconAppChipView
- if (isRtl) {
- primaryAppChipView.setSplitTranslationY(
- (groupedTaskViewHeight - primarySnapshotHeight).toFloat()
- )
- secondaryAppChipView.setSplitTranslationY(0f)
- } else {
- secondaryAppChipView.setSplitTranslationY(-primarySnapshotHeight.toFloat())
- primaryAppChipView.setSplitTranslationY(0f)
- }
- }
- splitConfig.initiatedFromSeascape -> {
- // if the split was initiated from seascape,
- // the task on the right (secondary) is slightly larger
- if (isRtl) {
- primaryIconView.translationY =
- (bottomToMidpointOffset - insetOffset + taskIconHeight).toFloat()
- secondaryIconView.translationY =
- (bottomToMidpointOffset - insetOffset).toFloat()
- } else {
- primaryIconView.translationY =
- (-bottomToMidpointOffset - insetOffset + taskIconHeight).toFloat()
- secondaryIconView.translationY =
- (-bottomToMidpointOffset - insetOffset).toFloat()
- }
- }
- else -> {
- // if not,
- // the task on the left (primary) is slightly larger
- if (isRtl) {
- primaryIconView.translationY =
- (bottomToMidpointOffset + taskIconHeight).toFloat()
- secondaryIconView.translationY = bottomToMidpointOffset.toFloat()
- } else {
- primaryIconView.translationY =
- (-bottomToMidpointOffset + taskIconHeight).toFloat()
- secondaryIconView.translationY = -bottomToMidpointOffset.toFloat()
- }
- }
- }
- primaryIconView.layoutParams = primaryIconParams
- secondaryIconView.layoutParams = secondaryIconParams
- }
-
override fun measureGroupedTaskViewThumbnailBounds(
primarySnapshot: View,
secondarySnapshot: View,
@@ -366,13 +281,8 @@
// (portrait bottom) and secondary is on the right (portrait top)
val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx
val totalThumbnailHeight = parentHeight - spaceAboveSnapshot
- val dividerBar =
- Math.round(
- totalThumbnailHeight *
- if (splitBoundsConfig.appsStackedVertically)
- splitBoundsConfig.dividerHeightPercent
- else splitBoundsConfig.dividerWidthPercent
- )
+ val dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig)
+
val (taskViewFirst, taskViewSecond) =
getGroupedTaskViewSizes(dp, splitBoundsConfig, parentWidth, parentHeight)
secondarySnapshot.translationY = 0f
@@ -398,13 +308,8 @@
// (portrait bottom) and secondary is on the right (portrait top)
val spaceAboveSnapshot = dp.overviewTaskThumbnailTopMarginPx
val totalThumbnailHeight = parentHeight - spaceAboveSnapshot
- val dividerBar =
- Math.round(
- totalThumbnailHeight *
- if (splitBoundsConfig.appsStackedVertically)
- splitBoundsConfig.dividerHeightPercent
- else splitBoundsConfig.dividerWidthPercent
- )
+ val dividerBar = getDividerBarSize(totalThumbnailHeight, splitBoundsConfig)
+
val taskPercent =
if (splitBoundsConfig.appsStackedVertically) {
splitBoundsConfig.topTaskPercent
@@ -430,4 +335,64 @@
override fun getTaskDragDisplacementFactor(isRtl: Boolean): Int = if (isRtl) -1 else 1
/* -------------------- */
+
+ override fun getSplitIconsPosition(
+ taskIconHeight: Int,
+ primarySnapshotHeight: Int,
+ totalThumbnailHeight: Int,
+ isRtl: Boolean,
+ overviewTaskMarginPx: Int,
+ dividerSize: Int,
+ ): SplitIconPositions {
+ return if (Flags.enableOverviewIconMenu()) {
+ if (isRtl) {
+ SplitIconPositions(
+ topLeftY = totalThumbnailHeight - primarySnapshotHeight,
+ bottomRightY = 0
+ )
+ } else {
+ SplitIconPositions(
+ topLeftY = 0,
+ bottomRightY = -(primarySnapshotHeight + dividerSize)
+ )
+ }
+ } else {
+ // In seascape, the icons are initially placed at the bottom start of the
+ // display (portrait locked). The values defined here are used to translate the icons
+ // from the bottom to the almost-center of the screen using the bottom margin.
+ // The primary snapshot is placed at the bottom, thus we translate the icons using
+ // the size of the primary snapshot minus the icon size for the top-left icon.
+ SplitIconPositions(
+ topLeftY = primarySnapshotHeight - taskIconHeight,
+ bottomRightY = primarySnapshotHeight + dividerSize
+ )
+ }
+ }
+
+ /**
+ * Updates icon view gravity and translation for split tasks
+ *
+ * @param iconView View to be updated
+ * @param translationY the translationY that should be applied
+ * @param isRtl Whether the layout direction is RTL (or false for LTR).
+ */
+ @SuppressLint("RtlHardcoded")
+ override fun updateSplitIconsPosition(iconView: View, translationY: Int, isRtl: Boolean) {
+ val layoutParams = iconView.layoutParams as FrameLayout.LayoutParams
+
+ if (Flags.enableOverviewIconMenu()) {
+ val appChipView = iconView as IconAppChipView
+ layoutParams.gravity =
+ if (isRtl) Gravity.TOP or Gravity.END else Gravity.BOTTOM or Gravity.START
+ appChipView.layoutParams = layoutParams
+ appChipView.setSplitTranslationX(0f)
+ appChipView.setSplitTranslationY(translationY.toFloat())
+ } else {
+ layoutParams.gravity = Gravity.BOTTOM or Gravity.LEFT
+ iconView.translationX = 0f
+ iconView.translationY = 0f
+ layoutParams.bottomMargin = translationY
+ iconView.layoutParams = layoutParams
+ }
+ }
}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 4985f0b..af1dd3a 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -463,7 +463,6 @@
protected final Rect mLastComputedTaskSize = new Rect();
protected final Rect mLastComputedGridSize = new Rect();
protected final Rect mLastComputedGridTaskSize = new Rect();
- protected final Rect mLastComputedDesktopTaskSize = new Rect();
private TaskView mSelectedTask = null;
// How much a task that is directly offscreen will be pushed out due to RecentsView scale/pivot.
@Nullable
@@ -1271,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);
}
@@ -1658,15 +1659,8 @@
removeView(runningTaskView);
mMovingTaskView = null;
runningTaskView.resetPersistentViewTransforms();
- int frontTaskIndex = 0;
- if (isDesktopModeSupported() && mDesktopTaskView != null
- && !runningTaskView.isDesktopTask()) {
- // If desktop mode is enabled, desktop task view is pinned at first position if present.
- // Move running task to position 1.
- frontTaskIndex = 1;
- }
- addView(runningTaskView, frontTaskIndex);
- setCurrentPage(frontTaskIndex);
+ addView(runningTaskView, 0);
+ setCurrentPage(0);
updateTaskSize();
}
@@ -1743,7 +1737,6 @@
// Clear out desktop view if it is set
mDesktopTaskView = null;
- DesktopTask desktopTask = null;
// Add views as children based on whether it's grouped or single task. Looping through
// taskGroups backwards populates the thumbnail grid from least recent to most recent.
@@ -1752,12 +1745,6 @@
boolean isRemovalNeeded = stagedTaskIdToBeRemovedFromGrid != INVALID_TASK_ID
&& groupTask.containsTask(stagedTaskIdToBeRemovedFromGrid);
- if (groupTask instanceof DesktopTask) {
- desktopTask = (DesktopTask) groupTask;
- // Desktop task will be added separately in the end
- continue;
- }
-
TaskView taskView;
if (isRemovalNeeded && groupTask.hasMultipleTasks()) {
// If we need to remove half of a pair of tasks, force a TaskView with Type.SINGLE
@@ -1788,6 +1775,10 @@
((GroupedTaskView) taskView).bind(leftTopTask, rightBottomTask, mOrientationState,
groupTask.mSplitBounds);
+ } else if (taskView instanceof DesktopTaskView) {
+ ((DesktopTaskView) taskView).bind(((DesktopTask) groupTask).tasks,
+ mOrientationState);
+ mDesktopTaskView = (DesktopTaskView) taskView;
} else {
taskView.bind(groupTask.task1, mOrientationState);
}
@@ -1800,19 +1791,6 @@
if (!taskGroups.isEmpty()) {
addView(mClearAllButton);
- if (isDesktopModeSupported()) {
- // Check if we have apps on the desktop
- if (desktopTask != null && !desktopTask.tasks.isEmpty()) {
- // If we are actively choosing apps for split, skip the desktop tile
- if (!getSplitSelectController().isSplitSelectActive()) {
- mDesktopTaskView = (DesktopTaskView) getTaskViewFromPool(
- TaskView.Type.DESKTOP);
- // Always add a desktop task to the first position
- addView(mDesktopTaskView, 0);
- mDesktopTaskView.bind(desktopTask.tasks, mOrientationState);
- }
- }
- }
}
// Keep same previous focused task
@@ -1820,12 +1798,6 @@
// If the list changed, maybe the focused task doesn't exist anymore
if (newFocusedTaskView == null && getTaskViewCount() > 0) {
newFocusedTaskView = getTaskViewAt(0);
- // Check if the first task is the desktop.
- // If first task is desktop, try to find another task to set as the focused task
- if (newFocusedTaskView != null && newFocusedTaskView.isDesktopTask()
- && getTaskViewCount() > 1) {
- newFocusedTaskView = getTaskViewAt(1);
- }
}
mFocusedTaskViewId = newFocusedTaskView != null && !enableGridOnlyOverview()
? newFocusedTaskView.getTaskViewId() : INVALID_TASK_ID;
@@ -1869,12 +1841,7 @@
if (hasAnyValidTaskIds(runningTaskId)) {
targetPage = indexOfChild(newRunningTaskView);
} else if (getTaskViewCount() > 0) {
- TaskView taskView = requireTaskViewAt(0);
- // If first task id desktop, try to find another task to set the target page
- if (taskView.isDesktopTask() && getTaskViewCount() > 1) {
- taskView = requireTaskViewAt(1);
- }
- targetPage = indexOfChild(taskView);
+ targetPage = indexOfChild(requireTaskViewAt(0));
}
}
if (targetPage != -1 && mCurrentPage != targetPage) {
@@ -2119,9 +2086,6 @@
mSizeStrategy.calculateGridSize(dp, mActivity, mLastComputedGridSize);
mSizeStrategy.calculateGridTaskSize(mActivity, dp, mLastComputedGridTaskSize,
getPagedOrientationHandler());
- if (isDesktopModeSupported()) {
- mSizeStrategy.calculateDesktopTaskSize(mActivity, dp, mLastComputedDesktopTaskSize);
- }
if (enableGridOnlyOverview()) {
mSizeStrategy.calculateCarouselTaskSize(mActivity, dp, mLastComputedCarouselTaskSize,
getPagedOrientationHandler());
@@ -2222,11 +2186,6 @@
return mLastComputedGridTaskSize;
}
- /** Gets the last computed desktop task size */
- public Rect getLastComputedDesktopTaskSize() {
- return mLastComputedDesktopTaskSize;
- }
-
public Rect getLastComputedCarouselTaskSize() {
return mLastComputedCarouselTaskSize;
}
@@ -2977,8 +2936,6 @@
TaskView homeTaskView = getHomeTaskView();
TaskView nextFocusedTaskView = null;
- int desktopTaskIndex = Integer.MAX_VALUE;
-
if (!isTaskDismissal) {
mTopRowIdSet.clear();
}
@@ -3005,21 +2962,6 @@
// If focused task is snapped, the row width is just task width and spacing.
snappedTaskRowWidth = taskWidthAndSpacing;
}
- } else if (taskView.isDesktopTask()) {
- // Desktop task was not focused. Pin it to the right of focused
- desktopTaskIndex = i;
- if (taskView.getVisibility() == View.GONE) {
- // Desktop task view is hidden, skip it from grid calculations
- continue;
- }
- if (!enableGridOnlyOverview()) {
- // Only apply x-translation when using legacy overview grid
- gridTranslations[i] += mIsRtl ? taskWidthAndSpacing : -taskWidthAndSpacing;
- }
-
- // Center view vertically in case it's from different orientation.
- taskView.setGridTranslationY((mLastComputedDesktopTaskSize.height() + taskTopMargin
- - taskView.getLayoutParams().height) / 2f);
} else {
if (i > focusedTaskIndex) {
// For tasks after the focused task, shift by focused task's width and spacing.
@@ -3060,7 +3002,7 @@
// Move horizontally into empty space.
float widthOffset = 0;
for (int j = i - 1; !topSet.contains(j) && j >= 0; j--) {
- if (j == focusedTaskIndex || j == desktopTaskIndex) {
+ if (j == focusedTaskIndex) {
continue;
}
widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing;
@@ -3079,7 +3021,7 @@
// Move horizontally into empty space.
float widthOffset = 0;
for (int j = i - 1; !bottomSet.contains(j) && j >= 0; j--) {
- if (j == focusedTaskIndex || j == desktopTaskIndex) {
+ if (j == focusedTaskIndex) {
continue;
}
widthOffset += requireTaskViewAt(j).getLayoutParams().width + mPageSpacing;
@@ -5545,10 +5487,6 @@
}
private int getFirstViewIndex() {
- if (isDesktopModeSupported() && mDesktopTaskView != null) {
- // Desktop task is at position 0, that is the first view
- return 0;
- }
TaskView focusedTaskView = mShowAsGridLastOnLayout ? getFocusedTaskView() : null;
return focusedTaskView != null ? indexOfChild(focusedTaskView) : 0;
}
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 14d7842..5123364 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -1737,12 +1737,7 @@
int boxWidth;
int boxHeight;
boolean isFocusedTask = isFocusedTask();
- if (isDesktopTask()) {
- Rect lastComputedDesktopTaskSize =
- getRecentsView().getLastComputedDesktopTaskSize();
- boxWidth = lastComputedDesktopTaskSize.width();
- boxHeight = lastComputedDesktopTaskSize.height();
- } else if (isFocusedTask) {
+ if (isFocusedTask) {
// Task will be focused and should use focused task size. Use focusTaskRatio
// that is associated with the original orientation of the focused task.
boxWidth = taskWidth;
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/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 684c090..a53bb4e 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -501,6 +501,7 @@
@Test
@PortraitLandscape
+ @ScreenRecord // b/326839375
public void testOverviewDeadzones() throws Exception {
startTestAppsWithCheck();
diff --git a/quickstep/tests/src/com/android/quickstep/orientation/LandscapePagedViewHandlerTest.kt b/quickstep/tests/src/com/android/quickstep/orientation/LandscapePagedViewHandlerTest.kt
new file mode 100644
index 0000000..ea52842
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/orientation/LandscapePagedViewHandlerTest.kt
@@ -0,0 +1,195 @@
+/*
+ * 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.orientation
+
+import android.platform.test.flag.junit.SetFlagsRule
+import android.view.Gravity
+import android.view.View
+import android.widget.FrameLayout
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.Flags
+import com.android.quickstep.orientation.LandscapePagedViewHandler.SplitIconPositions
+import com.android.quickstep.views.IconAppChipView
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.`when`
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+
+@RunWith(AndroidJUnit4::class)
+class LandscapePagedViewHandlerTest {
+
+ @get:Rule val setFlagsRule = SetFlagsRule()
+
+ private val sut = LandscapePagedViewHandler()
+
+ private fun enableGridOnlyOverview(isEnabled: Boolean) {
+ if (isEnabled) {
+ setFlagsRule.enableFlags(
+ Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW,
+ Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU
+ )
+ } else {
+ setFlagsRule.disableFlags(
+ Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW,
+ Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU
+ )
+ }
+ }
+
+ /** [ Test getSplitIconsPosition ] */
+ private fun getSplitIconsPosition(isRTL: Boolean): SplitIconPositions {
+ return sut.getSplitIconsPosition(
+ TASK_ICON_HEIGHT_PX,
+ PRIMARY_SNAPSHOT,
+ TOTAL_THUMBNAIL_HEIGHT,
+ isRTL,
+ OVERVIEW_TASK_MARGIN_PX,
+ DIVIDER_SIZE_PX,
+ )
+ }
+
+ @Test
+ fun testIcon_getSplitIconsPositions() {
+ enableGridOnlyOverview(false)
+
+ val (topLeftY, bottomRightY) = getSplitIconsPosition(isRTL = false)
+
+ // Top-Left icon should be at the end of the primary snapshot height
+ assertThat(topLeftY).isEqualTo(250)
+ // Bottom-Right icon should be at the end of the primary height + divider + icon size
+ assertThat(bottomRightY).isEqualTo(374)
+ }
+
+ @Test
+ fun testIcon_getSplitIconsPositions_isRTL() {
+ enableGridOnlyOverview(false)
+
+ val (topLeftY, bottomRightY) = getSplitIconsPosition(isRTL = true)
+
+ // Top-Left icon should be at the end of the primary snapshot height
+ assertThat(topLeftY).isEqualTo(250)
+ // Bottom-Right icon should be at the end of the primary height + divider + icon size
+ assertThat(bottomRightY).isEqualTo(374)
+ }
+
+ @Test
+ fun testChip_getSplitIconsPositions() {
+ enableGridOnlyOverview(true)
+
+ val (topLeftY, bottomRightY) = getSplitIconsPosition(isRTL = false)
+
+ // Top-Left app chip should always be at the initial position of the first snapshot
+ assertThat(topLeftY).isEqualTo(0)
+ // Bottom-Right app chip should be at the end of the primary height + divider
+ assertThat(bottomRightY).isEqualTo(266)
+ }
+
+ @Test
+ fun testChip_getSplitIconsPositions_isRTL() {
+ enableGridOnlyOverview(true)
+
+ val (topLeftY, bottomRightY) = getSplitIconsPosition(isRTL = true)
+
+ // TODO(b/326377497): When started in fake seascape and rotated to landscape,
+ // the icon chips are in RTL and wrongly positioned at the right side of the snapshot.
+ // Top-Left app chip should be placed at the top left of the first snapshot, but because
+ // this issue, it's displayed at the top-right of the second snapshot.
+ // The Bottom-Right app chip is displayed at the top-right of the first snapshot because
+ // of this issue.
+ assertThat(topLeftY).isEqualTo(0)
+ assertThat(bottomRightY).isEqualTo(-316)
+ }
+
+ /** Test updateSplitIconsPosition */
+ @Test
+ fun testIcon_updateSplitIconsPosition() {
+ enableGridOnlyOverview(false)
+
+ val expectedTranslationY = 250
+ val expectedGravity = Gravity.TOP or Gravity.RIGHT
+
+ val iconView = mock<View>()
+ val frameLayout = FrameLayout.LayoutParams(100, 100)
+ `when`(iconView.layoutParams).thenReturn(frameLayout)
+
+ sut.updateSplitIconsPosition(iconView, expectedTranslationY, false)
+ assertThat(frameLayout.gravity).isEqualTo(expectedGravity)
+ assertThat(frameLayout.topMargin).isEqualTo(expectedTranslationY)
+ verify(iconView).translationX = 0f
+ verify(iconView).translationY = 0f
+ }
+
+ @Test
+ fun testIcon_updateSplitIconsPosition_isRTL() {
+ enableGridOnlyOverview(false)
+
+ val expectedTranslationY = 250
+ val expectedGravity = Gravity.TOP or Gravity.RIGHT
+
+ val iconView = mock<View>()
+ val frameLayout = FrameLayout.LayoutParams(100, 100)
+ `when`(iconView.layoutParams).thenReturn(frameLayout)
+
+ sut.updateSplitIconsPosition(iconView, expectedTranslationY, true)
+ assertThat(frameLayout.gravity).isEqualTo(expectedGravity)
+ assertThat(frameLayout.topMargin).isEqualTo(expectedTranslationY)
+ verify(iconView).translationX = 0f
+ verify(iconView).translationY = 0f
+ }
+
+ @Test
+ fun testChip_updateSplitIconsPosition() {
+ enableGridOnlyOverview(true)
+
+ val expectedTranslationY = 250
+ val frameLayout = FrameLayout.LayoutParams(100, 100)
+ val iconView = mock<IconAppChipView>()
+ `when`(iconView.layoutParams).thenReturn(frameLayout)
+
+ sut.updateSplitIconsPosition(iconView, expectedTranslationY, false)
+ assertThat(frameLayout.gravity).isEqualTo(Gravity.TOP or Gravity.END)
+ verify(iconView).setSplitTranslationX(0f)
+ verify(iconView).setSplitTranslationY(expectedTranslationY.toFloat())
+ }
+
+ @Test
+ fun testChip_updateSplitIconsPosition_isRTL() {
+ enableGridOnlyOverview(true)
+
+ val expectedTranslationY = 250
+ val frameLayout = FrameLayout.LayoutParams(100, 100)
+ val iconView = mock<IconAppChipView>()
+ `when`(iconView.layoutParams).thenReturn(frameLayout)
+
+ sut.updateSplitIconsPosition(iconView, expectedTranslationY, true)
+ assertThat(frameLayout.gravity).isEqualTo(Gravity.BOTTOM or Gravity.START)
+ verify(iconView).setSplitTranslationX(0f)
+ verify(iconView).setSplitTranslationY(expectedTranslationY.toFloat())
+ }
+
+ private companion object {
+ const val TASK_ICON_HEIGHT_PX = 108
+ const val OVERVIEW_TASK_MARGIN_PX = 0
+ const val DIVIDER_SIZE_PX = 16
+ const val PRIMARY_SNAPSHOT = 250
+ const val SECONDARY_SNAPSHOT = 300
+ const val TOTAL_THUMBNAIL_HEIGHT = PRIMARY_SNAPSHOT + SECONDARY_SNAPSHOT + DIVIDER_SIZE_PX
+ }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/orientation/SeascapePagedViewHandlerTest.kt b/quickstep/tests/src/com/android/quickstep/orientation/SeascapePagedViewHandlerTest.kt
new file mode 100644
index 0000000..2bc182c
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/orientation/SeascapePagedViewHandlerTest.kt
@@ -0,0 +1,197 @@
+/*
+ * 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.orientation
+
+import android.platform.test.flag.junit.SetFlagsRule
+import android.view.Gravity
+import android.view.View
+import android.widget.FrameLayout
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.Flags
+import com.android.quickstep.orientation.LandscapePagedViewHandler.SplitIconPositions
+import com.android.quickstep.views.IconAppChipView
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.`when`
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+
+@RunWith(AndroidJUnit4::class)
+class SeascapePagedViewHandlerTest {
+
+ @get:Rule val setFlagsRule = SetFlagsRule()
+
+ private val sut = SeascapePagedViewHandler()
+
+ private fun enableGridOnlyOverview(isEnabled: Boolean) {
+ if (isEnabled) {
+ setFlagsRule.enableFlags(
+ Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW,
+ Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU
+ )
+ } else {
+ setFlagsRule.disableFlags(
+ Flags.FLAG_ENABLE_GRID_ONLY_OVERVIEW,
+ Flags.FLAG_ENABLE_OVERVIEW_ICON_MENU
+ )
+ }
+ }
+
+ /** [ Test getSplitIconsPosition ] */
+ private fun getSplitIconsPosition(isRTL: Boolean): SplitIconPositions {
+ return sut.getSplitIconsPosition(
+ TASK_ICON_HEIGHT_PX,
+ PRIMARY_SNAPSHOT,
+ TOTAL_THUMBNAIL_HEIGHT,
+ isRTL,
+ OVERVIEW_TASK_MARGIN_PX,
+ DIVIDER_SIZE_PX,
+ )
+ }
+
+ @Test
+ fun testIcon_getSplitIconsPositions() {
+ enableGridOnlyOverview(false)
+
+ val (topLeftY, bottomRightY) = getSplitIconsPosition(isRTL = false)
+
+ // The top-left icon is translated from the bottom of the screen to the end of
+ // the primary snapshot minus the icon size.
+ assertThat(topLeftY).isEqualTo(142)
+ // The bottom-right icon is placed at the end of the primary snapshot plus the divider.
+ assertThat(bottomRightY).isEqualTo(266)
+ }
+
+ @Test
+ fun testIcon_getSplitIconsPositions_isRTL() {
+ enableGridOnlyOverview(false)
+
+ val (topLeftY, bottomRightY) = getSplitIconsPosition(isRTL = true)
+
+ // The top-left icon is translated from the bottom of the screen to the end of
+ // the primary snapshot minus the icon size.
+ assertThat(topLeftY).isEqualTo(142)
+ // The bottom-right icon is placed at the end of the primary snapshot plus the divider.
+ assertThat(bottomRightY).isEqualTo(266)
+ }
+
+ @Test
+ fun testChip_getSplitIconsPositions() {
+ enableGridOnlyOverview(true)
+
+ val (topLeftY, bottomRightY) = getSplitIconsPosition(isRTL = false)
+
+ // Top-Left app chip should always be at the initial position of the first snapshot
+ assertThat(topLeftY).isEqualTo(0)
+ // Bottom-Right app chip should be at the end of the primary height + divider
+ assertThat(bottomRightY).isEqualTo(-266)
+ }
+
+ @Test
+ fun testChip_getSplitIconsPositions_isRTL() {
+ enableGridOnlyOverview(true)
+
+ val (topLeftY, bottomRightY) = getSplitIconsPosition(isRTL = true)
+
+ // TODO(b/326377497): When started in fake seascape and rotated to landscape,
+ // the icon chips are in RTL and wrongly positioned at the right side of the snapshot.
+ // Top-Left app chip should be placed at the top left of the first snapshot, but because
+ // this issue, it's displayed at the top-right of the second snapshot.
+ // The Bottom-Right app chip is displayed at the top-right of the first snapshot because
+ // of this issue.
+ assertThat(topLeftY).isEqualTo(316)
+ assertThat(bottomRightY).isEqualTo(0)
+ }
+
+ /** Test updateSplitIconsPosition */
+ @Test
+ fun testIcon_updateSplitIconsPosition() {
+ enableGridOnlyOverview(false)
+
+ val expectedTranslationY = 250
+ val expectedGravity = Gravity.BOTTOM or Gravity.LEFT
+
+ val iconView = mock<View>()
+ val frameLayout = FrameLayout.LayoutParams(100, 100)
+ `when`(iconView.layoutParams).thenReturn(frameLayout)
+
+ sut.updateSplitIconsPosition(iconView, expectedTranslationY, false)
+ assertThat(frameLayout.gravity).isEqualTo(expectedGravity)
+ assertThat(frameLayout.bottomMargin).isEqualTo(expectedTranslationY)
+ verify(iconView).translationX = 0f
+ verify(iconView).translationY = 0f
+ }
+
+ @Test
+ fun testIcon_updateSplitIconsPosition_isRTL() {
+ enableGridOnlyOverview(false)
+
+ val expectedTranslationY = 250
+ val expectedGravity = Gravity.BOTTOM or Gravity.LEFT
+
+ val iconView = mock<View>()
+ val frameLayout = FrameLayout.LayoutParams(100, 100)
+ `when`(iconView.layoutParams).thenReturn(frameLayout)
+
+ sut.updateSplitIconsPosition(iconView, expectedTranslationY, true)
+ assertThat(frameLayout.gravity).isEqualTo(expectedGravity)
+ assertThat(frameLayout.bottomMargin).isEqualTo(expectedTranslationY)
+ verify(iconView).translationX = 0f
+ verify(iconView).translationY = 0f
+ }
+
+ @Test
+ fun testChip_updateSplitIconsPosition() {
+ enableGridOnlyOverview(true)
+
+ val expectedTranslationY = 250
+ val frameLayout = FrameLayout.LayoutParams(100, 100)
+ val iconView = mock<IconAppChipView>()
+ `when`(iconView.layoutParams).thenReturn(frameLayout)
+
+ sut.updateSplitIconsPosition(iconView, expectedTranslationY, false)
+ assertThat(frameLayout.gravity).isEqualTo(Gravity.BOTTOM or Gravity.START)
+ verify(iconView).setSplitTranslationX(0f)
+ verify(iconView).setSplitTranslationY(expectedTranslationY.toFloat())
+ }
+
+ @Test
+ fun testChip_updateSplitIconsPosition_isRTL() {
+ enableGridOnlyOverview(true)
+
+ val expectedTranslationY = 250
+ val frameLayout = FrameLayout.LayoutParams(100, 100)
+ val iconView = mock<IconAppChipView>()
+ `when`(iconView.layoutParams).thenReturn(frameLayout)
+
+ sut.updateSplitIconsPosition(iconView, expectedTranslationY, true)
+ assertThat(frameLayout.gravity).isEqualTo(Gravity.TOP or Gravity.END)
+ verify(iconView).setSplitTranslationX(0f)
+ verify(iconView).setSplitTranslationY(expectedTranslationY.toFloat())
+ }
+
+ private companion object {
+ const val TASK_ICON_HEIGHT_PX = 108
+ const val OVERVIEW_TASK_MARGIN_PX = 0
+ const val DIVIDER_SIZE_PX = 16
+ const val PRIMARY_SNAPSHOT = 250
+ const val SECONDARY_SNAPSHOT = 300
+ const val TOTAL_THUMBNAIL_HEIGHT = PRIMARY_SNAPSHOT + SECONDARY_SNAPSHOT + DIVIDER_SIZE_PX
+ }
+}
diff --git a/res/drawable/ic_plus.xml b/res/drawable/ic_plus.xml
new file mode 100644
index 0000000..3ab926a
--- /dev/null
+++ b/res/drawable/ic_plus.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="19dp"
+ android:height="18dp"
+ android:viewportWidth="19"
+ android:viewportHeight="18">
+ <path
+ android:pathData="M15.5,9.75H10.25V15H8.75V9.75H3.5V8.25H8.75V3H10.25V8.25H15.5V9.75Z"
+ android:fillColor="#ffffff"/>
+</vector>
diff --git a/res/drawable/widget_cell_add_button_background.xml b/res/drawable/widget_cell_add_button_background.xml
new file mode 100644
index 0000000..860d1cd
--- /dev/null
+++ b/res/drawable/widget_cell_add_button_background.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<inset
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <ripple
+ android:color="?android:attr/colorControlHighlight">
+ <item>
+ <shape android:shape="rectangle">
+ <corners
+ android:radius="50dp"/>
+ <solid android:color="?attr/widgetPickerAddButtonBackgroundColor" />
+ </shape>
+ </item>
+ </ripple>
+</inset>
\ No newline at end of file
diff --git a/res/layout/widget_cell_content.xml b/res/layout/widget_cell_content.xml
index 0c606f6..106c5b7 100644
--- a/res/layout/widget_cell_content.xml
+++ b/res/layout/widget_cell_content.xml
@@ -45,40 +45,70 @@
android:layout_margin="@dimen/profile_badge_margin"/>
</com.android.launcher3.widget.WidgetCellPreview>
- <!-- The name of the widget. -->
- <TextView
- android:id="@+id/widget_name"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:ellipsize="end"
- android:fadingEdge="horizontal"
- android:gravity="center_horizontal|center_vertical"
- android:singleLine="true"
- android:maxLines="1"
- android:textColor="?android:attr/textColorPrimary"
- android:drawablePadding="@dimen/widget_cell_app_icon_padding"
- android:textSize="@dimen/widget_cell_font_size" />
-
- <!-- The original dimensions of the widget -->
- <TextView
- android:id="@+id/widget_dims"
+ <FrameLayout
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:gravity="center_horizontal"
- android:textColor="?android:attr/textColorSecondary"
- android:textSize="@dimen/widget_cell_font_size"
- android:alpha="0.7" />
+ android:layout_height="wrap_content">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:id="@+id/widget_text_container"
+ android:orientation="vertical">
+ <!-- The name of the widget. -->
+ <TextView
+ android:id="@+id/widget_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:fadingEdge="horizontal"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center_horizontal|center_vertical"
+ android:singleLine="true"
+ android:maxLines="1"
+ android:textColor="?android:attr/textColorPrimary"
+ android:drawablePadding="@dimen/widget_cell_app_icon_padding"
+ android:textSize="@dimen/widget_cell_font_size" />
- <TextView
- android:id="@+id/widget_description"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:gravity="center_horizontal"
- android:textSize="@dimen/widget_cell_font_size"
- android:textColor="?android:attr/textColorSecondary"
- android:maxLines="2"
- android:ellipsize="end"
- android:fadingEdge="horizontal"
- android:alpha="0.7" />
+ <!-- The original dimensions of the widget -->
+ <TextView
+ android:id="@+id/widget_dims"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:textColor="?android:attr/textColorSecondary"
+ android:textSize="@dimen/widget_cell_font_size"
+ android:alpha="0.7" />
-</merge>
\ No newline at end of file
+ <TextView
+ android:id="@+id/widget_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:textSize="@dimen/widget_cell_font_size"
+ android:textColor="?android:attr/textColorSecondary"
+ android:maxLines="2"
+ android:ellipsize="end"
+ android:fadingEdge="horizontal"
+ android:alpha="0.7" />
+ </LinearLayout>
+
+ <Button
+ android:id="@+id/widget_add_button"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/widget_cell_add_button_height"
+ android:layout_gravity="center"
+ android:minWidth="0dp"
+ android:paddingTop="@dimen/widget_cell_add_button_vertical_padding"
+ android:paddingBottom="@dimen/widget_cell_add_button_vertical_padding"
+ android:paddingStart="@dimen/widget_cell_add_button_start_padding"
+ android:paddingEnd="@dimen/widget_cell_add_button_end_padding"
+ android:text="@string/widget_add_button_label"
+ android:textColor="?attr/widgetPickerAddButtonTextColor"
+ android:textSize="@dimen/widget_cell_font_size"
+ android:gravity="center"
+ android:visibility="gone"
+ android:drawableLeft="@drawable/ic_plus"
+ android:drawablePadding="8dp"
+ android:drawableTint="?attr/widgetPickerAddButtonTextColor"
+ android:background="@drawable/widget_cell_add_button_background" />
+ </FrameLayout>
+</merge>
diff --git a/res/layout/widgets_full_sheet_paged_view.xml b/res/layout/widgets_full_sheet_paged_view.xml
index 1d37043..8dc785a 100644
--- a/res/layout/widgets_full_sheet_paged_view.xml
+++ b/res/layout/widgets_full_sheet_paged_view.xml
@@ -79,6 +79,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
android:background="@drawable/widgets_surface_background"
android:orientation="vertical"
android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin"
@@ -92,7 +93,7 @@
android:layout_height="64dp"
android:gravity="center_horizontal"
android:orientation="horizontal"
- android:paddingVertical="8dp"
+ android:paddingBottom="8dp"
android:paddingHorizontal="@dimen/widget_list_horizontal_margin"
android:background="?attr/widgetPickerPrimarySurfaceColor"
style="@style/TextHeadline"
diff --git a/res/layout/widgets_full_sheet_recyclerview.xml b/res/layout/widgets_full_sheet_recyclerview.xml
index dca08ff..5427732 100644
--- a/res/layout/widgets_full_sheet_recyclerview.xml
+++ b/res/layout/widgets_full_sheet_recyclerview.xml
@@ -29,7 +29,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/collapse_handle"
- android:paddingBottom="16dp"
+ android:paddingBottom="8dp"
android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin"
android:clipToOutline="true"
android:orientation="vertical">
@@ -62,6 +62,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
android:background="@drawable/widgets_surface_background"
android:orientation="vertical"
android:visibility="gone">
diff --git a/res/values-night-v31/colors.xml b/res/values-night-v31/colors.xml
index e462ae0..d23f4d1 100644
--- a/res/values-night-v31/colors.xml
+++ b/res/values-night-v31/colors.xml
@@ -46,6 +46,10 @@
@android:color/system_neutral2_200</color>
<color name="widget_picker_collapse_handle_color_dark">
@android:color/system_neutral2_700</color>
+ <color name="widget_picker_add_button_background_color_dark">
+ @android:color/system_accent1_200</color>
+ <color name="widget_picker_add_button_text_color_dark">
+ @android:color/system_accent1_800</color>
<color name="work_fab_bg_color">
@android:color/system_accent1_200</color>
diff --git a/res/values-v31/colors.xml b/res/values-v31/colors.xml
index f42b50f..fa87221 100644
--- a/res/values-v31/colors.xml
+++ b/res/values-v31/colors.xml
@@ -97,6 +97,10 @@
@android:color/system_neutral2_700</color>
<color name="widget_picker_collapse_handle_color_light">
@android:color/system_neutral2_200</color>
+ <color name="widget_picker_add_button_background_color_light">
+ @android:color/system_accent1_600</color>
+ <color name="widget_picker_add_button_text_color_light">
+ @android:color/system_accent1_0</color>
<color name="work_fab_bg_color">
@android:color/system_accent1_200</color>
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 57d1228..a1edbb9 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -72,6 +72,8 @@
<attr name="widgetPickerSelectedTabTextColor" format="color"/>
<attr name="widgetPickerUnselectedTabTextColor" format="color"/>
<attr name="widgetPickerCollapseHandleColor" format="color"/>
+ <attr name="widgetPickerAddButtonBackgroundColor" format="color"/>
+ <attr name="widgetPickerAddButtonTextColor" format="color"/>
<!-- BubbleTextView specific attributes. -->
<declare-styleable name="BubbleTextView">
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 7391990..a620eb0 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -113,6 +113,8 @@
<color name="widget_picker_selected_tab_text_color_light">#FFFFFF</color>
<color name="widget_picker_unselected_tab_text_color_light">#444746</color>
<color name="widget_picker_collapse_handle_color_light">#C4C7C5</color>
+ <color name="widget_picker_add_button_background_color_light">#0B57D0</color>
+ <color name="widget_picker_add_button_text_color_light">#0B57D0</color>
<color name="widget_picker_primary_surface_color_dark">#1F2020</color>
<color name="widget_picker_secondary_surface_color_dark">#393939</color>
@@ -128,6 +130,8 @@
<color name="widget_picker_selected_tab_text_color_dark">#2D312F</color>
<color name="widget_picker_unselected_tab_text_color_dark">#C4C7C5</color>
<color name="widget_picker_collapse_handle_color_dark">#444746</color>
+ <color name="widget_picker_add_button_background_color_dark">#062E6F</color>
+ <color name="widget_picker_add_button_text_color_dark">#FFFFFF</color>
<color name="material_color_on_secondary_fixed_variant">#3F4759</color>
<color name="material_color_on_tertiary_fixed_variant">#583E5B</color>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index c141095..d265790 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -180,6 +180,10 @@
<dimen name="widget_cell_font_size">14sp</dimen>
<dimen name="widget_cell_app_icon_size">24dp</dimen>
<dimen name="widget_cell_app_icon_padding">8dp</dimen>
+ <dimen name="widget_cell_add_button_height">48dp</dimen>
+ <dimen name="widget_cell_add_button_start_padding">8dp</dimen>
+ <dimen name="widget_cell_add_button_end_padding">16dp</dimen>
+ <dimen name="widget_cell_add_button_vertical_padding">10dp</dimen>
<dimen name="widget_tabs_button_horizontal_padding">4dp</dimen>
<dimen name="widget_tabs_horizontal_padding">16dp</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 7bf1c87..0fe9a9b 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -64,6 +64,12 @@
<!-- Spoken text for a screen reader. The placeholder text is the widget name.
[CHAR_LIMIT=none]-->
<string name="widget_preview_context_description"><xliff:g id="widget_name" example="Calendar month view">%1$s</xliff:g> widget</string>
+ <!-- Spoken text for a screen reader. The first placeholder text is the widget name, the
+ remaining placeholders are for the widget dimensions.
+ [CHAR_LIMIT=none]-->
+ <string name="widget_preview_name_and_dims_content_description">
+ <xliff:g id="widget_name" example="Calendar month view">%1$s</xliff:g> widget, %2$d wide by %3$d high
+ </string>
<!-- Message to tell the user to press and hold a widget/icon to add it to the home screen.
[CHAR LIMIT=NONE] -->
<string name="add_item_request_drag_hint">Touch & hold the widget to move it around the home screen</string>
@@ -125,6 +131,12 @@
<!-- A widget category label for grouping widgets related to note taking. [CHAR_LIMIT=30] -->
<string name="widget_category_note_taking">Note-taking</string>
+ <!-- Text on the button that adds a widget to the home screen. [CHAR_LIMIT=15] -->
+ <string name="widget_add_button_label">Add</string>
+ <!-- Accessibility content description for the button that adds a widget to the home screen. The
+ placeholder text is the widget name. [CHAR_LIMIT=none] -->
+ <string name="widget_add_button_content_description">Add <xliff:g id="widget_name" example="Calendar month view">%1$s</xliff:g> widget</string>
+
<!-- Title of a dialog. This dialog lets a user know how they can use widgets on their phone.
[CHAR_LIMIT=NONE] -->
<string name="widget_education_header">Useful info at your fingertips</string>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 9fc393b..c2875d9 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -261,6 +261,10 @@
@color/widget_picker_unselected_tab_text_color_light</item>
<item name="widgetPickerCollapseHandleColor">
@color/widget_picker_collapse_handle_color_light</item>
+ <item name="widgetPickerAddButtonBackgroundColor">
+ @color/widget_picker_add_button_background_color_light</item>
+ <item name="widgetPickerAddButtonTextColor">
+ @color/widget_picker_add_button_text_color_light</item>
</style>
<style name="WidgetContainerTheme.Dark" parent="AppTheme.Dark">
<item name="android:colorEdgeEffect">?android:attr/textColorSecondary</item>
@@ -292,6 +296,10 @@
@color/widget_picker_unselected_tab_text_color_dark</item>
<item name="widgetPickerCollapseHandleColor">
@color/widget_picker_collapse_handle_color_dark</item>
+ <item name="widgetPickerAddButtonBackgroundColor">
+ @color/widget_picker_add_button_background_color_dark</item>
+ <item name="widgetPickerAddButtonTextColor">
+ @color/widget_picker_add_button_text_color_dark</item>
</style>
<style name="FastScrollerPopup" parent="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle">
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 99fca62..be01d63 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -20,6 +20,7 @@
import static com.android.launcher3.LauncherAppState.ACTION_FORCE_ROLOAD;
import static com.android.launcher3.config.FeatureFlags.IS_STUDIO_BUILD;
+import static com.android.launcher3.icons.cache.BaseIconCache.EMPTY_CLASS_NAME;
import static com.android.launcher3.model.PackageUpdatedTask.OP_UPDATE;
import static com.android.launcher3.pm.UserCache.ACTION_PROFILE_AVAILABLE;
import static com.android.launcher3.pm.UserCache.ACTION_PROFILE_UNAVAILABLE;
@@ -27,6 +28,7 @@
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInstaller;
@@ -70,6 +72,7 @@
import com.android.launcher3.shortcuts.ShortcutRequest;
import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.ItemInfoMatcher;
+import com.android.launcher3.util.PackageManagerHelper;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.Preconditions;
@@ -443,9 +446,18 @@
@NonNull final BgDataModel dataModel, @NonNull final AllAppsList apps) {
IconCache iconCache = app.getIconCache();
final IntSet removedIds = new IntSet();
- HashSet<WorkspaceItemInfo> archivedItemsToCacheRefresh = new HashSet<>();
- HashSet<String> archivedPackagesToCacheRefresh = new HashSet<>();
+ HashSet<WorkspaceItemInfo> archivedWorkspaceItemsToCacheRefresh = new HashSet<>();
+ boolean isAppArchived = new PackageManagerHelper(
+ mApp.getContext()).isAppArchivedForUser(packageName, user);
synchronized (dataModel) {
+ if (isAppArchived) {
+ // Remove package icon cache entry for archived app in case of a session
+ // failure.
+ mApp.getIconCache().remove(
+ new ComponentName(packageName, packageName + EMPTY_CLASS_NAME),
+ user);
+ }
+
for (ItemInfo info : dataModel.itemsIdMap) {
if (info instanceof WorkspaceItemInfo
&& ((WorkspaceItemInfo) info).hasPromiseIconUi()
@@ -456,19 +468,16 @@
}
if (((WorkspaceItemInfo) info).isArchived()) {
WorkspaceItemInfo workspaceItem = (WorkspaceItemInfo) info;
- // Remove package cache icon for archived app in case of a session
- // failure.
- mApp.getIconCache().removeIconsForPkg(packageName, user);
// Refresh icons on the workspace for archived apps.
iconCache.getTitleAndIcon(workspaceItem,
workspaceItem.usingLowResIcon());
- archivedPackagesToCacheRefresh.add(packageName);
- archivedItemsToCacheRefresh.add(workspaceItem);
+ archivedWorkspaceItemsToCacheRefresh.add(workspaceItem);
}
}
}
- if (!archivedPackagesToCacheRefresh.isEmpty()) {
- apps.updateIconsAndLabels(archivedPackagesToCacheRefresh, user);
+
+ if (isAppArchived) {
+ apps.updateIconsAndLabels(new HashSet<>(List.of(packageName)), user);
}
}
@@ -477,8 +486,11 @@
ItemInfoMatcher.ofItemIds(removedIds),
"removed because install session failed");
}
- if (!archivedItemsToCacheRefresh.isEmpty()) {
- bindUpdatedWorkspaceItems(archivedItemsToCacheRefresh.stream().toList());
+ if (!archivedWorkspaceItemsToCacheRefresh.isEmpty()) {
+ bindUpdatedWorkspaceItems(
+ archivedWorkspaceItemsToCacheRefresh.stream().toList());
+ }
+ if (isAppArchived) {
bindApplicationsIfNeeded();
}
}
diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
index e861d38..f130b89 100644
--- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
@@ -405,7 +405,9 @@
} else if (item instanceof PendingAddItemInfo) {
PendingAddItemInfo info = (PendingAddItemInfo) item;
Workspace<?> workspace = mContext.getWorkspace();
- workspace.snapToPage(workspace.getPageIndexForScreenId(screenId));
+ workspace.post(
+ () -> workspace.snapToPage(workspace.getPageIndexForScreenId(screenId))
+ );
mContext.addPendingItem(info, LauncherSettings.Favorites.CONTAINER_DESKTOP,
screenId, coordinates, info.spanX, info.spanY);
} else if (item instanceof WorkspaceItemInfo) {
diff --git a/src/com/android/launcher3/allapps/AppInfoComparator.java b/src/com/android/launcher3/allapps/AppInfoComparator.java
index a0867db..bbf8e5a 100644
--- a/src/com/android/launcher3/allapps/AppInfoComparator.java
+++ b/src/com/android/launcher3/allapps/AppInfoComparator.java
@@ -18,6 +18,7 @@
import android.content.Context;
import android.os.Process;
import android.os.UserHandle;
+import android.text.TextUtils;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.pm.UserCache;
@@ -64,7 +65,7 @@
}
private String getSortingTitle(AppInfo info) {
- if (info.appTitle != null) {
+ if (!TextUtils.isEmpty(info.appTitle)) {
return info.appTitle.toString();
}
if (info.title != null) {
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index b28964a..6d64c22 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -184,11 +184,6 @@
"SECONDARY_DRAG_N_DROP_TO_PIN", DISABLED,
"Enable dragging and dropping to pin apps within secondary display");
- // TODO(Block 7): Clean up flags
- public static final BooleanFlag ENABLE_FORCED_MONO_ICON = getDebugFlag(270396209,
- "ENABLE_FORCED_MONO_ICON", DISABLED,
- "Enable the ability to generate monochromatic icons, if it is not provided by the app");
-
// TODO(Block 8): Clean up flags
// TODO(Block 9): Clean up flags
diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java
index 41e3ef0..1633eba 100644
--- a/src/com/android/launcher3/icons/IconCache.java
+++ b/src/com/android/launcher3/icons/IconCache.java
@@ -222,6 +222,7 @@
* Updates {@param application} only if a valid entry is found.
*/
public synchronized void updateTitleAndIcon(AppInfo application) {
+ boolean preferPackageIcon = application.isArchived();
CacheEntry entry = cacheLocked(application.componentName,
application.user, () -> null, mLauncherActivityInfoCachingLogic,
false, application.usingLowResIcon());
@@ -229,13 +230,12 @@
return;
}
- boolean preferPackageIcon = application.isArchived();
if (preferPackageIcon) {
String packageName = application.getTargetPackage();
CacheEntry packageEntry =
cacheLocked(new ComponentName(packageName, packageName + EMPTY_CLASS_NAME),
application.user, () -> null, mLauncherActivityInfoCachingLogic,
- false, application.usingLowResIcon());
+ true, application.usingLowResIcon());
applyPackageEntry(packageEntry, application, entry);
} else {
applyCacheEntry(entry, application);
diff --git a/src/com/android/launcher3/icons/LauncherIcons.java b/src/com/android/launcher3/icons/LauncherIcons.java
index a15348b..513377a 100644
--- a/src/com/android/launcher3/icons/LauncherIcons.java
+++ b/src/com/android/launcher3/icons/LauncherIcons.java
@@ -16,14 +16,13 @@
package com.android.launcher3.icons;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_FORCED_MONO_ICON;
-
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.UserHandle;
import androidx.annotation.NonNull;
+import com.android.launcher3.Flags;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.graphics.IconShape;
import com.android.launcher3.graphics.LauncherPreviewRenderer;
@@ -103,7 +102,7 @@
@Override
protected Drawable getMonochromeDrawable(Drawable base) {
Drawable mono = super.getMonochromeDrawable(base);
- if (mono != null || !ENABLE_FORCED_MONO_ICON.get()) {
+ if (mono != null || !Flags.forceMonochromeAppIcons()) {
return mono;
}
if (mMonochromeIconFactory == null) {
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 5cb1540..3ede267 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -751,6 +751,9 @@
+ " metric.")
LAUNCHER_SPLIT_SELECTION_EXIT_INTERRUPTED(1612),
+ @UiEvent(doc = "User tapped add widget button in widget sheet.")
+ LAUNCHER_WIDGET_ADD_BUTTON_TAP(1622),
+
// ADD MORE
;
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index 606918e..b66b96a 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -107,6 +107,7 @@
/**
* Returns whether the target app is archived for a given user
*/
+ @SuppressWarnings("NewApi")
public boolean isAppArchivedForUser(@NonNull final String packageName,
@NonNull final UserHandle user) {
if (!Utilities.enableSupportForArchiving()) {
diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java
index 54ce973..0a5127b 100644
--- a/src/com/android/launcher3/widget/BaseWidgetSheet.java
+++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java
@@ -18,6 +18,7 @@
import static com.android.app.animation.Interpolators.EMPHASIZED;
import static com.android.launcher3.Flags.enableCategorizedWidgetSuggestions;
import static com.android.launcher3.Flags.enableUnfoldedTwoPanePicker;
+import static com.android.launcher3.Flags.enableWidgetTapToAdd;
import static com.android.launcher3.LauncherPrefs.WIDGETS_EDUCATION_TIP_SEEN;
import android.content.Context;
@@ -41,8 +42,10 @@
import com.android.launcher3.Insettable;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherPrefs;
+import com.android.launcher3.PendingAddItemInfo;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
+import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.popup.PopupDataProvider;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
@@ -73,6 +76,8 @@
private boolean mDisableNavBarScrim = false;
+ @Nullable private WidgetCell mWidgetCellWithAddButton = null;
+
public BaseWidgetSheet(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContentHorizontalMargin = getWidgetListHorizontalMargin();
@@ -123,13 +128,49 @@
@Override
public final void onClick(View v) {
- if (v instanceof WidgetCell) {
- mActivityContext.getItemOnClickListener().onClick(v);
- } else if (v.getParent() instanceof WidgetCell wc) {
+ WidgetCell wc;
+ if (v instanceof WidgetCell view) {
+ wc = view;
+ } else if (v.getParent() instanceof WidgetCell parent) {
+ wc = parent;
+ } else {
+ return;
+ }
+
+ if (enableWidgetTapToAdd()) {
+ if (mWidgetCellWithAddButton != null) {
+ // If there is a add button currently showing, hide it.
+ mWidgetCellWithAddButton.hideAddButton(/* animate= */ true);
+ }
+
+ if (mWidgetCellWithAddButton != wc) {
+ // If click is on a cell not showing an add button, show it now.
+ final PendingAddItemInfo info = (PendingAddItemInfo) wc.getTag();
+ if (mActivityContext instanceof Launcher) {
+ wc.showAddButton((view) -> addWidget(info));
+ } else {
+ wc.showAddButton((view) -> mActivityContext.getItemOnClickListener()
+ .onClick(wc));
+ }
+ }
+
+ mWidgetCellWithAddButton = mWidgetCellWithAddButton != wc ? wc : null;
+ } else {
mActivityContext.getItemOnClickListener().onClick(wc);
}
}
+ /**
+ * Click handler for tap to add button.
+ */
+ public void addWidget(PendingAddItemInfo info) {
+ mActivityContext.getStatsLogManager().logger().withItemInfo(info).log(
+ StatsLogManager.LauncherEvent.LAUNCHER_WIDGET_ADD_BUTTON_TAP);
+ handleClose(true);
+ Launcher.getLauncher(mActivityContext).getAccessibilityDelegate()
+ .addToWorkspace(info, /*accessibility=*/ false);
+ }
+
@Override
public boolean onLongClick(View v) {
TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "Widgets.onLongClick");
diff --git a/src/com/android/launcher3/widget/LauncherWidgetHolder.java b/src/com/android/launcher3/widget/LauncherWidgetHolder.java
index 2fcf8c5..0fb4e09 100644
--- a/src/com/android/launcher3/widget/LauncherWidgetHolder.java
+++ b/src/com/android/launcher3/widget/LauncherWidgetHolder.java
@@ -349,7 +349,13 @@
@NonNull
public final AppWidgetHostView attachViewToHostAndGetAttachedView(
@NonNull LauncherAppWidgetHostView view) {
- if (mViews.get(view.getAppWidgetId()) != view) {
+
+ // Binder can also inflate placeholder widgets in case of backup-restore. Skip
+ // attaching such widgets
+ boolean isRealWidget = ((view instanceof PendingAppWidgetHostView pw)
+ ? pw.isDeferredWidget() : true)
+ && view.getAppWidgetInfo() != null;
+ if (isRealWidget && mViews.get(view.getAppWidgetId()) != view) {
view = recycleExistingView(view);
mViews.put(view.getAppWidgetId(), view);
}
diff --git a/src/com/android/launcher3/widget/WidgetCell.java b/src/com/android/launcher3/widget/WidgetCell.java
index aaefe60..3dff555 100644
--- a/src/com/android/launcher3/widget/WidgetCell.java
+++ b/src/com/android/launcher3/widget/WidgetCell.java
@@ -18,6 +18,7 @@
import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN;
+import static com.android.launcher3.Flags.enableWidgetTapToAdd;
import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_WIDGETS_TRAY;
import static com.android.launcher3.widget.LauncherAppWidgetProviderInfo.fromProviderInfo;
import static com.android.launcher3.widget.util.WidgetSizes.getWidgetItemSizePx;
@@ -36,6 +37,7 @@
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
@@ -77,6 +79,7 @@
private static final boolean DEBUG = false;
private static final int FADE_IN_DURATION_MS = 90;
+ private static final int ADD_BUTTON_FADE_DURATION_MS = 300;
/**
* The requested scale of the preview container. It can be lower than this as well.
@@ -89,6 +92,8 @@
private TextView mWidgetName;
private TextView mWidgetDims;
private TextView mWidgetDescription;
+ private Button mWidgetAddButton;
+ private LinearLayout mWidgetTextContainer;
private WidgetItem mItem;
private Size mWidgetSize;
@@ -141,6 +146,11 @@
mWidgetName = findViewById(R.id.widget_name);
mWidgetDims = findViewById(R.id.widget_dims);
mWidgetDescription = findViewById(R.id.widget_description);
+ mWidgetTextContainer = findViewById(R.id.widget_text_container);
+ mWidgetAddButton = findViewById(R.id.widget_add_button);
+ if (enableWidgetTapToAdd()) {
+ mWidgetAddButton.setVisibility(INVISIBLE);
+ }
}
public void setRemoteViewsPreview(RemoteViews view) {
@@ -181,6 +191,10 @@
showDescription(true);
showDimensions(true);
+ if (enableWidgetTapToAdd()) {
+ hideAddButton(/* animate= */ false);
+ }
+
if (mActiveRequest != null) {
mActiveRequest.cancel();
mActiveRequest = null;
@@ -223,12 +237,8 @@
initPreviewContainerSizeAndScale();
mWidgetName.setText(mItem.label);
- mWidgetName.setContentDescription(
- context.getString(R.string.widget_preview_context_description, mItem.label));
mWidgetDims.setText(context.getString(R.string.widget_dims_format,
mItem.spanX, mItem.spanY));
- mWidgetDims.setContentDescription(context.getString(
- R.string.widget_accessible_dims_format, mItem.spanX, mItem.spanY));
if (!TextUtils.isEmpty(mItem.description)) {
mWidgetDescription.setText(mItem.description);
mWidgetDescription.setVisibility(VISIBLE);
@@ -236,6 +246,14 @@
mWidgetDescription.setVisibility(GONE);
}
+ // Setting the content description on the WidgetCell itself ensures that it remains
+ // screen reader focusable when the add button is showing and the text is hidden.
+ setContentDescription(createContentDescription(context));
+ if (mWidgetAddButton != null) {
+ mWidgetAddButton.setContentDescription(context.getString(
+ R.string.widget_add_button_content_description, mItem.label));
+ }
+
if (item.activityInfo != null) {
setTag(new PendingAddShortcutInfo(item.activityInfo));
} else {
@@ -285,6 +303,16 @@
mPreviewContainerScale = Math.min(scaleX, scaleY);
}
+ private String createContentDescription(Context context) {
+ String contentDescription =
+ context.getString(R.string.widget_preview_name_and_dims_content_description,
+ mItem.label, mItem.spanX, mItem.spanY);
+ if (!TextUtils.isEmpty(mItem.description)) {
+ contentDescription += " " + mItem.description;
+ }
+ return contentDescription;
+ }
+
private void setAppWidgetHostViewPreview(
NavigableAppWidgetHostView appWidgetHostViewPreview,
LauncherAppWidgetProviderInfo providerInfo,
@@ -517,4 +545,55 @@
mIconLoadRequest = null;
}
}
+
+ /**
+ * Show tap to add button.
+ * @param callback Callback to be set on the button.
+ */
+ public void showAddButton(View.OnClickListener callback) {
+ mWidgetAddButton.setAlpha(0F);
+ mWidgetAddButton.setVisibility(VISIBLE);
+ mWidgetAddButton.setOnClickListener(callback);
+ mWidgetAddButton.animate().cancel();
+ mWidgetAddButton.animate()
+ .alpha(1F)
+ .setDuration(ADD_BUTTON_FADE_DURATION_MS);
+
+ mWidgetTextContainer.animate().cancel();
+ mWidgetTextContainer.animate()
+ .alpha(0F)
+ .setDuration(ADD_BUTTON_FADE_DURATION_MS)
+ .withEndAction(() -> {
+ mWidgetTextContainer.setVisibility(INVISIBLE);
+ });
+ }
+
+ /**
+ * Hide tap to add button.
+ */
+ public void hideAddButton(boolean animate) {
+ mWidgetAddButton.setOnClickListener(null);
+ mWidgetAddButton.animate().cancel();
+ mWidgetTextContainer.animate().cancel();
+
+ if (!animate) {
+ mWidgetAddButton.setVisibility(INVISIBLE);
+ mWidgetTextContainer.setVisibility(VISIBLE);
+ mWidgetTextContainer.setAlpha(1F);
+ return;
+ }
+
+ mWidgetAddButton.animate()
+ .alpha(0F)
+ .setDuration(ADD_BUTTON_FADE_DURATION_MS)
+ .withEndAction(() -> {
+ mWidgetAddButton.setVisibility(INVISIBLE);
+ });
+
+ mWidgetTextContainer.setAlpha(0F);
+ mWidgetTextContainer.setVisibility(VISIBLE);
+ mWidgetTextContainer.animate()
+ .alpha(1F)
+ .setDuration(ADD_BUTTON_FADE_DURATION_MS);
+ }
}
diff --git a/src/com/android/launcher3/widget/WidgetsBottomSheet.java b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
index 89f8181..e6b9c9b 100644
--- a/src/com/android/launcher3/widget/WidgetsBottomSheet.java
+++ b/src/com/android/launcher3/widget/WidgetsBottomSheet.java
@@ -190,9 +190,7 @@
mWidgetCellHorizontalPadding)
.forEach(row -> {
TableRow tableRow = new TableRow(getContext());
- // Vertically center align items, so that even if they don't fill bounds,
- // they can look organized when placed together in a row.
- tableRow.setGravity(Gravity.CENTER_VERTICAL);
+ tableRow.setGravity(Gravity.TOP);
row.forEach(widgetItem -> {
WidgetCell widget = addItemCell(tableRow);
widget.applyFromCellItem(widgetItem);
diff --git a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
index 811759d..3b661d0 100644
--- a/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
+++ b/src/com/android/launcher3/widget/picker/WidgetRecommendationsView.java
@@ -182,7 +182,6 @@
// Since the title is outside the paging scroll, we update the title on page switch.
mRecommendationPageTitle.setText(mCategoryTitles.get(getNextPage()));
super.notifyPageSwitchListener(prevPage);
- requestLayout();
}
}
diff --git a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
index 36f8bf9..a7f7785 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinder.java
@@ -148,9 +148,7 @@
tableRow = (TableRow) table.getChildAt(i);
} else {
tableRow = new TableRow(table.getContext());
- // Vertically center align items, so that even if they don't fill bounds, they
- // can look organized when placed together in a row.
- tableRow.setGravity(Gravity.CENTER_VERTICAL);
+ tableRow.setGravity(Gravity.TOP);
table.addView(tableRow);
}
if (tableRow.getChildCount() > widgetItems.size()) {
diff --git a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
index 76b8401..7a2b4ef 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsRecommendationTableLayout.java
@@ -104,9 +104,7 @@
for (int i = 0; i < recommendationTable.size(); i++) {
List<WidgetItem> widgetItems = recommendationTable.get(i);
TableRow tableRow = new TableRow(getContext());
- // Vertically center align items, so that even if they don't fill bounds, they can
- // look organized when placed together in a row.
- tableRow.setGravity(Gravity.CENTER_VERTICAL);
+ tableRow.setGravity(Gravity.TOP);
for (WidgetItem widgetItem : widgetItems) {
WidgetCell widgetCell = addItemCell(tableRow);
widgetCell.applyFromCellItem(widgetItem);
diff --git a/tests/src/com/android/launcher3/LauncherIntentTest.java b/tests/src/com/android/launcher3/LauncherIntentTest.java
index e2971e8..e8822c3 100644
--- a/tests/src/com/android/launcher3/LauncherIntentTest.java
+++ b/tests/src/com/android/launcher3/LauncherIntentTest.java
@@ -29,6 +29,7 @@
import com.android.launcher3.allapps.SearchRecyclerView;
import com.android.launcher3.ui.AbstractLauncherUiTest;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -39,6 +40,7 @@
public final Intent allAppsIntent = new Intent(Intent.ACTION_ALL_APPS);
@Test
+ @Ignore("b/329152799")
public void testAllAppsIntent() {
// setup by moving to home
mLauncher.goHome();
diff --git a/tests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt b/tests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt
index edd2652..460058b 100644
--- a/tests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt
+++ b/tests/src/com/android/launcher3/widget/GeneratedPreviewTest.kt
@@ -12,6 +12,7 @@
import android.platform.test.annotations.RequiresFlagsEnabled
import android.platform.test.flag.junit.CheckFlagsRule
import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.widget.RemoteViews
import androidx.test.core.app.ApplicationProvider.getApplicationContext
@@ -53,7 +54,14 @@
context = getApplicationContext()
generatedPreview = RemoteViews(context.packageName, generatedPreviewLayout)
widgetCell =
- LayoutInflater.from(ActivityContextWrapper(context))
+ LayoutInflater.from(
+ ActivityContextWrapper(
+ ContextThemeWrapper(
+ context,
+ com.android.launcher3.R.style.WidgetContainerTheme
+ )
+ )
+ )
.inflate(com.android.launcher3.R.layout.widget_cell, null) as WidgetCell
appWidgetProviderInfo =
AppWidgetProviderInfo()
diff --git a/tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java b/tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
index 0286279..85fb380 100644
--- a/tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
+++ b/tests/src/com/android/launcher3/widget/picker/WidgetsListTableViewHolderBinderTest.java
@@ -29,6 +29,7 @@
import android.content.Context;
import android.graphics.Bitmap;
import android.os.UserHandle;
+import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
@@ -96,7 +97,8 @@
mViewHolderBinder = new WidgetsListTableViewHolderBinder(
mContext,
- LayoutInflater.from(mContext),
+ LayoutInflater.from(new ContextThemeWrapper(mContext,
+ com.android.launcher3.R.style.WidgetContainerTheme)),
mOnIconClickListener,
mOnLongClickListener);
}