Merge "Update two pane sheet widget picker to use RelativeLayout since ConstraintLayout was causing test failures" into main
diff --git a/protos/launcher_atom.proto b/protos/launcher_atom.proto
index 8240f11..7c648b6 100644
--- a/protos/launcher_atom.proto
+++ b/protos/launcher_atom.proto
@@ -138,7 +138,7 @@
}
}
-// Next value 55
+// Next value 54
enum Attribute {
option allow_alias = true;
@@ -183,6 +183,7 @@
ALL_APPS_SEARCH_RESULT_CHROMETAB = 24;
ALL_APPS_SEARCH_RESULT_NAVVYSITE = 25 [deprecated = true];
ALL_APPS_SEARCH_RESULT_TIPS = 26;
+ ALL_APPS_SEARCH_RESULT_QS_TILE = 27;
ALL_APPS_SEARCH_RESULT_PEOPLE_TILE = 27 [deprecated = true];
ALL_APPS_SEARCH_RESULT_LEGACY_SHORTCUT = 30;
ALL_APPS_SEARCH_RESULT_ASSISTANT_MEMORY = 31;
@@ -192,7 +193,6 @@
ALL_APPS_SEARCH_RESULT_LOCATION = 50;
ALL_APPS_SEARCH_RESULT_TEXT_HEADER = 51;
ALL_APPS_SEARCH_RESULT_NO_FULFILLMENT = 52;
- ALL_APPS_SEARCH_RESULT_QS_TILE = 53;
// Result sources
DATA_SOURCE_APPSEARCH_APP_PREVIEW = 45;
@@ -200,7 +200,7 @@
DATA_SOURCE_APPSEARCH_CATEGORY_SRP_PREVIEW = 48;
DATA_SOURCE_APPSEARCH_ENTITY_SRP_PREVIEW = 49;
DATA_SOURCE_AIAI_SEARCH_ROOT = 47;
- DATA_SOURCE_LAUNCHER = 54;
+ DATA_SOURCE_LAUNCHER = 53;
// Web suggestions provided by AGA
ALL_APPS_SEARCH_RESULT_WEB_SUGGEST = 39;
diff --git a/quickstep/AndroidManifest.xml b/quickstep/AndroidManifest.xml
index 82f8ebe..db46508 100644
--- a/quickstep/AndroidManifest.xml
+++ b/quickstep/AndroidManifest.xml
@@ -43,6 +43,10 @@
<uses-permission android:name="android.permission.SYSTEM_APPLICATION_OVERLAY" />
+ <!-- Permission required to start a WidgetPickerActivity. -->
+ <permission android:name="${packageName}.permission.START_WIDGET_PICKER_ACTIVITY"
+ android:protectionLevel="signature|privileged" />
+
<application android:backupAgent="com.android.launcher3.LauncherBackupAgent"
android:fullBackupOnly="true"
android:fullBackupContent="@xml/backupscheme"
@@ -133,6 +137,20 @@
</intent-filter>
</activity>
+ <activity android:name="com.android.launcher3.WidgetPickerActivity"
+ android:theme="@style/WidgetPickerActivityTheme"
+ android:excludeFromRecents="true"
+ android:autoRemoveFromRecents="true"
+ android:showOnLockScreen="true"
+ android:launchMode="singleTop"
+ android:exported="true"
+ android:permission="android.permission.START_WIDGET_PICKER_ACTIVITY">
+ <intent-filter>
+ <action android:name="android.intent.action.PICK" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
</application>
</manifest>
diff --git a/quickstep/res/layout/widget_picker_activity.xml b/quickstep/res/layout/widget_picker_activity.xml
new file mode 100644
index 0000000..3388e40
--- /dev/null
+++ b/quickstep/res/layout/widget_picker_activity.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<com.android.launcher3.dragndrop.SimpleDragLayer
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/drag_layer"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:importantForAccessibility="no" />
diff --git a/quickstep/res/values/styles.xml b/quickstep/res/values/styles.xml
index ae62c26..bdc86b2 100644
--- a/quickstep/res/values/styles.xml
+++ b/quickstep/res/values/styles.xml
@@ -311,4 +311,9 @@
<item name="android:letterSpacing">0.025</item>
<item name="android:lineHeight">20sp</item>
</style>
+
+ <style name="WidgetPickerActivityTheme" parent="@android:style/Theme.Translucent.NoTitleBar">
+ <item name="widgetsTheme">@style/WidgetContainerTheme</item>
+ <item name="android:windowBackground">@android:color/transparent</item>
+ </style>
</resources>
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index 8db63e3..4898761 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -1654,9 +1654,10 @@
addCujInstrumentation(anim, playFallBackAnimation
? CUJ_APP_CLOSE_TO_HOME_FALLBACK : CUJ_APP_CLOSE_TO_HOME);
- anim.addListener(new AnimationSuccessListener() {
+ anim.addListener(new AnimatorListenerAdapter() {
@Override
- public void onAnimationSuccess(Animator animator) {
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
AccessibilityManagerCompat.sendTestProtocolEventToTest(
mLauncher, WALLPAPER_OPEN_ANIMATION_FINISHED_MESSAGE);
}
diff --git a/quickstep/src/com/android/launcher3/WidgetPickerActivity.java b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
new file mode 100644
index 0000000..43716ab
--- /dev/null
+++ b/quickstep/src/com/android/launcher3/WidgetPickerActivity.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3;
+
+import static android.view.WindowInsets.Type.navigationBars;
+import static android.view.WindowInsets.Type.statusBars;
+
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
+
+import android.os.Bundle;
+import android.view.WindowInsetsController;
+import android.view.WindowManager;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.dragndrop.SimpleDragLayer;
+import com.android.launcher3.model.WidgetsModel;
+import com.android.launcher3.popup.PopupDataProvider;
+import com.android.launcher3.widget.BaseWidgetSheet;
+import com.android.launcher3.widget.model.WidgetsListBaseEntry;
+import com.android.launcher3.widget.picker.WidgetsFullSheet;
+
+import java.util.ArrayList;
+
+/** An Activity that can host Launcher's widget picker. */
+public class WidgetPickerActivity extends BaseActivity {
+ private SimpleDragLayer<WidgetPickerActivity> mDragLayer;
+ private WidgetsModel mModel;
+ private final PopupDataProvider mPopupDataProvider = new PopupDataProvider(i -> {});
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER);
+
+ LauncherAppState app = LauncherAppState.getInstance(this);
+ InvariantDeviceProfile idp = app.getInvariantDeviceProfile();
+
+ mDeviceProfile = idp.getDeviceProfile(this);
+ mModel = new WidgetsModel();
+
+ setContentView(R.layout.widget_picker_activity);
+ mDragLayer = findViewById(R.id.drag_layer);
+ mDragLayer.recreateControllers();
+
+ WindowInsetsController wc = mDragLayer.getWindowInsetsController();
+ wc.hide(navigationBars() + statusBars());
+
+ BaseWidgetSheet widgetSheet = WidgetsFullSheet.show(this, true);
+ widgetSheet.disableNavBarScrim(true);
+ widgetSheet.addOnCloseListener(this::finish);
+
+ refreshAndBindWidgets();
+ }
+
+ @NonNull
+ @Override
+ public PopupDataProvider getPopupDataProvider() {
+ return mPopupDataProvider;
+ }
+
+ @Override
+ public SimpleDragLayer<WidgetPickerActivity> getDragLayer() {
+ return mDragLayer;
+ }
+
+ private void refreshAndBindWidgets() {
+ MODEL_EXECUTOR.execute(() -> {
+ LauncherAppState app = LauncherAppState.getInstance(this);
+ mModel.update(app, null);
+ final ArrayList<WidgetsListBaseEntry> widgets =
+ mModel.getWidgetsListForPicker(app.getContext());
+ MAIN_EXECUTOR.execute(() -> mPopupDataProvider.setAllWidgets(widgets));
+ });
+ }
+}
diff --git a/quickstep/src/com/android/launcher3/taskbar/DesktopNavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/DesktopNavbarButtonsViewController.java
index 29c5204..0a9dfff 100644
--- a/quickstep/src/com/android/launcher3/taskbar/DesktopNavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/DesktopNavbarButtonsViewController.java
@@ -18,12 +18,15 @@
import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_NOTIFICATIONS;
import static com.android.launcher3.taskbar.TaskbarNavButtonController.BUTTON_QUICK_SETTINGS;
+import android.content.Context;
import android.content.pm.ActivityInfo.Config;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.R;
/**
@@ -40,8 +43,8 @@
private TaskbarControllers mControllers;
public DesktopNavbarButtonsViewController(TaskbarActivityContext context,
- FrameLayout navButtonsView) {
- super(context, navButtonsView);
+ @Nullable Context navigationBarPanelContext, FrameLayout navButtonsView) {
+ super(context, navigationBarPanelContext, navButtonsView);
mContext = context;
mNavButtonsView = navButtonsView;
mNavButtonContainer = mNavButtonsView.findViewById(R.id.end_nav_buttons);
diff --git a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
index 3514447..bed4c37 100644
--- a/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/NavbarButtonsViewController.java
@@ -53,6 +53,7 @@
import android.annotation.DrawableRes;
import android.annotation.IdRes;
import android.annotation.LayoutRes;
+import android.content.Context;
import android.content.pm.ActivityInfo.Config;
import android.content.res.ColorStateList;
import android.content.res.Resources;
@@ -80,6 +81,8 @@
import android.widget.ImageView;
import android.widget.LinearLayout;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.LauncherAnimUtils;
import com.android.launcher3.R;
@@ -146,6 +149,7 @@
private int mState;
private final TaskbarActivityContext mContext;
+ private final @Nullable Context mNavigationBarPanelContext;
private final WindowManagerProxy mWindowManagerProxy;
private final FrameLayout mNavButtonsView;
private final LinearLayout mNavButtonContainer;
@@ -203,8 +207,10 @@
private final RecentsHitboxExtender mHitboxExtender = new RecentsHitboxExtender();
private ImageView mRecentsButton;
- public NavbarButtonsViewController(TaskbarActivityContext context, FrameLayout navButtonsView) {
+ public NavbarButtonsViewController(TaskbarActivityContext context,
+ @Nullable Context navigationBarPanelContext, FrameLayout navButtonsView) {
mContext = context;
+ mNavigationBarPanelContext = navigationBarPanelContext;
mWindowManagerProxy = WindowManagerProxy.INSTANCE.get(mContext);
mNavButtonsView = navButtonsView;
mNavButtonContainer = mNavButtonsView.findViewById(R.id.end_nav_buttons);
@@ -312,7 +318,8 @@
rotationButton.hide();
mControllers.rotationButtonController.setRotationButton(rotationButton, null);
} else {
- mFloatingRotationButton = new FloatingRotationButton(mContext,
+ mFloatingRotationButton = new FloatingRotationButton(
+ ENABLE_TASKBAR_NAVBAR_UNIFICATION ? mNavigationBarPanelContext : mContext,
R.string.accessibility_rotate_button,
R.layout.rotate_suggestion,
R.id.rotate_suggestion,
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 60ee38f..38ee4ac 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -74,9 +74,11 @@
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.LauncherPrefs;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.R;
import com.android.launcher3.anim.AnimatorPlaybackController;
+import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.dot.DotInfo;
import com.android.launcher3.folder.Folder;
@@ -106,6 +108,7 @@
import com.android.launcher3.touch.ItemClickHandler;
import com.android.launcher3.touch.ItemClickHandler.ItemClickProxy;
import com.android.launcher3.util.ActivityOptionsWrapper;
+import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.Executors;
import com.android.launcher3.util.NavigationMode;
@@ -127,6 +130,7 @@
import java.io.PrintWriter;
import java.util.Collections;
+import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
@@ -143,6 +147,8 @@
private static final String WINDOW_TITLE = "Taskbar";
+ private final @Nullable Context mNavigationBarPanelContext;
+
private final TaskbarDragLayer mDragLayer;
private final TaskbarControllers mControllers;
@@ -178,11 +184,15 @@
private DeviceProfile mPersistentTaskbarDeviceProfile;
- public TaskbarActivityContext(Context windowContext, DeviceProfile launcherDp,
+ private final LauncherPrefs mLauncherPrefs;
+
+ public TaskbarActivityContext(Context windowContext,
+ @Nullable Context navigationBarPanelContext, DeviceProfile launcherDp,
TaskbarNavButtonController buttonController, ScopedUnfoldTransitionProgressProvider
unfoldTransitionProgressProvider) {
super(windowContext);
+ mNavigationBarPanelContext = navigationBarPanelContext;
applyDeviceProfile(launcherDp);
final Resources resources = getResources();
@@ -256,8 +266,10 @@
new TaskbarDragController(this),
buttonController,
isDesktopMode
- ? new DesktopNavbarButtonsViewController(this, navButtonsView)
- : new NavbarButtonsViewController(this, navButtonsView),
+ ? new DesktopNavbarButtonsViewController(this, mNavigationBarPanelContext,
+ navButtonsView)
+ : new NavbarButtonsViewController(this, mNavigationBarPanelContext,
+ navButtonsView),
rotationButtonController,
new TaskbarDragLayerController(this, mDragLayer),
new TaskbarViewController(this, taskbarView),
@@ -285,6 +297,8 @@
new KeyboardQuickSwitchController(),
new TaskbarPinningController(this),
bubbleControllersOptional);
+
+ mLauncherPrefs = LauncherPrefs.get(this);
}
/** Updates {@link DeviceProfile} instances for any Taskbar windows. */
@@ -402,6 +416,11 @@
getDeviceProfile().toSmallString());
}
+ @NonNull
+ public LauncherPrefs getLauncherPrefs() {
+ return mLauncherPrefs;
+ }
+
/**
* Returns the View bounds of transient taskbar.
*/
@@ -975,6 +994,8 @@
}
protected void onTaskbarIconClicked(View view) {
+ TaskbarUIController taskbarUIController = mControllers.uiController;
+ RecentsView recents = taskbarUIController.getRecentsView();
boolean shouldCloseAllOpenViews = true;
Object tag = view.getTag();
if (tag instanceof Task) {
@@ -982,41 +1003,26 @@
ActivityManagerWrapper.getInstance().startActivityFromRecents(task.key,
ActivityOptions.makeBasic());
mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
- } else if (tag instanceof FolderInfo) {
+ } else if (tag instanceof FolderInfo fi && fi.itemType == Favorites.ITEM_TYPE_FOLDER) {
+ // Tapping an expandable folder icon on Taskbar
shouldCloseAllOpenViews = false;
- FolderIcon folderIcon = (FolderIcon) view;
- Folder folder = folderIcon.getFolder();
-
- folder.setOnFolderStateChangedListener(newState -> {
- if (newState == Folder.STATE_OPEN) {
- setTaskbarWindowFocusableForIme(true);
- } else if (newState == Folder.STATE_CLOSED) {
- // Defer by a frame to ensure we're no longer fullscreen and thus won't jump.
- getDragLayer().post(() -> setTaskbarWindowFocusableForIme(false));
- folder.setOnFolderStateChangedListener(null);
- }
- });
-
- setTaskbarWindowFullscreen(true);
-
- getDragLayer().post(() -> {
- folder.animateOpen();
- getStatsLogManager().logger().withItemInfo(folder.mInfo).log(LAUNCHER_FOLDER_OPEN);
-
- folder.iterateOverItems((itemInfo, itemView) -> {
- mControllers.taskbarViewController
- .setClickAndLongClickListenersForIcon(itemView);
- // To play haptic when dragging, like other Taskbar items do.
- itemView.setHapticFeedbackEnabled(true);
- return false;
- });
- });
+ expandFolder((FolderIcon) view);
+ } else if (tag instanceof FolderInfo fi && fi.itemType == Favorites.ITEM_TYPE_APP_PAIR) {
+ // Tapping an app pair icon on Taskbar
+ if (recents != null && recents.isSplitSelectionActive()) {
+ // TODO (b/274835596): Implement "can't split with this" bounce animation
+ Toast.makeText(this, "Unable to split with an app pair. Select another app.",
+ Toast.LENGTH_SHORT).show();
+ } else {
+ // Else launch the selected app pair
+ launchFromTaskbarPreservingSplitIfVisible(recents, view, fi.contents);
+ mControllers.uiController.onTaskbarIconLaunched(fi);
+ mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
+ }
} else if (tag instanceof WorkspaceItemInfo) {
// Tapping a launchable icon on Taskbar
WorkspaceItemInfo info = (WorkspaceItemInfo) tag;
if (!info.isDisabled() || !ItemClickHandler.handleDisabledItemClicked(info, this)) {
- TaskbarUIController taskbarUIController = mControllers.uiController;
- RecentsView recents = taskbarUIController.getRecentsView();
if (recents != null && recents.isSplitSelectionActive()) {
// If we are selecting a second app for split, launch the split tasks
taskbarUIController.triggerSecondAppForSplit(info, info.intent, view);
@@ -1044,7 +1050,8 @@
getSystemService(LauncherApps.class)
.startShortcut(packageName, id, null, null, info.user);
} else {
- launchFromTaskbarPreservingSplitIfVisible(recents, info);
+ launchFromTaskbarPreservingSplitIfVisible(
+ recents, view, Collections.singletonList(info));
}
} catch (NullPointerException
@@ -1055,7 +1062,21 @@
Log.e(TAG, "Unable to launch. tag=" + info + " intent=" + intent, e);
return;
}
+ }
+ // If the app was launched from a folder, stash the taskbar after it closes
+ Folder f = Folder.getOpen(this);
+ if (f != null && f.getInfo().id == info.container) {
+ f.addOnFolderStateChangedListener(new Folder.OnFolderStateChangedListener() {
+ @Override
+ public void onFolderStateChanged(int newState) {
+ if (newState == Folder.STATE_CLOSED) {
+ f.removeOnFolderStateChangedListener(this);
+ mControllers.taskbarStashController
+ .updateAndAnimateTransientTaskbar(true);
+ }
+ }
+ });
}
mControllers.uiController.onTaskbarIconLaunched(info);
mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
@@ -1063,14 +1084,13 @@
} else if (tag instanceof AppInfo) {
// Tapping an item in AllApps
AppInfo info = (AppInfo) tag;
- TaskbarUIController taskbarUIController = mControllers.uiController;
- RecentsView recents = taskbarUIController.getRecentsView();
if (recents != null
&& taskbarUIController.getRecentsView().isSplitSelectionActive()) {
// If we are selecting a second app for split, launch the split tasks
taskbarUIController.triggerSecondAppForSplit(info, info.intent, view);
} else {
- launchFromTaskbarPreservingSplitIfVisible(recents, info);
+ launchFromTaskbarPreservingSplitIfVisible(
+ recents, view, Collections.singletonList(info));
}
mControllers.uiController.onTaskbarIconLaunched(info);
mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true);
@@ -1092,17 +1112,22 @@
* (potentially breaking a split pair).
*/
private void launchFromTaskbarPreservingSplitIfVisible(@Nullable RecentsView recents,
- ItemInfo info) {
+ @Nullable View launchingIconView, List<? extends ItemInfo> itemInfos) {
if (recents == null) {
return;
}
+
+ boolean findExactPairMatch = itemInfos.size() == 2;
+ // Convert the list of ItemInfo instances to a list of ComponentKeys
+ List<ComponentKey> componentKeys =
+ itemInfos.stream().map(ItemInfo::getComponentKey).toList();
recents.getSplitSelectController().findLastActiveTasksAndRunCallback(
- Collections.singletonList(info.getComponentKey()),
+ componentKeys,
+ findExactPairMatch,
foundTasks -> {
@Nullable Task foundTask = foundTasks.get(0);
if (foundTask != null) {
- TaskView foundTaskView =
- recents.getTaskViewByTaskId(foundTask.key.id);
+ TaskView foundTaskView = recents.getTaskViewByTaskId(foundTask.key.id);
if (foundTaskView != null
&& foundTaskView.isVisibleToUser()) {
TestLogging.recordEvent(
@@ -1111,8 +1136,16 @@
return;
}
}
- startItemInfoActivity(info);
- });
+
+ if (findExactPairMatch) {
+ // We did not find the app pair we were looking for, so launch one.
+ recents.getSplitSelectController().getAppPairsController().launchAppPair(
+ (AppPairIcon) launchingIconView);
+ } else {
+ startItemInfoActivity(itemInfos.get(0));
+ }
+ }
+ );
}
private void startItemInfoActivity(ItemInfo info) {
@@ -1134,6 +1167,41 @@
}
}
+ /** Expands a folder icon when it is clicked */
+ private void expandFolder(FolderIcon folderIcon) {
+ Folder folder = folderIcon.getFolder();
+
+ folder.setPriorityOnFolderStateChangedListener(
+ new Folder.OnFolderStateChangedListener() {
+ @Override
+ public void onFolderStateChanged(int newState) {
+ if (newState == Folder.STATE_OPEN) {
+ setTaskbarWindowFocusableForIme(true);
+ } else if (newState == Folder.STATE_CLOSED) {
+ // Defer by a frame to ensure we're no longer fullscreen and thus
+ // won't jump.
+ getDragLayer().post(() -> setTaskbarWindowFocusableForIme(false));
+ folder.setPriorityOnFolderStateChangedListener(null);
+ }
+ }
+ });
+
+ setTaskbarWindowFullscreen(true);
+
+ getDragLayer().post(() -> {
+ folder.animateOpen();
+ getStatsLogManager().logger().withItemInfo(folder.mInfo).log(LAUNCHER_FOLDER_OPEN);
+
+ folder.iterateOverItems((itemInfo, itemView) -> {
+ mControllers.taskbarViewController
+ .setClickAndLongClickListenersForIcon(itemView);
+ // To play haptic when dragging, like other Taskbar items do.
+ itemView.setHapticFeedbackEnabled(true);
+ return false;
+ });
+ });
+ }
+
/**
* Returns whether the taskbar is currently visually stashed.
*/
@@ -1142,15 +1210,6 @@
}
/**
- * Called when we detect a long press in the nav region before passing the gesture slop.
- *
- * @return Whether taskbar handled the long press, and thus should cancel the gesture.
- */
- public boolean onLongPressToUnstashTaskbar() {
- return mControllers.taskbarStashController.onLongPressToUnstashTaskbar();
- }
-
- /**
* Called when we want to unstash taskbar when user performs swipes up gesture.
*/
public void onSwipeToUnstashTaskbar() {
@@ -1205,28 +1264,7 @@
* @param animateForward Whether to animate towards the unstashed hint state or back to stashed.
*/
public void startTaskbarUnstashHint(boolean animateForward) {
- // TODO(b/270395798): Clean up forceUnstash after removing long-press unstashing code.
- startTaskbarUnstashHint(animateForward, /* forceUnstash = */ false);
- }
-
- /**
- * Called when we detect a motion down or up/cancel in the nav region while stashed.
- *
- * @param animateForward Whether to animate towards the unstashed hint state or back to stashed.
- * @param forceUnstash Whether we force the unstash hint.
- */
- public void startTaskbarUnstashHint(boolean animateForward, boolean forceUnstash) {
- // TODO(b/270395798): Clean up forceUnstash after removing long-press unstashing code.
- mControllers.taskbarStashController.startUnstashHint(animateForward, forceUnstash);
- }
-
- /**
- * Enables manual taskbar stashing. This method should only be used for tests that need to
- * stash/unstash the taskbar.
- */
- @VisibleForTesting
- public void enableManualStashingDuringTests(boolean enableManualStashing) {
- mControllers.taskbarStashController.enableManualStashingDuringTests(enableManualStashing);
+ mControllers.taskbarStashController.startUnstashHint(animateForward);
}
/**
@@ -1239,15 +1277,12 @@
}
/**
- * Unstashes the Taskbar if it is stashed. This method should only be used to unstash the
- * taskbar at the end of a test.
+ * Unstashes the Taskbar if it is stashed.
*/
@VisibleForTesting
public void unstashTaskbarIfStashed() {
if (DisplayController.isTransientTaskbar(this)) {
mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(false);
- } else {
- mControllers.taskbarStashController.onLongPressToUnstashTaskbar();
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java
index ffaee45..333c07b 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarForceVisibleImmersiveController.java
@@ -22,7 +22,7 @@
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
import static com.android.launcher3.taskbar.NavbarButtonsViewController.ALPHA_INDEX_IMMERSIVE_MODE;
-import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_IMMERSIVE_MODE;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY;
import android.os.Bundle;
import android.os.Handler;
@@ -84,7 +84,7 @@
/** Update values tracked via sysui flags. */
public void updateSysuiFlags(int sysuiFlags) {
- mIsImmersiveMode = (sysuiFlags & SYSUI_STATE_IMMERSIVE_MODE) != 0;
+ mIsImmersiveMode = (sysuiFlags & SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) == 0;
if (mContext.isNavBarForceVisible()) {
if (mIsImmersiveMode) {
startIconDimming();
@@ -158,8 +158,7 @@
@Override
public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
- if (!isNavbarShownInImmersiveMode()
- || mControllers.taskbarStashController.supportsManualStashing()) {
+ if (!isNavbarShownInImmersiveMode()) {
return false;
}
return onControllerTouchEvent(ev);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index c0b07e7..bbac116 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -106,6 +106,7 @@
Settings.Secure.NAV_BAR_KIDS_MODE);
private final Context mContext;
+ private final @Nullable Context mNavigationBarPanelContext;
private WindowManager mWindowManager;
private FrameLayout mTaskbarRootLayout;
private boolean mAddedWindow;
@@ -198,6 +199,9 @@
mContext = service.createWindowContext(display,
ENABLE_TASKBAR_NAVBAR_UNIFICATION ? TYPE_NAVIGATION_BAR : TYPE_NAVIGATION_BAR_PANEL,
null);
+ mNavigationBarPanelContext = ENABLE_TASKBAR_NAVBAR_UNIFICATION
+ ? service.createWindowContext(display, TYPE_NAVIGATION_BAR_PANEL, null)
+ : null;
if (enableTaskbarNoRecreate()) {
mWindowManager = mContext.getSystemService(WindowManager.class);
mTaskbarRootLayout = new FrameLayout(mContext) {
@@ -435,8 +439,9 @@
}
if (enableTaskbarNoRecreate() || mTaskbarActivityContext == null) {
- mTaskbarActivityContext = new TaskbarActivityContext(mContext, dp,
- mNavButtonController, mUnfoldProgressProvider);
+ mTaskbarActivityContext = new TaskbarActivityContext(mContext,
+ mNavigationBarPanelContext, dp, mNavButtonController,
+ mUnfoldProgressProvider);
} else {
mTaskbarActivityContext.updateDeviceProfile(dp);
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt
index cbfa024..6cb28ee 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarPinningController.kt
@@ -16,7 +16,9 @@
package com.android.launcher3.taskbar
import android.animation.AnimatorSet
+import android.annotation.SuppressLint
import android.view.View
+import androidx.annotation.VisibleForTesting
import androidx.core.animation.doOnEnd
import com.android.launcher3.LauncherPrefs
import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING
@@ -31,46 +33,68 @@
private lateinit var controllers: TaskbarControllers
private lateinit var taskbarSharedState: TaskbarSharedState
- private val launcherPrefs = LauncherPrefs.get(context)
+ private lateinit var launcherPrefs: LauncherPrefs
private val statsLogManager = context.statsLogManager
- private var isAnimatingTaskbarPinning = false
+ @VisibleForTesting var isAnimatingTaskbarPinning = false
+ @VisibleForTesting lateinit var onCloseCallback: (preferenceChanged: Boolean) -> Unit
+ @SuppressLint("VisibleForTests")
fun init(taskbarControllers: TaskbarControllers, sharedState: TaskbarSharedState) {
controllers = taskbarControllers
taskbarSharedState = sharedState
+ launcherPrefs = context.launcherPrefs
+ onCloseCallback =
+ fun(didPreferenceChange: Boolean) {
+ statsLogManager.logger().log(LAUNCHER_TASKBAR_DIVIDER_MENU_CLOSE)
+ context.dragLayer.post { context.onPopupVisibilityChanged(false) }
+
+ if (!didPreferenceChange) {
+ return
+ }
+ val animateToValue =
+ if (!launcherPrefs.get(TASKBAR_PINNING)) {
+ PINNING_PERSISTENT
+ } else {
+ PINNING_TRANSIENT
+ }
+ taskbarSharedState.taskbarWasPinned = animateToValue == PINNING_TRANSIENT
+ animateTaskbarPinning(animateToValue)
+ }
}
fun showPinningView(view: View) {
context.isTaskbarWindowFullscreen = true
-
view.post {
- val popupView = createAndPopulate(view, context)
+ val popupView = getPopupView(view)
popupView.requestFocus()
-
- popupView.onCloseCallback =
- callback@{ didPreferenceChange ->
- statsLogManager.logger().log(LAUNCHER_TASKBAR_DIVIDER_MENU_CLOSE)
- context.dragLayer.post { context.onPopupVisibilityChanged(false) }
-
- if (!didPreferenceChange) {
- return@callback
- }
- val animateToValue =
- if (!launcherPrefs.get(TASKBAR_PINNING)) {
- PINNING_PERSISTENT
- } else {
- PINNING_TRANSIENT
- }
- taskbarSharedState.taskbarWasPinned = animateToValue == PINNING_TRANSIENT
- animateTaskbarPinning(animateToValue)
- }
+ popupView.onCloseCallback = onCloseCallback
context.onPopupVisibilityChanged(true)
popupView.show()
statsLogManager.logger().log(LAUNCHER_TASKBAR_DIVIDER_MENU_OPEN)
}
}
- private fun animateTaskbarPinning(animateToValue: Float) {
+ @VisibleForTesting
+ fun getPopupView(view: View): TaskbarDividerPopupView<*> {
+ return createAndPopulate(view, context)
+ }
+
+ @VisibleForTesting
+ fun animateTaskbarPinning(animateToValue: Float) {
+ val taskbarViewController = controllers.taskbarViewController
+ val animatorSet =
+ getAnimatorSetForTaskbarPinningAnimation(animateToValue).apply {
+ doOnEnd { recreateTaskbarAndUpdatePinningValue() }
+ duration = PINNING_ANIMATION_DURATION
+ }
+ controllers.taskbarOverlayController.hideWindow()
+ updateIsAnimatingTaskbarPinningAndNotifyTaskbarDragLayer(true)
+ taskbarViewController.animateAwayNotificationDotsDuringTaskbarPinningAnimation()
+ animatorSet.start()
+ }
+
+ @VisibleForTesting
+ fun getAnimatorSetForTaskbarPinningAnimation(animateToValue: Float): AnimatorSet {
val animatorSet = AnimatorSet()
val taskbarViewController = controllers.taskbarViewController
val dragLayerController = controllers.taskbarDragLayerController
@@ -82,13 +106,7 @@
taskbarViewController.taskbarIconTranslationXForPinning.animateToValue(animateToValue)
)
- controllers.taskbarOverlayController.hideWindow()
-
- animatorSet.doOnEnd { recreateTaskbarAndUpdatePinningValue() }
- animatorSet.duration = PINNING_ANIMATION_DURATION
- updateIsAnimatingTaskbarPinningAndNotifyTaskbarDragLayer(true)
- taskbarViewController.animateAwayNotificationDotsDuringTaskbarPinningAnimation()
- animatorSet.start()
+ return animatorSet
}
private fun updateIsAnimatingTaskbarPinningAndNotifyTaskbarDragLayer(isAnimating: Boolean) {
@@ -96,7 +114,8 @@
context.dragLayer.setAnimatingTaskbarPinning(isAnimating)
}
- private fun recreateTaskbarAndUpdatePinningValue() {
+ @VisibleForTesting
+ fun recreateTaskbarAndUpdatePinningValue() {
updateIsAnimatingTaskbarPinningAndNotifyTaskbarDragLayer(false)
launcherPrefs.put(TASKBAR_PINNING, !launcherPrefs.get(TASKBAR_PINNING))
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java
index 176a8c5..1224b3f 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarSharedState.java
@@ -73,9 +73,17 @@
};
// Allows us to shift translation logic when doing taskbar pinning animation.
- public Boolean startTaskbarVariantIsTransient = true;
+ public boolean startTaskbarVariantIsTransient = true;
// To track if taskbar was pinned using taskbar pinning feature at the time of recreate,
// so we can unstash transient taskbar when we un-pinning taskbar.
- public Boolean taskbarWasPinned = false;
+ private boolean mTaskbarWasPinned = false;
+
+ public boolean getTaskbarWasPinned() {
+ return mTaskbarWasPinned;
+ }
+
+ public void setTaskbarWasPinned(boolean taskbarWasPinned) {
+ mTaskbarWasPinned = taskbarWasPinned;
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarShortcutMenuAccessibilityDelegate.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarShortcutMenuAccessibilityDelegate.java
index e8c8fc4..bfbecf3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarShortcutMenuAccessibilityDelegate.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarShortcutMenuAccessibilityDelegate.java
@@ -34,6 +34,7 @@
import com.android.launcher3.accessibility.BaseAccessibilityDelegate;
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.model.data.ItemInfo;
+import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.notification.NotificationListener;
import com.android.launcher3.util.ShortcutUtil;
@@ -84,9 +85,9 @@
@Override
protected boolean performAction(View host, ItemInfo item, int action, boolean fromKeyboard) {
- if (item instanceof WorkspaceItemInfo
+ if (item instanceof ItemInfoWithIcon
&& (action == MOVE_TO_TOP_OR_LEFT || action == MOVE_TO_BOTTOM_OR_RIGHT)) {
- WorkspaceItemInfo info = (WorkspaceItemInfo) item;
+ ItemInfoWithIcon info = (ItemInfoWithIcon) item;
int side = action == MOVE_TO_TOP_OR_LEFT
? STAGE_POSITION_TOP_OR_LEFT : STAGE_POSITION_BOTTOM_OR_RIGHT;
@@ -97,10 +98,11 @@
.withInstanceId(instanceIds.second)
.log(getLogEventForPosition(side));
- if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
+ if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
+ && item instanceof WorkspaceItemInfo) {
SystemUiProxy.INSTANCE.get(mContext).startShortcut(
info.getIntent().getPackage(),
- info.getDeepShortcutId(),
+ ((WorkspaceItemInfo) info).getDeepShortcutId(),
side,
/* bundleOpts= */ null,
info.user,
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
index a34df4f..c74ddcb 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarStashController.java
@@ -15,17 +15,12 @@
*/
package com.android.launcher3.taskbar;
-import static android.view.HapticFeedbackConstants.LONG_PRESS;
import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS;
import static com.android.app.animation.Interpolators.EMPHASIZED;
import static com.android.app.animation.Interpolators.FINAL_FRAME;
import static com.android.app.animation.Interpolators.INSTANT;
import static com.android.app.animation.Interpolators.LINEAR;
-import static com.android.launcher3.LauncherPrefs.TASKBAR_PINNING;
-import static com.android.launcher3.config.FeatureFlags.enableTaskbarPinning;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_LONGPRESS_HIDE;
-import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_LONGPRESS_SHOW;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TRANSIENT_TASKBAR_HIDE;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TRANSIENT_TASKBAR_SHOW;
import static com.android.launcher3.taskbar.TaskbarKeyguardController.MASK_ANY_SYSUI_LOCKED;
@@ -44,7 +39,6 @@
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.app.RemoteAction;
-import android.content.SharedPreferences;
import android.graphics.drawable.Icon;
import android.os.SystemClock;
import android.util.Log;
@@ -62,9 +56,7 @@
import com.android.internal.jank.InteractionJankMonitor;
import com.android.launcher3.Alarm;
import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.LauncherPrefs;
import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimatedFloat;
import com.android.launcher3.anim.AnimatorListeners;
import com.android.launcher3.util.DisplayController;
@@ -86,27 +78,26 @@
private static final boolean DEBUG = false;
public static final int FLAG_IN_APP = 1 << 0;
- public static final int FLAG_STASHED_IN_APP_MANUAL = 1 << 1; // long press, persisted
- public static final int FLAG_STASHED_IN_APP_SYSUI = 1 << 2; // shade open, ...
- public static final int FLAG_STASHED_IN_APP_SETUP = 1 << 3; // setup wizard and AllSetActivity
- public static final int FLAG_STASHED_IN_APP_IME = 1 << 4; // IME is visible
- public static final int FLAG_IN_STASHED_LAUNCHER_STATE = 1 << 5;
- public static final int FLAG_STASHED_IN_TASKBAR_ALL_APPS = 1 << 6; // All apps is visible.
- public static final int FLAG_IN_SETUP = 1 << 7; // In the Setup Wizard
- public static final int FLAG_STASHED_SMALL_SCREEN = 1 << 8; // phone screen gesture nav, stashed
- public static final int FLAG_STASHED_IN_APP_AUTO = 1 << 9; // Autohide (transient taskbar).
- public static final int FLAG_STASHED_SYSUI = 1 << 10; // app pinning,...
- public static final int FLAG_STASHED_DEVICE_LOCKED = 1 << 11; // device is locked: keyguard, ...
- public static final int FLAG_IN_OVERVIEW = 1 << 12; // launcher is in overview
+ public static final int FLAG_STASHED_IN_APP_SYSUI = 1 << 1; // shade open, ...
+ public static final int FLAG_STASHED_IN_APP_SETUP = 1 << 2; // setup wizard and AllSetActivity
+ public static final int FLAG_STASHED_IN_APP_IME = 1 << 3; // IME is visible
+ public static final int FLAG_IN_STASHED_LAUNCHER_STATE = 1 << 4;
+ public static final int FLAG_STASHED_IN_TASKBAR_ALL_APPS = 1 << 5; // All apps is visible.
+ public static final int FLAG_IN_SETUP = 1 << 6; // In the Setup Wizard
+ public static final int FLAG_STASHED_SMALL_SCREEN = 1 << 7; // phone screen gesture nav, stashed
+ public static final int FLAG_STASHED_IN_APP_AUTO = 1 << 8; // Autohide (transient taskbar).
+ public static final int FLAG_STASHED_SYSUI = 1 << 9; // app pinning,...
+ public static final int FLAG_STASHED_DEVICE_LOCKED = 1 << 10; // device is locked: keyguard, ...
+ public static final int FLAG_IN_OVERVIEW = 1 << 11; // launcher is in overview
// If any of these flags are enabled, isInApp should return true.
private static final int FLAGS_IN_APP = FLAG_IN_APP | FLAG_IN_SETUP;
// If we're in an app and any of these flags are enabled, taskbar should be stashed.
- private static final int FLAGS_STASHED_IN_APP = FLAG_STASHED_IN_APP_MANUAL
- | FLAG_STASHED_IN_APP_SYSUI | FLAG_STASHED_IN_APP_SETUP
- | FLAG_STASHED_IN_APP_IME | FLAG_STASHED_IN_TASKBAR_ALL_APPS
- | FLAG_STASHED_SMALL_SCREEN | FLAG_STASHED_IN_APP_AUTO;
+ private static final int FLAGS_STASHED_IN_APP = FLAG_STASHED_IN_APP_SYSUI
+ | FLAG_STASHED_IN_APP_SETUP | FLAG_STASHED_IN_APP_IME
+ | FLAG_STASHED_IN_TASKBAR_ALL_APPS | FLAG_STASHED_SMALL_SCREEN
+ | FLAG_STASHED_IN_APP_AUTO;
// If any of these flags are enabled, inset apps by our stashed height instead of our unstashed
// height. This way the reported insets are consistent even during transitions out of the app.
@@ -167,21 +158,11 @@
private static final long TASKBAR_STASH_ICON_ALPHA_HOME_TO_APP_START_DELAY = 66;
/**
- * The scale that TaskbarView animates to when hinting towards the stashed state.
- */
- private static final float STASHED_TASKBAR_HINT_SCALE = 0.9f;
-
- /**
* The scale that the stashed handle animates to when hinting towards the unstashed state.
*/
private static final float UNSTASHED_TASKBAR_HANDLE_HINT_SCALE = 1.1f;
/**
- * The SharedPreferences key for whether user has manually stashed the taskbar.
- */
- private static final String SHARED_PREFS_STASHED_KEY = "taskbar_is_stashed";
-
- /**
* Whether taskbar should be stashed out of the box.
*/
private static final boolean DEFAULT_STASHED_PREF = false;
@@ -224,7 +205,6 @@
private @interface StashAnimation {}
private final TaskbarActivityContext mActivity;
- private final SharedPreferences mPrefs;
private final int mStashedHeight;
private final int mUnstashedHeight;
private final SystemUiProxy mSystemUiProxy;
@@ -253,8 +233,6 @@
private boolean mIsImeShowing;
private boolean mIsImeSwitcherShowing;
- private boolean mEnableManualStashingDuringTests = false;
-
private final Alarm mTimeoutAlarm = new Alarm();
private boolean mEnableBlockingTimeoutDuringTests = false;
@@ -274,7 +252,6 @@
public TaskbarStashController(TaskbarActivityContext activity) {
mActivity = activity;
- mPrefs = LauncherPrefs.getPrefs(mActivity);
mSystemUiProxy = SystemUiProxy.INSTANCE.get(activity);
mAccessibilityManager = mActivity.getSystemService(AccessibilityManager.class);
@@ -328,17 +305,9 @@
mTaskbarStashedHandleHintScale = stashedHandleController.getStashedHandleHintScale();
boolean isTransientTaskbar = DisplayController.isTransientTaskbar(mActivity);
- // We use supportsVisualStashing() here instead of supportsManualStashing() because we want
- // it to work properly for tests that recreate taskbar. This check is here just to ensure
- // that taskbar unstashes when going to 3 button mode (supportsVisualStashing() false).
- boolean isManuallyStashedInApp = supportsVisualStashing()
- && !isTransientTaskbar
- && !enableTaskbarPinning()
- && mPrefs.getBoolean(SHARED_PREFS_STASHED_KEY, DEFAULT_STASHED_PREF);
boolean isInSetup = !mActivity.isUserSetupComplete() || setupUIVisible;
- updateStateForFlag(FLAG_STASHED_IN_APP_MANUAL, isManuallyStashedInApp);
updateStateForFlag(FLAG_STASHED_IN_APP_AUTO,
- isTransientTaskbar && !mTaskbarSharedState.taskbarWasPinned);
+ isTransientTaskbar && !mTaskbarSharedState.getTaskbarWasPinned());
updateStateForFlag(FLAG_STASHED_IN_APP_SETUP, isInSetup);
updateStateForFlag(FLAG_IN_SETUP, isInSetup);
updateStateForFlag(FLAG_STASHED_SMALL_SCREEN, isPhoneMode()
@@ -347,7 +316,7 @@
// us that we're paused until a bit later. This avoids flickering upon recreating taskbar.
updateStateForFlag(FLAG_IN_APP, true);
applyState(/* duration = */ 0);
- if (mTaskbarSharedState.taskbarWasPinned) {
+ if (mTaskbarSharedState.getTaskbarWasPinned()) {
tryStartTaskbarTimeout();
}
notifyStashChange(/* visible */ false, /* stashed */ isStashedInApp());
@@ -362,28 +331,6 @@
}
/**
- * Returns whether the user can manually stash the taskbar based on the current device state.
- */
- protected boolean supportsManualStashing() {
- if (enableTaskbarPinning() && LauncherPrefs.get(mActivity).get(TASKBAR_PINNING)) {
- return false;
- }
- return supportsVisualStashing()
- && isInApp()
- && (!Utilities.isRunningInTestHarness() || mEnableManualStashingDuringTests)
- && !DisplayController.isTransientTaskbar(mActivity);
- }
-
- /**
- * Enables support for manual stashing. This should only be used to add this functionality
- * to Launcher specific tests.
- */
- @VisibleForTesting
- public void enableManualStashingDuringTests(boolean enableManualStashing) {
- mEnableManualStashingDuringTests = enableManualStashing;
- }
-
- /**
* Enables the auto timeout for taskbar stashing. This method should only be used for taskbar
* testing.
*/
@@ -577,53 +524,6 @@
/* shouldBubblesFollow= */ !bubbleBarExpanded);
}
- /**
- * Should be called when long pressing the nav region when taskbar is present.
- * @return Whether taskbar was stashed and now is unstashed.
- */
- public boolean onLongPressToUnstashTaskbar() {
- if (!isStashed()) {
- // We only listen for long press on the nav region to unstash the taskbar. To stash the
- // taskbar, we use an OnLongClickListener on TaskbarView instead.
- return false;
- }
- if (!canCurrentlyManuallyUnstash()) {
- return false;
- }
- if (updateAndAnimateIsManuallyStashedInApp(false)) {
- mControllers.taskbarActivityContext.getDragLayer().performHapticFeedback(LONG_PRESS);
- return true;
- }
- return false;
- }
-
- /**
- * Returns whether taskbar will unstash when long pressing it based on the current state. The
- * only time this is true is if the user is in an app and the taskbar is only stashed because
- * the user previously long pressed to manually stash (not due to other reasons like IME).
- */
- private boolean canCurrentlyManuallyUnstash() {
- return (mState & (FLAG_IN_APP | FLAGS_STASHED_IN_APP))
- == (FLAG_IN_APP | FLAG_STASHED_IN_APP_MANUAL);
- }
-
- /**
- * Updates whether we should stash the taskbar when in apps, and animates to the changed state.
- * @return Whether we started an animation to either be newly stashed or unstashed.
- */
- public boolean updateAndAnimateIsManuallyStashedInApp(boolean isManuallyStashedInApp) {
- if (!supportsManualStashing()) {
- return false;
- }
- if (hasAnyFlag(FLAG_STASHED_IN_APP_MANUAL) != isManuallyStashedInApp) {
- mPrefs.edit().putBoolean(SHARED_PREFS_STASHED_KEY, isManuallyStashedInApp).apply();
- updateStateForFlag(FLAG_STASHED_IN_APP_MANUAL, isManuallyStashedInApp);
- applyState();
- return true;
- }
- return false;
- }
-
/** Toggles the Taskbar's stash state. */
public void toggleTaskbarStash() {
if (!DisplayController.isTransientTaskbar(mActivity) || !hasAnyFlag(FLAGS_IN_APP)) return;
@@ -910,21 +810,6 @@
}
});
}
- /**
- * Creates and starts a partial stash animation, hinting at the new state that will trigger when
- * long press is detected.
- * @param animateForward Whether we are going towards the new stashed state or returning to the
- * unstashed state.
- */
- public void startStashHint(boolean animateForward) {
- if (isStashed() || !supportsManualStashing()) {
- // Already stashed, no need to hint in that direction.
- return;
- }
- mIconScaleForStash.animateToValue(
- animateForward ? STASHED_TASKBAR_HINT_SCALE : 1)
- .setDuration(TASKBAR_HINT_STASH_DURATION).start();
- }
/**
* Creates and starts a partial unstash animation, hinting at the new state that will trigger
@@ -932,19 +817,12 @@
*
* @param animateForward Whether we are going towards the new unstashed state or returning to
* the stashed state.
- * @param forceUnstash Whether we force the unstash hint to animate.
*/
- protected void startUnstashHint(boolean animateForward, boolean forceUnstash) {
+ protected void startUnstashHint(boolean animateForward) {
if (!isStashed()) {
// Already unstashed, no need to hint in that direction.
return;
}
- // TODO(b/270395798): Clean up after removing long-press unstashing code path.
- if (!canCurrentlyManuallyUnstash() && !forceUnstash) {
- // If any other flags are causing us to be stashed, long press won't cause us to
- // unstash, so don't hint that it will.
- return;
- }
mTaskbarStashedHandleHintScale.animateToValue(
animateForward ? UNSTASHED_TASKBAR_HANDLE_HINT_SCALE : 1)
.setDuration(TASKBAR_HINT_STASH_DURATION).start();
@@ -1097,13 +975,6 @@
mControllers.taskbarAutohideSuspendController.updateFlag(
TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_IN_LAUNCHER, !isInApp());
}
- if (hasAnyFlag(changedFlags, FLAG_STASHED_IN_APP_MANUAL)) {
- if (hasAnyFlag(FLAG_STASHED_IN_APP_MANUAL)) {
- mActivity.getStatsLogManager().logger().log(LAUNCHER_TASKBAR_LONGPRESS_HIDE);
- } else {
- mActivity.getStatsLogManager().logger().log(LAUNCHER_TASKBAR_LONGPRESS_SHOW);
- }
- }
if (hasAnyFlag(changedFlags, FLAG_STASHED_IN_APP_AUTO)) {
mActivity.getStatsLogManager().logger().log(hasAnyFlag(FLAG_STASHED_IN_APP_AUTO)
? LAUNCHER_TRANSIENT_TASKBAR_HIDE
@@ -1227,7 +1098,6 @@
private static String getStateString(int flags) {
StringJoiner sj = new StringJoiner("|");
appendFlag(sj, flags, FLAGS_IN_APP, "FLAG_IN_APP");
- appendFlag(sj, flags, FLAG_STASHED_IN_APP_MANUAL, "FLAG_STASHED_IN_APP_MANUAL");
appendFlag(sj, flags, FLAG_STASHED_IN_APP_SYSUI, "FLAG_STASHED_IN_APP_SYSUI");
appendFlag(sj, flags, FLAG_STASHED_IN_APP_SETUP, "FLAG_STASHED_IN_APP_SETUP");
appendFlag(sj, flags, FLAG_STASHED_IN_APP_IME, "FLAG_STASHED_IN_APP_IME");
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
index aee3c6f..a29a25c 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarUIController.java
@@ -216,6 +216,7 @@
recentsView.getSplitSelectController().findLastActiveTasksAndRunCallback(
Collections.singletonList(splitSelectSource.itemInfo.getComponentKey()),
+ false /* findExactPairMatch */,
foundTasks -> {
@Nullable Task foundTask = foundTasks.get(0);
splitSelectSource.alreadyRunningTaskId = foundTask == null
@@ -234,6 +235,7 @@
RecentsView recents = getRecentsView();
recents.getSplitSelectController().findLastActiveTasksAndRunCallback(
Collections.singletonList(info.getComponentKey()),
+ false /* findExactPairMatch */,
foundTasks -> {
@Nullable Task foundTask = foundTasks.get(0);
if (foundTask != null) {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 1be1798..2ab0066 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -19,6 +19,8 @@
import static android.view.accessibility.AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED;
import static com.android.launcher3.Flags.enableCursorHoverStates;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_FOLDER;
import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR;
import static com.android.launcher3.config.FeatureFlags.enableTaskbarPinning;
import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR;
@@ -47,6 +49,7 @@
import com.android.launcher3.Insettable;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
+import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.icons.ThemedIconDrawable;
import com.android.launcher3.model.data.FolderInfo;
@@ -261,8 +264,6 @@
mIconClickListener = mControllerCallbacks.getIconOnClickListener();
mIconLongClickListener = mControllerCallbacks.getIconOnLongClickListener();
- setOnLongClickListener(mControllerCallbacks.getBackgroundOnLongClickListener());
-
if (mAllAppsButton != null) {
mAllAppsButton.setOnClickListener(mControllerCallbacks.getAllAppsButtonClickListener());
}
@@ -309,12 +310,14 @@
// Replace any Hotseat views with the appropriate type if it's not already that type.
final int expectedLayoutResId;
- boolean isFolder = false;
+ boolean isCollection = false;
if (hotseatItemInfo.isPredictedItem()) {
expectedLayoutResId = R.layout.taskbar_predicted_app_icon;
- } else if (hotseatItemInfo instanceof FolderInfo) {
- expectedLayoutResId = R.layout.folder_icon;
- isFolder = true;
+ } else if (hotseatItemInfo instanceof FolderInfo fi) {
+ expectedLayoutResId = fi.itemType == ITEM_TYPE_APP_PAIR
+ ? R.layout.app_pair_icon
+ : R.layout.folder_icon;
+ isCollection = true;
} else {
expectedLayoutResId = R.layout.taskbar_app_icon;
}
@@ -325,7 +328,7 @@
// see if the view can be reused
if ((hotseatView.getSourceLayoutResId() != expectedLayoutResId)
- || (isFolder && (hotseatView.getTag() != hotseatItemInfo))) {
+ || (isCollection && (hotseatView.getTag() != hotseatItemInfo))) {
// Unlike for BubbleTextView, we can't reapply a new FolderInfo after inflation,
// so if the info changes we need to reinflate. This should only happen if a new
// folder is dragged to the position that another folder previously existed.
@@ -338,12 +341,23 @@
}
if (hotseatView == null) {
- if (isFolder) {
+ if (isCollection) {
FolderInfo folderInfo = (FolderInfo) hotseatItemInfo;
- FolderIcon folderIcon = FolderIcon.inflateFolderAndIcon(expectedLayoutResId,
- mActivityContext, this, folderInfo);
- folderIcon.setTextVisible(false);
- hotseatView = folderIcon;
+ switch (hotseatItemInfo.itemType) {
+ case ITEM_TYPE_FOLDER:
+ hotseatView = FolderIcon.inflateFolderAndIcon(
+ expectedLayoutResId, mActivityContext, this, folderInfo);
+ ((FolderIcon) hotseatView).setTextVisible(false);
+ break;
+ case ITEM_TYPE_APP_PAIR:
+ hotseatView = AppPairIcon.inflateIcon(
+ expectedLayoutResId, mActivityContext, this, folderInfo);
+ ((AppPairIcon) hotseatView).setTextVisible(false);
+ break;
+ default:
+ throw new IllegalStateException(
+ "Unexpected item type: " + hotseatItemInfo.itemType);
+ }
} else {
hotseatView = inflate(expectedLayoutResId);
}
@@ -513,24 +527,6 @@
}
}
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- if (mIconLayoutBounds.left <= event.getX() && event.getX() <= mIconLayoutBounds.right) {
- // Don't allow long pressing between icons, or above/below them.
- return true;
- }
- if (mControllerCallbacks.onTouchEvent(event)) {
- int oldAction = event.getAction();
- try {
- event.setAction(MotionEvent.ACTION_CANCEL);
- return super.onTouchEvent(event);
- } finally {
- event.setAction(oldAction);
- }
- }
- return super.onTouchEvent(event);
- }
-
/**
* Returns whether the given MotionEvent, *in screen coorindates*, is within any Taskbar item's
* touch bounds.
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
index 8a7a98c..14ab471 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarViewController.java
@@ -22,7 +22,6 @@
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X;
import static com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y;
import static com.android.launcher3.Utilities.mapRange;
-import static com.android.launcher3.Utilities.squaredHypot;
import static com.android.launcher3.anim.AnimatedFloat.VALUE;
import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
@@ -899,53 +898,12 @@
return mControllers.taskbarDragController::startDragOnLongClick;
}
- public View.OnLongClickListener getBackgroundOnLongClickListener() {
- return view -> mControllers.taskbarStashController
- .updateAndAnimateIsManuallyStashedInApp(true);
- }
-
/** Gets the hover listener for the provided icon view. */
public View.OnHoverListener getIconOnHoverListener(View icon) {
return new TaskbarHoverToolTipController(mActivity, mTaskbarView, icon);
}
/**
- * Get the first chance to handle TaskbarView#onTouchEvent, and return whether we want to
- * consume the touch so TaskbarView treats it as an ACTION_CANCEL.
- * TODO(b/270395798): We can remove this entirely once we remove the Transient Taskbar flag.
- */
- public boolean onTouchEvent(MotionEvent motionEvent) {
- final float x = motionEvent.getRawX();
- final float y = motionEvent.getRawY();
- switch (motionEvent.getAction()) {
- case MotionEvent.ACTION_DOWN:
- mDownX = x;
- mDownY = y;
- mControllers.taskbarStashController.startStashHint(/* animateForward = */ true);
- mCanceledStashHint = false;
- break;
- case MotionEvent.ACTION_MOVE:
- if (!mCanceledStashHint
- && squaredHypot(mDownX - x, mDownY - y) > mSquaredTouchSlop) {
- mControllers.taskbarStashController.startStashHint(
- /* animateForward= */ false);
- mCanceledStashHint = true;
- return true;
- }
- break;
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_CANCEL:
- if (!mCanceledStashHint) {
- mControllers.taskbarStashController.startStashHint(
- /* animateForward= */ false);
- }
- break;
- }
-
- return false;
- }
-
- /**
* Notifies launcher to update icon alignment.
*/
public void notifyIconLayoutBoundsChanged() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
index 5ce2a7a..964d329 100644
--- a/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsSlideInView.java
@@ -222,7 +222,7 @@
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
mNoIntercept = !mAppsView.shouldContainerScroll(ev)
|| getTopOpenViewWithType(
- mActivityContext, TYPE_ACCESSIBLE & ~TYPE_TASKBAR_OVERLAYS) != null;
+ mActivityContext, TYPE_TOUCH_CONTROLLER_NO_INTERCEPT) != null;
}
return super.onControllerInterceptTouchEvent(ev);
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java
index c4eeea7..adbec65 100644
--- a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java
@@ -16,6 +16,7 @@
package com.android.launcher3.taskbar.overlay;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_CONSUME_IME_INSETS;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
import static com.android.launcher3.AbstractFloatingView.TYPE_ALL;
@@ -187,6 +188,7 @@
layoutParams.setFitInsetsTypes(0); // Handled by container view.
layoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
layoutParams.setSystemApplicationOverlay(true);
+ layoutParams.privateFlags = PRIVATE_FLAG_CONSUME_IME_INSETS;
return layoutParams;
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 89b7fa4..14e258b 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -106,6 +106,7 @@
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.appprediction.PredictionRowView;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.desktop.DesktopRecentsTransitionController;
@@ -116,7 +117,6 @@
import com.android.launcher3.model.BgDataModel.FixedContainerItems;
import com.android.launcher3.model.WellbeingModel;
import com.android.launcher3.model.data.ItemInfo;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.popup.SystemShortcut;
import com.android.launcher3.proxy.ProxyActivityStarter;
import com.android.launcher3.statehandlers.DepthController;
@@ -642,6 +642,7 @@
// using that.
mSplitSelectStateController.findLastActiveTasksAndRunCallback(
Collections.singletonList(splitSelectSource.itemInfo.getComponentKey()),
+ false /* findExactPairMatch */,
foundTasks -> {
@Nullable Task foundTask = foundTasks.get(0);
boolean taskWasFound = foundTask != null;
@@ -1283,8 +1284,8 @@
/**
* Launches two apps as an app pair.
*/
- public void launchAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
- mSplitSelectStateController.getAppPairsController().launchAppPair(app1, app2);
+ public void launchAppPair(AppPairIcon appPairIcon) {
+ mSplitSelectStateController.getAppPairsController().launchAppPair(appPairIcon);
}
public boolean canStartHomeSafely() {
diff --git a/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsUI.java b/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsUI.java
index 301fbe4..c1a85fa 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsUI.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/flags/DeveloperOptionsUI.java
@@ -28,6 +28,7 @@
import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_HAPTIC_HINT_START_SCALE_PERCENT;
import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_SLOP_PERCENTAGE;
import static com.android.launcher3.LauncherPrefs.LONG_PRESS_NAV_HANDLE_TIMEOUT_MS;
+import static com.android.launcher3.LauncherPrefs.PRIVATE_SPACE_APPS;
import static com.android.launcher3.settings.SettingsActivity.EXTRA_FRAGMENT_HIGHLIGHT_KEY;
import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.PLUGIN_CHANGED;
import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.pluginEnabledKey;
@@ -67,6 +68,7 @@
import androidx.preference.SwitchPreference;
import com.android.launcher3.ConstantItem;
+import com.android.launcher3.Flags;
import com.android.launcher3.LauncherPrefs;
import com.android.launcher3.R;
import com.android.launcher3.config.FeatureFlags;
@@ -115,6 +117,9 @@
addAllAppsFromOverviewCatergory();
}
addCustomLpnhCategory();
+ if (Flags.enablePrivateSpace()) {
+ addCustomPrivateAppsCategory();
+ }
}
private void filterPreferences(String query, PreferenceGroup pg) {
@@ -365,6 +370,12 @@
}
}
+ private void addCustomPrivateAppsCategory() {
+ PreferenceCategory category = newCategory("Apps in Private Space Config");
+ category.addPreference(createSeekBarPreference(
+ "Number of Apps to put in private region", 0, 100, 1, PRIVATE_SPACE_APPS));
+ }
+
/**
* Create a preference with text and a seek bar. Should be added to a PreferenceCategory.
*
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
index 8cbf239..2c937b0 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java
@@ -15,8 +15,7 @@
*/
package com.android.launcher3.uioverrides.touchcontrollers;
-import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE;
-import static com.android.launcher3.AbstractFloatingView.TYPE_ALL_APPS_EDU;
+import static com.android.launcher3.AbstractFloatingView.TYPE_TOUCH_CONTROLLER_NO_INTERCEPT;
import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
import static com.android.launcher3.LauncherState.ALL_APPS;
import static com.android.launcher3.LauncherState.NORMAL;
@@ -84,7 +83,7 @@
return false;
}
}
- if (getTopOpenViewWithType(mLauncher, TYPE_ACCESSIBLE | TYPE_ALL_APPS_EDU) != null) {
+ if (getTopOpenViewWithType(mLauncher, TYPE_TOUCH_CONTROLLER_NO_INTERCEPT) != null) {
return false;
}
return true;
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
index 3d94857..19bfe06 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java
@@ -15,7 +15,7 @@
*/
package com.android.launcher3.uioverrides.touchcontrollers;
-import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE;
+import static com.android.launcher3.AbstractFloatingView.TYPE_TOUCH_CONTROLLER_NO_INTERCEPT;
import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH;
@@ -112,7 +112,8 @@
// If we are already animating from a previous state, we can intercept.
return true;
}
- if (AbstractFloatingView.getTopOpenViewWithType(mActivity, TYPE_ACCESSIBLE) != null) {
+ if (AbstractFloatingView.getTopOpenViewWithType(
+ mActivity, TYPE_TOUCH_CONTROLLER_NO_INTERCEPT) != null) {
return false;
}
return isRecentsInteractive();
diff --git a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
index f801b3b..0303791 100644
--- a/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
+++ b/quickstep/src/com/android/quickstep/QuickstepTestInformationHandler.java
@@ -82,27 +82,11 @@
return response;
}
- case TestProtocol.REQUEST_ENABLE_MANUAL_TASKBAR_STASHING:
- runOnTISBinder(tisBinder -> {
- enableManualTaskbarStashing(tisBinder, true);
- });
- return response;
-
- case TestProtocol.REQUEST_DISABLE_MANUAL_TASKBAR_STASHING:
- runOnTISBinder(tisBinder -> {
- enableManualTaskbarStashing(tisBinder, false);
- });
- return response;
-
case TestProtocol.REQUEST_UNSTASH_TASKBAR_IF_STASHED:
runOnTISBinder(tisBinder -> {
- enableManualTaskbarStashing(tisBinder, true);
-
// Allow null-pointer to catch illegal states.
tisBinder.getTaskbarManager().getCurrentActivityContext()
.unstashTaskbarIfStashed();
-
- enableManualTaskbarStashing(tisBinder, false);
});
return response;
@@ -187,13 +171,6 @@
return super.isLauncherInitialized() && TouchInteractionService.isInitialized();
}
- private void enableManualTaskbarStashing(
- TouchInteractionService.TISBinder tisBinder, boolean enable) {
- // Allow null-pointer to catch illegal states.
- tisBinder.getTaskbarManager().getCurrentActivityContext().enableManualStashingDuringTests(
- enable);
- }
-
private void enableBlockingTimeout(
TouchInteractionService.TISBinder tisBinder, boolean enable) {
TaskbarActivityContext context = tisBinder.getTaskbarManager().getCurrentActivityContext();
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 27de20c..94ed5b9 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -17,7 +17,6 @@
import static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE;
-import static com.android.launcher3.testing.shared.TestProtocol.testLogD;
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.SplitConfigurationOptions.StagePosition;
@@ -102,6 +101,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
+import java.util.List;
/**
* Holds the reference to SystemUI.
@@ -147,6 +147,9 @@
private IDesktopTaskListener mDesktopTaskListener;
private final LinkedHashMap<RemoteTransition, TransitionFilter> mRemoteTransitions =
new LinkedHashMap<>();
+
+ private final List<Runnable> mStateChangeCallbacks = new ArrayList<>();
+
private IBinder mOriginalTransactionToken = null;
private IOnBackInvokedCallback mBackToLauncherCallback;
private IRemoteAnimationRunner mBackToLauncherRunner;
@@ -268,6 +271,7 @@
setDesktopTaskListener(mDesktopTaskListener);
setAssistantOverridesRequested(
AssistUtils.newInstance(mContext).getSysUiAssistOverrideInvocationTypes());
+ mStateChangeCallbacks.forEach(Runnable::run);
}
/**
@@ -278,6 +282,20 @@
setProxy(null, null, null, null, null, null, null, null, null, null, null, null, null);
}
+ /**
+ * Adds a callback to be notified whenever the active state changes
+ */
+ public void addOnStateChangeListener(Runnable callback) {
+ mStateChangeCallbacks.add(callback);
+ }
+
+ /**
+ * Removes a previously added state change callback
+ */
+ public void removeOnStateChangeListener(Runnable callback) {
+ mStateChangeCallbacks.remove(callback);
+ }
+
// TODO(141886704): Find a way to remove this
public void setLastSystemUiStateFlags(int stateFlags) {
mLastSystemUiStateFlags = stateFlags;
@@ -1082,6 +1100,25 @@
}
/**
+ * Returns a surface which can be used to attach overlays to home task or null if
+ * the task doesn't exist or sysui is not connected
+ */
+ @Nullable
+ public SurfaceControl getHomeTaskOverlayContainer() {
+ // Use a local reference as this method can be called on a worker thread, which can lead
+ // to NullPointer exceptions if mShellTransitions is modified on the main thread.
+ IShellTransitions shellTransitions = mShellTransitions;
+ if (shellTransitions != null) {
+ try {
+ return mShellTransitions.getHomeTaskOverlayContainer();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed call getOverlayContainerForTask", e);
+ }
+ }
+ return null;
+ }
+
+ /**
* Use SystemUI's transaction-queue instead of Launcher's independent one. This is necessary
* if Launcher and SystemUI need to coordinate transactions (eg. for shell transitions).
*/
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index ddddc89..11c5ab4 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -18,8 +18,6 @@
import static android.view.RemoteAnimationTarget.MODE_CLOSING;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
-import static android.view.WindowManager.TRANSIT_OPEN;
-import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static com.android.app.animation.Interpolators.LINEAR;
import static com.android.app.animation.Interpolators.TOUCH_RESPONSE;
@@ -421,106 +419,34 @@
* Technically this case should be taken care of by
* {@link #composeRecentsSplitLaunchAnimatorLegacy} below, but the way we launch tasks whether
* it's a single task or multiple tasks results in different entry-points.
- *
- * If it is null, then it will simply fade in the starting apps and fade out launcher (for the
- * case where launcher handles animating starting split tasks from app icon)
*/
public static void composeRecentsSplitLaunchAnimator(GroupedTaskView launchingTaskView,
@NonNull StateManager stateManager, @Nullable DepthController depthController,
- int initialTaskId, int secondTaskId, @NonNull TransitionInfo transitionInfo,
- SurfaceControl.Transaction t, @NonNull Runnable finishCallback) {
- if (launchingTaskView != null) {
- AnimatorSet animatorSet = new AnimatorSet();
- animatorSet.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- finishCallback.run();
- }
- });
-
- final RemoteAnimationTarget[] appTargets =
- RemoteAnimationTargetCompat.wrapApps(transitionInfo, t, null /* leashMap */);
- final RemoteAnimationTarget[] wallpaperTargets =
- RemoteAnimationTargetCompat.wrapNonApps(
- transitionInfo, true /* wallpapers */, t, null /* leashMap */);
- final RemoteAnimationTarget[] nonAppTargets =
- RemoteAnimationTargetCompat.wrapNonApps(
- transitionInfo, false /* wallpapers */, t, null /* leashMap */);
- final RecentsView recentsView = launchingTaskView.getRecentsView();
- composeRecentsLaunchAnimator(animatorSet, launchingTaskView,
- appTargets, wallpaperTargets, nonAppTargets,
- true, stateManager,
- recentsView, depthController);
-
- t.apply();
- animatorSet.start();
- return;
- }
-
- TransitionInfo.Change splitRoot1 = null;
- TransitionInfo.Change splitRoot2 = null;
- final ArrayList<SurfaceControl> openingTargets = new ArrayList<>();
- for (int i = 0; i < transitionInfo.getChanges().size(); ++i) {
- final TransitionInfo.Change change = transitionInfo.getChanges().get(i);
- if (change.getTaskInfo() == null) {
- continue;
- }
- final int taskId = change.getTaskInfo().taskId;
- final int mode = change.getMode();
-
- // Find the target tasks' root tasks since those are the split stages that need to
- // be animated (the tasks themselves are children and thus inherit animation).
- if (taskId == initialTaskId || taskId == secondTaskId) {
- if (!(mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) {
- throw new IllegalStateException(
- "Expected task to be showing, but it is " + mode);
- }
- }
- if (taskId == initialTaskId) {
- splitRoot1 = change.getParent() == null ? change :
- transitionInfo.getChange(change.getParent());
- openingTargets.add(splitRoot1.getLeash());
- }
- if (taskId == secondTaskId) {
- splitRoot2 = change.getParent() == null ? change :
- transitionInfo.getChange(change.getParent());
- openingTargets.add(splitRoot2.getLeash());
- }
- }
-
- SurfaceControl.Transaction animTransaction = new SurfaceControl.Transaction();
- ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
- animator.setDuration(SPLIT_LAUNCH_DURATION);
- animator.addUpdateListener(valueAnimator -> {
- float progress = valueAnimator.getAnimatedFraction();
- for (SurfaceControl leash: openingTargets) {
- animTransaction.setAlpha(leash, progress);
- }
- animTransaction.apply();
- });
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animation) {
- for (SurfaceControl leash: openingTargets) {
- animTransaction.show(leash)
- .setAlpha(leash, 0.0f);
- }
- animTransaction.apply();
- }
-
+ @NonNull TransitionInfo transitionInfo, SurfaceControl.Transaction t,
+ @NonNull Runnable finishCallback) {
+ AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
finishCallback.run();
}
});
- if (splitRoot1 != null && splitRoot1.getParent() != null) {
- // Set the highest level split root alpha; we could technically use the parent of either
- // splitRoot1 or splitRoot2
- t.setAlpha(transitionInfo.getChange(splitRoot1.getParent()).getLeash(), 1f);
- }
+ final RemoteAnimationTarget[] appTargets =
+ RemoteAnimationTargetCompat.wrapApps(transitionInfo, t, null /* leashMap */);
+ final RemoteAnimationTarget[] wallpaperTargets =
+ RemoteAnimationTargetCompat.wrapNonApps(
+ transitionInfo, true /* wallpapers */, t, null /* leashMap */);
+ final RemoteAnimationTarget[] nonAppTargets =
+ RemoteAnimationTargetCompat.wrapNonApps(
+ transitionInfo, false /* wallpapers */, t, null /* leashMap */);
+ final RecentsView recentsView = launchingTaskView.getRecentsView();
+ composeRecentsLaunchAnimator(animatorSet, launchingTaskView, appTargets, wallpaperTargets,
+ nonAppTargets, /* launcherClosing */ true, stateManager, recentsView,
+ depthController);
+
t.apply();
- animator.start();
+ animatorSet.start();
}
/**
diff --git a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
index 12a8bd9..cd180ba 100644
--- a/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
+++ b/quickstep/src/com/android/quickstep/inputconsumers/TaskbarUnstashInputConsumer.java
@@ -22,15 +22,12 @@
import static com.android.launcher3.Flags.enableCursorHoverStates;
import static com.android.launcher3.MotionEventsUtils.isTrackpadMotionEvent;
-import static com.android.launcher3.Utilities.squaredHypot;
import static com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_TOUCHING;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.PointF;
import android.graphics.Rect;
-import android.view.GestureDetector;
-import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
@@ -39,7 +36,6 @@
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
import com.android.launcher3.taskbar.TaskbarActivityContext;
import com.android.launcher3.taskbar.TaskbarThresholdUtils;
import com.android.launcher3.taskbar.TaskbarTranslationController.TransitionCallback;
@@ -51,22 +47,13 @@
import com.android.systemui.shared.system.InputMonitorCompat;
/**
- * Listens for touch and hover events to unstash the Taskbar.
- *
- * <p>Cancels the current gesture if the long press causes the Taskbar to be unstashed.
+ * Listens for touch (swipe) and hover events to unstash the Taskbar.
*/
public class TaskbarUnstashInputConsumer extends DelegateInputConsumer {
private final TaskbarActivityContext mTaskbarActivityContext;
private final OverviewCommandHelper mOverviewCommandHelper;
- private final GestureDetector mLongPressDetector;
- private final float mSquaredTouchSlop;
-
- private float mLongPressDownX, mLongPressDownY;
- private boolean mCanceledUnstashHint;
private final float mUnstashArea;
- private final float mScreenWidth;
-
private final int mTaskbarNavThreshold;
private final int mTaskbarNavThresholdY;
private final boolean mIsTaskbarAllAppsOpen;
@@ -96,10 +83,7 @@
super(delegate, inputMonitor);
mTaskbarActivityContext = taskbarActivityContext;
mOverviewCommandHelper = overviewCommandHelper;
- // TODO(b/270395798): remove this when cleaning up old Persistent Taskbar code.
- mSquaredTouchSlop = Utilities.squaredTouchSlop(context);
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
- mScreenWidth = taskbarActivityContext.getDeviceProfile().widthPx;
Resources res = context.getResources();
mUnstashArea = res.getDimensionPixelSize(R.dimen.taskbar_unstash_input_area);
@@ -111,13 +95,6 @@
mIsTransientTaskbar = DisplayController.isTransientTaskbar(context);
- mLongPressDetector = new GestureDetector(context, new SimpleOnGestureListener() {
- @Override
- public void onLongPress(MotionEvent motionEvent) {
- onLongPressDetected(motionEvent);
- }
- });
-
mBottomScreenEdge = res.getDimensionPixelSize(
R.dimen.taskbar_stashed_screen_edge_hover_deadzone_height);
mStashedTaskbarBottomEdge =
@@ -135,7 +112,6 @@
@Override
public void onMotionEvent(MotionEvent ev) {
- mLongPressDetector.onTouchEvent(ev);
if (mState != STATE_ACTIVE) {
boolean isStashedTaskbarHovered = isMouseEvent(ev)
&& isStashedTaskbarHovered((int) ev.getX(), (int) ev.getY());
@@ -152,15 +128,6 @@
mHasPassedTaskbarNavThreshold = false;
mTaskbarActivityContext.setAutohideSuspendFlag(
FLAG_AUTOHIDE_SUSPEND_TOUCHING, true);
- if (isInTaskbarArea(x)) {
- if (!mIsTransientTaskbar) {
- mLongPressDownX = x;
- mLongPressDownY = y;
- mTaskbarActivityContext.startTaskbarUnstashHint(
- /* animateForward = */ true);
- mCanceledUnstashHint = false;
- }
- }
if (mTransitionCallback != null && !mIsTaskbarAllAppsOpen) {
mTransitionCallback.onActionDown();
}
@@ -181,15 +148,6 @@
}
break;
case MotionEvent.ACTION_MOVE:
- if (!mIsTransientTaskbar
- && !mCanceledUnstashHint
- && squaredHypot(mLongPressDownX - x, mLongPressDownY - y)
- > mSquaredTouchSlop) {
- mTaskbarActivityContext.startTaskbarUnstashHint(
- /* animateForward = */ false);
- mCanceledUnstashHint = true;
- }
-
int pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex == INVALID_POINTER_ID) {
break;
@@ -283,10 +241,6 @@
}
private void cleanupAfterMotionEvent() {
- if (!mIsTransientTaskbar && !mCanceledUnstashHint) {
- mTaskbarActivityContext.startTaskbarUnstashHint(
- /* animateForward = */ false);
- }
mTaskbarActivityContext.setAutohideSuspendFlag(
FLAG_AUTOHIDE_SUSPEND_TOUCHING, false);
if (mTransitionCallback != null) {
@@ -298,12 +252,6 @@
mIsPassedBubbleBarSlop = false;
}
- private boolean isInTaskbarArea(float x) {
- float areaFromMiddle = mUnstashArea / 2.0f;
- float distFromMiddle = Math.abs(mScreenWidth / 2.0f - x);
- return distFromMiddle < areaFromMiddle;
- }
-
private boolean isInBubbleBarArea(float x) {
if (mTaskbarActivityContext == null || !mIsTransientTaskbar) {
return false;
@@ -320,17 +268,6 @@
}
}
- private void onLongPressDetected(MotionEvent motionEvent) {
- if (mTaskbarActivityContext != null
- && isInTaskbarArea(motionEvent.getRawX())
- && !mIsTransientTaskbar) {
- boolean taskBarPressed = mTaskbarActivityContext.onLongPressToUnstashTaskbar();
- if (taskBarPressed) {
- setActive(motionEvent);
- }
- }
- }
-
/**
* Listen for hover events for the stashed taskbar.
*
@@ -389,7 +326,7 @@
}
private void startStashedTaskbarHover(boolean isHovered) {
- mTaskbarActivityContext.startTaskbarUnstashHint(isHovered, /* forceUnstash = */ true);
+ mTaskbarActivityContext.startTaskbarUnstashHint(isHovered);
mIsStashedTaskbarHovered = isHovered;
}
diff --git a/quickstep/src/com/android/quickstep/util/AnimUtils.java b/quickstep/src/com/android/quickstep/util/AnimUtils.java
index b7b7825..7fbbb6e 100644
--- a/quickstep/src/com/android/quickstep/util/AnimUtils.java
+++ b/quickstep/src/com/android/quickstep/util/AnimUtils.java
@@ -39,4 +39,13 @@
? SplitAnimationTimings.TABLET_SPLIT_TO_CONFIRM
: SplitAnimationTimings.PHONE_SPLIT_TO_CONFIRM;
}
+
+ /**
+ * Fetches device-specific timings for the app pair launch animation.
+ */
+ public static SplitAnimationTimings getDeviceAppPairLaunchTimings(boolean isTablet) {
+ return isTablet
+ ? SplitAnimationTimings.TABLET_APP_PAIR_LAUNCH
+ : SplitAnimationTimings.PHONE_APP_PAIR_LAUNCH;
+ }
}
diff --git a/quickstep/src/com/android/quickstep/util/AppPairLaunchTimings.kt b/quickstep/src/com/android/quickstep/util/AppPairLaunchTimings.kt
new file mode 100644
index 0000000..086c8af
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/AppPairLaunchTimings.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util
+
+import com.android.app.animation.Interpolators
+
+/** Timings for the app pair launch animation. */
+abstract class AppPairLaunchTimings : SplitAnimationTimings {
+ protected abstract val STAGED_RECT_SLIDE_DURATION: Int
+
+ // Common timings that apply to app pair launches on any type of device
+ override fun getStagedRectSlideStart() = 0
+ override fun getStagedRectSlideEnd() = stagedRectSlideStart + STAGED_RECT_SLIDE_DURATION
+ override fun getPlaceholderFadeInStart() = 0
+ override fun getPlaceholderFadeInEnd() = 0
+ override fun getPlaceholderIconFadeInStart() = 0
+ override fun getPlaceholderIconFadeInEnd() = 0
+
+ private val iconFadeStart: Int
+ get() = getStagedRectSlideEnd()
+ private val iconFadeEnd: Int
+ get() = iconFadeStart + 83
+ private val appRevealStart: Int
+ get() = getStagedRectSlideEnd() + 67
+ private val appRevealEnd: Int
+ get() = appRevealStart + 217
+ private val cellSplitStart: Int
+ get() = (getStagedRectSlideEnd() * 0.83f).toInt()
+ private val cellSplitEnd: Int
+ get() = cellSplitStart + 500
+
+ override fun getStagedRectXInterpolator() = Interpolators.EMPHASIZED_COMPLEMENT
+ override fun getStagedRectYInterpolator() = Interpolators.EMPHASIZED
+ override fun getStagedRectScaleXInterpolator() = Interpolators.EMPHASIZED
+ override fun getStagedRectScaleYInterpolator() = Interpolators.EMPHASIZED
+ override fun getCellSplitInterpolator() = Interpolators.EMPHASIZED
+ override fun getIconFadeInterpolator() = Interpolators.LINEAR
+
+ override fun getCellSplitStartOffset(): Float {
+ return cellSplitStart.toFloat() / getDuration()
+ }
+ override fun getCellSplitEndOffset(): Float {
+ return cellSplitEnd.toFloat() / getDuration()
+ }
+ override fun getIconFadeStartOffset(): Float {
+ return iconFadeStart.toFloat() / getDuration()
+ }
+ override fun getIconFadeEndOffset(): Float {
+ return iconFadeEnd.toFloat() / getDuration()
+ }
+ override fun getAppRevealStartOffset(): Float {
+ return appRevealStart.toFloat() / getDuration()
+ }
+ override fun getAppRevealEndOffset(): Float {
+ return appRevealEnd.toFloat() / getDuration()
+ }
+ abstract override fun getDuration(): Int
+}
diff --git a/quickstep/src/com/android/quickstep/util/AppPairsController.java b/quickstep/src/com/android/quickstep/util/AppPairsController.java
index cc3b54b..3ca2531 100644
--- a/quickstep/src/com/android/quickstep/util/AppPairsController.java
+++ b/quickstep/src/com/android/quickstep/util/AppPairsController.java
@@ -36,6 +36,7 @@
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.R;
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
+import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.model.data.FolderInfo;
@@ -120,11 +121,14 @@
* Launches an app pair by searching the RecentsModel for running instances of each app, and
* staging either those running instances or launching the apps as new Intents.
*/
- public void launchAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
+ public void launchAppPair(AppPairIcon appPairIcon) {
+ WorkspaceItemInfo app1 = appPairIcon.getInfo().contents.get(0);
+ WorkspaceItemInfo app2 = appPairIcon.getInfo().contents.get(1);
ComponentKey app1Key = new ComponentKey(app1.getTargetComponent(), app1.user);
ComponentKey app2Key = new ComponentKey(app2.getTargetComponent(), app2.user);
mSplitSelectStateController.findLastActiveTasksAndRunCallback(
Arrays.asList(app1Key, app2Key),
+ false /* findExactPairMatch */,
foundTasks -> {
@Nullable Task foundTask1 = foundTasks.get(0);
Intent task1Intent;
@@ -151,9 +155,12 @@
app2.intent, app2.user);
}
+ mSplitSelectStateController.setLaunchingIconView(appPairIcon);
+
mSplitSelectStateController.launchSplitTasks(
AppPairsController.convertRankToSnapPosition(app1.rank));
- });
+ }
+ );
}
/**
diff --git a/quickstep/src/com/android/quickstep/util/PhoneAppPairLaunchTimings.kt b/quickstep/src/com/android/quickstep/util/PhoneAppPairLaunchTimings.kt
new file mode 100644
index 0000000..beab90f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/PhoneAppPairLaunchTimings.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util
+
+/** Timings for the app pair launch animation on phones. */
+class PhoneAppPairLaunchTimings : AppPairLaunchTimings(), SplitAnimationTimings {
+ override val STAGED_RECT_SLIDE_DURATION = 500
+ override fun getDuration() = SplitAnimationTimings.PHONE_APP_PAIR_LAUNCH_DURATION
+}
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index dfbd32c..ade8074 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -21,20 +21,37 @@
import android.animation.AnimatorListenerAdapter
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
+import android.animation.ValueAnimator
+import android.app.ActivityManager.RunningTaskInfo
import android.graphics.Bitmap
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.drawable.Drawable
+import android.view.RemoteAnimationTarget
+import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
import android.view.View
+import android.view.WindowManager
+import android.window.TransitionInfo
+import android.window.TransitionInfo.Change
+import androidx.annotation.VisibleForTesting
import com.android.app.animation.Interpolators
import com.android.launcher3.DeviceProfile
+import com.android.launcher3.Launcher
+import com.android.launcher3.QuickstepTransitionManager
import com.android.launcher3.Utilities
import com.android.launcher3.anim.PendingAnimation
+import com.android.launcher3.apppairs.AppPairIcon
import com.android.launcher3.config.FeatureFlags
+import com.android.launcher3.statehandlers.DepthController
+import com.android.launcher3.statemanager.StateManager
import com.android.launcher3.statemanager.StatefulActivity
import com.android.launcher3.util.SplitConfigurationOptions.SplitSelectSource
import com.android.launcher3.views.BaseDragLayer
+import com.android.quickstep.TaskViewUtils
+import com.android.quickstep.views.FloatingAppPairView
import com.android.quickstep.views.FloatingTaskView
+import com.android.quickstep.views.GroupedTaskView
import com.android.quickstep.views.RecentsView
import com.android.quickstep.views.SplitInstructionsView
import com.android.quickstep.views.TaskThumbnailView
@@ -308,6 +325,407 @@
pendingAnimation.buildAnim().start()
}
+ /**
+ * Called when launching a specific pair of apps, e.g. when tapping a pair of apps in Overview,
+ * or launching an app pair from its Home icon. Selects the appropriate launch animation and
+ * plays it.
+ */
+ fun playSplitLaunchAnimation(
+ launchingTaskView: GroupedTaskView?,
+ launchingIconView: AppPairIcon?,
+ initialTaskId: Int,
+ secondTaskId: Int,
+ apps: Array<RemoteAnimationTarget>?,
+ wallpapers: Array<RemoteAnimationTarget>?,
+ nonApps: Array<RemoteAnimationTarget>?,
+ stateManager: StateManager<*>,
+ depthController: DepthController?,
+ info: TransitionInfo?,
+ t: Transaction?,
+ finishCallback: Runnable
+ ) {
+ if (info == null && t == null) {
+ // (Legacy animation) Tapping a split tile in Overview
+ // TODO (b/315490678): Ensure that this works with app pairs flow
+ check(apps != null && wallpapers != null && nonApps != null) {
+ "trying to call composeRecentsSplitLaunchAnimatorLegacy, but encountered an " +
+ "unexpected null"
+ }
+
+ composeRecentsSplitLaunchAnimatorLegacy(
+ launchingTaskView,
+ initialTaskId,
+ secondTaskId,
+ apps,
+ wallpapers,
+ nonApps,
+ stateManager,
+ depthController,
+ finishCallback
+ )
+
+ return
+ }
+
+ if (launchingTaskView != null) {
+ // Tapping a split tile in Overview
+ check(info != null && t != null) {
+ "trying to launch a GroupedTaskView, but encountered an unexpected null"
+ }
+
+ composeRecentsSplitLaunchAnimator(
+ launchingTaskView,
+ stateManager,
+ depthController,
+ info,
+ t,
+ finishCallback
+ )
+ } else if (launchingIconView != null) {
+ // Tapping an app pair icon
+ check(info != null && t != null) {
+ "trying to launch an app pair icon, but encountered an unexpected null"
+ }
+
+ composeIconSplitLaunchAnimator(
+ launchingIconView,
+ initialTaskId,
+ secondTaskId,
+ info,
+ t,
+ finishCallback
+ )
+ } else {
+ // Fallback case: simple fade-in animation
+ check(info != null && t != null) {
+ "trying to call composeFadeInSplitLaunchAnimator, but encountered an " +
+ "unexpected null"
+ }
+
+ composeFadeInSplitLaunchAnimator(initialTaskId, secondTaskId, info, t, finishCallback)
+ }
+ }
+
+ /**
+ * When the user taps a split tile in Overview, this will play the tasks' launch animation from
+ * the position of the tapped tile.
+ */
+ @VisibleForTesting
+ fun composeRecentsSplitLaunchAnimator(
+ launchingTaskView: GroupedTaskView,
+ stateManager: StateManager<*>,
+ depthController: DepthController?,
+ info: TransitionInfo,
+ t: Transaction,
+ finishCallback: Runnable
+ ) {
+ TaskViewUtils.composeRecentsSplitLaunchAnimator(
+ launchingTaskView,
+ stateManager,
+ depthController,
+ info,
+ t,
+ finishCallback
+ )
+ }
+
+ /**
+ * LEGACY VERSION: When the user taps a split tile in Overview, this will play the tasks' launch
+ * animation from the position of the tapped tile.
+ */
+ @VisibleForTesting
+ fun composeRecentsSplitLaunchAnimatorLegacy(
+ launchingTaskView: GroupedTaskView?,
+ initialTaskId: Int,
+ secondTaskId: Int,
+ apps: Array<RemoteAnimationTarget>,
+ wallpapers: Array<RemoteAnimationTarget>,
+ nonApps: Array<RemoteAnimationTarget>,
+ stateManager: StateManager<*>,
+ depthController: DepthController?,
+ finishCallback: Runnable
+ ) {
+ TaskViewUtils.composeRecentsSplitLaunchAnimatorLegacy(
+ launchingTaskView,
+ initialTaskId,
+ secondTaskId,
+ apps,
+ wallpapers,
+ nonApps,
+ stateManager,
+ depthController,
+ finishCallback
+ )
+ }
+
+ /**
+ * When the user taps an app pair icon to launch split, this will play the tasks' launch
+ * animation from the position of the icon.
+ */
+ @VisibleForTesting
+ fun composeIconSplitLaunchAnimator(
+ launchingIconView: AppPairIcon,
+ initialTaskId: Int,
+ secondTaskId: Int,
+ transitionInfo: TransitionInfo,
+ t: Transaction,
+ finishCallback: Runnable
+ ) {
+ val launcher = Launcher.getLauncher(launchingIconView.context)
+ val dp = launcher.deviceProfile
+
+ // Create an AnimatorSet that will run both shell and launcher transitions together
+ val launchAnimation = AnimatorSet()
+ val progressUpdater = ValueAnimator.ofFloat(0f, 1f)
+ val timings = AnimUtils.getDeviceAppPairLaunchTimings(dp.isTablet)
+ progressUpdater.setDuration(timings.getDuration().toLong())
+ progressUpdater.interpolator = Interpolators.LINEAR
+
+ // Find the root shell leash that we want to fade in (parent of both app windows and
+ // the divider). For simplicity, we search using the initialTaskId.
+ var rootShellLayer: SurfaceControl? = null
+ var dividerPos = 0
+
+ for (change in transitionInfo.changes) {
+ val taskInfo: RunningTaskInfo = change.taskInfo ?: continue
+ val taskId = taskInfo.taskId
+ val mode = change.mode
+
+ if (taskId == initialTaskId || taskId == secondTaskId) {
+ check(
+ mode == WindowManager.TRANSIT_OPEN || mode == WindowManager.TRANSIT_TO_FRONT
+ ) {
+ "Expected task to be showing, but it is $mode"
+ }
+ }
+
+ if (taskId == initialTaskId) {
+ var splitRoot1 = change
+ val parentToken = change.parent
+ if (parentToken != null) {
+ splitRoot1 = transitionInfo.getChange(parentToken) ?: change
+ }
+
+ val topLevelToken = splitRoot1.parent
+ if (topLevelToken != null) {
+ rootShellLayer = transitionInfo.getChange(topLevelToken)?.leash
+ }
+
+ dividerPos =
+ if (dp.isLeftRightSplit) change.endAbsBounds.right
+ else change.endAbsBounds.bottom
+ }
+ }
+
+ check(rootShellLayer != null) {
+ "Could not find a TransitionInfo.Change matching the initialTaskId"
+ }
+
+ // Shell animation: the apps are revealed toward end of the launch animation
+ progressUpdater.addUpdateListener { valueAnimator: ValueAnimator ->
+ val progress =
+ Interpolators.clampToProgress(
+ Interpolators.LINEAR,
+ valueAnimator.animatedFraction,
+ timings.appRevealStartOffset,
+ timings.appRevealEndOffset
+ )
+
+ // Set the alpha of the shell layer (2 apps + divider)
+ t.setAlpha(rootShellLayer, progress)
+ t.apply()
+ }
+
+ // Create a new floating view in Launcher, positioned above the launching icon
+ val drawableArea = launchingIconView.iconDrawableArea
+ val appIcon1 = launchingIconView.info.contents[0].newIcon(launchingIconView.context)
+ val appIcon2 = launchingIconView.info.contents[1].newIcon(launchingIconView.context)
+ appIcon1.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx)
+ appIcon2.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx)
+ val floatingView =
+ FloatingAppPairView.getFloatingAppPairView(
+ launcher,
+ drawableArea,
+ appIcon1,
+ appIcon2,
+ dividerPos
+ )
+
+ // Launcher animation: animate the floating view, expanding to fill the display surface
+ progressUpdater.addUpdateListener(
+ object : MultiValueUpdateListener() {
+ var mDx =
+ FloatProp(
+ floatingView.startingPosition.left,
+ dp.widthPx / 2f - floatingView.startingPosition.width() / 2f,
+ 0f /* delay */,
+ timings.getDuration().toFloat(),
+ Interpolators.clampToProgress(
+ timings.getStagedRectXInterpolator(),
+ timings.stagedRectSlideStartOffset,
+ timings.stagedRectSlideEndOffset
+ )
+ )
+ var mDy =
+ FloatProp(
+ floatingView.startingPosition.top,
+ dp.heightPx / 2f - floatingView.startingPosition.height() / 2f,
+ 0f /* delay */,
+ timings.getDuration().toFloat(),
+ Interpolators.clampToProgress(
+ Interpolators.EMPHASIZED,
+ timings.stagedRectSlideStartOffset,
+ timings.stagedRectSlideEndOffset
+ )
+ )
+ var mScaleX =
+ FloatProp(
+ 1f /* start */,
+ dp.widthPx / floatingView.startingPosition.width(),
+ 0f /* delay */,
+ timings.getDuration().toFloat(),
+ Interpolators.clampToProgress(
+ Interpolators.EMPHASIZED,
+ timings.stagedRectSlideStartOffset,
+ timings.stagedRectSlideEndOffset
+ )
+ )
+ var mScaleY =
+ FloatProp(
+ 1f /* start */,
+ dp.heightPx / floatingView.startingPosition.height(),
+ 0f /* delay */,
+ timings.getDuration().toFloat(),
+ Interpolators.clampToProgress(
+ Interpolators.EMPHASIZED,
+ timings.stagedRectSlideStartOffset,
+ timings.stagedRectSlideEndOffset
+ )
+ )
+
+ override fun onUpdate(percent: Float, initOnly: Boolean) {
+ floatingView.progress = percent
+ floatingView.x = mDx.value
+ floatingView.y = mDy.value
+ floatingView.scaleX = mScaleX.value
+ floatingView.scaleY = mScaleY.value
+ floatingView.invalidate()
+ }
+ }
+ )
+
+ // When animation ends, remove the floating view and run finishCallback
+ progressUpdater.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ safeRemoveViewFromDragLayer(launcher, floatingView)
+ finishCallback.run()
+ }
+ }
+ )
+
+ launchAnimation.play(progressUpdater)
+ launchAnimation.start()
+ }
+
+ /**
+ * If we are launching split screen without any special animation from a starting View, we
+ * simply fade in the starting apps and fade out launcher.
+ */
+ @VisibleForTesting
+ fun composeFadeInSplitLaunchAnimator(
+ initialTaskId: Int,
+ secondTaskId: Int,
+ transitionInfo: TransitionInfo,
+ t: Transaction,
+ finishCallback: Runnable
+ ) {
+ var splitRoot1: Change? = null
+ var splitRoot2: Change? = null
+ val openingTargets = ArrayList<SurfaceControl>()
+ for (change in transitionInfo.changes) {
+ val taskInfo: RunningTaskInfo = change.taskInfo ?: continue
+ val taskId = taskInfo.taskId
+ val mode = change.mode
+
+ // Find the target tasks' root tasks since those are the split stages that need to
+ // be animated (the tasks themselves are children and thus inherit animation).
+ if (taskId == initialTaskId || taskId == secondTaskId) {
+ check(
+ mode == WindowManager.TRANSIT_OPEN || mode == WindowManager.TRANSIT_TO_FRONT
+ ) {
+ "Expected task to be showing, but it is $mode"
+ }
+ }
+
+ if (taskId == initialTaskId) {
+ splitRoot1 = change
+ val parentToken1 = change.parent
+ if (parentToken1 != null) {
+ splitRoot1 = transitionInfo.getChange(parentToken1) ?: change
+ }
+
+ if (splitRoot1?.leash != null) {
+ openingTargets.add(splitRoot1.leash)
+ }
+ }
+
+ if (taskId == secondTaskId) {
+ splitRoot2 = change
+ val parentToken2 = change.parent
+ if (parentToken2 != null) {
+ splitRoot2 = transitionInfo.getChange(parentToken2) ?: change
+ }
+
+ if (splitRoot2?.leash != null) {
+ openingTargets.add(splitRoot2.leash)
+ }
+ }
+ }
+
+ val animTransaction = Transaction()
+ val animator = ValueAnimator.ofFloat(0f, 1f)
+ animator.setDuration(QuickstepTransitionManager.SPLIT_LAUNCH_DURATION.toLong())
+ animator.addUpdateListener { valueAnimator: ValueAnimator ->
+ val progress = valueAnimator.animatedFraction
+ for (leash in openingTargets) {
+ animTransaction.setAlpha(leash, progress)
+ }
+ animTransaction.apply()
+ }
+
+ animator.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animation: Animator) {
+ for (leash in openingTargets) {
+ animTransaction.show(leash).setAlpha(leash, 0.0f)
+ }
+ animTransaction.apply()
+ }
+
+ override fun onAnimationEnd(animation: Animator) {
+ finishCallback.run()
+ }
+ }
+ )
+
+ if (splitRoot1 != null) {
+ // Set the highest level split root alpha; we could technically use the parent of
+ // either splitRoot1 or splitRoot2
+ val parentToken = splitRoot1.parent
+ var rootLayer: Change? = null
+ if (parentToken != null) {
+ rootLayer = transitionInfo.getChange(parentToken)
+ }
+ if (rootLayer != null && rootLayer.leash != null) {
+ t.setAlpha(rootLayer.leash, 1f)
+ }
+ }
+
+ t.apply()
+ animator.start()
+ }
+
private fun safeRemoveViewFromDragLayer(launcher: StatefulActivity<*>, view: View?) {
if (view != null) {
launcher.dragLayer.removeView(view)
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java b/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java
index 93f2255..b618546 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationTimings.java
@@ -21,31 +21,44 @@
import android.view.animation.Interpolator;
/**
- * An interface that supports the centralization of timing information for splitscreen animations.
+ * Organizes timing information for split screen animations.
*/
public interface SplitAnimationTimings {
+ /** Total duration (ms) for initiating split screen (staging the first app) on tablets. */
int TABLET_ENTER_DURATION = 866;
+ /** Total duration (ms) for confirming split screen (selecting the second app) on tablets. */
int TABLET_CONFIRM_DURATION = 500;
-
+ /** Total duration (ms) for initiating split screen (staging the first app) on phones. */
int PHONE_ENTER_DURATION = 517;
+ /** Total duration (ms) for confirming split screen (selecting the second app) on phones. */
int PHONE_CONFIRM_DURATION = 333;
-
+ /** Total duration (ms) for aborting split screen (before selecting the second app). */
int ABORT_DURATION = 500;
+ /** Total duration (ms) for launching an app pair from its icon on tablets. */
+ int TABLET_APP_PAIR_LAUNCH_DURATION = 998;
+ /** Total duration (ms) for launching an app pair from its icon on phones. */
+ int PHONE_APP_PAIR_LAUNCH_DURATION = 915;
+ // Initialize timing classes so they can be accessed statically
SplitAnimationTimings TABLET_OVERVIEW_TO_SPLIT = new TabletOverviewToSplitTimings();
SplitAnimationTimings TABLET_HOME_TO_SPLIT = new TabletHomeToSplitTimings();
SplitAnimationTimings TABLET_SPLIT_TO_CONFIRM = new TabletSplitToConfirmTimings();
-
SplitAnimationTimings PHONE_OVERVIEW_TO_SPLIT = new PhoneOverviewToSplitTimings();
SplitAnimationTimings PHONE_SPLIT_TO_CONFIRM = new PhoneSplitToConfirmTimings();
+ SplitAnimationTimings TABLET_APP_PAIR_LAUNCH = new TabletAppPairLaunchTimings();
+ SplitAnimationTimings PHONE_APP_PAIR_LAUNCH = new PhoneAppPairLaunchTimings();
- // Shared methods
+ // Shared methods: all split animations have these parameters
int getDuration();
+ /** Start fading in the floating view tile at this time (in ms). */
int getPlaceholderFadeInStart();
int getPlaceholderFadeInEnd();
+ /** Start fading in the app icon at this time (in ms). */
int getPlaceholderIconFadeInStart();
int getPlaceholderIconFadeInEnd();
+ /** Start translating the floating view tile at this time (in ms). */
int getStagedRectSlideStart();
+ /** The floating tile has reached its final position at this time (in ms). */
int getStagedRectSlideEnd();
Interpolator getStagedRectXInterpolator();
Interpolator getStagedRectYInterpolator();
@@ -70,6 +83,11 @@
return (float) getStagedRectSlideEnd() / getDuration();
}
+ // DEFAULT VALUES: We define default values here so that SplitAnimationTimings can be used
+ // flexibly in animation-running functions, e.g. a single function that handles 2 types of split
+ // animations. The values are not intended to be used, and can safely be removed if refactoring
+ // these classes.
+
// Defaults for OverviewToSplit
default float getGridSlideStartOffset() { return 0; }
default float getGridSlideStaggerOffset() { return 0; }
@@ -94,5 +112,13 @@
// Defaults for SplitToConfirm
default float getInstructionsFadeStartOffset() { return 0; }
default float getInstructionsFadeEndOffset() { return 0; }
+
+ // Defaults for AppPair
+ default float getCellSplitStartOffset() { return 0; }
+ default float getCellSplitEndOffset() { return 0; }
+ default float getAppRevealStartOffset() { return 0; }
+ default float getAppRevealEndOffset() { return 0; }
+ default Interpolator getCellSplitInterpolator() { return LINEAR; }
+ default Interpolator getIconFadeInterpolator() { return LINEAR; }
}
diff --git a/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
index 596bb47..38bbe60 100644
--- a/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitScreenUtils.kt
@@ -56,4 +56,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
index 8b27a85..d5899e4 100644
--- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java
@@ -71,6 +71,7 @@
import com.android.launcher3.Launcher;
import com.android.launcher3.R;
import com.android.launcher3.anim.PendingAnimation;
+import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.icons.IconProvider;
import com.android.launcher3.logging.StatsLogManager;
@@ -92,7 +93,6 @@
import com.android.quickstep.SplitSelectionListener;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.TaskAnimationManager;
-import com.android.quickstep.TaskViewUtils;
import com.android.quickstep.views.FloatingTaskView;
import com.android.quickstep.views.GroupedTaskView;
import com.android.quickstep.views.RecentsView;
@@ -141,6 +141,8 @@
/** If not null, this is the TaskView we want to launch from */
@Nullable
private GroupedTaskView mLaunchingTaskView;
+ /** If not null, this is the icon we want to launch from */
+ private AppPairIcon mLaunchingIconView;
/** True when the first selected split app is being launched in fullscreen. */
private boolean mLaunchingFirstAppFullscreen;
@@ -214,15 +216,16 @@
}
/**
- * Maps a List<ComponentKey> to List<@Nullable Task>, searching through active Tasks in
- * RecentsModel. If found, the Task will be the most recently-interacted-with instance of that
- * Task. Then runs the given callback on that List.
- * <p>
- * Used in various task-switching or splitscreen operations when we need to check if there is a
- * currently running Task of a certain type and use the most recent one.
+ * Given a list of task keys, searches through active Tasks in RecentsModel to find the last
+ * active instances of these tasks. Returns an empty array if there is no such running task.
+ *
+ * @param componentKeys The list of ComponentKeys to search for.
+ * @param callback The callback that will be executed on the list of found tasks.
+ * @param findExactPairMatch If {@code true}, only finds tasks that contain BOTH of the wanted
+ * tasks (i.e. searching for a running pair of tasks.)
*/
- public void findLastActiveTasksAndRunCallback(
- @Nullable List<ComponentKey> componentKeys, Consumer<List<Task>> callback) {
+ public void findLastActiveTasksAndRunCallback(@Nullable List<ComponentKey> componentKeys,
+ boolean findExactPairMatch, Consumer<List<Task>> callback) {
mRecentTasksModel.getTasks(taskGroups -> {
if (componentKeys == null || componentKeys.isEmpty()) {
callback.accept(Collections.emptyList());
@@ -230,27 +233,40 @@
}
List<Task> lastActiveTasks = new ArrayList<>();
- // For each key we are looking for, add to lastActiveTasks with the corresponding Task
- // (or null if not found).
- for (ComponentKey key : componentKeys) {
- Task lastActiveTask = null;
+
+ if (findExactPairMatch) {
// Loop through tasks in reverse, since they are ordered with most-recent tasks last
for (int i = taskGroups.size() - 1; i >= 0; i--) {
GroupTask groupTask = taskGroups.get(i);
- Task task1 = groupTask.task1;
- // Don't add duplicate Tasks
- if (isInstanceOfComponent(task1, key) && !lastActiveTasks.contains(task1)) {
- lastActiveTask = task1;
- break;
- }
- Task task2 = groupTask.task2;
- if (isInstanceOfComponent(task2, key) && !lastActiveTasks.contains(task2)) {
- lastActiveTask = task2;
+ if (isInstanceOfAppPair(
+ groupTask, componentKeys.get(0), componentKeys.get(1))) {
+ lastActiveTasks.add(groupTask.task1);
break;
}
}
+ } else {
+ // For each key we are looking for, add to lastActiveTasks with the corresponding
+ // Task (or do nothing if not found).
+ for (ComponentKey key : componentKeys) {
+ Task lastActiveTask = null;
+ // Loop through tasks in reverse, since they are ordered with recent tasks last
+ for (int i = taskGroups.size() - 1; i >= 0; i--) {
+ GroupTask groupTask = taskGroups.get(i);
+ Task task1 = groupTask.task1;
+ // Don't add duplicate Tasks
+ if (isInstanceOfComponent(task1, key) && !lastActiveTasks.contains(task1)) {
+ lastActiveTask = task1;
+ break;
+ }
+ Task task2 = groupTask.task2;
+ if (isInstanceOfComponent(task2, key) && !lastActiveTasks.contains(task2)) {
+ lastActiveTask = task2;
+ break;
+ }
+ }
- lastActiveTasks.add(lastActiveTask);
+ lastActiveTasks.add(lastActiveTask);
+ }
}
callback.accept(lastActiveTasks);
@@ -272,6 +288,19 @@
}
/**
+ * Checks if a given GroupTask is a pair of apps that matches two given ComponentKeys. We check
+ * both permutations because task order is not guaranteed in GroupTasks.
+ */
+ public boolean isInstanceOfAppPair(GroupTask groupTask, @NonNull ComponentKey componentKey1,
+ @NonNull ComponentKey componentKey2) {
+ return ((isInstanceOfComponent(groupTask.task1, componentKey1)
+ && isInstanceOfComponent(groupTask.task2, componentKey2))
+ ||
+ (isInstanceOfComponent(groupTask.task1, componentKey2)
+ && isInstanceOfComponent(groupTask.task2, componentKey1)));
+ }
+
+ /**
* Listener will only get callbacks going forward from the point of registration. No
* methods will be fired upon registering.
*/
@@ -634,8 +663,20 @@
};
MAIN_EXECUTOR.execute(() -> {
- TaskViewUtils.composeRecentsSplitLaunchAnimator(mLaunchingTaskView, mStateManager,
- mDepthController, mInitialTaskId, mSecondTaskId, info, t, () -> {
+ // Only animate from taskView if it's already visible
+ boolean shouldLaunchFromTaskView = mLaunchingTaskView != null &&
+ mLaunchingTaskView.getRecentsView().isTaskViewVisible(mLaunchingTaskView);
+ mSplitAnimationController.playSplitLaunchAnimation(
+ shouldLaunchFromTaskView ? mLaunchingTaskView : null,
+ mLaunchingIconView,
+ mInitialTaskId,
+ mSecondTaskId,
+ null /* apps */,
+ null /* wallpapers */,
+ null /* nonApps */,
+ mStateManager,
+ mDepthController,
+ info, t, () -> {
finishAdapter.run();
cleanup(true /*success*/);
});
@@ -691,9 +732,10 @@
RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps,
Runnable finishedCallback) {
postAsyncCallback(mHandler,
- () -> TaskViewUtils.composeRecentsSplitLaunchAnimatorLegacy(
- mLaunchingTaskView, mInitialTaskId, mSecondTaskId, apps, wallpapers,
- nonApps, mStateManager, mDepthController, () -> {
+ () -> mSplitAnimationController.playSplitLaunchAnimation(mLaunchingTaskView,
+ mLaunchingIconView, mInitialTaskId, mSecondTaskId, apps, wallpapers,
+ nonApps, mStateManager, mDepthController, null /* info */, null /* t */,
+ () -> {
finishedCallback.run();
if (mSuccessCallback != null) {
mSuccessCallback.accept(true);
@@ -726,6 +768,7 @@
dispatchOnSplitSelectionExit();
mRecentsAnimationRunning = false;
mLaunchingTaskView = null;
+ mLaunchingIconView = null;
mAnimateCurrentTaskDismissal = false;
mDismissingFromSplitPair = false;
mFirstFloatingTaskView = null;
@@ -786,6 +829,10 @@
return mAppPairsController;
}
+ public void setLaunchingIconView(AppPairIcon launchingIconView) {
+ mLaunchingIconView = launchingIconView;
+ }
+
public BackPressHandler getSplitBackHandler() {
return mSplitBackHandler;
}
diff --git a/quickstep/src/com/android/quickstep/util/TabletAppPairLaunchTimings.kt b/quickstep/src/com/android/quickstep/util/TabletAppPairLaunchTimings.kt
new file mode 100644
index 0000000..fb2d63f
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/TabletAppPairLaunchTimings.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.util
+
+/** Timings for the app pair launch animation on tablets. */
+class TabletAppPairLaunchTimings : AppPairLaunchTimings(), SplitAnimationTimings {
+ override val STAGED_RECT_SLIDE_DURATION = 600
+ override fun getDuration() = SplitAnimationTimings.TABLET_APP_PAIR_LAUNCH_DURATION
+}
diff --git a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
new file mode 100644
index 0000000..3a5873b
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt
@@ -0,0 +1,358 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.views
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.ColorFilter
+import android.graphics.Paint
+import android.graphics.PixelFormat
+import android.graphics.RectF
+import android.graphics.drawable.Drawable
+import android.os.Build
+import android.view.animation.Interpolator
+import com.android.app.animation.Interpolators
+import com.android.launcher3.Launcher
+import com.android.launcher3.R
+import com.android.launcher3.Utilities
+import com.android.quickstep.util.AnimUtils
+import com.android.systemui.shared.system.QuickStepContract
+
+/**
+ * A Drawable that is drawn onto [FloatingAppPairView] every frame during the app pair launch
+ * animation. Consists of a rectangular background that splits into two, and two app icons that
+ * increase in size during the animation.
+ */
+class FloatingAppPairBackground(
+ context: Context,
+ private val floatingView: FloatingAppPairView, // the view that we will draw this background on
+ private val appIcon1: Drawable,
+ private val appIcon2: Drawable,
+ dividerPos: Int
+) : Drawable() {
+ companion object {
+ // Design specs -- app icons start small and expand during the animation
+ private val STARTING_ICON_SIZE_PX = Utilities.dpToPx(22f)
+ private val ENDING_ICON_SIZE_PX = Utilities.dpToPx(66f)
+
+ // Null values to use with drawDoubleRoundRect(), since there doesn't seem to be any other
+ // API for drawing rectangles with 4 different corner radii.
+ private val EMPTY_RECT = RectF()
+ private val ARRAY_OF_ZEROES = FloatArray(8)
+ }
+
+ private val launcher: Launcher
+ private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
+
+ // Animation interpolators
+ private val expandXInterpolator: Interpolator
+ private val expandYInterpolator: Interpolator
+ private val cellSplitInterpolator: Interpolator
+ private val iconFadeInterpolator: Interpolator
+
+ // Device-specific measurements
+ private val deviceCornerRadius: Float
+ private val deviceHalfDividerSize: Float
+ private val desiredSplitRatio: Float
+
+ init {
+ launcher = Launcher.getLauncher(context)
+ val dp = launcher.deviceProfile
+ // Set up background paint color
+ val ta = context.theme.obtainStyledAttributes(R.styleable.FolderIconPreview)
+ backgroundPaint.style = Paint.Style.FILL
+ backgroundPaint.color = ta.getColor(R.styleable.FolderIconPreview_folderPreviewColor, 0)
+ ta.recycle()
+ // Set up timings and interpolators
+ val timings = AnimUtils.getDeviceAppPairLaunchTimings(launcher.deviceProfile.isTablet)
+ expandXInterpolator =
+ Interpolators.clampToProgress(
+ timings.getStagedRectScaleXInterpolator(),
+ timings.stagedRectSlideStartOffset,
+ timings.stagedRectSlideEndOffset
+ )
+ expandYInterpolator =
+ Interpolators.clampToProgress(
+ timings.getStagedRectScaleYInterpolator(),
+ timings.stagedRectSlideStartOffset,
+ timings.stagedRectSlideEndOffset
+ )
+ cellSplitInterpolator =
+ Interpolators.clampToProgress(
+ timings.cellSplitInterpolator,
+ timings.cellSplitStartOffset,
+ timings.cellSplitEndOffset
+ )
+ iconFadeInterpolator =
+ Interpolators.clampToProgress(
+ timings.iconFadeInterpolator,
+ timings.iconFadeStartOffset,
+ timings.iconFadeEndOffset
+ )
+
+ // Find device-specific measurements
+ deviceCornerRadius = QuickStepContract.getWindowCornerRadius(launcher)
+ deviceHalfDividerSize =
+ launcher.resources.getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2f
+ val dividerCenterPos = dividerPos + deviceHalfDividerSize
+ desiredSplitRatio =
+ if (dp.isLeftRightSplit) dividerCenterPos / dp.widthPx
+ else dividerCenterPos / dp.heightPx
+ }
+
+ override fun draw(canvas: Canvas) {
+ if (launcher.deviceProfile.isLandscape) {
+ drawLeftRightSplit(canvas)
+ } else {
+ drawTopBottomSplit(canvas)
+ }
+ }
+
+ /** When device is in landscape, we draw the rectangles with a left-right split. */
+ private fun drawLeftRightSplit(canvas: Canvas) {
+ val progress = floatingView.progress
+
+ // Since the entire floating app pair surface is scaling up during this animation, we
+ // scale down most of these drawn elements so that they appear the proper size on-screen.
+ val scaleFactorX = floatingView.scaleX
+ val scaleFactorY = floatingView.scaleY
+
+ // Get the bounds where we will draw the background image
+ val width = bounds.width().toFloat()
+ val height = bounds.height().toFloat()
+
+ // Get device-specific measurements
+ val cornerRadiusX = deviceCornerRadius / scaleFactorX
+ val cornerRadiusY = deviceCornerRadius / scaleFactorY
+ val halfDividerSize = deviceHalfDividerSize / scaleFactorX
+
+ // Calculate changing measurements for background
+ // We add one pixel to some measurements to create a smooth edge with no gaps
+ val onePixel = 1f / scaleFactorX
+ val changingDividerSize =
+ (cellSplitInterpolator.getInterpolation(progress) * halfDividerSize) - onePixel
+ val changingInnerRadiusX = cellSplitInterpolator.getInterpolation(progress) * cornerRadiusX
+ val changingInnerRadiusY = cellSplitInterpolator.getInterpolation(progress) * cornerRadiusY
+ val dividerCenterPos = width * desiredSplitRatio
+
+ // The left half of the background image
+ val leftSide = RectF(
+ 0f,
+ 0f,
+ dividerCenterPos - changingDividerSize,
+ height
+ )
+ // The right half of the background image
+ val rightSide = RectF(
+ dividerCenterPos + changingDividerSize,
+ 0f,
+ width,
+ height
+ )
+
+ // Draw background
+ drawCustomRoundedRect(
+ canvas,
+ leftSide,
+ floatArrayOf(
+ cornerRadiusX, cornerRadiusY,
+ changingInnerRadiusX, changingInnerRadiusY,
+ changingInnerRadiusX, changingInnerRadiusY,
+ cornerRadiusX, cornerRadiusY
+ )
+ )
+ drawCustomRoundedRect(
+ canvas,
+ rightSide,
+ floatArrayOf(
+ changingInnerRadiusX, changingInnerRadiusY,
+ cornerRadiusX, cornerRadiusY,
+ cornerRadiusX, cornerRadiusY,
+ changingInnerRadiusX, changingInnerRadiusY
+ )
+ )
+
+ // Calculate changing measurements for icons.
+ val changingIconSizeX =
+ (STARTING_ICON_SIZE_PX +
+ ((ENDING_ICON_SIZE_PX - STARTING_ICON_SIZE_PX) *
+ expandXInterpolator.getInterpolation(progress))) / scaleFactorX
+ val changingIconSizeY =
+ (STARTING_ICON_SIZE_PX +
+ ((ENDING_ICON_SIZE_PX - STARTING_ICON_SIZE_PX) *
+ expandYInterpolator.getInterpolation(progress))) / scaleFactorY
+
+ val changingIcon1Left = ((width / 2f - halfDividerSize) / 2f) - (changingIconSizeX / 2f)
+ val changingIcon2Left =
+ (width - ((width / 2f - halfDividerSize) / 2f)) - (changingIconSizeX / 2f)
+ val changingIconTop = (height / 2f) - (changingIconSizeY / 2f)
+ val changingIconScaleX = changingIconSizeX / appIcon1.bounds.width()
+ val changingIconScaleY = changingIconSizeY / appIcon1.bounds.height()
+ val changingIconAlpha =
+ (255 - (255 * iconFadeInterpolator.getInterpolation(progress))).toInt()
+
+ // Draw first icon
+ canvas.save()
+ canvas.translate(changingIcon1Left, changingIconTop)
+ canvas.scale(changingIconScaleX, changingIconScaleY)
+ appIcon1.alpha = changingIconAlpha
+ appIcon1.draw(canvas)
+ canvas.restore()
+
+ // Draw second icon
+ canvas.save()
+ canvas.translate(changingIcon2Left, changingIconTop)
+ canvas.scale(changingIconScaleX, changingIconScaleY)
+ appIcon2.alpha = changingIconAlpha
+ appIcon2.draw(canvas)
+ canvas.restore()
+ }
+
+ /** When device is in portrait, we draw the rectangles with a top-bottom split. */
+ private fun drawTopBottomSplit(canvas: Canvas) {
+ val progress = floatingView.progress
+
+ // Since the entire floating app pair surface is scaling up during this animation, we
+ // scale down most of these drawn elements so that they appear the proper size on-screen.
+ val scaleFactorX = floatingView.scaleX
+ val scaleFactorY = floatingView.scaleY
+
+ // Get the bounds where we will draw the background image
+ val width = bounds.width().toFloat()
+ val height = bounds.height().toFloat()
+
+ // Get device-specific measurements
+ val cornerRadiusX = deviceCornerRadius / scaleFactorX
+ val cornerRadiusY = deviceCornerRadius / scaleFactorY
+ val halfDividerSize = deviceHalfDividerSize / scaleFactorY
+
+ // Calculate changing measurements for background
+ // We add one pixel to some measurements to create a smooth edge with no gaps
+ val onePixel = 1f / scaleFactorY
+ val changingDividerSize =
+ (cellSplitInterpolator.getInterpolation(progress) * halfDividerSize) - onePixel
+ val changingInnerRadiusX = cellSplitInterpolator.getInterpolation(progress) * cornerRadiusX
+ val changingInnerRadiusY = cellSplitInterpolator.getInterpolation(progress) * cornerRadiusY
+ val dividerCenterPos = height * desiredSplitRatio
+
+ // The top half of the background image
+ val topSide = RectF(
+ 0f,
+ 0f,
+ width,
+ dividerCenterPos - changingDividerSize
+ )
+ // The bottom half of the background image
+ val bottomSide = RectF(
+ 0f,
+ dividerCenterPos + changingDividerSize,
+ width,
+ height
+ )
+
+ // Draw background
+ drawCustomRoundedRect(
+ canvas,
+ topSide,
+ floatArrayOf(
+ cornerRadiusX, cornerRadiusY,
+ cornerRadiusX, cornerRadiusY,
+ changingInnerRadiusX, changingInnerRadiusY,
+ changingInnerRadiusX, changingInnerRadiusY
+ )
+ )
+ drawCustomRoundedRect(
+ canvas,
+ bottomSide,
+ floatArrayOf(
+ changingInnerRadiusX, changingInnerRadiusY,
+ changingInnerRadiusX, changingInnerRadiusY,
+ cornerRadiusX, cornerRadiusY,
+ cornerRadiusX, cornerRadiusY
+ )
+ )
+
+ // Calculate changing measurements for icons.
+ val changingIconSizeX =
+ (STARTING_ICON_SIZE_PX +
+ ((ENDING_ICON_SIZE_PX - STARTING_ICON_SIZE_PX) *
+ expandXInterpolator.getInterpolation(progress))) / scaleFactorX
+ val changingIconSizeY =
+ (STARTING_ICON_SIZE_PX +
+ ((ENDING_ICON_SIZE_PX - STARTING_ICON_SIZE_PX) *
+ expandYInterpolator.getInterpolation(progress))) / scaleFactorY
+
+ val changingIconLeft = (width / 2f) - (changingIconSizeX / 2f)
+ val changingIcon1Top = (((height / 2f) - halfDividerSize) / 2f) - (changingIconSizeY / 2f)
+ val changingIcon2Top =
+ (height - (((height / 2f) - halfDividerSize) / 2f)) - (changingIconSizeY / 2f)
+ val changingIconScaleX = changingIconSizeX / appIcon1.bounds.width()
+ val changingIconScaleY = changingIconSizeY / appIcon1.bounds.height()
+ val changingIconAlpha =
+ (255 - 255 * iconFadeInterpolator.getInterpolation(progress)).toInt()
+
+ // Draw first icon
+ canvas.save()
+ canvas.translate(changingIconLeft, changingIcon1Top)
+ canvas.scale(changingIconScaleX, changingIconScaleY)
+ appIcon1.alpha = changingIconAlpha
+ appIcon1.draw(canvas)
+ canvas.restore()
+
+ // Draw second icon
+ canvas.save()
+ canvas.translate(changingIconLeft, changingIcon2Top)
+ canvas.scale(changingIconScaleX, changingIconScaleY)
+ appIcon2.alpha = changingIconAlpha
+ appIcon2.draw(canvas)
+ canvas.restore()
+ }
+
+ /**
+ * Draws a rectangle with custom rounded corners.
+ *
+ * @param c The Canvas to draw on.
+ * @param rect The bounds of the rectangle.
+ * @param radii An array of 8 radii for the corners: top left x, top left y, top right x, top
+ * right y, bottom right x, and so on.
+ */
+ private fun drawCustomRoundedRect(c: Canvas, rect: RectF, radii: FloatArray) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ // Canvas.drawDoubleRoundRect is supported from Q onward
+ c.drawDoubleRoundRect(rect, radii, EMPTY_RECT, ARRAY_OF_ZEROES, backgroundPaint)
+ } else {
+ // Fallback rectangle with uniform rounded corners
+ val scaleFactorX = floatingView.scaleX
+ val scaleFactorY = floatingView.scaleY
+ val cornerRadiusX = QuickStepContract.getWindowCornerRadius(launcher) / scaleFactorX
+ val cornerRadiusY = QuickStepContract.getWindowCornerRadius(launcher) / scaleFactorY
+ c.drawRoundRect(rect, cornerRadiusX, cornerRadiusY, backgroundPaint)
+ }
+ }
+
+ override fun getOpacity(): Int {
+ return PixelFormat.OPAQUE
+ }
+
+ override fun setAlpha(i: Int) {
+ // Required by Drawable but not used.
+ }
+
+ override fun setColorFilter(colorFilter: ColorFilter?) {
+ // Required by Drawable but not used.
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt b/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt
new file mode 100644
index 0000000..e90aa13
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.quickstep.views
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.graphics.RectF
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import com.android.launcher3.R
+import com.android.launcher3.Utilities
+import com.android.launcher3.statemanager.StatefulActivity
+import com.android.launcher3.views.BaseDragLayer
+
+/**
+ * A temporary View that is created for the app pair launch animation and destroyed at the end.
+ * Matches the size & position of the app pair icon graphic, and expands to full screen.
+ */
+class FloatingAppPairView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
+ FrameLayout(context, attrs) {
+ companion object {
+ fun getFloatingAppPairView(
+ launcher: StatefulActivity<*>,
+ originalView: View,
+ appIcon1: Drawable,
+ appIcon2: Drawable,
+ dividerPos: Int
+ ): FloatingAppPairView {
+ val dragLayer: ViewGroup = launcher.getDragLayer()
+ val floatingView =
+ launcher
+ .getLayoutInflater()
+ .inflate(R.layout.floating_app_pair_view, dragLayer, false)
+ as FloatingAppPairView
+ floatingView.init(launcher, originalView, appIcon1, appIcon2, dividerPos)
+ dragLayer.addView(floatingView, dragLayer.childCount - 1)
+ return floatingView
+ }
+ }
+
+ val startingPosition = RectF()
+ private lateinit var background: FloatingAppPairBackground
+ var progress = 0f
+
+ /** Initializes the view, copying the bounds and location of the original icon view. */
+ fun init(
+ launcher: StatefulActivity<*>,
+ originalView: View,
+ appIcon1: Drawable,
+ appIcon2: Drawable,
+ dividerPos: Int
+ ) {
+ val viewBounds = Rect(0, 0, originalView.width, originalView.height)
+ Utilities.getBoundsForViewInDragLayer(
+ launcher.getDragLayer(),
+ originalView,
+ viewBounds,
+ false /* ignoreTransform */,
+ null /* recycle */,
+ startingPosition
+ )
+ val lp =
+ BaseDragLayer.LayoutParams(
+ Math.round(startingPosition.width()),
+ Math.round(startingPosition.height())
+ )
+ lp.ignoreInsets = true
+
+ // Position the floating view exactly on top of the original
+ lp.topMargin = Math.round(startingPosition.top)
+ lp.leftMargin = Math.round(startingPosition.left)
+
+ layout(lp.leftMargin, lp.topMargin, lp.leftMargin + lp.width, lp.topMargin + lp.height)
+ layoutParams = lp
+
+ // Prepare to draw app pair icon background
+ background = FloatingAppPairBackground(context, this, appIcon1, appIcon2, dividerPos)
+ background.setBounds(0, 0, lp.width, lp.height)
+ }
+
+ override fun dispatchDraw(canvas: Canvas) {
+ super.dispatchDraw(canvas)
+ background.draw(canvas)
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 7e1034b..87cee63 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -2124,8 +2124,7 @@
for (int i = 0; i < taskCount; i++) {
TaskView taskView = requireTaskViewAt(i);
taskView.updateTaskSize();
- taskView.getPrimaryNonGridTranslationProperty().set(taskView, accumulatedTranslationX);
- taskView.getSecondaryNonGridTranslationProperty().set(taskView, 0f);
+ taskView.setNonGridTranslationX(accumulatedTranslationX);
taskView.setNonGridPivotTranslationX(translateXToMiddle);
// Compensate space caused by TaskView scaling.
float widthDiff =
@@ -2642,23 +2641,25 @@
if (endState.displayOverviewTasksAsGrid(mActivity.getDeviceProfile())) {
TaskView runningTaskView = getRunningTaskView();
float runningTaskPrimaryGridTranslation = 0;
+ float runningTaskSecondaryGridTranslation = 0;
if (runningTaskView != null) {
// Apply the grid translation to running task unless it's being snapped to
// and removes the current translation applied to the running task.
- runningTaskPrimaryGridTranslation = mOrientationHandler.getPrimaryValue(
- runningTaskView.getGridTranslationX(),
- runningTaskView.getGridTranslationY())
- - runningTaskView.getPrimaryNonGridTranslationProperty().get(
- runningTaskView);
+ runningTaskPrimaryGridTranslation = runningTaskView.getGridTranslationX()
+ - runningTaskView.getNonGridTranslationX();
+ runningTaskSecondaryGridTranslation = runningTaskView.getGridTranslationY();
}
for (TaskViewSimulator tvs : taskViewSimulators) {
if (animatorSet == null) {
setGridProgress(1);
tvs.taskPrimaryTranslation.value = runningTaskPrimaryGridTranslation;
+ tvs.taskSecondaryTranslation.value = runningTaskSecondaryGridTranslation;
} else {
animatorSet.play(ObjectAnimator.ofFloat(this, RECENTS_GRID_PROGRESS, 1));
animatorSet.play(tvs.taskPrimaryTranslation.animateToValue(
runningTaskPrimaryGridTranslation));
+ animatorSet.play(tvs.taskSecondaryTranslation.animateToValue(
+ runningTaskSecondaryGridTranslation));
}
}
}
@@ -3123,6 +3124,14 @@
+ snappedTaskNonGridScrollAdjustment);
}
+ final TaskView runningTask = getRunningTaskView();
+ if (showAsGrid() && enableGridOnlyOverview() && runningTask != null) {
+ runActionOnRemoteHandles(
+ remoteTargetHandle -> remoteTargetHandle.getTaskViewSimulator()
+ .taskSecondaryTranslation.value = runningTask.getGridTranslationY()
+ );
+ }
+
mClearAllButton.setGridTranslationPrimary(
clearAllTotalTranslationX - snappedTaskGridTranslationX);
mClearAllButton.setGridScrollOffset(
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index af4f402..b42f055 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -119,6 +119,8 @@
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.QuickStepContract;
+import kotlin.Unit;
+
import java.lang.annotation.Retention;
import java.util.Arrays;
import java.util.Collections;
@@ -127,8 +129,6 @@
import java.util.function.Consumer;
import java.util.stream.Stream;
-import kotlin.Unit;
-
/**
* A task in the Recents view.
*/
@@ -304,32 +304,6 @@
}
};
- private static final FloatProperty<TaskView> NON_GRID_TRANSLATION_X =
- new FloatProperty<TaskView>("nonGridTranslationX") {
- @Override
- public void setValue(TaskView taskView, float v) {
- taskView.setNonGridTranslationX(v);
- }
-
- @Override
- public Float get(TaskView taskView) {
- return taskView.mNonGridTranslationX;
- }
- };
-
- private static final FloatProperty<TaskView> NON_GRID_TRANSLATION_Y =
- new FloatProperty<TaskView>("nonGridTranslationY") {
- @Override
- public void setValue(TaskView taskView, float v) {
- taskView.setNonGridTranslationY(v);
- }
-
- @Override
- public Float get(TaskView taskView) {
- return taskView.mNonGridTranslationY;
- }
- };
-
public static final FloatProperty<TaskView> GRID_END_TRANSLATION_X =
new FloatProperty<TaskView>("gridEndTranslationX") {
@Override
@@ -386,7 +360,6 @@
// Applied as a complement to gridTranslation, for adjusting the carousel overview and quick
// switch.
private float mNonGridTranslationX;
- private float mNonGridTranslationY;
private float mNonGridPivotTranslationX;
// Used when in SplitScreenSelectState
private float mSplitSelectTranslationY;
@@ -1323,7 +1296,7 @@
}
protected void resetPersistentViewTransforms() {
- mNonGridTranslationX = mNonGridTranslationY = mGridTranslationX =
+ mNonGridTranslationX = mGridTranslationX =
mGridTranslationY = mBoxTranslationY = mNonGridPivotTranslationX = 0f;
resetViewTransforms();
}
@@ -1494,14 +1467,16 @@
applyTranslationY();
}
- private void setNonGridTranslationX(float nonGridTranslationX) {
- mNonGridTranslationX = nonGridTranslationX;
- applyTranslationX();
+ public float getNonGridTranslationX() {
+ return mNonGridTranslationX;
}
- private void setNonGridTranslationY(float nonGridTranslationY) {
- mNonGridTranslationY = nonGridTranslationY;
- applyTranslationY();
+ /**
+ * Updates X coordinate of non-grid translation.
+ */
+ public void setNonGridTranslationX(float nonGridTranslationX) {
+ mNonGridTranslationX = nonGridTranslationX;
+ applyTranslationX();
}
public void setGridTranslationX(float gridTranslationX) {
@@ -1540,7 +1515,7 @@
if (gridEnabled) {
scrollAdjustment += mGridTranslationX;
} else {
- scrollAdjustment += getPrimaryNonGridTranslationProperty().get(this);
+ scrollAdjustment += getNonGridTranslationX();
}
return scrollAdjustment;
}
@@ -1586,9 +1561,7 @@
* change according to a temporary state (e.g. task offset).
*/
public float getPersistentTranslationY() {
- return mBoxTranslationY
- + getNonGridTrans(mNonGridTranslationY)
- + getGridTrans(mGridTranslationY);
+ return mBoxTranslationY + getGridTrans(mGridTranslationY);
}
public FloatProperty<TaskView> getPrimarySplitTranslationProperty() {
@@ -1626,16 +1599,6 @@
TASK_RESISTANCE_TRANSLATION_X, TASK_RESISTANCE_TRANSLATION_Y);
}
- public FloatProperty<TaskView> getPrimaryNonGridTranslationProperty() {
- return getPagedOrientationHandler().getPrimaryValue(
- NON_GRID_TRANSLATION_X, NON_GRID_TRANSLATION_Y);
- }
-
- public FloatProperty<TaskView> getSecondaryNonGridTranslationProperty() {
- return getPagedOrientationHandler().getSecondaryValue(
- NON_GRID_TRANSLATION_X, NON_GRID_TRANSLATION_Y);
- }
-
@Override
public boolean hasOverlappingRendering() {
// TODO: Clip-out the icon region from the thumbnail, since they are overlapping.
diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
index eced5a9..8d54dce 100644
--- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
+++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java
@@ -144,7 +144,7 @@
.around(new TestStabilityRule())
.around(new NavigationModeSwitchRule(mLauncher))
.around(new FailureWatcher(mLauncher, viewCaptureRule::getViewCaptureData))
- .around(viewCaptureRule)
+ // .around(viewCaptureRule) b/315482167
.around(new TestIsolationRule(mLauncher, false))
.around(setLauncherCommand);
diff --git a/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java b/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java
similarity index 83%
rename from quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
rename to quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java
index 1129a33..728fe67 100644
--- a/quickstep/tests/src/com/android/quickstep/DigitalWellBeingToastTest.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplDigitalWellBeingToastTest.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package com.android.quickstep;
import static androidx.test.InstrumentationRegistry.getInstrumentation;
@@ -27,7 +42,7 @@
@LargeTest
@RunWith(AndroidJUnit4.class)
-public class DigitalWellBeingToastTest extends AbstractQuickStepTest {
+public class TaplDigitalWellBeingToastTest extends AbstractQuickStepTest {
private static final String CALCULATOR_PACKAGE =
resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR);
diff --git a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java b/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java
similarity index 96%
rename from quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
rename to quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java
index 85440e9..7e274e8 100644
--- a/quickstep/tests/src/com/android/quickstep/StartLauncherViaGestureTests.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplStartLauncherViaGestureTests.java
@@ -32,7 +32,7 @@
@LargeTest
@RunWith(AndroidJUnit4.class)
-public class StartLauncherViaGestureTests extends AbstractQuickStepTest {
+public class TaplStartLauncherViaGestureTests extends AbstractQuickStepTest {
static final int STRESS_REPEAT_COUNT = 10;
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
index 0bcdb19..6cbe171 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsQuickstep.java
@@ -355,6 +355,8 @@
public void testPressBack() throws Exception {
InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
READ_DEVICE_CONFIG_PERMISSION);
+ // Debug if we need to goHome to prevent wrong previous state b/315525621
+ mLauncher.goHome();
assumeFalse(FeatureFlags.ENABLE_BACK_SWIPE_LAUNCHER_ANIMATION.get());
mLauncher.getWorkspace().switchToAllApps();
mLauncher.pressBack();
diff --git a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
index 0eec8b7..3465f23 100644
--- a/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplTestsTrackpad.java
@@ -86,7 +86,7 @@
mLauncher.setTrackpadGestureType(TrackpadGestureType.THREE_FINGER);
startTestActivity(2);
- mLauncher.pressBack();
+ mLauncher.getLaunchedAppState().pressBackToWorkspace();
} finally {
instrumentation.getUiAutomation().dropShellPermissionIdentity();
}
diff --git a/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java b/quickstep/tests/src/com/android/quickstep/TaplViewInflationDuringSwipeUp.java
similarity index 99%
rename from quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
rename to quickstep/tests/src/com/android/quickstep/TaplViewInflationDuringSwipeUp.java
index 2318f54..f76e17a 100644
--- a/quickstep/tests/src/com/android/quickstep/ViewInflationDuringSwipeUp.java
+++ b/quickstep/tests/src/com/android/quickstep/TaplViewInflationDuringSwipeUp.java
@@ -85,7 +85,7 @@
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
-public class ViewInflationDuringSwipeUp extends AbstractQuickStepTest {
+public class TaplViewInflationDuringSwipeUp extends AbstractQuickStepTest {
private SparseArray<ViewConfiguration> mConfigMap;
private InitTracker mInitTracker;
diff --git a/quickstep/tests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt b/quickstep/tests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt
new file mode 100644
index 0000000..dbe4624
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/taskbar/controllers/TaskbarPinningControllerTest.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.quickstep.taskbar.controllers
+
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.LauncherPrefs
+import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING
+import com.android.launcher3.logging.StatsLogManager
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_DIVIDER_MENU_CLOSE
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASKBAR_DIVIDER_MENU_OPEN
+import com.android.launcher3.taskbar.TaskbarActivityContext
+import com.android.launcher3.taskbar.TaskbarBaseTestCase
+import com.android.launcher3.taskbar.TaskbarDividerPopupView
+import com.android.launcher3.taskbar.TaskbarDragLayer
+import com.android.launcher3.taskbar.TaskbarPinningController
+import com.android.launcher3.taskbar.TaskbarPinningController.Companion.PINNING_PERSISTENT
+import com.android.launcher3.taskbar.TaskbarPinningController.Companion.PINNING_TRANSIENT
+import com.android.launcher3.taskbar.TaskbarSharedState
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class TaskbarPinningControllerTest : TaskbarBaseTestCase() {
+ private val taskbarDragLayer = mock<TaskbarDragLayer>()
+ private val taskbarSharedState = mock<TaskbarSharedState>()
+ private val launcherPrefs = mock<LauncherPrefs> { on { get(TASKBAR_PINNING) } doReturn false }
+ private val statsLogger = mock<StatsLogManager.StatsLogger>()
+ private val statsLogManager = mock<StatsLogManager> { on { logger() } doReturn statsLogger }
+ private lateinit var pinningController: TaskbarPinningController
+
+ @Before
+ override fun setup() {
+ super.setup()
+ whenever(taskbarActivityContext.launcherPrefs).thenReturn(launcherPrefs)
+ whenever(taskbarActivityContext.dragLayer).thenReturn(taskbarDragLayer)
+ whenever(taskbarActivityContext.statsLogManager).thenReturn(statsLogManager)
+ pinningController = spy(TaskbarPinningController(taskbarActivityContext))
+ pinningController.init(taskbarControllers, taskbarSharedState)
+ }
+
+ @Test
+ fun testOnCloseCallback_whenClosingPopupView_shouldLogStatsForClosingPopupMenu() {
+ pinningController.onCloseCallback(false)
+ verify(statsLogger, times(1)).log(LAUNCHER_TASKBAR_DIVIDER_MENU_CLOSE)
+ }
+
+ @Test
+ fun testOnCloseCallback_whenClosingPopupView_shouldPostVisibilityChangedToDragLayer() {
+ val argumentCaptor = argumentCaptor<Runnable>()
+ pinningController.onCloseCallback(false)
+ verify(taskbarDragLayer, times(1)).post(argumentCaptor.capture())
+
+ val runnable = argumentCaptor.lastValue
+ assertThat(runnable).isNotNull()
+
+ runnable.run()
+ verify(taskbarActivityContext, times(1)).onPopupVisibilityChanged(false)
+ }
+
+ @Test
+ fun testOnCloseCallback_whenPreferenceUnchanged_shouldNotAnimateTaskbarPinning() {
+ pinningController.onCloseCallback(false)
+ verify(taskbarSharedState, never()).taskbarWasPinned = true
+ verify(pinningController, never()).animateTaskbarPinning(any())
+ }
+
+ @Test
+ fun testOnCloseCallback_whenPreferenceChanged_shouldAnimateToPinnedTaskbar() {
+ whenever(launcherPrefs.get(TASKBAR_PINNING)).thenReturn(false)
+ doNothing().whenever(pinningController).animateTaskbarPinning(any())
+
+ pinningController.onCloseCallback(true)
+
+ verify(taskbarSharedState, times(1)).taskbarWasPinned = false
+ verify(pinningController, times(1)).animateTaskbarPinning(PINNING_PERSISTENT)
+ }
+
+ @Test
+ fun testOnCloseCallback_whenPreferenceChanged_shouldAnimateToTransientTaskbar() {
+ whenever(launcherPrefs.get(TASKBAR_PINNING)).thenReturn(true)
+ doNothing().whenever(pinningController).animateTaskbarPinning(any())
+
+ pinningController.onCloseCallback(true)
+
+ verify(taskbarSharedState, times(1)).taskbarWasPinned = true
+ verify(pinningController, times(1)).animateTaskbarPinning(PINNING_TRANSIENT)
+ }
+
+ @Test
+ fun testShowPinningView_whenShowingPinningView_shouldSetTaskbarWindowFullscreenAndPostRunnableToView() {
+ val popupView =
+ mock<TaskbarDividerPopupView<TaskbarActivityContext>> {
+ on { requestFocus() } doReturn true
+ }
+ val view = mock<View>()
+ val argumentCaptor = argumentCaptor<Runnable>()
+ doReturn(popupView).whenever(pinningController).getPopupView(view)
+
+ pinningController.showPinningView(view)
+
+ verify(view, times(1)).post(argumentCaptor.capture())
+
+ val runnable = argumentCaptor.lastValue
+ assertThat(runnable).isNotNull()
+ runnable.run()
+
+ verify(pinningController, times(1)).getPopupView(view)
+ verify(popupView, times(1)).requestFocus()
+ verify(popupView, times(1)).onCloseCallback = any()
+ verify(taskbarActivityContext, times(1)).onPopupVisibilityChanged(true)
+ verify(popupView, times(1)).show()
+ verify(statsLogger, times(1)).log(LAUNCHER_TASKBAR_DIVIDER_MENU_OPEN)
+ }
+
+ @Test
+ fun testAnimateTaskbarPinning_whenAnimationEnds_shouldInvokeCallbackDoOnEnd() {
+ val animatorSet = spy(AnimatorSet())
+ doReturn(animatorSet)
+ .whenever(pinningController)
+ .getAnimatorSetForTaskbarPinningAnimation(PINNING_PERSISTENT)
+ doNothing().whenever(animatorSet).start()
+ pinningController.animateTaskbarPinning(PINNING_PERSISTENT)
+ animatorSet.listeners[0].onAnimationEnd(ObjectAnimator())
+ verify(pinningController, times(1)).recreateTaskbarAndUpdatePinningValue()
+ }
+
+ @Test
+ fun testAnimateTaskbarPinning_whenAnimatingToPersistentTaskbar_shouldAnimateToPinnedTaskbar() {
+ val animatorSet = spy(AnimatorSet())
+ doReturn(animatorSet)
+ .whenever(pinningController)
+ .getAnimatorSetForTaskbarPinningAnimation(PINNING_PERSISTENT)
+ doNothing().whenever(animatorSet).start()
+ pinningController.animateTaskbarPinning(PINNING_PERSISTENT)
+
+ verify(taskbarOverlayController, times(1)).hideWindow()
+ verify(pinningController, times(1))
+ .getAnimatorSetForTaskbarPinningAnimation(PINNING_PERSISTENT)
+ verify(taskbarViewController, times(1))
+ .animateAwayNotificationDotsDuringTaskbarPinningAnimation()
+ verify(taskbarDragLayer, times(1)).setAnimatingTaskbarPinning(true)
+ assertThat(pinningController.isAnimatingTaskbarPinning).isTrue()
+ assertThat(animatorSet.listeners).isNotNull()
+ }
+
+ @Test
+ fun testAnimateTaskbarPinning_whenAnimatingToTransientTaskbar_shouldAnimateToTransientTaskbar() {
+ val animatorSet = spy(AnimatorSet())
+ doReturn(animatorSet)
+ .whenever(pinningController)
+ .getAnimatorSetForTaskbarPinningAnimation(PINNING_TRANSIENT)
+ doNothing().whenever(animatorSet).start()
+ pinningController.animateTaskbarPinning(PINNING_TRANSIENT)
+
+ verify(taskbarOverlayController, times(1)).hideWindow()
+ verify(pinningController, times(1))
+ .getAnimatorSetForTaskbarPinningAnimation(PINNING_TRANSIENT)
+ verify(taskbarDragLayer, times(1)).setAnimatingTaskbarPinning(true)
+ assertThat(pinningController.isAnimatingTaskbarPinning).isTrue()
+ verify(taskbarViewController, times(1))
+ .animateAwayNotificationDotsDuringTaskbarPinningAnimation()
+ assertThat(animatorSet.listeners).isNotNull()
+ }
+
+ @Test
+ fun testRecreateTaskbarAndUpdatePinningValue_whenAnimationEnds_shouldUpdateTaskbarPinningLauncherPref() {
+ pinningController.recreateTaskbarAndUpdatePinningValue()
+ verify(taskbarDragLayer, times(1)).setAnimatingTaskbarPinning(false)
+ assertThat(pinningController.isAnimatingTaskbarPinning).isFalse()
+ verify(launcherPrefs, times(1)).put(TASKBAR_PINNING, true)
+ }
+}
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
index 50803fe..86018b1 100644
--- a/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
@@ -19,8 +19,13 @@
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
+import android.view.SurfaceControl.Transaction
import android.view.View
+import android.window.TransitionInfo
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.launcher3.apppairs.AppPairIcon
+import com.android.launcher3.statehandlers.DepthController
+import com.android.launcher3.statemanager.StateManager
import com.android.launcher3.util.SplitConfigurationOptions
import com.android.quickstep.views.GroupedTaskView
import com.android.quickstep.views.IconView
@@ -32,13 +37,18 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doNothing
import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
class SplitAnimationControllerTest {
private val taskId = 9
+ private val taskId2 = 10
private val mockSplitSelectStateController: SplitSelectStateController = mock()
// TaskView
@@ -52,12 +62,19 @@
private val mockTask: Task = mock()
private val mockTaskKey: Task.TaskKey = mock()
private val mockTaskIdAttributeContainer: TaskIdAttributeContainer = mock()
+ // AppPairIcon
+ private val mockAppPairIcon: AppPairIcon = mock()
// SplitSelectSource
private val splitSelectSource: SplitConfigurationOptions.SplitSelectSource = mock()
private val mockSplitSourceDrawable: Drawable = mock()
private val mockSplitSourceView: View = mock()
+ private val stateManager: StateManager<*> = mock()
+ private val depthController: DepthController = mock()
+ private val transitionInfo: TransitionInfo = mock()
+ private val transaction: Transaction = mock()
+
lateinit var splitAnimationController: SplitAnimationController
@Before
@@ -172,4 +189,110 @@
splitAnimInitProps.iconDrawable
)
}
+
+ @Test
+ fun playsAppropriateSplitLaunchAnimation_playsLegacyLaunchCorrectly() {
+ val spySplitAnimationController = spy(splitAnimationController)
+ doNothing()
+ .whenever(spySplitAnimationController)
+ .composeRecentsSplitLaunchAnimatorLegacy(
+ any(), any(), any(), any(), any(), any(), any(), any(), any())
+
+ spySplitAnimationController.playSplitLaunchAnimation(
+ mockGroupedTaskView,
+ null /* launchingIconView */,
+ taskId,
+ taskId2,
+ arrayOf() /* apps */,
+ arrayOf() /* wallpapers */,
+ arrayOf() /* nonApps */,
+ stateManager,
+ depthController,
+ null /* info */,
+ null /* t */,
+ {} /* finishCallback */
+ )
+
+ verify(spySplitAnimationController)
+ .composeRecentsSplitLaunchAnimatorLegacy(
+ any(), any(), any(), any(), any(), any(), any(), any(), any())
+ }
+
+ @Test
+ fun playsAppropriateSplitLaunchAnimation_playsRecentsLaunchCorrectly() {
+ val spySplitAnimationController = spy(splitAnimationController)
+ doNothing()
+ .whenever(spySplitAnimationController)
+ .composeRecentsSplitLaunchAnimator(any(), any(), any(), any(), any(), any())
+
+ spySplitAnimationController.playSplitLaunchAnimation(
+ mockGroupedTaskView,
+ null /* launchingIconView */,
+ taskId,
+ taskId2,
+ null /* apps */,
+ null /* wallpapers */,
+ null /* nonApps */,
+ stateManager,
+ depthController,
+ transitionInfo,
+ transaction,
+ {} /* finishCallback */
+ )
+
+ verify(spySplitAnimationController)
+ .composeRecentsSplitLaunchAnimator(any(), any(), any(), any(), any(), any())
+ }
+
+ @Test
+ fun playsAppropriateSplitLaunchAnimation_playsIconLaunchCorrectly() {
+ val spySplitAnimationController = spy(splitAnimationController)
+ doNothing()
+ .whenever(spySplitAnimationController)
+ .composeIconSplitLaunchAnimator(any(), any(), any(), any(), any(), any())
+
+ spySplitAnimationController.playSplitLaunchAnimation(
+ null /* launchingTaskView */,
+ mockAppPairIcon,
+ taskId,
+ taskId2,
+ null /* apps */,
+ null /* wallpapers */,
+ null /* nonApps */,
+ stateManager,
+ depthController,
+ transitionInfo,
+ transaction,
+ {} /* finishCallback */
+ )
+
+ verify(spySplitAnimationController)
+ .composeIconSplitLaunchAnimator(any(), any(), any(), any(), any(), any())
+ }
+
+ @Test
+ fun playsAppropriateSplitLaunchAnimation_playsFadeInLaunchCorrectly() {
+ val spySplitAnimationController = spy(splitAnimationController)
+ doNothing()
+ .whenever(spySplitAnimationController)
+ .composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any())
+
+ spySplitAnimationController.playSplitLaunchAnimation(
+ null /* launchingTaskView */,
+ null /* launchingIconView */,
+ taskId,
+ taskId2,
+ null /* apps */,
+ null /* wallpapers */,
+ null /* nonApps */,
+ stateManager,
+ depthController,
+ transitionInfo,
+ transaction,
+ {} /* finishCallback */
+ )
+
+ verify(spySplitAnimationController)
+ .composeFadeInSplitLaunchAnimator(any(), any(), any(), any(), any())
+ }
}
diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
index f292f9a..f41ac48 100644
--- a/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
@@ -114,6 +114,7 @@
argumentCaptor<Consumer<ArrayList<GroupTask>>> {
splitSelectStateController.findLastActiveTasksAndRunCallback(
listOf(nonMatchingComponent),
+ false /* findExactPairMatch */,
taskConsumer
)
verify(recentsModel).getTasks(capture())
@@ -166,6 +167,7 @@
argumentCaptor<Consumer<ArrayList<GroupTask>>> {
splitSelectStateController.findLastActiveTasksAndRunCallback(
listOf(matchingComponent),
+ false /* findExactPairMatch */,
taskConsumer
)
verify(recentsModel).getTasks(capture())
@@ -206,6 +208,7 @@
argumentCaptor<Consumer<ArrayList<GroupTask>>> {
splitSelectStateController.findLastActiveTasksAndRunCallback(
listOf(nonPrimaryUserComponent),
+ false /* findExactPairMatch */,
taskConsumer
)
verify(recentsModel).getTasks(capture())
@@ -261,6 +264,7 @@
argumentCaptor<Consumer<ArrayList<GroupTask>>> {
splitSelectStateController.findLastActiveTasksAndRunCallback(
listOf(nonPrimaryUserComponent),
+ false /* findExactPairMatch */,
taskConsumer
)
verify(recentsModel).getTasks(capture())
@@ -313,6 +317,7 @@
argumentCaptor<Consumer<ArrayList<GroupTask>>> {
splitSelectStateController.findLastActiveTasksAndRunCallback(
listOf(matchingComponent),
+ false /* findExactPairMatch */,
taskConsumer
)
verify(recentsModel).getTasks(capture())
@@ -366,6 +371,7 @@
argumentCaptor<Consumer<ArrayList<GroupTask>>> {
splitSelectStateController.findLastActiveTasksAndRunCallback(
listOf(nonMatchingComponent, matchingComponent),
+ false /* findExactPairMatch */,
taskConsumer
)
verify(recentsModel).getTasks(capture())
@@ -418,6 +424,7 @@
argumentCaptor<Consumer<ArrayList<GroupTask>>> {
splitSelectStateController.findLastActiveTasksAndRunCallback(
listOf(matchingComponent, matchingComponent),
+ false /* findExactPairMatch */,
taskConsumer
)
verify(recentsModel).getTasks(capture())
@@ -483,6 +490,59 @@
argumentCaptor<Consumer<ArrayList<GroupTask>>> {
splitSelectStateController.findLastActiveTasksAndRunCallback(
listOf(matchingComponent, matchingComponent),
+ false /* findExactPairMatch */,
+ taskConsumer
+ )
+ verify(recentsModel).getTasks(capture())
+ }
+ .lastValue
+
+ // Send our mocked tasks
+ consumer.accept(tasks)
+ }
+
+ @Test
+ fun activeTasks_multipleSearchShouldFindExactPairMatch() {
+ val matchingPackage = "hotdog"
+ val matchingClass = "juice"
+ val matchingComponent =
+ ComponentKey(ComponentName(matchingPackage, matchingClass), primaryUserHandle)
+ val matchingPackage2 = "pomegranate"
+ val matchingClass2 = "juice"
+ val matchingComponent2 =
+ ComponentKey(ComponentName(matchingPackage2, matchingClass2), primaryUserHandle)
+
+ val groupTask1 =
+ generateGroupTask(ComponentName("hotdog", "pie"), ComponentName("pumpkin", "pie"))
+ val groupTask2 =
+ generateGroupTask(
+ ComponentName(matchingPackage2, matchingClass2),
+ ComponentName(matchingPackage, matchingClass)
+ )
+ val groupTask3 =
+ generateGroupTask(
+ ComponentName("hotdog", "pie"),
+ ComponentName(matchingPackage, matchingClass)
+ )
+ val tasks: ArrayList<GroupTask> = ArrayList()
+ tasks.add(groupTask3)
+ tasks.add(groupTask2)
+ tasks.add(groupTask1)
+
+ // Assertions happen in the callback we get from what we pass into
+ // #findLastActiveTasksAndRunCallback
+ val taskConsumer =
+ Consumer<List<Task>> {
+ assertEquals("Expected array length 1", 1, it.size)
+ assertEquals("Found wrong task", it[0], groupTask2.task1)
+ }
+
+ // Capture callback from recentsModel#getTasks()
+ val consumer =
+ argumentCaptor<Consumer<ArrayList<GroupTask>>> {
+ splitSelectStateController.findLastActiveTasksAndRunCallback(
+ listOf(matchingComponent2, matchingComponent),
+ true /* findExactPairMatch */,
taskConsumer
)
verify(recentsModel).getTasks(capture())
diff --git a/res/layout/add_item_confirmation_activity.xml b/res/layout/add_item_confirmation_activity.xml
index e29e1b1..d113a38 100644
--- a/res/layout/add_item_confirmation_activity.xml
+++ b/res/layout/add_item_confirmation_activity.xml
@@ -16,7 +16,7 @@
** limitations under the License.
*/
-->
-<com.android.launcher3.dragndrop.AddItemDragLayer
+<com.android.launcher3.dragndrop.SimpleDragLayer
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/add_item_drag_layer"
android:layout_width="match_parent"
@@ -121,6 +121,6 @@
</LinearLayout>
</com.android.launcher3.widget.AddItemWidgetsBottomSheet>
-</com.android.launcher3.dragndrop.AddItemDragLayer>
+</com.android.launcher3.dragndrop.SimpleDragLayer>
diff --git a/res/layout/app_pair_icon.xml b/res/layout/app_pair_icon.xml
index 2b9a98b..4e2dd58 100644
--- a/res/layout/app_pair_icon.xml
+++ b/res/layout/app_pair_icon.xml
@@ -20,6 +20,11 @@
android:layout_height="match_parent"
android:orientation="vertical"
android:focusable="true" >
+ <com.android.launcher3.apppairs.AppPairIconGraphic
+ android:id="@+id/app_pair_icon_graphic"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:focusable="false" />
<com.android.launcher3.views.DoubleShadowBubbleTextView
style="@style/BaseIcon.Workspace"
android:id="@+id/app_pair_icon_name"
diff --git a/res/layout/floating_app_pair_view.xml b/res/layout/floating_app_pair_view.xml
new file mode 100644
index 0000000..88ec655
--- /dev/null
+++ b/res/layout/floating_app_pair_view.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.android.quickstep.views.FloatingAppPairView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+</com.android.quickstep.views.FloatingAppPairView>
\ No newline at end of file
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 20e7089..8d84c90 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -164,7 +164,19 @@
<!-- numFolderRows & numFolderColumns defaults to numRows & numColumns, if not specified -->
<attr name="numFolderRows" format="integer" />
+ <!-- defaults to numFolderRows, if not specified -->
+ <attr name="numFolderRowsLandscape" format="integer" />
+ <!-- defaults to numFolderRows, if not specified -->
+ <attr name="numFolderRowsTwoPanelLandscape" format="integer" />
+ <!-- defaults to numFolderRows, if not specified -->
+ <attr name="numFolderRowsTwoPanelPortrait" format="integer" />
<attr name="numFolderColumns" format="integer" />
+ <!-- defaults to numFolderColumns, if not specified -->
+ <attr name="numFolderColumnsLandscape" format="integer" />
+ <!-- defaults to numFolderColumns, if not specified -->
+ <attr name="numFolderColumnsTwoPanelLandscape" format="integer" />
+ <!-- defaults to numFolderColumns, if not specified -->
+ <attr name="numFolderColumnsTwoPanelPortrait" format="integer" />
<!-- Support attributes in FolderStyle -->
<attr name="folderStyle" format="reference" />
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 242c439..8cb6c71 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -125,6 +125,9 @@
<dimen name="all_apps_tip_bottom_margin">8dp</dimen>
<dimen name="all_apps_height_extra">6dp</dimen>
<dimen name="all_apps_paged_view_top_padding">40dp</dimen>
+ <dimen name="all_apps_recycler_view_decorator_padding">1dp</dimen>
+ <dimen name="all_apps_recycler_view_decorator_group_radius">28dp</dimen>
+ <dimen name="all_apps_recycler_view_decorator_result_radius">4dp</dimen>
<dimen name="all_apps_icon_drawable_padding">8dp</dimen>
<dimen name="all_apps_predicted_icon_vertical_padding">8dp</dimen>
diff --git a/src/com/android/launcher3/AbstractFloatingView.java b/src/com/android/launcher3/AbstractFloatingView.java
index b845c88..f72c556 100644
--- a/src/com/android/launcher3/AbstractFloatingView.java
+++ b/src/com/android/launcher3/AbstractFloatingView.java
@@ -104,6 +104,7 @@
public static final int TYPE_ADD_TO_HOME_CONFIRMATION = 1 << 19;
public static final int TYPE_TASKBAR_OVERLAY_PROXY = 1 << 20;
public static final int TYPE_TASKBAR_PINNING_POPUP = 1 << 21;
+ public static final int TYPE_PIN_IME_POPUP = 1 << 22;
public static final int TYPE_ALL = TYPE_FOLDER | TYPE_ACTION_POPUP
| TYPE_WIDGETS_BOTTOM_SHEET | TYPE_WIDGET_RESIZE_FRAME | TYPE_WIDGETS_FULL_SHEET
@@ -112,17 +113,18 @@
| TYPE_ICON_SURFACE | TYPE_DRAG_DROP_POPUP | TYPE_PIN_WIDGET_FROM_EXTERNAL_POPUP
| TYPE_WIDGETS_EDUCATION_DIALOG | TYPE_TASKBAR_EDUCATION_DIALOG | TYPE_TASKBAR_ALL_APPS
| TYPE_OPTIONS_POPUP_DIALOG | TYPE_ADD_TO_HOME_CONFIRMATION
- | TYPE_TASKBAR_OVERLAY_PROXY | TYPE_TASKBAR_PINNING_POPUP;
+ | TYPE_TASKBAR_OVERLAY_PROXY | TYPE_TASKBAR_PINNING_POPUP | TYPE_PIN_IME_POPUP;
// Type of popups which should be kept open during launcher rebind
public static final int TYPE_REBIND_SAFE = TYPE_WIDGETS_FULL_SHEET
| TYPE_WIDGETS_BOTTOM_SHEET | TYPE_ON_BOARD_POPUP | TYPE_DISCOVERY_BOUNCE
| TYPE_ALL_APPS_EDU | TYPE_ICON_SURFACE | TYPE_WIDGETS_EDUCATION_DIALOG
| TYPE_TASKBAR_EDUCATION_DIALOG | TYPE_TASKBAR_ALL_APPS | TYPE_OPTIONS_POPUP_DIALOG
- | TYPE_TASKBAR_OVERLAY_PROXY;
+ | TYPE_TASKBAR_OVERLAY_PROXY | TYPE_PIN_IME_POPUP;
+ /** Type of popups that should get exclusive accessibility focus. */
public static final int TYPE_ACCESSIBLE = TYPE_ALL & ~TYPE_DISCOVERY_BOUNCE & ~TYPE_LISTENER
- & ~TYPE_ALL_APPS_EDU;
+ & ~TYPE_ALL_APPS_EDU & ~TYPE_TASKBAR_ALL_APPS & ~TYPE_PIN_IME_POPUP;
// These view all have particular operation associated with swipe down interaction.
public static final int TYPE_STATUS_BAR_SWIPE_DOWN_DISALLOW = TYPE_WIDGETS_BOTTOM_SHEET |
@@ -133,7 +135,12 @@
public static final int TYPE_TASKBAR_OVERLAYS =
TYPE_TASKBAR_ALL_APPS | TYPE_TASKBAR_EDUCATION_DIALOG;
- public static final int TYPE_ALL_EXCEPT_ON_BOARD_POPUP = TYPE_ALL & ~TYPE_ON_BOARD_POPUP;
+ // Floating views that a TouchController should not try to intercept touches from.
+ public static final int TYPE_TOUCH_CONTROLLER_NO_INTERCEPT = TYPE_ALL & ~TYPE_DISCOVERY_BOUNCE
+ & ~TYPE_LISTENER & ~TYPE_TASKBAR_OVERLAYS;
+
+ public static final int TYPE_ALL_EXCEPT_ON_BOARD_POPUP = TYPE_ALL & ~TYPE_ON_BOARD_POPUP
+ & ~TYPE_PIN_IME_POPUP;
protected boolean mIsOpen;
diff --git a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
index 641fd83..429978e 100644
--- a/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
+++ b/src/com/android/launcher3/AppWidgetsRestoredReceiver.java
@@ -9,9 +9,13 @@
import android.content.Intent;
import android.util.Log;
+import com.android.launcher3.logging.FileLog;
+import com.android.launcher3.provider.RestoreDbTask;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.widget.LauncherWidgetHolder;
+import java.util.Arrays;
+
public class AppWidgetsRestoredReceiver extends BroadcastReceiver {
private static final String TAG = "AppWidgetsRestoredReceiver";
@@ -20,8 +24,11 @@
public void onReceive(final Context context, Intent intent) {
if (AppWidgetManager.ACTION_APPWIDGET_HOST_RESTORED.equals(intent.getAction())) {
int hostId = intent.getIntExtra(AppWidgetManager.EXTRA_HOST_ID, 0);
- Log.d(TAG, "Widget ID map received for host:" + hostId);
+ Log.d(TAG, "onReceive: Widget ID map received for host:" + hostId);
if (hostId != LauncherWidgetHolder.APPWIDGET_HOST_ID) {
+ Log.w(TAG, "onReceive: hostId does not match Launcher."
+ + " Expected: " + LauncherWidgetHolder.APPWIDGET_HOST_ID
+ + ", Actual: " + hostId);
return;
}
@@ -31,8 +38,18 @@
LauncherPrefs.get(context).putSync(
OLD_APP_WIDGET_IDS.to(IntArray.wrap(oldIds).toConcatString()),
APP_WIDGET_IDS.to(IntArray.wrap(newIds).toConcatString()));
+ FileLog.d(TAG, "onReceive: Valid Widget IDs received."
+ + " old IDs=" + Arrays.toString(oldIds)
+ + ", new IDs=" + Arrays.toString(newIds));
+ if (!RestoreDbTask.isPending(context)) {
+ FileLog.w(TAG, "onReceive: Restored App Widget Ids received but Launcher"
+ + " restore is not pending. New widget Ids might not get restored.");
+ }
} else {
- Log.e(TAG, "Invalid host restored received");
+ Log.e(TAG, "onReceive: Invalid widget ids received for Launcher"
+ + ", skipping restore of widget ids."
+ + " newIds=" + Arrays.toString(newIds)
+ + ", oldIds=" + Arrays.toString(oldIds));
}
}
}
diff --git a/src/com/android/launcher3/DeviceProfile.java b/src/com/android/launcher3/DeviceProfile.java
index 1451b98..b6e8ec3 100644
--- a/src/com/android/launcher3/DeviceProfile.java
+++ b/src/com/android/launcher3/DeviceProfile.java
@@ -182,6 +182,8 @@
public int cellYPaddingPx = -1;
// Folder
+ public final int numFolderRows;
+ public final int numFolderColumns;
public final float folderLabelTextScale;
public int folderLabelTextSizePx;
public int folderFooterHeightPx;
@@ -439,6 +441,8 @@
}
folderLabelTextScale = res.getFloat(R.dimen.folder_label_text_scale);
+ numFolderRows = inv.numFolderRows[mTypeIndex];
+ numFolderColumns = inv.numFolderColumns[mTypeIndex];
if (mIsScalableGrid && inv.folderStyle != INVALID_RESOURCE_HANDLE) {
TypedArray folderStyle = context.obtainStyledAttributes(inv.folderStyle,
@@ -645,11 +649,11 @@
isTwoPanels ? inv.folderSpecsTwoPanelId : inv.folderSpecsId),
ResponsiveSpecType.Folder);
mResponsiveFolderWidthSpec = folderSpecs.getCalculatedSpec(responsiveAspectRatio,
- DimensionType.WIDTH, inv.numFolderColumns,
+ DimensionType.WIDTH, numFolderColumns,
mResponsiveWorkspaceWidthSpec.getAvailableSpace(),
mResponsiveWorkspaceWidthSpec);
mResponsiveFolderHeightSpec = folderSpecs.getCalculatedSpec(responsiveAspectRatio,
- DimensionType.HEIGHT, inv.numFolderRows,
+ DimensionType.HEIGHT, numFolderRows,
mResponsiveWorkspaceHeightSpec.getAvailableSpace(),
mResponsiveWorkspaceHeightSpec);
@@ -1406,16 +1410,16 @@
Point totalWorkspacePadding = getTotalWorkspacePadding();
// Check if the folder fit within the available height.
- float contentUsedHeight = folderCellHeightPx * inv.numFolderRows
- + ((inv.numFolderRows - 1) * folderCellLayoutBorderSpacePx.y)
+ float contentUsedHeight = folderCellHeightPx * numFolderRows
+ + ((numFolderRows - 1) * folderCellLayoutBorderSpacePx.y)
+ folderFooterHeightPx
+ folderContentPaddingTop;
int contentMaxHeight = availableHeightPx - totalWorkspacePadding.y;
float scaleY = contentMaxHeight / contentUsedHeight;
// Check if the folder fit within the available width.
- float contentUsedWidth = folderCellWidthPx * inv.numFolderColumns
- + ((inv.numFolderColumns - 1) * folderCellLayoutBorderSpacePx.x)
+ float contentUsedWidth = folderCellWidthPx * numFolderColumns
+ + ((numFolderColumns - 1) * folderCellLayoutBorderSpacePx.x)
+ folderContentPaddingLeftRight * 2;
int contentMaxWidth = availableWidthPx - totalWorkspacePadding.x;
float scaleX = contentMaxWidth / contentUsedWidth;
@@ -1451,7 +1455,7 @@
}
// Recalculating padding and cell height
- folderChildDrawablePaddingPx = getNormalizedFolderChildDrawablePaddingPx(textHeight);
+ folderChildDrawablePaddingPx = mResponsiveWorkspaceCellSpec.getIconDrawablePadding();
CellContentDimensions cellContentDimensions = new CellContentDimensions(
folderChildIconSizePx,
@@ -2045,8 +2049,8 @@
writer.println(prefix + pxToDpStr("iconTextSizePx", iconTextSizePx));
writer.println(prefix + pxToDpStr("iconDrawablePaddingPx", iconDrawablePaddingPx));
- writer.println(prefix + "\tinv.numFolderRows: " + inv.numFolderRows);
- writer.println(prefix + "\tinv.numFolderColumns: " + inv.numFolderColumns);
+ writer.println(prefix + "\tnumFolderRows: " + numFolderRows);
+ writer.println(prefix + "\tnumFolderColumns: " + numFolderColumns);
writer.println(prefix + pxToDpStr("folderCellWidthPx", folderCellWidthPx));
writer.println(prefix + pxToDpStr("folderCellHeightPx", folderCellHeightPx));
writer.println(prefix + pxToDpStr("folderChildIconSizePx", folderChildIconSizePx));
diff --git a/src/com/android/launcher3/InvariantDeviceProfile.java b/src/com/android/launcher3/InvariantDeviceProfile.java
index 567d0c5..5721ed3 100644
--- a/src/com/android/launcher3/InvariantDeviceProfile.java
+++ b/src/com/android/launcher3/InvariantDeviceProfile.java
@@ -122,8 +122,8 @@
/**
* Number of icons per row and column in the folder.
*/
- public int numFolderRows;
- public int numFolderColumns;
+ public int[] numFolderRows;
+ public int[] numFolderColumns;
public float[] iconSize;
public float[] iconTextSize;
public int iconBitmapSize;
@@ -298,11 +298,15 @@
* Reinitialize the current grid after a restore, where some grids might now be disabled.
*/
public void reinitializeAfterRestore(Context context) {
- FileLog.d(TAG, "Reinitializing grid after restore");
String currentGridName = getCurrentGridName(context);
String currentDbFile = dbFile;
String newGridName = initGrid(context, currentGridName);
String newDbFile = dbFile;
+ FileLog.d(TAG, "Reinitializing grid after restore."
+ + " currentGridName=" + currentGridName
+ + ", currentDbFile=" + currentDbFile
+ + ", newGridName=" + newGridName
+ + ", newDbFile=" + newDbFile);
if (!newDbFile.equals(currentDbFile)) {
FileLog.d(TAG, "Restored grid is disabled : " + currentGridName
+ ", migrating to: " + newGridName
@@ -810,8 +814,8 @@
public final int numSearchContainerColumns;
public final int deviceCategory;
- private final int numFolderRows;
- private final int numFolderColumns;
+ private final int[] numFolderRows = new int[COUNT_SIZES];
+ private final int[] numFolderColumns = new int[COUNT_SIZES];
private final @StyleRes int folderStyle;
private final @StyleRes int cellStyle;
@@ -888,11 +892,39 @@
a.getResourceId(R.styleable.GridDisplayOption_inlineNavButtonsEndSpacing,
R.dimen.taskbar_button_margin_default);
- numFolderRows = a.getInt(
+ numFolderRows[INDEX_DEFAULT] = a.getInt(
R.styleable.GridDisplayOption_numFolderRows, numRows);
- numFolderColumns = a.getInt(
+ numFolderColumns[INDEX_DEFAULT] = a.getInt(
R.styleable.GridDisplayOption_numFolderColumns, numColumns);
+ if (FeatureFlags.enableResponsiveWorkspace()) {
+ numFolderRows[INDEX_LANDSCAPE] = a.getInt(
+ R.styleable.GridDisplayOption_numFolderRowsLandscape,
+ numFolderRows[INDEX_DEFAULT]);
+ numFolderColumns[INDEX_LANDSCAPE] = a.getInt(
+ R.styleable.GridDisplayOption_numFolderColumnsLandscape,
+ numFolderColumns[INDEX_DEFAULT]);
+ numFolderRows[INDEX_TWO_PANEL_PORTRAIT] = a.getInt(
+ R.styleable.GridDisplayOption_numFolderRowsTwoPanelPortrait,
+ numFolderRows[INDEX_DEFAULT]);
+ numFolderColumns[INDEX_TWO_PANEL_PORTRAIT] = a.getInt(
+ R.styleable.GridDisplayOption_numFolderColumnsTwoPanelPortrait,
+ numFolderColumns[INDEX_DEFAULT]);
+ numFolderRows[INDEX_TWO_PANEL_LANDSCAPE] = a.getInt(
+ R.styleable.GridDisplayOption_numFolderRowsTwoPanelLandscape,
+ numFolderRows[INDEX_DEFAULT]);
+ numFolderColumns[INDEX_TWO_PANEL_LANDSCAPE] = a.getInt(
+ R.styleable.GridDisplayOption_numFolderColumnsTwoPanelLandscape,
+ numFolderColumns[INDEX_DEFAULT]);
+ } else {
+ numFolderRows[INDEX_LANDSCAPE] = numFolderRows[INDEX_DEFAULT];
+ numFolderColumns[INDEX_LANDSCAPE] = numFolderColumns[INDEX_DEFAULT];
+ numFolderRows[INDEX_TWO_PANEL_PORTRAIT] = numFolderRows[INDEX_DEFAULT];
+ numFolderColumns[INDEX_TWO_PANEL_PORTRAIT] = numFolderColumns[INDEX_DEFAULT];
+ numFolderRows[INDEX_TWO_PANEL_LANDSCAPE] = numFolderRows[INDEX_DEFAULT];
+ numFolderColumns[INDEX_TWO_PANEL_LANDSCAPE] = numFolderColumns[INDEX_DEFAULT];
+ }
+
folderStyle = a.getResourceId(R.styleable.GridDisplayOption_folderStyle,
INVALID_RESOURCE_HANDLE);
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 126efbb..5adfd43 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -80,7 +80,6 @@
import static com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_ACTIVITY_ON_CREATE;
import static com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION;
import static com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_VIEW_INFLATION;
-import static com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_ASYNC;
import static com.android.launcher3.logging.StatsLogManager.StatsLatencyLogger.LatencyType.COLD;
import static com.android.launcher3.logging.StatsLogManager.StatsLatencyLogger.LatencyType.COLD_DEVICE_REBOOTING;
import static com.android.launcher3.logging.StatsLogManager.StatsLatencyLogger.LatencyType.WARM;
@@ -139,7 +138,6 @@
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnPreDrawListener;
import android.view.WindowManager.LayoutParams;
import android.view.accessibility.AccessibilityEvent;
@@ -159,7 +157,6 @@
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
import com.android.launcher3.allapps.ActivityAllAppsContainerView;
import com.android.launcher3.allapps.AllAppsRecyclerView;
-import com.android.launcher3.allapps.AllAppsStore;
import com.android.launcher3.allapps.AllAppsTransitionController;
import com.android.launcher3.allapps.DiscoveryBounce;
import com.android.launcher3.anim.AnimationSuccessListener;
@@ -189,6 +186,7 @@
import com.android.launcher3.logging.InstanceIdSequence;
import com.android.launcher3.logging.StartupLatencyLogger;
import com.android.launcher3.logging.StatsLogManager;
+import com.android.launcher3.logging.StatsLogManager.LauncherLatencyEvent;
import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.model.ItemInstallQueue;
import com.android.launcher3.model.ModelWriter;
@@ -791,7 +789,7 @@
if (info.container >= 0) {
View folderIcon = getWorkspace().getHomescreenIconByItemId(info.container);
if (folderIcon instanceof FolderIcon && folderIcon.getTag() instanceof FolderInfo) {
- if (new FolderGridOrganizer(getDeviceProfile().inv)
+ if (new FolderGridOrganizer(getDeviceProfile())
.setFolderInfo((FolderInfo) folderIcon.getTag())
.isItemInPreview(info.rank)) {
folderIcon.invalidate();
@@ -847,6 +845,17 @@
return screenId;
}
+ /**
+ * Process any pending activity result if it was put on hold for any reason like item binding.
+ */
+ public void processActivityResult() {
+ if (mPendingActivityResult != null) {
+ handleActivityResult(mPendingActivityResult.requestCode,
+ mPendingActivityResult.resultCode, mPendingActivityResult.data);
+ mPendingActivityResult = null;
+ }
+ }
+
private void handleActivityResult(
final int requestCode, final int resultCode, final Intent data) {
if (isWorkspaceLoading()) {
@@ -1711,7 +1720,11 @@
TextKeyListener.getInstance().release();
mModelCallbacks.clearPendingBinds();
LauncherAppState.getIDP(this).removeOnChangeListener(this);
-
+ // if Launcher activity is recreated, {@link Window} including {@link ViewTreeObserver}
+ // could be preserved in {@link ActivityThread#scheduleRelaunchActivity(IBinder)} if the
+ // previous activity has not stopped, which could happen when wallpaper detects a color
+ // changes while launcher is still loading.
+ getRootView().getViewTreeObserver().removeOnPreDrawListener(mOnInitialBindListener);
mOverlayManager.onActivityDestroyed();
}
@@ -2460,39 +2473,20 @@
}
}
- @Override
+ /**
+ * Call back when ModelCallbacks finish binding the Launcher data.
+ */
@TargetApi(Build.VERSION_CODES.S)
- public void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks,
- int workspaceItemCount, boolean isBindSync) {
- mModelCallbacks.setSynchronouslyBoundPages(boundPages);
- mModelCallbacks.setPagesToBindSynchronously(new IntSet());
-
- mModelCallbacks.clearPendingBinds();
- ViewOnDrawExecutor executor = new ViewOnDrawExecutor(pendingTasks);
- mModelCallbacks.setPendingExecutor(executor);
- if (!isInState(ALL_APPS)) {
- mAppsView.getAppsStore().enableDeferUpdates(AllAppsStore.DEFER_UPDATES_NEXT_DRAW);
- pendingTasks.add(() -> mAppsView.getAppsStore().disableDeferUpdates(
- AllAppsStore.DEFER_UPDATES_NEXT_DRAW));
- }
-
+ public void bindComplete(int workspaceItemCount, boolean isBindSync) {
if (mOnInitialBindListener != null) {
getRootView().getViewTreeObserver().removeOnPreDrawListener(mOnInitialBindListener);
mOnInitialBindListener = null;
}
-
- executor.onLoadAnimationCompleted();
- executor.attachTo(this);
- if (Utilities.ATLEAST_S) {
- Trace.endAsyncSection(DISPLAY_WORKSPACE_TRACE_METHOD_NAME,
- DISPLAY_WORKSPACE_TRACE_COOKIE);
- }
if (!isBindSync) {
mStartupLatencyLogger
.logCardinality(workspaceItemCount)
- .logEnd(LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_ASYNC);
+ .logEnd(LauncherLatencyEvent.LAUNCHER_LATENCY_STARTUP_WORKSPACE_LOADER_ASYNC);
}
-
MAIN_EXECUTOR.getHandler().postAtFrontOfQueue(() -> {
mStartupLatencyLogger
.logEnd(LAUNCHER_LATENCY_STARTUP_TOTAL_DURATION)
@@ -2503,15 +2497,13 @@
COLD_STARTUP_TRACE_COOKIE);
}
});
- getRootView().getViewTreeObserver().addOnDrawListener(
- new ViewTreeObserver.OnDrawListener() {
- @Override
- public void onDraw() {
- MAIN_EXECUTOR.getHandler().postAtFrontOfQueue(
- () -> getRootView().getViewTreeObserver()
- .removeOnDrawListener(this));
- }
- });
+ }
+
+ @Override
+ public void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks,
+ int workspaceItemCount, boolean isBindSync) {
+ mModelCallbacks.onInitialBindComplete(boundPages, pendingTasks, workspaceItemCount,
+ isBindSync);
}
/**
@@ -2520,34 +2512,7 @@
* Implementation of the method from LauncherModel.Callbacks.
*/
public void finishBindingItems(IntSet pagesBoundFirst) {
- TraceHelper.INSTANCE.beginSection("finishBindingItems");
- mWorkspace.restoreInstanceStateForRemainingPages();
-
- mModelCallbacks.setWorkspaceLoading(false);
-
- if (mPendingActivityResult != null) {
- handleActivityResult(mPendingActivityResult.requestCode,
- mPendingActivityResult.resultCode, mPendingActivityResult.data);
- mPendingActivityResult = null;
- }
-
- int currentPage = pagesBoundFirst != null && !pagesBoundFirst.isEmpty()
- ? mWorkspace.getPageIndexForScreenId(pagesBoundFirst.getArray().get(0))
- : PagedView.INVALID_PAGE;
- // When undoing the removal of the last item on a page, return to that page.
- // Since we are just resetting the current page without user interaction,
- // override the previous page so we don't log the page switch.
- mWorkspace.setCurrentPage(currentPage, currentPage /* overridePrevPage */);
- mModelCallbacks.setPagesToBindSynchronously(new IntSet());
-
- // Cache one page worth of icons
- getViewCache().setCacheSize(R.layout.folder_application,
- mDeviceProfile.inv.numFolderColumns * mDeviceProfile.inv.numFolderRows);
- getViewCache().setCacheSize(R.layout.folder_page, 2);
-
- TraceHelper.INSTANCE.endSection();
- mWorkspace.removeExtraEmptyScreen(/* stripEmptyScreens= */ true);
- mWorkspace.mPageIndicator.setAreScreensBinding(false, mDeviceProfile.isTwoPanels);
+ mModelCallbacks.finishBindingItems(pagesBoundFirst);
}
private boolean canAnimatePageChange() {
@@ -3224,7 +3189,7 @@
* Handles an app pair launch; overridden in
* {@link com.android.launcher3.uioverrides.QuickstepLauncher}
*/
- public void launchAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) {
+ public void launchAppPair(AppPairIcon appPairIcon) {
// Overridden
}
diff --git a/src/com/android/launcher3/LauncherPrefs.kt b/src/com/android/launcher3/LauncherPrefs.kt
index a05b0f5..78056e6 100644
--- a/src/com/android/launcher3/LauncherPrefs.kt
+++ b/src/com/android/launcher3/LauncherPrefs.kt
@@ -364,6 +364,13 @@
EncryptionType.MOVE_TO_DEVICE_PROTECTED
)
@JvmField
+ val PRIVATE_SPACE_APPS =
+ nonRestorableItem(
+ "pref_private_space_apps",
+ 0,
+ EncryptionType.MOVE_TO_DEVICE_PROTECTED
+ )
+ @JvmField
val THEMED_ICONS =
backedUpItem(Themes.KEY_THEMED_ICONS, false, EncryptionType.MOVE_TO_DEVICE_PROTECTED)
@JvmField val PROMISE_ICON_IDS = backedUpItem(InstallSessionHelper.PROMISE_ICON_IDS, "")
diff --git a/src/com/android/launcher3/ModelCallbacks.kt b/src/com/android/launcher3/ModelCallbacks.kt
index 51d7690..5172999 100644
--- a/src/com/android/launcher3/ModelCallbacks.kt
+++ b/src/com/android/launcher3/ModelCallbacks.kt
@@ -1,6 +1,11 @@
package com.android.launcher3
+import android.annotation.TargetApi
+import android.os.Build
+import android.os.Trace
+import android.view.ViewTreeObserver.OnDrawListener
import androidx.annotation.UiThread
+import com.android.launcher3.LauncherConstants.TraceEvents
import com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID
import com.android.launcher3.allapps.AllAppsStore
import com.android.launcher3.config.FeatureFlags
@@ -13,12 +18,12 @@
import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.popup.PopupContainerWithArrow
import com.android.launcher3.util.ComponentKey
+import com.android.launcher3.util.Executors
import com.android.launcher3.util.IntArray as LIntArray
-import com.android.launcher3.util.IntArray
import com.android.launcher3.util.IntSet as LIntSet
-import com.android.launcher3.util.IntSet
import com.android.launcher3.util.PackageUserKey
import com.android.launcher3.util.Preconditions
+import com.android.launcher3.util.RunnableList
import com.android.launcher3.util.TraceHelper
import com.android.launcher3.util.ViewOnDrawExecutor
import com.android.launcher3.widget.PendingAddWidgetInfo
@@ -65,6 +70,78 @@
TraceHelper.INSTANCE.endSection()
}
+ @TargetApi(Build.VERSION_CODES.S)
+ override fun onInitialBindComplete(
+ boundPages: LIntSet,
+ pendingTasks: RunnableList,
+ workspaceItemCount: Int,
+ isBindSync: Boolean
+ ) {
+ synchronouslyBoundPages = boundPages
+ pagesToBindSynchronously = LIntSet()
+ clearPendingBinds()
+ val executor = ViewOnDrawExecutor(pendingTasks)
+ pendingExecutor = executor
+ if (!launcher.isInState(LauncherState.ALL_APPS)) {
+ launcher.appsView.appsStore.enableDeferUpdates(AllAppsStore.DEFER_UPDATES_NEXT_DRAW)
+ pendingTasks.add {
+ launcher.appsView.appsStore.disableDeferUpdates(
+ AllAppsStore.DEFER_UPDATES_NEXT_DRAW
+ )
+ }
+ }
+ executor.onLoadAnimationCompleted()
+ executor.attachTo(launcher)
+ if (Utilities.ATLEAST_S) {
+ Trace.endAsyncSection(
+ TraceEvents.DISPLAY_WORKSPACE_TRACE_METHOD_NAME,
+ TraceEvents.DISPLAY_WORKSPACE_TRACE_COOKIE
+ )
+ }
+ launcher.bindComplete(workspaceItemCount, isBindSync)
+ launcher.rootView.viewTreeObserver.addOnDrawListener(
+ object : OnDrawListener {
+ override fun onDraw() {
+ Executors.MAIN_EXECUTOR.handler.postAtFrontOfQueue {
+ launcher.rootView.getViewTreeObserver().removeOnDrawListener(this)
+ }
+ }
+ }
+ )
+ }
+
+ /**
+ * Callback saying that there aren't any more items to bind.
+ *
+ * Implementation of the method from LauncherModel.Callbacks.
+ */
+ override fun finishBindingItems(pagesBoundFirst: LIntSet?) {
+ TraceHelper.INSTANCE.beginSection("finishBindingItems")
+ val deviceProfile = launcher.deviceProfile
+ launcher.workspace.restoreInstanceStateForRemainingPages()
+ workspaceLoading = false
+ launcher.processActivityResult()
+ val currentPage =
+ if (pagesBoundFirst != null && !pagesBoundFirst.isEmpty)
+ launcher.workspace.getPageIndexForScreenId(pagesBoundFirst.array[0])
+ else PagedView.INVALID_PAGE
+ // When undoing the removal of the last item on a page, return to that page.
+ // Since we are just resetting the current page without user interaction,
+ // override the previous page so we don't log the page switch.
+ launcher.workspace.setCurrentPage(currentPage, currentPage /* overridePrevPage */)
+ pagesToBindSynchronously = LIntSet()
+
+ // Cache one page worth of icons
+ launcher.viewCache.setCacheSize(
+ R.layout.folder_application,
+ deviceProfile.numFolderColumns * deviceProfile.numFolderRows
+ )
+ launcher.viewCache.setCacheSize(R.layout.folder_page, 2)
+ TraceHelper.INSTANCE.endSection()
+ launcher.workspace.removeExtraEmptyScreen(/* stripEmptyScreens= */ true)
+ launcher.workspace.pageIndicator.setAreScreensBinding(false, deviceProfile.isTwoPanels)
+ }
+
/**
* Clear any pending bind callbacks. This is called when is loader is planning to perform a full
* rebind from scratch.
@@ -225,7 +302,7 @@
)
}
- override fun bindScreens(orderedScreenIds: IntArray) {
+ override fun bindScreens(orderedScreenIds: LIntArray) {
launcher.workspace.pageIndicator.setAreScreensBinding(
true,
launcher.deviceProfile.isTwoPanels
@@ -255,7 +332,7 @@
}
override fun bindAppsAdded(
- newScreens: IntArray?,
+ newScreens: LIntArray?,
addNotAnimated: java.util.ArrayList<ItemInfo?>?,
addAnimated: java.util.ArrayList<ItemInfo?>?
) {
@@ -280,7 +357,7 @@
launcher.workspace.removeExtraEmptyScreen(false)
}
- private fun bindAddScreens(orderedScreenIdsArg: IntArray) {
+ private fun bindAddScreens(orderedScreenIdsArg: LIntArray) {
var orderedScreenIds = orderedScreenIdsArg
if (launcher.deviceProfile.isTwoPanels) {
if (FeatureFlags.FOLDABLE_SINGLE_PAGE.get()) {
@@ -288,7 +365,7 @@
} else {
// Some empty pages might have been removed while the phone was in a single panel
// mode, so we want to add those empty pages back.
- val screenIds = IntSet.wrap(orderedScreenIds)
+ val screenIds = LIntSet.wrap(orderedScreenIds)
orderedScreenIds.forEach { screenId: Int ->
screenIds.add(launcher.workspace.getScreenPair(screenId))
}
@@ -311,8 +388,8 @@
* Remove odd number because they are already included when isTwoPanels and add the pair screen
* if not present.
*/
- private fun filterTwoPanelScreenIds(orderedScreenIds: IntArray): IntArray {
- val screenIds = IntSet.wrap(orderedScreenIds)
+ private fun filterTwoPanelScreenIds(orderedScreenIds: LIntArray): LIntArray {
+ val screenIds = LIntSet.wrap(orderedScreenIds)
orderedScreenIds
.filter { screenId -> screenId % 2 == 1 }
.forEach { screenId ->
diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
index e5a223a..7f1d216 100644
--- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java
@@ -407,7 +407,7 @@
// If exiting search, revert predictive back scale on all apps
mAllAppsTransitionController.animateAllAppsToNoScale();
}
- mSearchTransitionController.animateToSearchState(goingToSearch, durationMs,
+ mSearchTransitionController.animateToState(goingToSearch, durationMs,
/* onEndRunnable = */ () -> {
mIsSearching = goingToSearch;
updateSearchResultsVisibility();
diff --git a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
index b0f13ef..36a44cc 100644
--- a/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
+++ b/src/com/android/launcher3/allapps/AllAppsRecyclerView.java
@@ -36,7 +36,11 @@
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.Log;
+import android.view.View;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.util.Consumer;
import androidx.recyclerview.widget.RecyclerView;
import com.android.launcher3.DeviceProfile;
@@ -57,6 +61,7 @@
protected static final String TAG = "AllAppsRecyclerView";
private static final boolean DEBUG = false;
private static final boolean DEBUG_LATENCY = Utilities.isPropertyEnabled(SEARCH_LOGGING);
+ private Consumer<View> mChildAttachedConsumer;
protected final int mNumAppsPerRow;
private final AllAppsFastScrollHelper mFastScrollHelper;
@@ -282,6 +287,22 @@
}
}
+ /**
+ * This will be called just before a new child is attached to the window. Passing in null will
+ * remove the consumer.
+ */
+ protected void setChildAttachedConsumer(@Nullable Consumer<View> childAttachedConsumer) {
+ mChildAttachedConsumer = childAttachedConsumer;
+ }
+
+ @Override
+ public void onChildAttachedToWindow(@NonNull View child) {
+ if (mChildAttachedConsumer != null) {
+ mChildAttachedConsumer.accept(child);
+ }
+ super.onChildAttachedToWindow(child);
+ }
+
@Override
public int getScrollBarTop() {
return ActivityContext.lookupContext(getContext()).getAppsView().isSearchSupported()
diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
index 328516e..1782791 100644
--- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
+++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java
@@ -15,6 +15,10 @@
*/
package com.android.launcher3.allapps;
+import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_BOTTOM_LEFT;
+import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_BOTTOM_RIGHT;
+import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_NOTHING;
+
import android.content.Context;
import androidx.annotation.Nullable;
@@ -318,6 +322,10 @@
case PrivateProfileManager.STATE_ENABLED:
// Add PS Apps only in Enabled State.
addAppsWithSections(mPrivateApps, position);
+ if (mActivityContext.getAppsView() != null) {
+ mActivityContext.getAppsView().getActiveRecyclerView()
+ .scrollToBottomWithMotion();
+ }
break;
}
}
@@ -325,8 +333,34 @@
private void addAppsWithSections(List<AppInfo> appList, int startPosition) {
String lastSectionName = null;
+ boolean hasPrivateApps = false;
+ if (mPrivateProviderManager != null) {
+ hasPrivateApps = appList.stream().
+ allMatch(mPrivateProviderManager.getItemInfoMatcher());
+ }
+ int privateAppCount = 0;
+ int numberOfColumns = mActivityContext.getDeviceProfile().numShownAllAppsColumns;
+ int numberOfAppRows = (int) Math.ceil((double) appList.size() / numberOfColumns);
for (AppInfo info : appList) {
- mAdapterItems.add(AdapterItem.asApp(info));
+ // Apply decorator to private apps.
+ if (hasPrivateApps) {
+ int roundRegion = ROUND_NOTHING;
+ if ((privateAppCount / numberOfColumns) == numberOfAppRows - 1) {
+ if ((privateAppCount % numberOfColumns) == 0) {
+ // App is the first column
+ roundRegion = ROUND_BOTTOM_LEFT;
+ } else if ((privateAppCount % numberOfColumns) == numberOfColumns-1) {
+ roundRegion = ROUND_BOTTOM_RIGHT;
+ }
+ }
+ mAdapterItems.add(AdapterItem.asAppWithDecorationInfo(info,
+ new SectionDecorationInfo(mActivityContext.getApplicationContext(),
+ roundRegion,
+ true /* decorateTogether */)));
+ privateAppCount += 1;
+ } else {
+ mAdapterItems.add(AdapterItem.asApp(info));
+ }
String sectionName = info.sectionName;
// Create a new section if the section names do not match
diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
index 5e26ea5..5eeb259 100644
--- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
+++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java
@@ -15,6 +15,12 @@
*/
package com.android.launcher3.allapps;
+import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_BOTTOM_LEFT;
+import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_BOTTOM_RIGHT;
+import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_TOP_LEFT;
+import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_TOP_RIGHT;
+import static com.android.launcher3.allapps.UserProfileManager.STATE_DISABLED;
+
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
@@ -25,6 +31,7 @@
import android.widget.RelativeLayout;
import android.widget.TextView;
+import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.android.launcher3.BubbleTextView;
@@ -92,7 +99,8 @@
public int rowAppIndex;
// The associated ItemInfoWithIcon for the item
public AppInfo itemInfo = null;
-
+ // Private App Decorator
+ public SectionDecorationInfo decorationInfo = null;
public AdapterItem(int viewType) {
this.viewType = viewType;
}
@@ -106,6 +114,13 @@
return item;
}
+ public static AdapterItem asAppWithDecorationInfo(AppInfo appInfo,
+ SectionDecorationInfo decorationInfo) {
+ AdapterItem item = asApp(appInfo);
+ item.decorationInfo = decorationInfo;
+ return item;
+ }
+
protected boolean isCountedForAccessibility() {
return viewType == VIEW_TYPE_ICON;
}
@@ -125,9 +140,17 @@
return itemInfo == null && other.itemInfo == null;
}
- /** Sets the alpha of the decorator for this item. Returns true if successful. */
- public boolean setDecorationFillAlpha(int alpha) {
- return false;
+ @Nullable
+ public SectionDecorationInfo getDecorationInfo() {
+ return decorationInfo;
+ }
+
+ /** Sets the alpha of the decorator for this item. */
+ protected void setDecorationFillAlpha(int alpha) {
+ if (decorationInfo == null || decorationInfo.getDecorationHandler() == null) {
+ return;
+ }
+ decorationInfo.getDecorationHandler().setFillAlpha(alpha);
}
}
@@ -249,6 +272,15 @@
assert mPrivateSpaceHeaderViewController != null;
assert psHeaderLayout != null;
mPrivateSpaceHeaderViewController.addPrivateSpaceHeaderViewElements(psHeaderLayout);
+ AdapterItem adapterItem = mApps.getAdapterItems().get(position);
+ int roundRegions = ROUND_TOP_LEFT | ROUND_TOP_RIGHT;
+ if (mPrivateSpaceHeaderViewController.getPrivateProfileManager().getCurrentState()
+ == STATE_DISABLED) {
+ roundRegions |= (ROUND_BOTTOM_LEFT | ROUND_BOTTOM_RIGHT);
+ }
+ adapterItem.decorationInfo =
+ new SectionDecorationInfo(mActivityContext, roundRegions,
+ false /* decorateTogether */);
break;
case VIEW_TYPE_ALL_APPS_DIVIDER:
case VIEW_TYPE_WORK_DISABLED_CARD:
diff --git a/src/com/android/launcher3/allapps/PrivateAppsSectionDecorator.java b/src/com/android/launcher3/allapps/PrivateAppsSectionDecorator.java
index f4ed754..8712b84 100644
--- a/src/com/android/launcher3/allapps/PrivateAppsSectionDecorator.java
+++ b/src/com/android/launcher3/allapps/PrivateAppsSectionDecorator.java
@@ -16,97 +16,55 @@
package com.android.launcher3.allapps;
-import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_ICON;
-import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_HEADER;
-
-import android.content.Context;
import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.Path;
-import android.graphics.RectF;
import android.view.View;
-import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
-import com.android.launcher3.R;
-import com.android.launcher3.pm.UserCache;
-import com.android.launcher3.views.ActivityContext;
+import java.util.HashMap;
/**
* Decorator which changes the background color for Private Space Icon Rows in AllAppsContainer.
*/
public class PrivateAppsSectionDecorator extends RecyclerView.ItemDecoration {
- private final Path mTmpPath = new Path();
- private final RectF mTmpRect = new RectF();
- private final Context mContext;
+ private static final String PRIVATE_APP_SECTION = "private_apps";
private final AlphabeticalAppsList<?> mAppsList;
- private final UserCache mUserCache;
- private final Paint mPaint;
- private final int mCornerRadius;
- public PrivateAppsSectionDecorator(Context context, AlphabeticalAppsList<?> appsList) {
- mContext = context;
+ public PrivateAppsSectionDecorator(AlphabeticalAppsList<?> appsList) {
mAppsList = appsList;
- mUserCache = UserCache.getInstance(context);
- mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- mPaint.setColor(ContextCompat.getColor(context,
- R.color.material_color_surface_container_high));
- mCornerRadius = context.getResources().getDimensionPixelSize(
- R.dimen.ps_container_corner_radius);
}
/** Decorates Private Space Header and Icon Rows to give the shape of a container. */
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
- mTmpPath.reset();
- mTmpRect.setEmpty();
- int numCol = ActivityContext.lookupContext(mContext).getDeviceProfile()
- .numShownAllAppsColumns;
+ HashMap<String, SectionDecorationHandler.UnionDecorationHandler> deferredDecorations =
+ new HashMap<>();
for (int i = 0; i < parent.getChildCount(); i++) {
View view = parent.getChildAt(i);
int position = parent.getChildAdapterPosition(view);
BaseAllAppsAdapter.AdapterItem adapterItem = mAppsList.getAdapterItems().get(position);
- // Rectangle that covers the bottom half of the PS Header View when Space is unlocked.
- if (adapterItem.viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER) {
- // We flatten the bottom corners of the rectangle, so that it merges with
- // the private space app row decorator.
- mTmpRect.set(
- view.getLeft(),
- view.getTop() + (float) (view.getBottom() - view.getTop()) / 2,
- view.getRight(),
- view.getBottom());
- mTmpPath.addRect(mTmpRect, Path.Direction.CW);
- c.drawPath(mTmpPath, mPaint);
- } else if (adapterItem.viewType == VIEW_TYPE_ICON
- && mUserCache.getUserInfo(adapterItem.itemInfo.user).isPrivate()
- // No decoration for any private space app icon other than those at first row.
- && adapterItem.rowAppIndex == 0) {
- c.drawPath(getPrivateAppRowPath(parent, view, position, numCol), mPaint);
+ SectionDecorationInfo info = adapterItem.decorationInfo;
+ if (info == null) {
+ continue;
+ }
+ SectionDecorationHandler decorationHandler = info.getDecorationHandler();
+ if (info.shouldDecorateItemsTogether()) {
+ SectionDecorationHandler.UnionDecorationHandler unionHandler =
+ deferredDecorations.getOrDefault(
+ PRIVATE_APP_SECTION,
+ new SectionDecorationHandler.UnionDecorationHandler(
+ decorationHandler, parent.getPaddingLeft(),
+ parent.getPaddingRight()));
+ unionHandler.addChild(decorationHandler, view, true /* applyBackground */);
+ deferredDecorations.put(PRIVATE_APP_SECTION, unionHandler);
+ } else {
+ decorationHandler.onFocusDraw(c, view);
}
}
- }
-
- /** Returns the path to be decorated for Private Space App Row */
- private Path getPrivateAppRowPath(RecyclerView parent, View iconView, int adapterPosition,
- int numCol) {
- // We always decorate the entire app row here.
- // As the iconView just represents the first icon of the row, we get the right margin of
- // our decorator using the parent view.
- mTmpRect.set(iconView.getLeft(),
- iconView.getTop(),
- parent.getRight() - parent.getPaddingRight(),
- iconView.getBottom());
- // Decorates last app row with rounded bottom corners.
- if (adapterPosition + numCol >= mAppsList.getAdapterItems().size()) {
- float[] mCornersBot = new float[]{0, 0, 0, 0, mCornerRadius, mCornerRadius,
- mCornerRadius, mCornerRadius};
- mTmpPath.addRoundRect(mTmpRect, mCornersBot, Path.Direction.CW);
- } else {
- // Decorate other rows as a plain rectangle
- mTmpPath.addRect(mTmpRect, Path.Direction.CW);
+ for (SectionDecorationHandler.UnionDecorationHandler decorationHandler
+ : deferredDecorations.values()) {
+ decorationHandler.onGroupDecorate(c);
}
- return mTmpPath;
}
}
diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java
index 334d5c1..693681b 100644
--- a/src/com/android/launcher3/allapps/PrivateProfileManager.java
+++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java
@@ -30,6 +30,7 @@
import androidx.annotation.VisibleForTesting;
+import com.android.launcher3.Flags;
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.util.Preconditions;
@@ -47,6 +48,7 @@
private static final String SAFETY_CENTER_INTENT = Intent.ACTION_SAFETY_CENTER;
private static final String PS_SETTINGS_FRAGMENT_KEY = ":settings:fragment_args_key";
private static final String PS_SETTINGS_FRAGMENT_VALUE = "AndroidPrivateSpace_personal";
+ private static final int ANIMATION_DURATION = 2000;
private final ActivityAllAppsContainerView<?> mAllApps;
private final Predicate<UserHandle> mPrivateProfileMatcher;
private PrivateAppsSectionDecorator mPrivateAppsSectionDecorator;
@@ -125,7 +127,6 @@
// Create a new decorator instance if not already available.
if (mPrivateAppsSectionDecorator == null) {
mPrivateAppsSectionDecorator = new PrivateAppsSectionDecorator(
- mAllApps.mActivityContext,
mainAdapterHolder.mAppsList);
}
for (int i = 0; i < mainAdapterHolder.mRecyclerView.getItemDecorationCount(); i++) {
@@ -137,6 +138,13 @@
}
// Add Private Space Decorator to the Recycler view.
mainAdapterHolder.mRecyclerView.addItemDecoration(mPrivateAppsSectionDecorator);
+ if (Flags.privateSpaceAnimation() && mAllApps.getActiveRecyclerView()
+ == mainAdapterHolder.mRecyclerView) {
+ RecyclerViewAnimationController recyclerViewAnimationController =
+ new RecyclerViewAnimationController(mAllApps);
+ recyclerViewAnimationController.animateToState(true /* expand */,
+ ANIMATION_DURATION, () -> {});
+ }
} else {
// Remove Private Space Decorator from the Recycler view.
if (mPrivateAppsSectionDecorator != null) {
diff --git a/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java b/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java
index e0ca947..568ce32 100644
--- a/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java
+++ b/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewController.java
@@ -105,4 +105,8 @@
transitionImage.setVisibility(View.GONE);
}
}
+
+ PrivateProfileManager getPrivateProfileManager() {
+ return mPrivateProfileManager;
+ }
}
diff --git a/src/com/android/launcher3/allapps/RecyclerViewAnimationController.java b/src/com/android/launcher3/allapps/RecyclerViewAnimationController.java
new file mode 100644
index 0000000..6209393
--- /dev/null
+++ b/src/com/android/launcher3/allapps/RecyclerViewAnimationController.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps;
+
+import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
+
+import static com.android.app.animation.Interpolators.DECELERATE_1_7;
+import static com.android.app.animation.Interpolators.INSTANT;
+import static com.android.app.animation.Interpolators.clampToProgress;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
+import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
+import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
+
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.graphics.drawable.Drawable;
+import android.util.FloatProperty;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.model.data.ItemInfo;
+
+import java.util.List;
+
+public class RecyclerViewAnimationController {
+
+ private static final String LOG_TAG = "AnimationCtrl";
+
+ /**
+ * These values represent points on the [0, 1] animation progress spectrum. They are used to
+ * animate items in the {@link SearchRecyclerView} and private space container in
+ * {@link AllAppsRecyclerView}.
+ */
+ protected static final float TOP_CONTENT_FADE_PROGRESS_START = 0.133f;
+ protected static final float CONTENT_FADE_PROGRESS_DURATION = 0.083f;
+ protected static final float TOP_BACKGROUND_FADE_PROGRESS_START = 0.633f;
+ protected static final float BACKGROUND_FADE_PROGRESS_DURATION = 0.15f;
+ // Progress before next item starts fading.
+ protected static final float CONTENT_STAGGER = 0.01f;
+
+ protected static final FloatProperty<RecyclerViewAnimationController> PROGRESS =
+ new FloatProperty<RecyclerViewAnimationController>("expansionProgress") {
+ @Override
+ public Float get(RecyclerViewAnimationController controller) {
+ return controller.getAnimationProgress();
+ }
+
+ @Override
+ public void setValue(RecyclerViewAnimationController controller, float progress) {
+ controller.setAnimationProgress(progress);
+ }
+ };
+
+ protected final ActivityAllAppsContainerView<?> mAllAppsContainerView;
+ protected ObjectAnimator mAnimator = null;
+ private float mAnimatorProgress = 1f;
+
+ public RecyclerViewAnimationController(ActivityAllAppsContainerView<?> allAppsContainerView) {
+ mAllAppsContainerView = allAppsContainerView;
+ }
+
+ /**
+ * Updates the children views of the current recyclerView based on the current animation
+ * progress.
+ *
+ * @return the total height of animating views (may exclude at most one row of app icons
+ * depending on which recyclerView is being acted upon).
+ */
+ protected int onProgressUpdated(float expansionProgress) {
+ int numItemsAnimated = 0;
+ int totalHeight = 0;
+ int appRowHeight = 0;
+ boolean appRowComplete = false;
+ Integer top = null;
+ AllAppsRecyclerView allAppsRecyclerView = getRecyclerView();
+
+ for (int i = 0; i < allAppsRecyclerView.getChildCount(); i++) {
+ View currentView = allAppsRecyclerView.getChildAt(i);
+ if (currentView == null) {
+ continue;
+ }
+ if (top == null) {
+ top = currentView.getTop();
+ }
+ int adapterPosition = allAppsRecyclerView.getChildAdapterPosition(currentView);
+ List<BaseAllAppsAdapter.AdapterItem> allAppsAdapters = allAppsRecyclerView.getApps()
+ .getAdapterItems();
+ if (adapterPosition < 0 || adapterPosition >= allAppsAdapters.size()) {
+ continue;
+ }
+ BaseAllAppsAdapter.AdapterItem adapterItemAtPosition =
+ allAppsAdapters.get(adapterPosition);
+ int spanIndex = getSpanIndex(allAppsRecyclerView, adapterPosition);
+ appRowComplete |= appRowHeight > 0 && spanIndex == 0;
+
+ float backgroundAlpha = 1f;
+ boolean hasDecorationInfo = adapterItemAtPosition.getDecorationInfo() != null;
+ boolean shouldAnimate = shouldAnimate(currentView, hasDecorationInfo, appRowComplete);
+
+ if (shouldAnimate) {
+ if (spanIndex > 0) {
+ // Animate this item with the previous item on the same row.
+ numItemsAnimated--;
+ }
+ // Adjust background (or decorator) alpha based on start progress and stagger.
+ backgroundAlpha = getAdjustedBackgroundAlpha(numItemsAnimated);
+ }
+
+ Drawable background = currentView.getBackground();
+ if (background != null && currentView instanceof ViewGroup currentViewGroup) {
+ currentView.setAlpha(1f);
+ // Apply content alpha to each child, since the view needs to be fully opaque for
+ // the background to show properly.
+ for (int j = 0; j < currentViewGroup.getChildCount(); j++) {
+ setViewAdjustedContentAlpha(currentViewGroup.getChildAt(j), numItemsAnimated,
+ shouldAnimate);
+ }
+
+ // Apply background alpha to the background drawable directly.
+ background.setAlpha((int) (255 * backgroundAlpha));
+ } else {
+ // Adjust content alpha based on start progress and stagger.
+ setViewAdjustedContentAlpha(currentView, numItemsAnimated, shouldAnimate);
+
+ // Apply background alpha to decorator if possible.
+ setAdjustedAdapterItemDecorationBackgroundAlpha(
+ allAppsRecyclerView.getApps().getAdapterItems().get(adapterPosition),
+ numItemsAnimated);
+
+ // Apply background alpha to view's background (e.g. for Search Edu card).
+ if (background != null) {
+ background.setAlpha((int) (255 * backgroundAlpha));
+ }
+ }
+
+ float scaleY = 1;
+ if (shouldAnimate) {
+ scaleY = 1 - getAnimationProgress();
+ // Update number of search results that has been animated.
+ numItemsAnimated++;
+ }
+ int scaledHeight = (int) (currentView.getHeight() * scaleY);
+ currentView.setScaleY(scaleY);
+
+ // For rows with multiple elements, only count the height once and translate elements to
+ // the same y position.
+ int y = top + totalHeight;
+ if (spanIndex > 0) {
+ // Continuation of an existing row; move this item into the row.
+ y -= scaledHeight;
+ } else {
+ // Start of a new row contributes to total height.
+ totalHeight += scaledHeight;
+ if (!shouldAnimate) {
+ appRowHeight = scaledHeight;
+ }
+ }
+ currentView.setY(y);
+ }
+ return totalHeight - appRowHeight;
+ }
+
+ protected void animateToState(boolean expand, long duration, Runnable onEndRunnable) {
+ float targetProgress = expand ? 0 : 1;
+ if (mAnimator != null) {
+ mAnimator.cancel();
+ }
+ mAnimator = ObjectAnimator.ofFloat(this, PROGRESS, targetProgress);
+
+ TimeInterpolator timeInterpolator = getInterpolator();
+ if (timeInterpolator == INSTANT) {
+ duration = 0;
+ }
+
+ mAnimator.addListener(forEndCallback(() -> mAnimator = null));
+ mAnimator.setDuration(duration).setInterpolator(timeInterpolator);
+ mAnimator.addListener(forSuccessCallback(onEndRunnable));
+ mAnimator.start();
+ getRecyclerView().setChildAttachedConsumer(this::onChildAttached);
+ }
+
+ /** Called just before a child is attached to the RecyclerView. */
+ private void onChildAttached(View child) {
+ // Avoid allocating hardware layers for alpha changes.
+ child.forceHasOverlappingRendering(false);
+ child.setPivotY(0);
+ if (getAnimationProgress() > 0 && getAnimationProgress() < 1) {
+ // Before the child is rendered, apply the animation including it to avoid flicker.
+ onProgressUpdated(getAnimationProgress());
+ } else {
+ // Apply default states without processing the full layout.
+ child.setAlpha(1);
+ child.setScaleY(1);
+ child.setTranslationY(0);
+ int adapterPosition = getRecyclerView().getChildAdapterPosition(child);
+ List<BaseAllAppsAdapter.AdapterItem> allAppsAdapters =
+ getRecyclerView().getApps().getAdapterItems();
+ if (adapterPosition >= 0 && adapterPosition < allAppsAdapters.size()) {
+ allAppsAdapters.get(adapterPosition).setDecorationFillAlpha(255);
+ }
+ if (child instanceof ViewGroup childGroup) {
+ for (int i = 0; i < childGroup.getChildCount(); i++) {
+ childGroup.getChildAt(i).setAlpha(1f);
+ }
+ }
+ if (child.getBackground() != null) {
+ child.getBackground().setAlpha(255);
+ }
+ }
+ }
+
+ /** @return the column that the view at this position is found (0 assumed if indeterminate). */
+ protected int getSpanIndex(AllAppsRecyclerView appsRecyclerView, int adapterPosition) {
+ if (adapterPosition == NO_POSITION) {
+ Log.w(LOG_TAG, "Can't determine span index - child not found in adapter");
+ return 0;
+ }
+ if (!(appsRecyclerView.getAdapter() instanceof AllAppsGridAdapter<?>)) {
+ Log.e(LOG_TAG, "Search RV doesn't have an AllAppsGridAdapter?");
+ // This case shouldn't happen, but for debug devices we will continue to create a more
+ // visible crash.
+ if (!Utilities.IS_DEBUG_DEVICE) {
+ return 0;
+ }
+ }
+ AllAppsGridAdapter<?> adapter = (AllAppsGridAdapter<?>) appsRecyclerView.getAdapter();
+ return adapter.getSpanIndex(adapterPosition);
+ }
+
+ protected TimeInterpolator getInterpolator() {
+ return DECELERATE_1_7;
+ }
+
+ protected AllAppsRecyclerView getRecyclerView() {
+ return mAllAppsContainerView.mAH.get(ActivityAllAppsContainerView.AdapterHolder.MAIN)
+ .mRecyclerView;
+ }
+
+ /** Returns true if a transition animation is currently in progress. */
+ protected boolean isRunning() {
+ return mAnimator != null;
+ }
+
+ /** Should only animate if the view is an app icon and if it has a decoration info. */
+ protected boolean shouldAnimate(View view, boolean hasDecorationInfo,
+ boolean firstAppRowComplete) {
+ return isAppIcon(view) && hasDecorationInfo;
+ }
+
+ private float getAdjustedContentAlpha(int itemsAnimated) {
+ float startContentFadeProgress = Math.max(0,
+ TOP_CONTENT_FADE_PROGRESS_START - CONTENT_STAGGER * itemsAnimated);
+ float endContentFadeProgress = Math.min(1,
+ startContentFadeProgress + CONTENT_FADE_PROGRESS_DURATION);
+ return 1 - clampToProgress(mAnimatorProgress,
+ startContentFadeProgress, endContentFadeProgress);
+ }
+
+ private float getAdjustedBackgroundAlpha(int itemsAnimated) {
+ float startBackgroundFadeProgress = Math.max(0,
+ TOP_BACKGROUND_FADE_PROGRESS_START - CONTENT_STAGGER * itemsAnimated);
+ float endBackgroundFadeProgress = Math.min(1,
+ startBackgroundFadeProgress + BACKGROUND_FADE_PROGRESS_DURATION);
+ return 1 - clampToProgress(mAnimatorProgress,
+ startBackgroundFadeProgress, endBackgroundFadeProgress);
+ }
+
+ private void setViewAdjustedContentAlpha(View view, int numberOfItemsAnimated,
+ boolean shouldAnimate) {
+ view.setAlpha(shouldAnimate ? getAdjustedContentAlpha(numberOfItemsAnimated) : 1f);
+ }
+
+ private void setAdjustedAdapterItemDecorationBackgroundAlpha(
+ BaseAllAppsAdapter.AdapterItem adapterItem, int numberOfItemsAnimated) {
+ adapterItem.setDecorationFillAlpha((int)
+ (255 * getAdjustedBackgroundAlpha(numberOfItemsAnimated)));
+ }
+
+ private float getAnimationProgress() {
+ return mAnimatorProgress;
+ }
+
+ private void setAnimationProgress(float expansionProgress) {
+ mAnimatorProgress = expansionProgress;
+ onProgressUpdated(expansionProgress);
+ }
+
+ protected boolean isAppIcon(View item) {
+ return item instanceof BubbleTextView && item.getTag() instanceof ItemInfo
+ && ((ItemInfo) item.getTag()).itemType == ITEM_TYPE_APPLICATION;
+ }
+}
diff --git a/src/com/android/launcher3/allapps/SearchRecyclerView.java b/src/com/android/launcher3/allapps/SearchRecyclerView.java
index 9d1dfc0..68f9f11 100644
--- a/src/com/android/launcher3/allapps/SearchRecyclerView.java
+++ b/src/com/android/launcher3/allapps/SearchRecyclerView.java
@@ -27,8 +27,6 @@
/** A RecyclerView for AllApps Search results. */
public class SearchRecyclerView extends AllAppsRecyclerView {
- private Consumer<View> mChildAttachedConsumer;
-
public SearchRecyclerView(Context context) {
this(context, null);
}
@@ -46,11 +44,6 @@
super(context, attrs, defStyleAttr, defStyleRes);
}
- /** This will be called just before a new child is attached to the window. */
- public void setChildAttachedConsumer(Consumer<View> childAttachedConsumer) {
- mChildAttachedConsumer = childAttachedConsumer;
- }
-
@Override
protected void updatePoolSize() {
RecycledViewPool pool = getRecycledViewPool();
@@ -67,12 +60,4 @@
public RecyclerViewFastScroller getScrollbar() {
return null;
}
-
- @Override
- public void onChildAttachedToWindow(@NonNull View child) {
- if (mChildAttachedConsumer != null) {
- mChildAttachedConsumer.accept(child);
- }
- super.onChildAttachedToWindow(child);
- }
}
diff --git a/src/com/android/launcher3/allapps/SearchTransitionController.java b/src/com/android/launcher3/allapps/SearchTransitionController.java
index eb1bc0a..d5c3b57 100644
--- a/src/com/android/launcher3/allapps/SearchTransitionController.java
+++ b/src/com/android/launcher3/allapps/SearchTransitionController.java
@@ -18,34 +18,21 @@
import static android.view.View.VISIBLE;
-import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
-
import static com.android.app.animation.Interpolators.DECELERATE_1_7;
import static com.android.app.animation.Interpolators.INSTANT;
import static com.android.app.animation.Interpolators.clampToProgress;
-import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
import static com.android.launcher3.anim.AnimatorListeners.forEndCallback;
import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
-import android.graphics.drawable.Drawable;
-import android.util.FloatProperty;
-import android.util.Log;
import android.view.View;
-import android.view.ViewGroup;
import android.view.animation.Interpolator;
-import com.android.launcher3.BubbleTextView;
import com.android.launcher3.R;
-import com.android.launcher3.Utilities;
-import com.android.launcher3.config.FeatureFlags;
-import com.android.launcher3.model.data.ItemInfo;
/** Coordinates the transition between Search and A-Z in All Apps. */
-public class SearchTransitionController {
-
- private static final String LOG_TAG = "SearchTransitionCtrl";
+public class SearchTransitionController extends RecyclerViewAnimationController {
// Interpolator when the user taps the QSB while already in All Apps.
private static final Interpolator INTERPOLATOR_WITHIN_ALL_APPS = DECELERATE_1_7;
@@ -53,42 +40,10 @@
// happening simultaneously.
private static final Interpolator INTERPOLATOR_TRANSITIONING_TO_ALL_APPS = INSTANT;
- /**
- * These values represent points on the [0, 1] animation progress spectrum. They are used to
- * animate items in the {@link SearchRecyclerView}.
- */
- private static final float TOP_CONTENT_FADE_PROGRESS_START = 0.133f;
- private static final float CONTENT_FADE_PROGRESS_DURATION = 0.083f;
- private static final float TOP_BACKGROUND_FADE_PROGRESS_START = 0.633f;
- private static final float BACKGROUND_FADE_PROGRESS_DURATION = 0.15f;
- private static final float CONTENT_STAGGER = 0.01f; // Progress before next item starts fading.
-
- private static final FloatProperty<SearchTransitionController> SEARCH_TO_AZ_PROGRESS =
- new FloatProperty<SearchTransitionController>("searchToAzProgress") {
- @Override
- public Float get(SearchTransitionController controller) {
- return controller.getSearchToAzProgress();
- }
-
- @Override
- public void setValue(SearchTransitionController controller, float progress) {
- controller.setSearchToAzProgress(progress);
- }
- };
-
- private final ActivityAllAppsContainerView<?> mAllAppsContainerView;
-
- private ObjectAnimator mSearchToAzAnimator = null;
- private float mSearchToAzProgress = 1f;
private boolean mSkipNextAnimationWithinAllApps;
public SearchTransitionController(ActivityAllAppsContainerView<?> allAppsContainerView) {
- mAllAppsContainerView = allAppsContainerView;
- }
-
- /** Returns true if a transition animation is currently in progress. */
- public boolean isRunning() {
- return mSearchToAzAnimator != null;
+ super(allAppsContainerView);
}
/**
@@ -101,51 +56,31 @@
* @param onEndRunnable will be called when the animation finishes, unless another animation is
* scheduled in the meantime
*/
- public void animateToSearchState(boolean goingToSearch, long duration, Runnable onEndRunnable) {
- float targetProgress = goingToSearch ? 0 : 1;
-
- if (mSearchToAzAnimator != null) {
- mSearchToAzAnimator.cancel();
- }
-
- mSearchToAzAnimator = ObjectAnimator.ofFloat(this, SEARCH_TO_AZ_PROGRESS, targetProgress);
- boolean inAllApps = mAllAppsContainerView.isInAllApps();
- TimeInterpolator timeInterpolator =
- inAllApps ? INTERPOLATOR_WITHIN_ALL_APPS : INTERPOLATOR_TRANSITIONING_TO_ALL_APPS;
- if (mSkipNextAnimationWithinAllApps) {
- timeInterpolator = INSTANT;
- mSkipNextAnimationWithinAllApps = false;
- }
- if (timeInterpolator == INSTANT) {
- duration = 0; // Don't want to animate when coming from QSB.
- }
- mSearchToAzAnimator.setDuration(duration).setInterpolator(timeInterpolator);
- mSearchToAzAnimator.addListener(forEndCallback(() -> mSearchToAzAnimator = null));
+ @Override
+ protected void animateToState(boolean goingToSearch, long duration, Runnable onEndRunnable) {
+ super.animateToState(goingToSearch, duration, onEndRunnable);
if (!goingToSearch) {
- mSearchToAzAnimator.addListener(forSuccessCallback(() -> {
+ mAnimator.addListener(forSuccessCallback(() -> {
mAllAppsContainerView.getFloatingHeaderView().setFloatingRowsCollapsed(false);
mAllAppsContainerView.getFloatingHeaderView().reset(false /* animate */);
mAllAppsContainerView.getAppsRecyclerViewContainer().setTranslationY(0);
}));
}
- mSearchToAzAnimator.addListener(forSuccessCallback(onEndRunnable));
-
mAllAppsContainerView.getFloatingHeaderView().setFloatingRowsCollapsed(true);
mAllAppsContainerView.getFloatingHeaderView().setVisibility(VISIBLE);
mAllAppsContainerView.getFloatingHeaderView().maybeSetTabVisibility(VISIBLE);
mAllAppsContainerView.getAppsRecyclerViewContainer().setVisibility(VISIBLE);
- getSearchRecyclerView().setVisibility(VISIBLE);
- getSearchRecyclerView().setChildAttachedConsumer(this::onSearchChildAttached);
- mSearchToAzAnimator.start();
+ getRecyclerView().setVisibility(VISIBLE);
}
- private SearchRecyclerView getSearchRecyclerView() {
+ @Override
+ protected SearchRecyclerView getRecyclerView() {
return mAllAppsContainerView.getSearchRecyclerView();
}
- private void setSearchToAzProgress(float searchToAzProgress) {
- mSearchToAzProgress = searchToAzProgress;
- int searchHeight = updateSearchRecyclerViewProgress();
+ @Override
+ protected int onProgressUpdated(float searchToAzProgress) {
+ int searchHeight = super.onProgressUpdated(searchToAzProgress);
FloatingHeaderView headerView = mAllAppsContainerView.getFloatingHeaderView();
@@ -171,179 +106,27 @@
appsContainer.setTranslationY(appsTranslationY);
// Fade apps out with tabs (in 20% of the total animation).
appsContainer.setAlpha(clampToProgress(searchToAzProgress, 0.8f, 1f));
+ return searchHeight;
}
/**
- * Updates the children views of SearchRecyclerView based on the current animation progress.
- *
- * @return the total height of animating views (excluding at most one row of app icons).
+ * Should only animate if the view is not an app icon or if the app row is complete.
*/
- private int updateSearchRecyclerViewProgress() {
- int numSearchResultsAnimated = 0;
- int totalHeight = 0;
- int appRowHeight = 0;
- boolean appRowComplete = false;
- Integer top = null;
- SearchRecyclerView searchRecyclerView = getSearchRecyclerView();
-
- for (int i = 0; i < searchRecyclerView.getChildCount(); i++) {
- View searchResultView = searchRecyclerView.getChildAt(i);
- if (searchResultView == null) {
- continue;
- }
-
- if (top == null) {
- top = searchResultView.getTop();
- }
-
- int adapterPosition = searchRecyclerView.getChildAdapterPosition(searchResultView);
- int spanIndex = getSpanIndex(searchRecyclerView, adapterPosition);
- appRowComplete |= appRowHeight > 0 && spanIndex == 0;
- // We don't animate the first (currently only) app row we see, as that is assumed to be
- // predicted/prefix-matched apps.
- boolean shouldAnimate = !isAppIcon(searchResultView) || appRowComplete;
-
- float contentAlpha = 1f;
- float backgroundAlpha = 1f;
- if (shouldAnimate) {
- if (spanIndex > 0) {
- // Animate this item with the previous item on the same row.
- numSearchResultsAnimated--;
- }
-
- // Adjust content alpha based on start progress and stagger.
- float startContentFadeProgress = Math.max(0,
- TOP_CONTENT_FADE_PROGRESS_START
- - CONTENT_STAGGER * numSearchResultsAnimated);
- float endContentFadeProgress = Math.min(1,
- startContentFadeProgress + CONTENT_FADE_PROGRESS_DURATION);
- contentAlpha = 1 - clampToProgress(mSearchToAzProgress,
- startContentFadeProgress, endContentFadeProgress);
-
- // Adjust background (or decorator) alpha based on start progress and stagger.
- float startBackgroundFadeProgress = Math.max(0,
- TOP_BACKGROUND_FADE_PROGRESS_START
- - CONTENT_STAGGER * numSearchResultsAnimated);
- float endBackgroundFadeProgress = Math.min(1,
- startBackgroundFadeProgress + BACKGROUND_FADE_PROGRESS_DURATION);
- backgroundAlpha = 1 - clampToProgress(mSearchToAzProgress,
- startBackgroundFadeProgress, endBackgroundFadeProgress);
-
- numSearchResultsAnimated++;
- }
-
- Drawable background = searchResultView.getBackground();
- if (background != null
- && searchResultView instanceof ViewGroup
- && FeatureFlags.ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES.get()) {
- searchResultView.setAlpha(1f);
-
- // Apply content alpha to each child, since the view needs to be fully opaque for
- // the background to show properly.
- ViewGroup searchResultViewGroup = (ViewGroup) searchResultView;
- for (int j = 0; j < searchResultViewGroup.getChildCount(); j++) {
- searchResultViewGroup.getChildAt(j).setAlpha(contentAlpha);
- }
-
- // Apply background alpha to the background drawable directly.
- background.setAlpha((int) (255 * backgroundAlpha));
- } else {
- searchResultView.setAlpha(contentAlpha);
-
- // Apply background alpha to decorator if possible.
- if (adapterPosition != NO_POSITION) {
- searchRecyclerView.getApps().getAdapterItems().get(adapterPosition)
- .setDecorationFillAlpha((int) (255 * backgroundAlpha));
- }
-
- // Apply background alpha to view's background (e.g. for Search Edu card).
- if (background != null) {
- background.setAlpha((int) (255 * backgroundAlpha));
- }
- }
-
- float scaleY = 1;
- if (shouldAnimate) {
- scaleY = 1 - mSearchToAzProgress;
- }
- int scaledHeight = (int) (searchResultView.getHeight() * scaleY);
- searchResultView.setScaleY(scaleY);
-
- // For rows with multiple elements, only count the height once and translate elements to
- // the same y position.
- int y = top + totalHeight;
- if (spanIndex > 0) {
- // Continuation of an existing row; move this item into the row.
- y -= scaledHeight;
- } else {
- // Start of a new row contributes to total height.
- totalHeight += scaledHeight;
- if (!shouldAnimate) {
- appRowHeight = scaledHeight;
- }
- }
- searchResultView.setY(y);
- }
-
- return totalHeight - appRowHeight;
+ @Override
+ protected boolean shouldAnimate(View view, boolean hasDecorationInfo, boolean appRowComplete) {
+ return !isAppIcon(view) || appRowComplete;
}
- /** @return the column that the view at this position is found (0 assumed if indeterminate). */
- private int getSpanIndex(SearchRecyclerView searchRecyclerView, int adapterPosition) {
- if (adapterPosition == NO_POSITION) {
- Log.w(LOG_TAG, "Can't determine span index - child not found in adapter");
- return 0;
+ @Override
+ protected TimeInterpolator getInterpolator() {
+ TimeInterpolator timeInterpolator =
+ mAllAppsContainerView.isInAllApps()
+ ? INTERPOLATOR_WITHIN_ALL_APPS : INTERPOLATOR_TRANSITIONING_TO_ALL_APPS;
+ if (mSkipNextAnimationWithinAllApps) {
+ timeInterpolator = INSTANT;
+ mSkipNextAnimationWithinAllApps = false;
}
- if (!(searchRecyclerView.getAdapter() instanceof AllAppsGridAdapter<?>)) {
- Log.e(LOG_TAG, "Search RV doesn't have an AllAppsGridAdapter?");
- // This case shouldn't happen, but for debug devices we will continue to create a more
- // visible crash.
- if (!Utilities.IS_DEBUG_DEVICE) {
- return 0;
- }
- }
- AllAppsGridAdapter<?> adapter = (AllAppsGridAdapter<?>) searchRecyclerView.getAdapter();
- return adapter.getSpanIndex(adapterPosition);
- }
-
- private boolean isAppIcon(View item) {
- return item instanceof BubbleTextView && item.getTag() instanceof ItemInfo
- && ((ItemInfo) item.getTag()).itemType == ITEM_TYPE_APPLICATION;
- }
-
- /** Called just before a child is attached to the SearchRecyclerView. */
- private void onSearchChildAttached(View child) {
- // Avoid allocating hardware layers for alpha changes.
- child.forceHasOverlappingRendering(false);
- child.setPivotY(0);
- if (mSearchToAzProgress > 0) {
- // Before the child is rendered, apply the animation including it to avoid flicker.
- updateSearchRecyclerViewProgress();
- } else {
- // Apply default states without processing the full layout.
- child.setAlpha(1);
- child.setScaleY(1);
- child.setTranslationY(0);
- int adapterPosition = getSearchRecyclerView().getChildAdapterPosition(child);
- if (adapterPosition != NO_POSITION) {
- getSearchRecyclerView().getApps().getAdapterItems().get(adapterPosition)
- .setDecorationFillAlpha(255);
- }
- if (child instanceof ViewGroup
- && FeatureFlags.ENABLE_SEARCH_RESULT_BACKGROUND_DRAWABLES.get()) {
- ViewGroup childGroup = (ViewGroup) child;
- for (int i = 0; i < childGroup.getChildCount(); i++) {
- childGroup.getChildAt(i).setAlpha(1f);
- }
- }
- if (child.getBackground() != null) {
- child.getBackground().setAlpha(255);
- }
- }
- }
-
- private float getSearchToAzProgress() {
- return mSearchToAzProgress;
+ return timeInterpolator;
}
/**
diff --git a/src/com/android/launcher3/allapps/SectionDecorationHandler.java b/src/com/android/launcher3/allapps/SectionDecorationHandler.java
new file mode 100644
index 0000000..f79b82c
--- /dev/null
+++ b/src/com/android/launcher3/allapps/SectionDecorationHandler.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.InsetDrawable;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+
+import com.android.launcher3.R;
+import com.android.launcher3.util.Themes;
+
+public class SectionDecorationHandler {
+
+ protected final Path mTmpPath = new Path();
+ protected final RectF mTmpRect = new RectF();
+
+ protected final int mCornerGroupRadius;
+ protected final int mCornerResultRadius;
+ protected final RectF mBounds = new RectF();
+ protected final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ protected final int mFocusAlpha = 255; // main focused item alpha
+ protected int mFillColor; // grouping color
+ protected int mFocusColor; // main focused item color
+ protected float mFillSpacing;
+ protected int mInlineRadius;
+ protected Context mContext;
+ protected float[] mCorners;
+ protected int mFillAlpha;
+ protected boolean mIsTopLeftRound;
+ protected boolean mIsTopRightRound;
+ protected boolean mIsBottomLeftRound;
+ protected boolean mIsBottomRightRound;
+ protected boolean mIsBottomRound;
+ protected boolean mIsTopRound;
+
+ public SectionDecorationHandler(Context context, int fillAlpha, boolean isTopLeftRound,
+ boolean isTopRightRound, boolean isBottomLeftRound,
+ boolean isBottomRightRound) {
+
+ mContext = context;
+ mFillAlpha = fillAlpha;
+ mFocusColor = ContextCompat.getColor(context,
+ R.color.material_color_surface_bright); // UX recommended
+ mFillColor = ContextCompat.getColor(context,
+ R.color.material_color_surface_container_high); // UX recommended
+
+ mIsTopLeftRound = isTopLeftRound;
+ mIsTopRightRound = isTopRightRound;
+ mIsBottomLeftRound = isBottomLeftRound;
+ mIsBottomRightRound = isBottomRightRound;
+ mIsBottomRound = mIsBottomLeftRound && mIsBottomRightRound;
+ mIsTopRound = mIsTopLeftRound && mIsTopRightRound;
+
+ mCornerGroupRadius = context.getResources().getDimensionPixelSize(
+ R.dimen.all_apps_recycler_view_decorator_group_radius);
+ mCornerResultRadius = context.getResources().getDimensionPixelSize(
+ R.dimen.all_apps_recycler_view_decorator_result_radius);
+
+ mInlineRadius = 0;
+ mFillSpacing = 0;
+ initCorners();
+ }
+
+ protected void initCorners() {
+ mCorners = new float[]{
+ mIsTopLeftRound ? mCornerGroupRadius : 0,
+ mIsTopLeftRound ? mCornerGroupRadius : 0, // Top left radius in px
+ mIsTopRightRound ? mCornerGroupRadius : 0,
+ mIsTopRightRound ? mCornerGroupRadius : 0, // Top right radius in px
+ mIsBottomRightRound ? mCornerGroupRadius : 0,
+ mIsBottomRightRound ? mCornerGroupRadius : 0, // Bottom right
+ mIsBottomLeftRound ? mCornerGroupRadius : 0,
+ mIsBottomLeftRound ? mCornerGroupRadius : 0 // Bottom left
+ };
+ }
+
+ protected void setFillAlpha(int fillAlpha) {
+ mFillAlpha = fillAlpha;
+ mPaint.setAlpha(mFillAlpha);
+ }
+
+ protected void onFocusDraw(Canvas canvas, @Nullable View view) {
+ if (view == null) {
+ return;
+ }
+ mPaint.setColor(mFillColor);
+ mPaint.setAlpha(mFillAlpha);
+ int scaledHeight = (int) (view.getHeight() * view.getScaleY());
+ mBounds.set(view.getLeft(), view.getY(), view.getRight(), view.getY() + scaledHeight);
+ onDraw(canvas);
+ }
+
+ protected void onDraw(Canvas canvas) {
+ mTmpPath.reset();
+ mTmpRect.set(mBounds.left + mFillSpacing,
+ mBounds.top + mFillSpacing,
+ mBounds.right - mFillSpacing,
+ mBounds.bottom - mFillSpacing);
+ mTmpPath.addRoundRect(mTmpRect, mCorners, Path.Direction.CW);
+ canvas.drawPath(mTmpPath, mPaint);
+ }
+
+ /** Sets the right background drawable to the view based on the give decoration info. */
+ public void applyBackground(View view, Context context,
+ @Nullable SectionDecorationInfo decorationInfo, boolean isHighlighted) {
+ int inset = context.getResources().getDimensionPixelSize(
+ R.dimen.all_apps_recycler_view_decorator_padding);
+ float radiusBottom = (decorationInfo == null || decorationInfo.isBottomRound()) ?
+ mCornerGroupRadius : mCornerResultRadius;
+ float radiusTop =
+ (decorationInfo == null || decorationInfo.isTopRound()) ?
+ mCornerGroupRadius : mCornerResultRadius;
+ int color = isHighlighted ? mFocusColor : mFillColor;
+
+ GradientDrawable shape = new GradientDrawable();
+ shape.setShape(GradientDrawable.RECTANGLE);
+ shape.setCornerRadii(new float[] {
+ radiusTop, radiusTop, // top-left
+ radiusTop, radiusTop, // top-right
+ radiusBottom, radiusBottom, // bottom-right
+ radiusBottom, radiusBottom // bottom-left
+ });
+ shape.setColor(color);
+
+ // Setting the background resets the padding, so we cache it and reset it afterwards.
+ int paddingLeft = view.getPaddingLeft();
+ int paddingTop = view.getPaddingTop();
+ int paddingRight = view.getPaddingRight();
+ int paddingBottom = view.getPaddingBottom();
+
+ view.setBackground(new InsetDrawable(shape, inset));
+
+ view.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
+ }
+
+ /**
+ * Section decorator that combines views and draws a single block decoration
+ */
+ public static class UnionDecorationHandler extends SectionDecorationHandler {
+
+ private final int mPaddingLeft;
+ private final int mPaddingRight;
+
+ public UnionDecorationHandler(
+ SectionDecorationHandler decorationHandler,
+ int paddingLeft, int paddingRight) {
+ super(decorationHandler.mContext, decorationHandler.mFillAlpha,
+ decorationHandler.mIsTopLeftRound, decorationHandler.mIsTopRightRound,
+ decorationHandler.mIsBottomLeftRound, decorationHandler.mIsBottomRightRound);
+ mPaddingLeft = paddingLeft;
+ mPaddingRight = paddingRight;
+ }
+
+ /**
+ * Expands decoration bounds to include child {@link PrivateAppsSectionDecorator}
+ */
+ public void addChild(SectionDecorationHandler child, View view, boolean applyBackground) {
+ int scaledHeight = (int) (view.getHeight() * view.getScaleY());
+ mBounds.union(view.getLeft(), view.getY(),
+ view.getRight(), view.getY() + scaledHeight);
+ if (applyBackground) {
+ applyBackground(view, mContext, null, false);
+ }
+ mIsBottomRound |= child.mIsBottomRound;
+ mIsBottomLeftRound |= child.mIsBottomLeftRound;
+ mIsBottomRightRound |= child.mIsBottomRightRound;
+ mIsTopRound |= child.mIsTopRound;
+ mIsTopLeftRound |= child.mIsTopLeftRound;
+ mIsTopRightRound |= child.mIsTopRightRound;
+ }
+
+ /**
+ * Draws group decoration to canvas
+ */
+ public void onGroupDecorate(Canvas canvas) {
+ initCorners();
+ mBounds.left = mPaddingLeft;
+ mBounds.right = canvas.getWidth() - mPaddingRight;
+ mPaint.setColor(mFillColor);
+ mPaint.setAlpha(mFillAlpha);
+ onDraw(canvas);
+ }
+ }
+}
diff --git a/src/com/android/launcher3/allapps/SectionDecorationInfo.java b/src/com/android/launcher3/allapps/SectionDecorationInfo.java
new file mode 100644
index 0000000..1fed2b6
--- /dev/null
+++ b/src/com/android/launcher3/allapps/SectionDecorationInfo.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.allapps;
+
+import android.content.Context;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+
+public class SectionDecorationInfo {
+
+ public static final int ROUND_NOTHING = 1 << 1;
+ public static final int ROUND_TOP_LEFT = 1 << 2;
+ public static final int ROUND_TOP_RIGHT = 1 << 3;
+ public static final int ROUND_BOTTOM_LEFT = 1 << 4;
+ public static final int ROUND_BOTTOM_RIGHT = 1 << 5;
+ public static final int DECORATOR_ALPHA = 255;
+
+ protected boolean mShouldDecorateItemsTogether;
+ private SectionDecorationHandler mDecorationHandler;
+ protected boolean mIsTopRound;
+ protected boolean mIsBottomRound;
+
+ public SectionDecorationInfo(Context context, int roundRegions, boolean decorateTogether) {
+ mDecorationHandler =
+ new SectionDecorationHandler(context, DECORATOR_ALPHA,
+ isFlagEnabled(roundRegions, ROUND_TOP_LEFT),
+ isFlagEnabled(roundRegions, ROUND_TOP_RIGHT),
+ isFlagEnabled(roundRegions, ROUND_BOTTOM_LEFT),
+ isFlagEnabled(roundRegions, ROUND_BOTTOM_RIGHT));
+ mShouldDecorateItemsTogether = decorateTogether;
+ mIsTopRound = isFlagEnabled(roundRegions, ROUND_TOP_LEFT) &&
+ isFlagEnabled(roundRegions, ROUND_TOP_RIGHT);
+ mIsBottomRound = isFlagEnabled(roundRegions, ROUND_BOTTOM_LEFT) &&
+ isFlagEnabled(roundRegions, ROUND_BOTTOM_RIGHT);
+ }
+
+ public SectionDecorationInfo(Context context, @NonNull Bundle target,
+ String targetLayoutType, @NonNull Bundle prevTarget, @NonNull Bundle nextTarget) {}
+
+ public SectionDecorationHandler getDecorationHandler() {
+ return mDecorationHandler;
+ }
+
+ private boolean isFlagEnabled(int canonicalFlag, int comparison) {
+ return (canonicalFlag & comparison) != 0;
+ }
+
+ /**
+ * Returns whether multiple {@link SectionDecorationInfo}s with the same sectionId should
+ * be grouped together.
+ */
+ public boolean shouldDecorateItemsTogether() {
+ return mShouldDecorateItemsTogether;
+ }
+
+ public boolean isTopRound() {
+ return mIsTopRound;
+ }
+
+ public boolean isBottomRound() {
+ return mIsBottomRound;
+ }
+}
diff --git a/src/com/android/launcher3/apppairs/AppPairIcon.java b/src/com/android/launcher3/apppairs/AppPairIcon.java
index 4cf6471..1d73441 100644
--- a/src/com/android/launcher3/apppairs/AppPairIcon.java
+++ b/src/com/android/launcher3/apppairs/AppPairIcon.java
@@ -17,11 +17,10 @@
package com.android.launcher3.apppairs;
import android.content.Context;
-import android.graphics.Canvas;
import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
+import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
@@ -30,9 +29,10 @@
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.R;
+import com.android.launcher3.Reorderable;
import com.android.launcher3.dragndrop.DraggableView;
import com.android.launcher3.model.data.FolderInfo;
-import com.android.launcher3.model.data.WorkspaceItemInfo;
+import com.android.launcher3.util.MultiTranslateDelegate;
import com.android.launcher3.views.ActivityContext;
import java.util.Collections;
@@ -44,39 +44,18 @@
* The app pair icon is two parallel background rectangles with rounded corners. Icons of the two
* member apps are set into these rectangles.
*/
-public class AppPairIcon extends FrameLayout implements DraggableView {
- /**
- * Design specs -- the below ratios are in relation to the size of a standard app icon.
- */
- private static final float OUTER_PADDING_SCALE = 1 / 30f;
- private static final float INNER_PADDING_SCALE = 1 / 24f;
- private static final float MEMBER_ICON_SCALE = 11 / 30f;
- private static final float CENTER_CHANNEL_SCALE = 1 / 30f;
- private static final float BIG_RADIUS_SCALE = 1 / 5f;
- private static final float SMALL_RADIUS_SCALE = 1 / 15f;
-
- // App pair icons are slightly smaller than regular icons, so we pad the icon by this much on
- // each side.
- float mOuterPadding;
- // Inside of the icon, the two member apps are padded by this much.
- float mInnerPadding;
- // The two member apps have icons that are this big (in diameter).
- float mMemberIconSize;
- // The size of the center channel.
- float mCenterChannelSize;
- // The large outer radius of the background rectangles.
- float mBigRadius;
- // The small inner radius of the background rectangles.
- float mSmallRadius;
- // The app pairs icon appears differently in portrait and landscape.
- boolean mIsLandscape;
-
- private ActivityContext mActivity;
+public class AppPairIcon extends FrameLayout implements DraggableView, Reorderable {
+ // A view that holds the app pair icon graphic.
+ private AppPairIconGraphic mIconGraphic;
// A view that holds the app pair's title.
private BubbleTextView mAppPairName;
// The underlying ItemInfo that stores info about the app pair members, etc.
private FolderInfo mInfo;
+ // Required for Reorderable -- handles translation and bouncing movements
+ private final MultiTranslateDelegate mTranslateDelegate = new MultiTranslateDelegate(this);
+ private float mScaleForReorderBounce = 1f;
+
public AppPairIcon(Context context, AttributeSet attrs) {
super(context, attrs);
}
@@ -103,7 +82,10 @@
icon.setTag(appPairInfo);
icon.setOnClickListener(activity.getItemOnClickListener());
icon.mInfo = appPairInfo;
- icon.mActivity = activity;
+
+ // Set up icon drawable area
+ icon.mIconGraphic = icon.findViewById(R.id.app_pair_icon_graphic);
+ icon.mIconGraphic.init(activity.getDeviceProfile(), icon);
// Set up app pair title
icon.mAppPairName = icon.findViewById(R.id.app_pair_icon_name);
@@ -121,85 +103,6 @@
return icon;
}
- @Override
- protected void dispatchDraw(Canvas canvas) {
- super.dispatchDraw(canvas);
-
- // Calculate device-specific measurements
- DeviceProfile grid = mActivity.getDeviceProfile();
- int defaultIconSize = grid.iconSizePx;
- mOuterPadding = OUTER_PADDING_SCALE * defaultIconSize;
- mInnerPadding = INNER_PADDING_SCALE * defaultIconSize;
- mMemberIconSize = MEMBER_ICON_SCALE * defaultIconSize;
- mCenterChannelSize = CENTER_CHANNEL_SCALE * defaultIconSize;
- mBigRadius = BIG_RADIUS_SCALE * defaultIconSize;
- mSmallRadius = SMALL_RADIUS_SCALE * defaultIconSize;
- mIsLandscape = grid.isLeftRightSplit;
-
- // Calculate drawable area position
- float leftBound = (canvas.getWidth() / 2f) - (defaultIconSize / 2f);
- float topBound = getPaddingTop();
-
- // Prepare to draw app pair icon background
- Drawable background = new AppPairIconBackground(getContext(), this);
- background.setBounds(0, 0, defaultIconSize, defaultIconSize);
-
- // Draw background
- canvas.save();
- canvas.translate(leftBound, topBound);
- background.draw(canvas);
- canvas.restore();
-
- // Prepare to draw icons
- WorkspaceItemInfo app1 = mInfo.contents.get(0);
- WorkspaceItemInfo app2 = mInfo.contents.get(1);
- Drawable app1Icon = app1.newIcon(getContext());
- Drawable app2Icon = app2.newIcon(getContext());
- app1Icon.setBounds(0, 0, defaultIconSize, defaultIconSize);
- app2Icon.setBounds(0, 0, defaultIconSize, defaultIconSize);
-
- // Draw first icon
- canvas.save();
- canvas.translate(leftBound, topBound);
- // The app icons are placed differently depending on device orientation.
- if (mIsLandscape) {
- canvas.translate(
- (defaultIconSize / 2f) - (mCenterChannelSize / 2f) - mInnerPadding
- - mMemberIconSize,
- (defaultIconSize / 2f) - (mMemberIconSize / 2f)
- );
- } else {
- canvas.translate(
- (defaultIconSize / 2f) - (mMemberIconSize / 2f),
- (defaultIconSize / 2f) - (mCenterChannelSize / 2f) - mInnerPadding
- - mMemberIconSize
- );
-
- }
- canvas.scale(MEMBER_ICON_SCALE, MEMBER_ICON_SCALE);
- app1Icon.draw(canvas);
- canvas.restore();
-
- // Draw second icon
- canvas.save();
- canvas.translate(leftBound, topBound);
- // The app icons are placed differently depending on device orientation.
- if (mIsLandscape) {
- canvas.translate(
- (defaultIconSize / 2f) + (mCenterChannelSize / 2f) + mInnerPadding,
- (defaultIconSize / 2f) - (mMemberIconSize / 2f)
- );
- } else {
- canvas.translate(
- (defaultIconSize / 2f) - (mMemberIconSize / 2f),
- (defaultIconSize / 2f) + (mCenterChannelSize / 2f) + mInnerPadding
- );
- }
- canvas.scale(MEMBER_ICON_SCALE, MEMBER_ICON_SCALE);
- app2Icon.draw(canvas);
- canvas.restore();
- }
-
/**
* Returns a formatted accessibility title for app pairs.
*/
@@ -207,17 +110,52 @@
return getContext().getString(R.string.app_pair_name_format, app1, app2);
}
+ // Required for DraggableView
@Override
public int getViewType() {
return DRAGGABLE_ICON;
}
+ // Required for DraggableView
@Override
- public void getWorkspaceVisualDragBounds(Rect bounds) {
- mAppPairName.getIconBounds(bounds);
+ public void getWorkspaceVisualDragBounds(Rect outBounds) {
+ mIconGraphic.getIconBounds(outBounds);
+ }
+
+ /** Sets the visibility of the icon's title text */
+ public void setTextVisible(boolean visible) {
+ if (visible) {
+ mAppPairName.setVisibility(VISIBLE);
+ } else {
+ mAppPairName.setVisibility(INVISIBLE);
+ }
+ }
+
+ // Required for Reorderable
+ @Override
+ public MultiTranslateDelegate getTranslateDelegate() {
+ return mTranslateDelegate;
+ }
+
+ // Required for Reorderable
+ @Override
+ public void setReorderBounceScale(float scale) {
+ mScaleForReorderBounce = scale;
+ super.setScaleX(scale);
+ super.setScaleY(scale);
+ }
+
+ // Required for Reorderable
+ @Override
+ public float getReorderBounceScale() {
+ return mScaleForReorderBounce;
}
public FolderInfo getInfo() {
return mInfo;
}
+
+ public View getIconDrawableArea() {
+ return mIconGraphic;
+ }
}
diff --git a/src/com/android/launcher3/apppairs/AppPairIconBackground.java b/src/com/android/launcher3/apppairs/AppPairIconBackground.java
index 735c82f..4e60ece 100644
--- a/src/com/android/launcher3/apppairs/AppPairIconBackground.java
+++ b/src/com/android/launcher3/apppairs/AppPairIconBackground.java
@@ -32,8 +32,8 @@
* A Drawable for the background behind the twin app icons (looks like two rectangles).
*/
class AppPairIconBackground extends Drawable {
- // The icon that we will draw this background on.
- private final AppPairIcon icon;
+ // The underlying view that we are drawing this background on.
+ private final AppPairIconGraphic icon;
private final Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
/**
@@ -44,8 +44,8 @@
private static final RectF EMPTY_RECT = new RectF();
private static final float[] ARRAY_OF_ZEROES = new float[8];
- AppPairIconBackground(Context context, AppPairIcon appPairIcon) {
- icon = appPairIcon;
+ AppPairIconBackground(Context context, AppPairIconGraphic iconGraphic) {
+ icon = iconGraphic;
// Set up background paint color
TypedArray ta = context.getTheme().obtainStyledAttributes(R.styleable.FolderIconPreview);
mBackgroundPaint.setStyle(Paint.Style.FILL);
@@ -56,7 +56,7 @@
@Override
public void draw(Canvas canvas) {
- if (icon.mIsLandscape) {
+ if (icon.isLeftRightSplit()) {
drawLeftRightSplit(canvas);
} else {
drawTopBottomSplit(canvas);
@@ -73,29 +73,29 @@
// The left half of the background image, excluding center channel
RectF leftSide = new RectF(
- icon.mOuterPadding,
- icon.mOuterPadding,
- (width / 2f) - (icon.mCenterChannelSize / 2f),
- height - icon.mOuterPadding
+ 0,
+ 0,
+ (width / 2f) - (icon.getCenterChannelSize() / 2f),
+ height
);
// The right half of the background image, excluding center channel
RectF rightSide = new RectF(
- (width / 2f) + (icon.mCenterChannelSize / 2f),
- icon.mOuterPadding,
- width - icon.mOuterPadding,
- height - icon.mOuterPadding
+ (width / 2f) + (icon.getCenterChannelSize() / 2f),
+ 0,
+ width,
+ height
);
drawCustomRoundedRect(canvas, leftSide, new float[]{
- icon.mBigRadius, icon.mBigRadius,
- icon.mSmallRadius, icon.mSmallRadius,
- icon.mSmallRadius, icon.mSmallRadius,
- icon.mBigRadius, icon.mBigRadius});
+ icon.getBigRadius(), icon.getBigRadius(),
+ icon.getSmallRadius(), icon.getSmallRadius(),
+ icon.getSmallRadius(), icon.getSmallRadius(),
+ icon.getBigRadius(), icon.getBigRadius()});
drawCustomRoundedRect(canvas, rightSide, new float[]{
- icon.mSmallRadius, icon.mSmallRadius,
- icon.mBigRadius, icon.mBigRadius,
- icon.mBigRadius, icon.mBigRadius,
- icon.mSmallRadius, icon.mSmallRadius});
+ icon.getSmallRadius(), icon.getSmallRadius(),
+ icon.getBigRadius(), icon.getBigRadius(),
+ icon.getBigRadius(), icon.getBigRadius(),
+ icon.getSmallRadius(), icon.getSmallRadius()});
}
/**
@@ -108,29 +108,29 @@
// The top half of the background image, excluding center channel
RectF topSide = new RectF(
- icon.mOuterPadding,
- icon.mOuterPadding,
- width - icon.mOuterPadding,
- (height / 2f) - (icon.mCenterChannelSize / 2f)
+ 0,
+ 0,
+ width,
+ (height / 2f) - (icon.getCenterChannelSize() / 2f)
);
// The bottom half of the background image, excluding center channel
RectF bottomSide = new RectF(
- icon.mOuterPadding,
- (height / 2f) + (icon.mCenterChannelSize / 2f),
- width - icon.mOuterPadding,
- height - icon.mOuterPadding
+ 0,
+ (height / 2f) + (icon.getCenterChannelSize() / 2f),
+ width,
+ height
);
drawCustomRoundedRect(canvas, topSide, new float[]{
- icon.mBigRadius, icon.mBigRadius,
- icon.mBigRadius, icon.mBigRadius,
- icon.mSmallRadius, icon.mSmallRadius,
- icon.mSmallRadius, icon.mSmallRadius});
+ icon.getBigRadius(), icon.getBigRadius(),
+ icon.getBigRadius(), icon.getBigRadius(),
+ icon.getSmallRadius(), icon.getSmallRadius(),
+ icon.getSmallRadius(), icon.getSmallRadius()});
drawCustomRoundedRect(canvas, bottomSide, new float[]{
- icon.mSmallRadius, icon.mSmallRadius,
- icon.mSmallRadius, icon.mSmallRadius,
- icon.mBigRadius, icon.mBigRadius,
- icon.mBigRadius, icon.mBigRadius});
+ icon.getSmallRadius(), icon.getSmallRadius(),
+ icon.getSmallRadius(), icon.getSmallRadius(),
+ icon.getBigRadius(), icon.getBigRadius(),
+ icon.getBigRadius(), icon.getBigRadius()});
}
/**
@@ -146,7 +146,7 @@
c.drawDoubleRoundRect(rect, radii, EMPTY_RECT, ARRAY_OF_ZEROES, mBackgroundPaint);
} else {
// Fallback rectangle with uniform rounded corners
- c.drawRoundRect(rect, icon.mBigRadius, icon.mBigRadius, mBackgroundPaint);
+ c.drawRoundRect(rect, icon.getBigRadius(), icon.getBigRadius(), mBackgroundPaint);
}
}
diff --git a/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
new file mode 100644
index 0000000..2945979
--- /dev/null
+++ b/src/com/android/launcher3/apppairs/AppPairIconGraphic.kt
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.apppairs
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.Gravity
+import android.widget.FrameLayout
+import com.android.launcher3.DeviceProfile
+
+/**
+ * A FrameLayout marking the area on an [AppPairIcon] where the visual icon will be drawn. One of
+ * two child UI elements on an [AppPairIcon], along with a BubbleTextView holding the text title.
+ */
+class AppPairIconGraphic @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
+ FrameLayout(context, attrs) {
+ companion object {
+ // Design specs -- the below ratios are in relation to the size of a standard app icon.
+ private const val OUTER_PADDING_SCALE = 1 / 30f
+ private const val INNER_PADDING_SCALE = 1 / 24f
+ private const val MEMBER_ICON_SCALE = 11 / 30f
+ private const val CENTER_CHANNEL_SCALE = 1 / 30f
+ private const val BIG_RADIUS_SCALE = 1 / 5f
+ private const val SMALL_RADIUS_SCALE = 1 / 15f
+ }
+
+ // App pair icons are slightly smaller than regular icons, so we pad the icon by this much on
+ // each side.
+ private var outerPadding = 0f
+ // Inside of the icon, the two member apps are padded by this much.
+ private var innerPadding = 0f
+ // The colored background (two rectangles in a square area) is this big.
+ private var backgroundSize = 0f
+ // The two member apps have icons that are this big (in diameter).
+ private var memberIconSize = 0f
+ // The size of the center channel.
+ var centerChannelSize = 0f
+ // The large outer radius of the background rectangles.
+ var bigRadius = 0f
+ // The small inner radius of the background rectangles.
+ var smallRadius = 0f
+ // The app pairs icon appears differently in portrait and landscape.
+ var isLeftRightSplit = false
+
+ private lateinit var parentIcon: AppPairIcon
+ private lateinit var appPairBackground: Drawable
+ private lateinit var appIcon1: Drawable
+ private lateinit var appIcon2: Drawable
+
+ fun init(grid: DeviceProfile, icon: AppPairIcon) {
+ // Calculate device-specific measurements
+ val defaultIconSize = grid.iconSizePx
+ outerPadding = OUTER_PADDING_SCALE * defaultIconSize
+ innerPadding = INNER_PADDING_SCALE * defaultIconSize
+ backgroundSize = defaultIconSize - outerPadding * 2
+ memberIconSize = MEMBER_ICON_SCALE * defaultIconSize
+ centerChannelSize = CENTER_CHANNEL_SCALE * defaultIconSize
+ bigRadius = BIG_RADIUS_SCALE * defaultIconSize
+ smallRadius = SMALL_RADIUS_SCALE * defaultIconSize
+ isLeftRightSplit = grid.isLeftRightSplit
+ parentIcon = icon
+
+ appPairBackground = AppPairIconBackground(context, this)
+ appPairBackground.setBounds(0, 0, backgroundSize.toInt(), backgroundSize.toInt())
+ appIcon1 = parentIcon.info.contents[0].newIcon(context)
+ appIcon2 = parentIcon.info.contents[1].newIcon(context)
+ appIcon1.setBounds(0, 0, memberIconSize.toInt(), memberIconSize.toInt())
+ appIcon2.setBounds(0, 0, memberIconSize.toInt(), memberIconSize.toInt())
+ }
+
+ /** Gets this icon graphic's bounds, with respect to the parent icon's coordinate system. */
+ fun getIconBounds(outBounds: Rect) {
+ outBounds.set(0, 0, backgroundSize.toInt(), backgroundSize.toInt())
+ outBounds.offset(
+ // x-coordinate in parent's coordinate system
+ ((parentIcon.width - backgroundSize) / 2).toInt(),
+ // y-coordinate in parent's coordinate system
+ parentIcon.paddingTop + outerPadding.toInt()
+ )
+ }
+
+ override fun dispatchDraw(canvas: Canvas) {
+ super.dispatchDraw(canvas)
+
+ // Center the drawable area in the larger icon canvas
+ val lp: LayoutParams = layoutParams as LayoutParams
+ lp.gravity = Gravity.CENTER_HORIZONTAL
+ lp.topMargin = outerPadding.toInt()
+ lp.height = backgroundSize.toInt()
+ lp.width = backgroundSize.toInt()
+ layoutParams = lp
+
+ // Draw background
+ appPairBackground.draw(canvas)
+
+ // Draw first icon
+ canvas.save()
+ // The app icons are placed differently depending on device orientation.
+ if (isLeftRightSplit) {
+ canvas.translate(innerPadding, height / 2f - memberIconSize / 2f)
+ } else {
+ canvas.translate(width / 2f - memberIconSize / 2f, innerPadding)
+ }
+ appIcon1.draw(canvas)
+ canvas.restore()
+
+ // Draw second icon
+ canvas.save()
+ // The app icons are placed differently depending on device orientation.
+ if (isLeftRightSplit) {
+ canvas.translate(
+ width - (innerPadding + memberIconSize),
+ height / 2f - memberIconSize / 2f
+ )
+ } else {
+ canvas.translate(
+ width / 2f - memberIconSize / 2f,
+ height - (innerPadding + memberIconSize)
+ )
+ }
+ appIcon2.draw(canvas)
+ canvas.restore()
+ }
+}
diff --git a/src/com/android/launcher3/celllayout/ReorderAlgorithm.java b/src/com/android/launcher3/celllayout/ReorderAlgorithm.java
index 42b6991..8754b74 100644
--- a/src/com/android/launcher3/celllayout/ReorderAlgorithm.java
+++ b/src/com/android/launcher3/celllayout/ReorderAlgorithm.java
@@ -285,6 +285,11 @@
return foundSolution;
}
+ private void revertDir(int[] direction) {
+ direction[0] *= -1;
+ direction[1] *= -1;
+ }
+
// This method tries to find a reordering solution which satisfies the push mechanic by trying
// to push items in each of the cardinal directions, in an order based on the direction vector
// passed.
@@ -293,91 +298,36 @@
if ((Math.abs(direction[0]) + Math.abs(direction[1])) > 1) {
// If the direction vector has two non-zero components, we try pushing
// separately in each of the components.
- int temp = direction[1];
- direction[1] = 0;
-
- if (pushViewsToTempLocation(intersectingViews, occupied, direction, ignoreView,
- solution)) {
- return true;
+ int temp;
+ for (int j = 0; j < 2; j++) {
+ for (int i = 1; i >= 0; i--) {
+ temp = direction[i];
+ direction[i] = 0;
+ if (pushViewsToTempLocation(intersectingViews, occupied, direction, ignoreView,
+ solution)) {
+ return true;
+ }
+ direction[i] = temp;
+ }
+ revertDir(direction);
}
- direction[1] = temp;
- temp = direction[0];
- direction[0] = 0;
-
- if (pushViewsToTempLocation(intersectingViews, occupied, direction, ignoreView,
- solution)) {
- return true;
- }
- // Revert the direction
- direction[0] = temp;
-
- // Now we try pushing in each component of the opposite direction
- direction[0] *= -1;
- direction[1] *= -1;
- temp = direction[1];
- direction[1] = 0;
- if (pushViewsToTempLocation(intersectingViews, occupied, direction, ignoreView,
- solution)) {
- return true;
- }
-
- direction[1] = temp;
- temp = direction[0];
- direction[0] = 0;
- if (pushViewsToTempLocation(intersectingViews, occupied, direction, ignoreView,
- solution)) {
- return true;
- }
- // revert the direction
- direction[0] = temp;
- direction[0] *= -1;
- direction[1] *= -1;
-
} else {
// If the direction vector has a single non-zero component, we push first in the
// direction of the vector
- if (pushViewsToTempLocation(intersectingViews, occupied, direction, ignoreView,
- solution)) {
- return true;
+ int temp;
+ for (int j = 0; j < 2; j++) {
+ for (int i = 0; i < 2; i++) {
+ if (pushViewsToTempLocation(intersectingViews, occupied, direction, ignoreView,
+ solution)) {
+ return true;
+ }
+ revertDir(direction);
+ }
+ // Swap the components
+ temp = direction[1];
+ direction[1] = direction[0];
+ direction[0] = temp;
}
- // Then we try the opposite direction
- direction[0] *= -1;
- direction[1] *= -1;
- if (pushViewsToTempLocation(intersectingViews, occupied, direction, ignoreView,
- solution)) {
- return true;
- }
- // Switch the direction back
- direction[0] *= -1;
- direction[1] *= -1;
-
- // If we have failed to find a push solution with the above, then we try
- // to find a solution by pushing along the perpendicular axis.
-
- // Swap the components
- int temp = direction[1];
- direction[1] = direction[0];
- direction[0] = temp;
- if (pushViewsToTempLocation(intersectingViews, occupied, direction, ignoreView,
- solution)) {
- return true;
- }
-
- // Then we try the opposite direction
- direction[0] *= -1;
- direction[1] *= -1;
- if (pushViewsToTempLocation(intersectingViews, occupied, direction, ignoreView,
- solution)) {
- return true;
- }
- // Switch the direction back
- direction[0] *= -1;
- direction[1] *= -1;
-
- // Swap the components back
- temp = direction[1];
- direction[1] = direction[0];
- direction[0] = temp;
}
return false;
}
diff --git a/src/com/android/launcher3/config/FeatureFlags.java b/src/com/android/launcher3/config/FeatureFlags.java
index 746f1cf..2f62840 100644
--- a/src/com/android/launcher3/config/FeatureFlags.java
+++ b/src/com/android/launcher3/config/FeatureFlags.java
@@ -219,9 +219,6 @@
public static final BooleanFlag NOTIFY_CRASHES = getDebugFlag(270393108, "NOTIFY_CRASHES",
TEAMFOOD, "Sends a notification whenever launcher encounters an uncaught exception.");
- public static final BooleanFlag ENABLE_TRANSIENT_TASKBAR = getDebugFlag(270395798,
- "ENABLE_TRANSIENT_TASKBAR", ENABLED, "Enables transient taskbar.");
-
public static final boolean ENABLE_TASKBAR_NAVBAR_UNIFICATION =
enableTaskbarNavbarUnification();
@@ -487,10 +484,10 @@
// TODO(Block 33): Clean up flags
public static final BooleanFlag ENABLE_ALL_APPS_RV_PREINFLATION = getDebugFlag(288161355,
- "ENABLE_ALL_APPS_RV_PREINFLATION", ENABLED,
+ "ENABLE_ALL_APPS_RV_PREINFLATION", TEAMFOOD,
"Enables preinflating all apps icons to avoid scrolling jank.");
public static final BooleanFlag ALL_APPS_GONE_VISIBILITY = getDebugFlag(291651514,
- "ALL_APPS_GONE_VISIBILITY", ENABLED,
+ "ALL_APPS_GONE_VISIBILITY", TEAMFOOD,
"Set all apps container view's hidden visibility to GONE instead of INVISIBLE.");
// TODO(Block 34): Empty block
diff --git a/src/com/android/launcher3/dragndrop/AddItemDragLayer.java b/src/com/android/launcher3/dragndrop/SimpleDragLayer.java
similarity index 70%
rename from src/com/android/launcher3/dragndrop/AddItemDragLayer.java
rename to src/com/android/launcher3/dragndrop/SimpleDragLayer.java
index 5b52c3d..e42ba72 100644
--- a/src/com/android/launcher3/dragndrop/AddItemDragLayer.java
+++ b/src/com/android/launcher3/dragndrop/SimpleDragLayer.java
@@ -20,18 +20,20 @@
import android.util.AttributeSet;
import com.android.launcher3.util.TouchController;
+import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.BaseDragLayer;
/**
- * Drag layer for {@link AddItemActivity}.
+ * A concrete {@link BaseDragLayer} that creates an empty list of {@link TouchController}s.
+ * @param <T> The {@link ActivityContext} hosting the drag layer.
*/
-public class AddItemDragLayer extends BaseDragLayer<AddItemActivity> {
+public class SimpleDragLayer<T extends Context & ActivityContext> extends BaseDragLayer<T> {
- public AddItemDragLayer(Context context, AttributeSet attrs) {
+ public SimpleDragLayer(Context context, AttributeSet attrs) {
this(context, attrs, /*alphaChannelCount= */ 1);
}
- public AddItemDragLayer(Context context, AttributeSet attrs, int alphaChannelCount) {
+ public SimpleDragLayer(Context context, AttributeSet attrs, int alphaChannelCount) {
super(context, attrs, alphaChannelCount);
}
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index 8bf7ec2..084f829 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -210,7 +210,9 @@
@ViewDebug.IntToString(from = STATE_OPEN, to = "STATE_OPEN"),
})
private int mState = STATE_CLOSED;
- private OnFolderStateChangedListener mOnFolderStateChangedListener;
+ private final List<OnFolderStateChangedListener> mOnFolderStateChangedListeners =
+ new ArrayList<>();
+ private OnFolderStateChangedListener mPriorityOnFolderStateChangedListener;
@ViewDebug.ExportedProperty(category = "launcher")
private boolean mRearrangeOnClose = false;
boolean mItemsInvalidated = false;
@@ -1082,7 +1084,7 @@
private void updateItemLocationsInDatabaseBatch(boolean isBind) {
FolderGridOrganizer verifier = new FolderGridOrganizer(
- mActivityContext.getDeviceProfile().inv).setFolderInfo(mInfo);
+ mActivityContext.getDeviceProfile()).setFolderInfo(mInfo);
ArrayList<ItemInfo> items = new ArrayList<>();
int total = mInfo.contents.size();
@@ -1381,7 +1383,7 @@
@Override
public void onAdd(WorkspaceItemInfo item, int rank) {
FolderGridOrganizer verifier = new FolderGridOrganizer(
- mActivityContext.getDeviceProfile().inv).setFolderInfo(mInfo);
+ mActivityContext.getDeviceProfile()).setFolderInfo(mInfo);
verifier.updateRankAndPos(item, rank);
mLauncherDelegate.getModelWriter().addOrMoveItemInDatabase(item, mInfo.id, 0, item.cellX,
item.cellY);
@@ -1665,18 +1667,43 @@
return windowBottomPx - folderBottomPx;
}
+ /**
+ * Save this listener for the special case of when we update the state and concurrently
+ * add another listener to {@link #mOnFolderStateChangedListeners} to avoid a
+ * ConcurrentModificationException
+ */
+ public void setPriorityOnFolderStateChangedListener(OnFolderStateChangedListener listener) {
+ mPriorityOnFolderStateChangedListener = listener;
+ }
+
private void setState(@FolderState int newState) {
mState = newState;
- if (mOnFolderStateChangedListener != null) {
- mOnFolderStateChangedListener.onFolderStateChanged(mState);
+ if (mPriorityOnFolderStateChangedListener != null) {
+ mPriorityOnFolderStateChangedListener.onFolderStateChanged(mState);
+ }
+ for (OnFolderStateChangedListener listener : mOnFolderStateChangedListeners) {
+ if (listener != null) {
+ listener.onFolderStateChanged(mState);
+ }
}
}
- public void setOnFolderStateChangedListener(@Nullable OnFolderStateChangedListener listener) {
- mOnFolderStateChangedListener = listener;
+ /**
+ * Adds the provided listener to the running list of Folder listeners
+ * {@link #mOnFolderStateChangedListeners}
+ */
+ public void addOnFolderStateChangedListener(@Nullable OnFolderStateChangedListener listener) {
+ if (listener != null) {
+ mOnFolderStateChangedListeners.add(listener);
+ }
}
- /** Listener that can be registered via {@link Folder#setOnFolderStateChangedListener} */
+ /** Removes the provided listener from the running list of Folder listeners */
+ public void removeOnFolderStateChangedListener(OnFolderStateChangedListener listener) {
+ mOnFolderStateChangedListeners.remove(listener);
+ }
+
+ /** Listener that can be registered via {@link #addOnFolderStateChangedListener} */
public interface OnFolderStateChangedListener {
/** See {@link Folder.FolderState} */
void onFolderStateChanged(@FolderState int newState);
diff --git a/src/com/android/launcher3/folder/FolderAnimationManager.java b/src/com/android/launcher3/folder/FolderAnimationManager.java
index 9e2e2bf..a91373b 100644
--- a/src/com/android/launcher3/folder/FolderAnimationManager.java
+++ b/src/com/android/launcher3/folder/FolderAnimationManager.java
@@ -96,7 +96,7 @@
mContext = folder.getContext();
mDeviceProfile = folder.mActivityContext.getDeviceProfile();
- mPreviewVerifier = new FolderGridOrganizer(mDeviceProfile.inv);
+ mPreviewVerifier = new FolderGridOrganizer(mDeviceProfile);
mIsOpening = isOpening;
diff --git a/src/com/android/launcher3/folder/FolderGridOrganizer.java b/src/com/android/launcher3/folder/FolderGridOrganizer.java
index 4be82ed..cc24761 100644
--- a/src/com/android/launcher3/folder/FolderGridOrganizer.java
+++ b/src/com/android/launcher3/folder/FolderGridOrganizer.java
@@ -20,7 +20,7 @@
import android.graphics.Point;
-import com.android.launcher3.InvariantDeviceProfile;
+import com.android.launcher3.DeviceProfile;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
@@ -41,11 +41,13 @@
private int mCountX;
private int mCountY;
private boolean mDisplayingUpperLeftQuadrant = false;
+ private static final int PREVIEW_MAX_ROWS = 2;
+ private static final int PREVIEW_MAX_COLUMNS = 2;
/**
* Note: must call {@link #setFolderInfo(FolderInfo)} manually for verifier to work.
*/
- public FolderGridOrganizer(InvariantDeviceProfile profile) {
+ public FolderGridOrganizer(DeviceProfile profile) {
mMaxCountX = profile.numFolderColumns;
mMaxCountY = profile.numFolderRows;
mMaxItemsPerPage = mMaxCountX * mMaxCountY;
@@ -127,6 +129,7 @@
/**
* Updates the item's cellX, cellY and rank corresponding to the provided rank.
+ *
* @return true if there was any change
*/
public boolean updateRankAndPos(ItemInfo item, int rank) {
@@ -189,7 +192,7 @@
if (page > 0 || mDisplayingUpperLeftQuadrant) {
int col = rank % mCountX;
int row = rank / mCountX;
- return col < 2 && row < 2;
+ return col < PREVIEW_MAX_COLUMNS && row < PREVIEW_MAX_ROWS;
}
return rank < MAX_NUM_ITEMS_IN_PREVIEW;
}
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index cb1dc4f..f058ae4 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -86,7 +86,6 @@
import java.util.List;
import java.util.function.Predicate;
-
/**
* An icon that can appear on in the workspace representing an {@link Folder}.
*/
@@ -221,7 +220,7 @@
icon.setAccessibilityDelegate(activity.getAccessibilityDelegate());
- icon.mPreviewVerifier = new FolderGridOrganizer(activity.getDeviceProfile().inv);
+ icon.mPreviewVerifier = new FolderGridOrganizer(activity.getDeviceProfile());
icon.mPreviewVerifier.setFolderInfo(folderInfo);
icon.updatePreviewItems(false);
@@ -634,6 +633,7 @@
}
}
+ /** Sets the visibility of the icon's title text */
public void setTextVisible(boolean visible) {
if (visible) {
mFolderName.setVisibility(VISIBLE);
diff --git a/src/com/android/launcher3/folder/FolderPagedView.java b/src/com/android/launcher3/folder/FolderPagedView.java
index 36e5e1b..f2bed92 100644
--- a/src/com/android/launcher3/folder/FolderPagedView.java
+++ b/src/com/android/launcher3/folder/FolderPagedView.java
@@ -37,8 +37,6 @@
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.CellLayout;
import com.android.launcher3.DeviceProfile;
-import com.android.launcher3.InvariantDeviceProfile;
-import com.android.launcher3.LauncherAppState;
import com.android.launcher3.PagedView;
import com.android.launcher3.R;
import com.android.launcher3.ShortcutAndWidgetContainer;
@@ -101,14 +99,15 @@
public FolderPagedView(Context context, AttributeSet attrs) {
super(context, attrs);
- InvariantDeviceProfile profile = LauncherAppState.getIDP(context);
+ ActivityContext activityContext = ActivityContext.lookupContext(context);
+ DeviceProfile profile = activityContext.getDeviceProfile();
mOrganizer = new FolderGridOrganizer(profile);
mIsRtl = Utilities.isRtl(getResources());
setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
mFocusIndicatorHelper = new ViewGroupFocusHelper(this);
- mViewCache = ActivityContext.lookupContext(context).getViewCache();
+ mViewCache = activityContext.getViewCache();
}
public void setFolder(Folder folder) {
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 7c422cd..d8388c2 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -549,12 +549,6 @@
@UiEvent(doc = "Launcher item drop failed since there was not enough room on the screen.")
LAUNCHER_ITEM_DROP_FAILED_INSUFFICIENT_SPACE(872),
- @UiEvent(doc = "User long pressed on the taskbar background to hide the taskbar")
- LAUNCHER_TASKBAR_LONGPRESS_HIDE(896),
-
- @UiEvent(doc = "User long pressed on the taskbar gesture handle to show the taskbar")
- LAUNCHER_TASKBAR_LONGPRESS_SHOW(897),
-
@UiEvent(doc = "User clicks on the search icon on header to launch search in app.")
LAUNCHER_ALLAPPS_SEARCHINAPP_LAUNCH(913),
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index f4ce360..a98ec64 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -411,7 +411,7 @@
mSessionHelper.getActiveSessions();
installingPkgs.forEach(mApp.getIconCache()::updateSessionCache);
FileLog.d(TAG, "loadWorkspace: Packages with active install sessions: "
- + installingPkgs.values());
+ + installingPkgs.keySet().stream().map(info -> info.mPackageName).toList());
final PackageUserKey tempPackageKey = new PackageUserKey(null, null);
mFirstScreenBroadcast = new FirstScreenBroadcast(installingPkgs);
@@ -440,10 +440,15 @@
mShortcutKeyToPinnedShortcuts.put(ShortcutKey.fromInfo(shortcut),
shortcut);
}
+ if (pinnedShortcuts.isEmpty()) {
+ FileLog.d(TAG, "No pinned shortcuts found for user " + user);
+ }
} else {
// Shortcut manager can fail due to some race condition when the
// lock state changes too frequently. For the purpose of the loading
// shortcuts, consider the user is still locked.
+ FileLog.d(TAG, "Shortcut request failed for user "
+ + user + ", user may still be locked.");
userUnlocked = false;
}
}
@@ -481,16 +486,17 @@
mItemsDeleted = c.commitDeleted();
// Sort the folder items, update ranks, and make sure all preview items are high res.
- FolderGridOrganizer verifier =
- new FolderGridOrganizer(mApp.getInvariantDeviceProfile());
+ List<FolderGridOrganizer> verifiers =
+ mApp.getInvariantDeviceProfile().supportedProfiles.stream().map(
+ FolderGridOrganizer::new).toList();
for (FolderInfo folder : mBgDataModel.folders) {
Collections.sort(folder.contents, Folder.ITEM_POS_COMPARATOR);
- verifier.setFolderInfo(folder);
+ verifiers.forEach(verifier -> verifier.setFolderInfo(folder));
int size = folder.contents.size();
// Update ranks here to ensure there are no gaps caused by removed folder items.
- // Ranks are the source of truth for folder items, so cellX and cellY can be ignored
- // for now. Database will be updated once user manually modifies folder.
+ // Ranks are the source of truth for folder items, so cellX and cellY can be
+ // ignored for now. Database will be updated once user manually modifies folder.
for (int rank = 0; rank < size; ++rank) {
WorkspaceItemInfo info = folder.contents.get(rank);
// rank is used differently in app pairs, so don't reset
@@ -498,9 +504,9 @@
info.rank = rank;
}
- if (info.usingLowResIcon()
- && info.itemType == Favorites.ITEM_TYPE_APPLICATION
- && verifier.isItemInPreview(info.rank)) {
+ if (info.usingLowResIcon() && info.itemType == Favorites.ITEM_TYPE_APPLICATION
+ && verifiers.stream().anyMatch(
+ verifier -> verifier.isItemInPreview(info.rank))) {
mIconCache.getTitleAndIcon(info, false);
}
}
@@ -589,17 +595,17 @@
// Package is not yet available but might be
// installed later.
FileLog.d(TAG, "package not yet restored: " + targetPkg);
-
tempPackageKey.update(targetPkg, c.user);
if (c.hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORE_STARTED)) {
// Restore has started once.
} else if (installingPkgs.containsKey(tempPackageKey)) {
// App restore has started. Update the flag
c.restoreFlag |= WorkspaceItemInfo.FLAG_RESTORE_STARTED;
- c.updater().put(Favorites.RESTORED,
- c.restoreFlag).commit();
+ FileLog.d(TAG, "restore started for installing app: " + targetPkg);
+ c.updater().put(Favorites.RESTORED, c.restoreFlag).commit();
} else {
- c.markDeleted("Unrestored app removed: " + targetPkg);
+ c.markDeleted("removing app that is not restored and not "
+ + "installing. package: " + targetPkg);
return;
}
} else if (pmHelper.isAppOnSdcard(targetPkg, c.user)) {
@@ -610,7 +616,7 @@
} else if (!isSdCardReady) {
// SdCard is not ready yet. Package might get available,
// once it is ready.
- Log.d(TAG, "Missing pkg, will check later: " + targetPkg);
+ Log.d(TAG, "Missing package, will check later: " + targetPkg);
mPendingPackages.add(new PackageUserKey(targetPkg, c.user));
// Add the icon on the workspace anyway.
allowMissingTarget = true;
@@ -646,7 +652,8 @@
ShortcutInfo pinnedShortcut = mShortcutKeyToPinnedShortcuts.get(key);
if (pinnedShortcut == null) {
// The shortcut is no longer valid.
- c.markDeleted("Pinned shortcut not found");
+ c.markDeleted("Pinned shortcut not found for package: "
+ + key.getPackageName());
return;
}
info = new WorkspaceItemInfo(pinnedShortcut, mApp.getContext());
@@ -816,7 +823,7 @@
} else {
Log.v(TAG, "Widget restore pending id=" + c.id
+ " appWidgetId=" + appWidgetId
- + " status =" + c.restoreFlag);
+ + " status=" + c.restoreFlag);
appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId, component);
appWidgetInfo.restoreStatus = c.restoreFlag;
diff --git a/src/com/android/launcher3/model/ModelDbController.java b/src/com/android/launcher3/model/ModelDbController.java
index 139efc3..d2b7161 100644
--- a/src/com/android/launcher3/model/ModelDbController.java
+++ b/src/com/android/launcher3/model/ModelDbController.java
@@ -62,6 +62,7 @@
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.Utilities;
+import com.android.launcher3.logging.FileLog;
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.provider.LauncherDbUtils;
import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
@@ -262,7 +263,7 @@
*/
public void tryMigrateDB() {
if (!migrateGridIfNeeded()) {
- Log.d(TAG, "Migration failed: resetting launcher database");
+ FileLog.d(TAG, "Migration failed: resetting launcher database");
createEmptyDB();
LauncherPrefs.get(mContext).putSync(
getEmptyDbCreatedKey(mOpenHelper.getDatabaseName()).to(true));
@@ -282,15 +283,17 @@
createDbIfNotExists();
if (LauncherPrefs.get(mContext).get(getEmptyDbCreatedKey())) {
// If we have already create a new DB, ignore migration
+ Log.d(TAG, "migrateGridIfNeeded: new DB already created, skipping migration");
return false;
}
InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext);
if (!GridSizeMigrationUtil.needsToMigrate(mContext, idp)) {
+ Log.d(TAG, "migrateGridIfNeeded: no grid migration needed");
return true;
}
String targetDbName = new DeviceGridState(idp).getDbFile();
if (TextUtils.equals(targetDbName, mOpenHelper.getDatabaseName())) {
- Log.e(TAG, "migrateGridIfNeeded - target db is same as current: " + targetDbName);
+ Log.e(TAG, "migrateGridIfNeeded: target db is same as current: " + targetDbName);
return false;
}
DatabaseHelper oldHelper = mOpenHelper;
@@ -299,6 +302,9 @@
try {
return GridSizeMigrationUtil.migrateGridIfNeeded(mContext, idp, mOpenHelper,
oldHelper.getWritableDatabase());
+ } catch (Exception e) {
+ FileLog.e(TAG, "Failed to migrate grid", e);
+ return false;
} finally {
if (mOpenHelper != oldHelper) {
oldHelper.close();
diff --git a/src/com/android/launcher3/model/data/FolderInfo.java b/src/com/android/launcher3/model/data/FolderInfo.java
index 9bf6d43..5b541d0 100644
--- a/src/com/android/launcher3/model/data/FolderInfo.java
+++ b/src/com/android/launcher3/model/data/FolderInfo.java
@@ -46,7 +46,6 @@
import java.util.OptionalInt;
import java.util.stream.IntStream;
-
/**
* Represents a folder containing shortcuts or apps.
*/
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index dbd13b3..dc8cd3a 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -89,9 +89,9 @@
public static final String APPWIDGET_OLD_IDS = "appwidget_old_ids";
public static final String APPWIDGET_IDS = "appwidget_ids";
-
private static final String[] DB_COLUMNS_TO_LOG = {"profileId", "title", "itemType", "screen",
- "container", "cellX", "cellY", "spanX", "spanY", "intent"};
+ "container", "cellX", "cellY", "spanX", "spanY", "intent", "appWidgetProvider",
+ "appWidgetId", "restored"};
/**
* Tries to restore the backup DB if needed
@@ -141,16 +141,17 @@
* 4. If restored from a single display backup, remove gaps between screenIds
* 5. Override shortcuts that need to be replaced.
*
- * @return number of items deleted.
+ * @return number of items deleted
*/
@VisibleForTesting
protected int sanitizeDB(Context context, ModelDbController controller, SQLiteDatabase db,
BackupManager backupManager) throws Exception {
- FileLog.d(TAG, "Old Launcher Database before sanitizing:");
+ logFavoritesTable(db, "Old Launcher Database before sanitizing:", null, null);
// Primary user ids
long myProfileId = controller.getSerialNumberForUser(myUserHandle());
long oldProfileId = getDefaultProfileId(db);
- FileLog.d(TAG, "sanitizeDB: myProfileId=" + myProfileId + " oldProfileId=" + oldProfileId);
+ FileLog.d(TAG, "sanitizeDB: myProfileId= " + myProfileId
+ + ", oldProfileId= " + oldProfileId);
LongSparseArray<Long> oldManagedProfileIds = getManagedProfileIds(db, oldProfileId);
LongSparseArray<Long> profileMapping = new LongSparseArray<>(oldManagedProfileIds.size()
+ 1);
@@ -182,7 +183,7 @@
final String[] args = new String[profileIds.length];
Arrays.fill(args, "?");
final String where = "profileId NOT IN (" + TextUtils.join(", ", Arrays.asList(args)) + ")";
- logUnrestoredItems(db, where, profileIds);
+ logFavoritesTable(db, "items to delete from unrestored profiles:", where, profileIds);
int itemsDeletedCount = db.delete(Favorites.TABLE_NAME, where, profileIds);
FileLog.d(TAG, itemsDeletedCount + " total items from unrestored user(s) were deleted");
@@ -242,47 +243,6 @@
}
/**
- * Queries and logs the items we will delete from unrestored profiles in the launcher db.
- * This is to understand why items might be missing during the restore process for Launcher.
- * @param database the Launcher db to query from.
- * @param where the SELECT statement to query items that will be deleted.
- * @param profileIds the profile ID's the user will be migrating to.
- */
- private void logUnrestoredItems(SQLiteDatabase database, String where, String[] profileIds) {
- try (Cursor itemsToDelete = database.query(
- /* table */ Favorites.TABLE_NAME,
- /* columns */ DB_COLUMNS_TO_LOG,
- /* selection */ where,
- /* selection args */ profileIds,
- /* groupBy */ null,
- /* having */ null,
- /* orderBy */ null
- )) {
- if (itemsToDelete.moveToFirst()) {
- String[] columnNames = itemsToDelete.getColumnNames();
- StringBuilder stringBuilder = new StringBuilder(
- "items to be deleted from the Favorites Table during restore:\n"
- );
- do {
- for (String columnName : columnNames) {
- stringBuilder.append(columnName)
- .append("=")
- .append(itemsToDelete.getString(
- itemsToDelete.getColumnIndex(columnName)))
- .append(" ");
- }
- stringBuilder.append("\n");
- } while (itemsToDelete.moveToNext());
- FileLog.d(TAG, stringBuilder.toString());
- } else {
- FileLog.d(TAG, "logDeletedItems: No items found to delete");
- }
- } catch (Exception e) {
- FileLog.e(TAG, "logDeletedItems: Error reading from database", e);
- }
- }
-
- /**
* Remove gaps between screenIds to make sure no empty pages are left in between.
*
* e.g. [0, 3, 4, 6, 7] -> [0, 1, 2, 3, 4]
@@ -396,7 +356,7 @@
IntArray.fromConcatString(lp.get(APP_WIDGET_IDS)).toArray(),
host);
} else {
- FileLog.d(TAG, "No app widget ids to restore.");
+ FileLog.d(TAG, "No app widget ids were received from backup to restore.");
}
lp.remove(APP_WIDGET_IDS, OLD_APP_WIDGET_IDS);
@@ -409,16 +369,16 @@
private void restoreAppWidgetIds(Context context, ModelDbController controller,
int[] oldWidgetIds, int[] newWidgetIds, @NonNull AppWidgetHost host) {
if (WidgetsModel.GO_DISABLE_WIDGETS) {
- Log.e(TAG, "Skipping widget ID remap as widgets not supported");
+ FileLog.e(TAG, "Skipping widget ID remap as widgets not supported");
host.deleteHost();
return;
}
if (!RestoreDbTask.isPending(context)) {
// Someone has already gone through our DB once, probably LoaderTask. Skip any further
// modifications of the DB.
- Log.e(TAG, "Skipping widget ID remap as DB already in use");
+ FileLog.e(TAG, "Skipping widget ID remap as DB already in use");
for (int widgetId : newWidgetIds) {
- Log.d(TAG, "Deleting widgetId: " + widgetId);
+ FileLog.d(TAG, "Deleting widgetId: " + widgetId);
host.deleteAppWidgetId(widgetId);
}
return;
@@ -426,7 +386,7 @@
final AppWidgetManager widgets = AppWidgetManager.getInstance(context);
- Log.d(TAG, "restoreAppWidgetIds: "
+ FileLog.d(TAG, "restoreAppWidgetIds: "
+ "oldWidgetIds=" + IntArray.wrap(oldWidgetIds).toConcatString()
+ ", newWidgetIds=" + IntArray.wrap(newWidgetIds).toConcatString());
@@ -434,7 +394,7 @@
logDatabaseWidgetInfo(controller);
for (int i = 0; i < oldWidgetIds.length; i++) {
- Log.i(TAG, "Widget state restore id " + oldWidgetIds[i] + " => " + newWidgetIds[i]);
+ FileLog.i(TAG, "Widget state restore id " + oldWidgetIds[i] + " => " + newWidgetIds[i]);
final AppWidgetProviderInfo provider = widgets.getAppWidgetInfo(newWidgetIds[i]);
final int state;
@@ -454,7 +414,7 @@
final String where = "appWidgetId=? and (restored & 1) = 1 and profileId=?";
String profileId = Long.toString(mainProfileId);
final String[] args = new String[] { oldWidgetId, profileId };
- Log.d(TAG, "restoreAppWidgetIds: querying profile id=" + profileId
+ FileLog.d(TAG, "restoreAppWidgetIds: querying profile id=" + profileId
+ " with controller profile ID=" + controllerProfileId);
int result = new ContentWriter(context,
new ContentWriter.CommitParams(controller, where, args))
@@ -463,7 +423,7 @@
.commit();
if (result == 0) {
// TODO(b/234700507): Remove the logs after the bug is fixed
- Log.e(TAG, "restoreAppWidgetIds: remapping failed since the widget is not in"
+ FileLog.e(TAG, "restoreAppWidgetIds: remapping failed since the widget is not in"
+ " the database anymore");
try (Cursor cursor = controller.getDb().query(
Favorites.TABLE_NAME,
@@ -471,7 +431,7 @@
"appWidgetId=?", new String[]{oldWidgetId}, null, null, null)) {
if (!cursor.moveToFirst()) {
// The widget no long exists.
- Log.d(TAG, "Deleting widgetId: " + newWidgetIds[i] + " with old id: "
+ FileLog.d(TAG, "Deleting widgetId: " + newWidgetIds[i] + " with old id: "
+ oldWidgetId);
host.deleteAppWidgetId(newWidgetIds[i]);
}
@@ -523,7 +483,7 @@
}
builder.append("]");
Log.d(TAG, "restoreAppWidgetIds: all widget ids in database: "
- + builder.toString());
+ + builder);
} catch (Exception ex) {
Log.e(TAG, "Getting widget ids from the database failed", ex);
}
@@ -572,4 +532,45 @@
Collectors.joining(" OR "));
}
+ /**
+ * Queries and logs the items from the Favorites table in the launcher db.
+ * This is to understand why items might be missing during the restore process for Launcher.
+ * @param database The Launcher db to query from.
+ * @param logHeader First line in log statement, used to explain what is being logged.
+ * @param where The SELECT statement to query items.
+ * @param profileIds The profile ID's for each user profile.
+ */
+ public static void logFavoritesTable(SQLiteDatabase database, @NonNull String logHeader,
+ String where, String[] profileIds) {
+ try (Cursor itemsToDelete = database.query(
+ /* table */ Favorites.TABLE_NAME,
+ /* columns */ DB_COLUMNS_TO_LOG,
+ /* selection */ where,
+ /* selection args */ profileIds,
+ /* groupBy */ null,
+ /* having */ null,
+ /* orderBy */ null
+ )) {
+ if (itemsToDelete.moveToFirst()) {
+ String[] columnNames = itemsToDelete.getColumnNames();
+ StringBuilder stringBuilder = new StringBuilder(logHeader + "\n");
+ do {
+ for (String columnName : columnNames) {
+ stringBuilder.append(columnName)
+ .append("=")
+ .append(itemsToDelete.getString(
+ itemsToDelete.getColumnIndex(columnName)))
+ .append(" ");
+ }
+ stringBuilder.append("\n");
+ } while (itemsToDelete.moveToNext());
+ FileLog.d(TAG, stringBuilder.toString());
+ } else {
+ FileLog.d(TAG, "logFavoritesTable: No items found from query for"
+ + "\"" + logHeader + "\"");
+ }
+ } catch (Exception e) {
+ FileLog.e(TAG, "logFavoritesTable: Error reading from database", e);
+ }
+ }
}
diff --git a/src/com/android/launcher3/responsive/HotseatSpecsProvider.kt b/src/com/android/launcher3/responsive/HotseatSpecsProvider.kt
index ebbff51..7502a43 100644
--- a/src/com/android/launcher3/responsive/HotseatSpecsProvider.kt
+++ b/src/com/android/launcher3/responsive/HotseatSpecsProvider.kt
@@ -40,13 +40,28 @@
return specsGroup
}
+ private fun getSpecIgnoringDimensionType(
+ availableSize: Int,
+ specsGroup: ResponsiveSpecGroup<HotseatSpec>
+ ): HotseatSpec? {
+ val specWidth = specsGroup.widthSpecs.firstOrNull { availableSize <= it.maxAvailableSize }
+ val specHeight = specsGroup.heightSpecs.firstOrNull { availableSize <= it.maxAvailableSize }
+ return specWidth ?: specHeight
+ }
+
fun getCalculatedSpec(
aspectRatio: Float,
dimensionType: DimensionType,
- availableSpace: Int
+ availableSpace: Int,
): CalculatedHotseatSpec {
val specsGroup = getSpecsByAspectRatio(aspectRatio)
- val spec = specsGroup.getSpec(dimensionType, availableSpace)
+
+ // TODO(b/315548992): Ignore the dimension type to prevent crash before launcher
+ // data migration is finished. The restore process allows the initialization of
+ // an invalid or disabled grid until the data is restored and migrated.
+ val spec = getSpecIgnoringDimensionType(availableSpace, specsGroup)
+ check(spec != null) { "No available spec found within $availableSpace. $specsGroup" }
+ // val spec = specsGroup.getSpec(dimensionType, availableSpace)
return CalculatedHotseatSpec(availableSpace, spec)
}
diff --git a/src/com/android/launcher3/responsive/ResponsiveSpecGroup.kt b/src/com/android/launcher3/responsive/ResponsiveSpecGroup.kt
index b233d7c..a758be8 100644
--- a/src/com/android/launcher3/responsive/ResponsiveSpecGroup.kt
+++ b/src/com/android/launcher3/responsive/ResponsiveSpecGroup.kt
@@ -18,6 +18,7 @@
import android.content.res.TypedArray
import com.android.launcher3.R
+import com.android.launcher3.responsive.ResponsiveSpec.Companion.ResponsiveSpecType
import com.android.launcher3.responsive.ResponsiveSpec.DimensionType
/**
@@ -54,10 +55,29 @@
} else {
heightSpecs.firstOrNull { availableSize <= it.maxAvailableSize }
}
- check(spec != null) { "No available $type spec found within $availableSize." }
+ check(spec != null) { "No available $type spec found within $availableSize. $this" }
return spec
}
+ override fun toString(): String {
+ fun printSpec(spec: IResponsiveSpec) =
+ when (spec.specType) {
+ ResponsiveSpecType.AllApps,
+ ResponsiveSpecType.Folder,
+ ResponsiveSpecType.Workspace -> (spec as ResponsiveSpec).toString()
+ ResponsiveSpecType.Hotseat -> (spec as HotseatSpec).toString()
+ ResponsiveSpecType.Cell -> (spec as CellSpec).toString()
+ }
+
+ val widthSpecsString = widthSpecs.joinToString(", ") { printSpec(it) }
+ val heightSpecsString = heightSpecs.joinToString(", ") { printSpec(it) }
+ return "ResponsiveSpecGroup(" +
+ "aspectRatio=${aspectRatio}, " +
+ "widthSpecs=[${widthSpecsString}], " +
+ "heightSpecs=[${heightSpecsString}]" +
+ ")"
+ }
+
companion object {
const val XML_GROUP_NAME = "specs"
diff --git a/src/com/android/launcher3/shortcuts/ShortcutRequest.java b/src/com/android/launcher3/shortcuts/ShortcutRequest.java
index 5291ce4..21efceb 100644
--- a/src/com/android/launcher3/shortcuts/ShortcutRequest.java
+++ b/src/com/android/launcher3/shortcuts/ShortcutRequest.java
@@ -24,10 +24,11 @@
import android.content.pm.LauncherApps.ShortcutQuery;
import android.content.pm.ShortcutInfo;
import android.os.UserHandle;
-import android.util.Log;
import androidx.annotation.Nullable;
+import com.android.launcher3.logging.FileLog;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -101,7 +102,7 @@
return new QueryResult(mContext.getSystemService(LauncherApps.class)
.getShortcuts(mQuery, mUserHandle));
} catch (SecurityException | IllegalStateException e) {
- Log.e(TAG, "Failed to query for shortcuts", e);
+ FileLog.e(TAG, "Failed to query for shortcuts", e);
return QueryResult.DEFAULT;
}
}
diff --git a/src/com/android/launcher3/touch/ItemClickHandler.java b/src/com/android/launcher3/touch/ItemClickHandler.java
index 3bce377..a9c2a2e 100644
--- a/src/com/android/launcher3/touch/ItemClickHandler.java
+++ b/src/com/android/launcher3/touch/ItemClickHandler.java
@@ -145,8 +145,8 @@
*/
private static void onClickAppPairIcon(View v) {
Launcher launcher = Launcher.getLauncher(v.getContext());
- FolderInfo folderInfo = ((AppPairIcon) v).getInfo();
- launcher.launchAppPair(folderInfo.contents.get(0), folderInfo.contents.get(1));
+ AppPairIcon appPairIcon = (AppPairIcon) v;
+ launcher.launchAppPair(appPairIcon);
}
/**
diff --git a/src/com/android/launcher3/util/DisplayController.java b/src/com/android/launcher3/util/DisplayController.java
index 045ca79..18f583d 100644
--- a/src/com/android/launcher3/util/DisplayController.java
+++ b/src/com/android/launcher3/util/DisplayController.java
@@ -22,7 +22,6 @@
import static com.android.launcher3.LauncherPrefs.TASKBAR_PINNING;
import static com.android.launcher3.LauncherPrefs.TASKBAR_PINNING_KEY;
import static com.android.launcher3.Utilities.dpiFromPx;
-import static com.android.launcher3.config.FeatureFlags.ENABLE_TRANSIENT_TASKBAR;
import static com.android.launcher3.config.FeatureFlags.enableTaskbarPinning;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.FlagDebugUtils.appendFlag;
@@ -410,18 +409,19 @@
* Returns whether taskbar is transient.
*/
public boolean isTransientTaskbar() {
- // TODO(b/258604917): Once ENABLE_TASKBAR_PINNING is enabled, remove usage of
- // sTransientTaskbarStatusForTests and update test to directly
- // toggle shred preference to switch transient taskbar on/of
- if (!Utilities.isRunningInTestHarness()
- && enableTaskbarPinning()
- && mIsTaskbarPinned) {
+ if (navigationMode != NavigationMode.NO_BUTTON) {
return false;
}
- return navigationMode == NavigationMode.NO_BUTTON
- && (Utilities.isRunningInTestHarness()
- ? sTransientTaskbarStatusForTests
- : ENABLE_TRANSIENT_TASKBAR.get() && !mIsTaskbarPinned);
+ if (Utilities.isRunningInTestHarness()) {
+ // TODO(b/258604917): Once ENABLE_TASKBAR_PINNING is enabled, remove usage of
+ // sTransientTaskbarStatusForTests and update test to directly
+ // toggle shared preference to switch transient taskbar on/off.
+ return sTransientTaskbarStatusForTests;
+ }
+ if (enableTaskbarPinning()) {
+ return !mIsTaskbarPinned;
+ }
+ return true;
}
/**
@@ -485,6 +485,7 @@
pw.println(" currentSize=" + info.currentSize);
info.mPerDisplayBounds.forEach((key, value) -> pw.println(
" perDisplayBounds - " + key + ": " + value));
+ pw.println(" isTransientTaskbar=" + info.isTransientTaskbar());
}
/**
diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java
index 9adc2ee..5171fa2 100644
--- a/src/com/android/launcher3/widget/BaseWidgetSheet.java
+++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java
@@ -68,6 +68,8 @@
protected int mNavBarScrimHeight;
private final Paint mNavBarScrimPaint;
+ private boolean mDisableNavBarScrim = false;
+
public BaseWidgetSheet(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContentHorizontalMargin = getResources().getDimensionPixelSize(
@@ -148,8 +150,15 @@
}
}
+ /** Enables or disables the sheet's nav bar scrim. */
+ public void disableNavBarScrim(boolean disable) {
+ mDisableNavBarScrim = disable;
+ }
+
private int getNavBarScrimHeight(WindowInsets insets) {
- if (Utilities.ATLEAST_Q) {
+ if (mDisableNavBarScrim) {
+ return 0;
+ } else if (Utilities.ATLEAST_Q) {
return insets.getTappableElementInsets().bottom;
} else {
return insets.getStableInsetBottom();
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait.txt
index 5f5cf5e..18752e9 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait.txt
@@ -38,8 +38,8 @@
iconSizePx: 147.0px (56.0dp)
iconTextSizePx: 38.0px (14.476191dp)
iconDrawablePaddingPx: 12.0px (4.571429dp)
- inv.numFolderRows: 4
- inv.numFolderColumns: 4
+ numFolderRows: 4
+ numFolderColumns: 4
folderCellWidthPx: 195.0px (74.28571dp)
folderCellHeightPx: 230.0px (87.61905dp)
folderChildIconSizePx: 147.0px (56.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait3Button.txt
index 02bab0e..c0de8d8 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/phonePortrait3Button.txt
@@ -38,8 +38,8 @@
iconSizePx: 147.0px (56.0dp)
iconTextSizePx: 38.0px (14.476191dp)
iconDrawablePaddingPx: 12.0px (4.571429dp)
- inv.numFolderRows: 4
- inv.numFolderColumns: 4
+ numFolderRows: 4
+ numFolderColumns: 4
folderCellWidthPx: 195.0px (74.28571dp)
folderCellHeightPx: 230.0px (87.61905dp)
folderChildIconSizePx: 147.0px (56.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar.txt
index 1ade779..920ba6f 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar.txt
@@ -38,8 +38,8 @@
iconSizePx: 147.0px (56.0dp)
iconTextSizePx: 0.0px (0.0dp)
iconDrawablePaddingPx: 0.0px (0.0dp)
- inv.numFolderRows: 4
- inv.numFolderColumns: 4
+ numFolderRows: 4
+ numFolderColumns: 4
folderCellWidthPx: 163.0px (62.095238dp)
folderCellHeightPx: 192.0px (73.14286dp)
folderChildIconSizePx: 123.0px (46.857143dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar3Button.txt
index b0b745d..65460ec 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/phoneVerticalBar3Button.txt
@@ -38,8 +38,8 @@
iconSizePx: 147.0px (56.0dp)
iconTextSizePx: 0.0px (0.0dp)
iconDrawablePaddingPx: 0.0px (0.0dp)
- inv.numFolderRows: 4
- inv.numFolderColumns: 4
+ numFolderRows: 4
+ numFolderColumns: 4
folderCellWidthPx: 173.0px (65.90476dp)
folderCellHeightPx: 205.0px (78.09524dp)
folderChildIconSizePx: 131.0px (49.904762dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape.txt
index d7f3c1a..1781673 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape.txt
@@ -38,8 +38,8 @@
iconSizePx: 120.0px (60.0dp)
iconTextSizePx: 28.0px (14.0dp)
iconDrawablePaddingPx: 9.0px (4.5dp)
- inv.numFolderRows: 3
- inv.numFolderColumns: 3
+ numFolderRows: 3
+ numFolderColumns: 3
folderCellWidthPx: 240.0px (120.0dp)
folderCellHeightPx: 208.0px (104.0dp)
folderChildIconSizePx: 120.0px (60.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape3Button.txt
index 20a2a99..bd9e267 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletLandscape3Button.txt
@@ -38,8 +38,8 @@
iconSizePx: 120.0px (60.0dp)
iconTextSizePx: 28.0px (14.0dp)
iconDrawablePaddingPx: 9.0px (4.5dp)
- inv.numFolderRows: 3
- inv.numFolderColumns: 3
+ numFolderRows: 3
+ numFolderColumns: 3
folderCellWidthPx: 240.0px (120.0dp)
folderCellHeightPx: 208.0px (104.0dp)
folderChildIconSizePx: 120.0px (60.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt
index 94022e4..e983ef7 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait.txt
@@ -38,8 +38,8 @@
iconSizePx: 120.0px (60.0dp)
iconTextSizePx: 28.0px (14.0dp)
iconDrawablePaddingPx: 9.0px (4.5dp)
- inv.numFolderRows: 3
- inv.numFolderColumns: 3
+ numFolderRows: 3
+ numFolderColumns: 3
folderCellWidthPx: 204.0px (102.0dp)
folderCellHeightPx: 240.0px (120.0dp)
folderChildIconSizePx: 120.0px (60.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt
index 7977204..aa92838 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/tabletPortrait3Button.txt
@@ -38,8 +38,8 @@
iconSizePx: 120.0px (60.0dp)
iconTextSizePx: 28.0px (14.0dp)
iconDrawablePaddingPx: 9.0px (4.5dp)
- inv.numFolderRows: 3
- inv.numFolderColumns: 3
+ numFolderRows: 3
+ numFolderColumns: 3
folderCellWidthPx: 204.0px (102.0dp)
folderCellHeightPx: 240.0px (120.0dp)
folderChildIconSizePx: 120.0px (60.0dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape.txt
index 0b17996..43e4a60 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape.txt
@@ -38,8 +38,8 @@
iconSizePx: 141.0px (53.714287dp)
iconTextSizePx: 34.0px (12.952381dp)
iconDrawablePaddingPx: 13.0px (4.952381dp)
- inv.numFolderRows: 3
- inv.numFolderColumns: 4
+ numFolderRows: 3
+ numFolderColumns: 4
folderCellWidthPx: 189.0px (72.0dp)
folderCellHeightPx: 219.0px (83.42857dp)
folderChildIconSizePx: 141.0px (53.714287dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button.txt
index 71fffe8..e7ea839 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelLandscape3Button.txt
@@ -38,8 +38,8 @@
iconSizePx: 141.0px (53.714287dp)
iconTextSizePx: 34.0px (12.952381dp)
iconDrawablePaddingPx: 13.0px (4.952381dp)
- inv.numFolderRows: 3
- inv.numFolderColumns: 4
+ numFolderRows: 3
+ numFolderColumns: 4
folderCellWidthPx: 189.0px (72.0dp)
folderCellHeightPx: 219.0px (83.42857dp)
folderChildIconSizePx: 141.0px (53.714287dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait.txt
index 5da4ed0..043380c 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait.txt
@@ -38,8 +38,8 @@
iconSizePx: 141.0px (53.714287dp)
iconTextSizePx: 34.0px (12.952381dp)
iconDrawablePaddingPx: 13.0px (4.952381dp)
- inv.numFolderRows: 3
- inv.numFolderColumns: 4
+ numFolderRows: 3
+ numFolderColumns: 4
folderCellWidthPx: 189.0px (72.0dp)
folderCellHeightPx: 219.0px (83.42857dp)
folderChildIconSizePx: 141.0px (53.714287dp)
diff --git a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button.txt b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button.txt
index 359e530..a1b3e95 100644
--- a/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button.txt
+++ b/tests/assets/dumpTests/DeviceProfileDumpTest/twoPanelPortrait3Button.txt
@@ -38,8 +38,8 @@
iconSizePx: 141.0px (53.714287dp)
iconTextSizePx: 34.0px (12.952381dp)
iconDrawablePaddingPx: 13.0px (4.952381dp)
- inv.numFolderRows: 3
- inv.numFolderColumns: 4
+ numFolderRows: 3
+ numFolderColumns: 4
folderCellWidthPx: 189.0px (72.0dp)
folderCellHeightPx: 219.0px (83.42857dp)
folderChildIconSizePx: 141.0px (53.714287dp)
diff --git a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
index e19905f..fcb5158 100644
--- a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -92,8 +92,6 @@
"is-launcher-activity-started";
public static final String REQUEST_FREEZE_APP_LIST = "freeze-app-list";
public static final String REQUEST_UNFREEZE_APP_LIST = "unfreeze-app-list";
- public static final String REQUEST_ENABLE_MANUAL_TASKBAR_STASHING = "enable-taskbar-stashing";
- public static final String REQUEST_DISABLE_MANUAL_TASKBAR_STASHING = "disable-taskbar-stashing";
public static final String REQUEST_ENABLE_BLOCK_TIMEOUT = "enable-block-timeout";
public static final String REQUEST_DISABLE_BLOCK_TIMEOUT = "disable-block-timeout";
public static final String REQUEST_ENABLE_TRANSIENT_TASKBAR = "enable-transient-taskbar";
diff --git a/tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt b/tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
index a421006..30b5663 100644
--- a/tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
+++ b/tests/src/com/android/launcher3/FakeInvariantDeviceProfileTest.kt
@@ -121,8 +121,8 @@
listOf(PointF(16f, 16f), PointF(16f, 16f), PointF(16f, 16f), PointF(16f, 16f))
.toTypedArray()
- numFolderRows = 3
- numFolderColumns = 3
+ numFolderRows = intArrayOf(3, 3, 3, 3)
+ numFolderColumns = intArrayOf(3, 3, 3, 3)
folderStyle = R.style.FolderStyleDefault
inlineNavButtonsEndSpacing = R.dimen.taskbar_button_margin_split
@@ -204,8 +204,8 @@
listOf(PointF(16f, 64f), PointF(64f, 16f), PointF(16f, 64f), PointF(16f, 64f))
.toTypedArray()
- numFolderRows = 3
- numFolderColumns = 3
+ numFolderRows = intArrayOf(3, 3, 3, 3)
+ numFolderColumns = intArrayOf(3, 3, 3, 3)
folderStyle = R.style.FolderStyleDefault
inlineNavButtonsEndSpacing = R.dimen.taskbar_button_margin_6_5
@@ -288,8 +288,8 @@
listOf(PointF(16f, 16f), PointF(16f, 16f), PointF(16f, 20f), PointF(20f, 20f))
.toTypedArray()
- numFolderRows = 3
- numFolderColumns = 3
+ numFolderRows = intArrayOf(3, 3, 3, 3)
+ numFolderColumns = intArrayOf(3, 3, 3, 3)
folderStyle = R.style.FolderStyleDefault
inlineNavButtonsEndSpacing = R.dimen.taskbar_button_margin_split
diff --git a/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllApps.java b/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllApps.java
index 4a42887..b4a5169 100644
--- a/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllApps.java
+++ b/tests/src/com/android/launcher3/allapps/TaplOpenCloseAllApps.java
@@ -208,9 +208,10 @@
InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
READ_DEVICE_CONFIG_PERMISSION);
assumeFalse(FeatureFlags.ENABLE_BACK_SWIPE_LAUNCHER_ANIMATION.get());
- mLauncher.getWorkspace().switchToAllApps();
- mLauncher.pressBack();
- mLauncher.getWorkspace();
+ mLauncher
+ .getWorkspace()
+ .switchToAllApps()
+ .pressBackToWorkspace();
waitForState("Launcher internal state didn't switch to Home", () -> LauncherState.NORMAL);
startAppFast(resolveSystemApp(Intent.CATEGORY_APP_CALCULATOR));
mLauncher.pressBack();
diff --git a/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java b/tests/src/com/android/launcher3/celllayout/TaplReorderWidgetsTest.java
similarity index 98%
rename from tests/src/com/android/launcher3/celllayout/ReorderWidgets.java
rename to tests/src/com/android/launcher3/celllayout/TaplReorderWidgetsTest.java
index 30bde0a..8bdcd03 100644
--- a/tests/src/com/android/launcher3/celllayout/ReorderWidgets.java
+++ b/tests/src/com/android/launcher3/celllayout/TaplReorderWidgetsTest.java
@@ -58,12 +58,12 @@
@SmallTest
@RunWith(AndroidJUnit4.class)
-public class ReorderWidgets extends AbstractLauncherUiTest {
+public class TaplReorderWidgetsTest extends AbstractLauncherUiTest {
@Rule
public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
- private static final String TAG = ReorderWidgets.class.getSimpleName();
+ private static final String TAG = TaplReorderWidgetsTest.class.getSimpleName();
private static final List<String> FOLDABLE_GRIDS = List.of("normal", "practical", "reasonable");
diff --git a/tests/src/com/android/launcher3/compat/PromiseIconUiTest.java b/tests/src/com/android/launcher3/compat/TaplPromiseIconUiTest.java
similarity index 98%
rename from tests/src/com/android/launcher3/compat/PromiseIconUiTest.java
rename to tests/src/com/android/launcher3/compat/TaplPromiseIconUiTest.java
index c5d5de8..8200c94 100644
--- a/tests/src/com/android/launcher3/compat/PromiseIconUiTest.java
+++ b/tests/src/com/android/launcher3/compat/TaplPromiseIconUiTest.java
@@ -41,7 +41,7 @@
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
-public class PromiseIconUiTest extends AbstractLauncherUiTest {
+public class TaplPromiseIconUiTest extends AbstractLauncherUiTest {
private int mSessionId = -1;
diff --git a/tests/src/com/android/launcher3/model/FolderIconLoadTest.kt b/tests/src/com/android/launcher3/model/FolderIconLoadTest.kt
new file mode 100644
index 0000000..60a4d2d
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/FolderIconLoadTest.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.launcher3.LauncherAppState
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.LauncherLayoutBuilder
+import com.android.launcher3.util.LauncherModelHelper
+import com.android.launcher3.util.LauncherModelHelper.*
+import com.android.launcher3.util.TestUtil
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import java.util.concurrent.CountDownLatch
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/** Tests to verify that folder icons are loaded with appropriate resolution */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FolderIconLoadTest {
+ private lateinit var modelHelper: LauncherModelHelper
+
+ private val uniqueActivities =
+ listOf(
+ TEST_ACTIVITY,
+ TEST_ACTIVITY2,
+ TEST_ACTIVITY3,
+ TEST_ACTIVITY4,
+ TEST_ACTIVITY5,
+ TEST_ACTIVITY6,
+ TEST_ACTIVITY7,
+ TEST_ACTIVITY8,
+ TEST_ACTIVITY9,
+ TEST_ACTIVITY10,
+ TEST_ACTIVITY11,
+ TEST_ACTIVITY12,
+ TEST_ACTIVITY13,
+ TEST_ACTIVITY14
+ )
+
+ @Before
+ fun setUp() {
+ modelHelper = LauncherModelHelper()
+ }
+
+ @After
+ @Throws(Exception::class)
+ fun tearDown() {
+ modelHelper.destroy()
+ TestUtil.uninstallDummyApp()
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun folderLoadedWithHighRes_2x2() {
+ val items = setupAndLoadFolder(4)
+ assertThat(items.size).isEqualTo(4)
+ verifyHighRes(items, 0, 1, 2, 3)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun folderLoadedWithHighRes_3x2() {
+ val items = setupAndLoadFolder(6)
+ assertThat(items.size).isEqualTo(6)
+ verifyHighRes(items, 0, 1, 3, 4)
+ verifyLowRes(items, 2, 5)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun folderLoadedWithHighRes_max_3x3() {
+ val idp = LauncherAppState.getIDP(modelHelper.sandboxContext)
+ idp.numFolderColumns = intArrayOf(3, 3, 3, 3)
+ idp.numFolderRows = intArrayOf(3, 3, 3, 3)
+ recreateSupportedDeviceProfiles()
+
+ val items = setupAndLoadFolder(14)
+ verifyHighRes(items, 0, 1, 3, 4)
+ verifyLowRes(items, 2, 5, 6, 7, 8, 9, 10, 11, 12, 13)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun folderLoadedWithHighRes_max_4x4() {
+ val idp = LauncherAppState.getIDP(modelHelper.sandboxContext)
+ idp.numFolderColumns = intArrayOf(4, 4, 4, 4)
+ idp.numFolderRows = intArrayOf(4, 4, 4, 4)
+ recreateSupportedDeviceProfiles()
+
+ val items = setupAndLoadFolder(14)
+ verifyHighRes(items, 0, 1, 4, 5)
+ verifyLowRes(items, 2, 3, 6, 7, 8, 9, 10, 11, 12, 13)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun folderLoadedWithHighRes_differentFolderConfigurations() {
+ val idp = LauncherAppState.getIDP(modelHelper.sandboxContext)
+ idp.numFolderColumns = intArrayOf(4, 3, 4, 4)
+ idp.numFolderRows = intArrayOf(4, 3, 4, 4)
+ recreateSupportedDeviceProfiles()
+
+ val items = setupAndLoadFolder(14)
+ verifyHighRes(items, 0, 1, 3, 4, 5)
+ verifyLowRes(items, 2, 6, 7, 8, 9, 10, 11, 12, 13)
+ }
+
+ @Throws(Exception::class)
+ private fun setupAndLoadFolder(itemCount: Int): ArrayList<WorkspaceItemInfo> {
+ val builder =
+ LauncherLayoutBuilder()
+ .atWorkspace(0, 0, 1)
+ .putFolder("Sample")
+ .apply {
+ for (i in 0..itemCount - 1) this.addApp(TEST_PACKAGE, uniqueActivities[i])
+ }
+ .build()
+
+ modelHelper.setupDefaultLayoutProvider(builder)
+ modelHelper.loadModelSync()
+
+ // The first load initializes the DB, load again so that icons are now used from the DB
+ // Wait for the icon cache to be updated and then reload
+ val app = LauncherAppState.getInstance(modelHelper.sandboxContext)
+ val cache = app.iconCache
+ while (cache.isIconUpdateInProgress) {
+ val wait = CountDownLatch(1)
+ Executors.MODEL_EXECUTOR.handler.postDelayed({ wait.countDown() }, 10)
+ wait.await()
+ }
+ TestUtil.runOnExecutorSync(Executors.MODEL_EXECUTOR) { cache.clearMemoryCache() }
+ // Reload again with correct icon state
+ app.model.forceReload()
+ modelHelper.loadModelSync()
+ val folders = modelHelper.getBgDataModel().folders
+
+ assertThat(folders.size()).isEqualTo(1)
+ assertThat(folders.valueAt(0).contents.size).isEqualTo(itemCount)
+ return folders.valueAt(0).contents
+ }
+
+ private fun verifyHighRes(items: ArrayList<WorkspaceItemInfo>, vararg indices: Int) {
+ for (index in indices) {
+ assertWithMessage("Index $index was not highRes")
+ .that(items[index].bitmap.isNullOrLowRes)
+ .isFalse()
+ }
+ }
+
+ private fun verifyLowRes(items: ArrayList<WorkspaceItemInfo>, vararg indices: Int) {
+ for (index in indices) {
+ assertWithMessage("Index $index was not lowRes")
+ .that(items[index].bitmap.isNullOrLowRes)
+ .isTrue()
+ }
+ }
+
+ /** Recreate DeviceProfiles after changing InvariantDeviceProfile */
+ private fun recreateSupportedDeviceProfiles() {
+ LauncherAppState.getIDP(modelHelper.sandboxContext).supportedProfiles =
+ LauncherAppState.getIDP(modelHelper.sandboxContext).supportedProfiles.map {
+ it.copy(modelHelper.sandboxContext)
+ }
+ }
+}
diff --git a/tests/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncherTest.java b/tests/src/com/android/launcher3/secondarydisplay/TaplSecondaryDisplayLauncherTest.java
similarity index 99%
rename from tests/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncherTest.java
rename to tests/src/com/android/launcher3/secondarydisplay/TaplSecondaryDisplayLauncherTest.java
index c7431f2..d7b9638 100644
--- a/tests/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncherTest.java
+++ b/tests/src/com/android/launcher3/secondarydisplay/TaplSecondaryDisplayLauncherTest.java
@@ -47,7 +47,7 @@
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
-public final class SecondaryDisplayLauncherTest extends AbstractLauncherUiTest {
+public final class TaplSecondaryDisplayLauncherTest extends AbstractLauncherUiTest {
private static final int WAIT_TIME_MS = 5000;
private static final int LONG_PRESS_DURATION_MS = 1000;
private static final int DRAG_TIME_MS = 160;
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 8ad2249..79d8c60 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -131,6 +131,8 @@
/** Detects activity leaks and throws an exception if a leak is found. */
public static void checkDetectedLeaks(LauncherInstrumentation launcher,
boolean requireOneActiveActivityUnused) {
+ if (TestStabilityRule.isPresubmit()) return; // b/313501215
+
final boolean requireOneActiveActivity =
false; // workaround for leaks when there is an unexpected Recents activity
@@ -255,7 +257,7 @@
final RuleChain inner = RuleChain
.outerRule(new PortraitLandscapeRunner(this))
.around(new FailureWatcher(mLauncher, viewCaptureRule::getViewCaptureData))
- .around(viewCaptureRule)
+ // .around(viewCaptureRule) // b/315482167
.around(new TestIsolationRule(mLauncher, true));
return TestHelpers.isInLauncherProcess()
diff --git a/tests/src/com/android/launcher3/ui/WorkProfileTest.java b/tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java
similarity index 99%
rename from tests/src/com/android/launcher3/ui/WorkProfileTest.java
rename to tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java
index 485ef94..f818564 100644
--- a/tests/src/com/android/launcher3/ui/WorkProfileTest.java
+++ b/tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java
@@ -48,7 +48,7 @@
import java.util.Objects;
import java.util.function.Predicate;
-public class WorkProfileTest extends AbstractLauncherUiTest {
+public class TaplWorkProfileTest extends AbstractLauncherUiTest {
private static final int WORK_PAGE = ActivityAllAppsContainerView.AdapterHolder.WORK;
diff --git a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java
similarity index 98%
rename from tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
rename to tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java
index 9fdd21a..d96287f 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddConfigWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplAddConfigWidgetTest.java
@@ -52,7 +52,7 @@
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
-public class AddConfigWidgetTest extends AbstractLauncherUiTest {
+public class TaplAddConfigWidgetTest extends AbstractLauncherUiTest {
@Rule
public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
@@ -82,6 +82,7 @@
runTest(false);
}
+
/**
* @param acceptConfig accept the config activity
*/
diff --git a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
similarity index 98%
rename from tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
rename to tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
index fd4b7f1..27fda9b 100644
--- a/tests/src/com/android/launcher3/ui/widget/AddWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplAddWidgetTest.java
@@ -42,7 +42,7 @@
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
-public class AddWidgetTest extends AbstractLauncherUiTest {
+public class TaplAddWidgetTest extends AbstractLauncherUiTest {
@Rule
public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
diff --git a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java b/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java
similarity index 99%
rename from tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
rename to tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java
index 32793cc..6aa746d 100644
--- a/tests/src/com/android/launcher3/ui/widget/BindWidgetTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplBindWidgetTest.java
@@ -76,7 +76,7 @@
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
-public class BindWidgetTest extends AbstractLauncherUiTest {
+public class TaplBindWidgetTest extends AbstractLauncherUiTest {
@Rule
public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
diff --git a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java b/tests/src/com/android/launcher3/ui/widget/TaplRequestPinItemTest.java
similarity index 99%
rename from tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
rename to tests/src/com/android/launcher3/ui/widget/TaplRequestPinItemTest.java
index 3c88f1d..f12f961 100644
--- a/tests/src/com/android/launcher3/ui/widget/RequestPinItemTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplRequestPinItemTest.java
@@ -59,7 +59,7 @@
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
-public class RequestPinItemTest extends AbstractLauncherUiTest {
+public class TaplRequestPinItemTest extends AbstractLauncherUiTest {
@Rule
public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
diff --git a/tests/src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java b/tests/src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java
index 465e9b4..bc73683 100644
--- a/tests/src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java
+++ b/tests/src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java
@@ -15,10 +15,6 @@
*/
package com.android.launcher3.ui.widget;
-import static com.android.launcher3.ui.AbstractLauncherUiTest.initialize;
-import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL;
-import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT;
-
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@@ -31,7 +27,6 @@
import com.android.launcher3.ui.AbstractLauncherUiTest;
import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
-import com.android.launcher3.util.rule.TestStabilityRule.Stability;
import com.android.launcher3.widget.picker.WidgetsFullSheet;
import com.android.launcher3.widget.picker.WidgetsRecyclerView;
@@ -65,11 +60,9 @@
* Open Widget picker, make sure the widget picker can scroll and then go to home screen.
*/
@Test
- @Stability(flavors = LOCAL | PLATFORM_POSTSUBMIT) // b/303263644
@ScreenRecord
@PortraitLandscape
public void testWidgets() {
- // Testing if this will fix b/303263644
mLauncher.goHome();
// Test opening widgets.
executeOnLauncher(launcher ->
diff --git a/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java
similarity index 98%
rename from tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
rename to tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java
index e21918f..a3d3344 100644
--- a/tests/src/com/android/launcher3/ui/workspace/ThemeIconsTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/TaplThemeIconsTest.java
@@ -49,7 +49,7 @@
* Note running these tests will clear the workspace on the device.
*/
@LargeTest
-public class ThemeIconsTest extends AbstractLauncherUiTest {
+public class TaplThemeIconsTest extends AbstractLauncherUiTest {
private static final String APP_NAME = "IconThemedActivity";
private static final String SHORTCUT_NAME = "Shortcut 1";
diff --git a/tests/src/com/android/launcher3/ui/workspace/TwoPanelWorkspaceTest.java b/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
similarity index 99%
rename from tests/src/com/android/launcher3/ui/workspace/TwoPanelWorkspaceTest.java
rename to tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
index e7112d1..3693163 100644
--- a/tests/src/com/android/launcher3/ui/workspace/TwoPanelWorkspaceTest.java
+++ b/tests/src/com/android/launcher3/ui/workspace/TaplTwoPanelWorkspaceTest.java
@@ -54,7 +54,7 @@
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
-public class TwoPanelWorkspaceTest extends AbstractLauncherUiTest {
+public class TaplTwoPanelWorkspaceTest extends AbstractLauncherUiTest {
private AutoCloseable mLauncherLayout;
diff --git a/tests/src/com/android/launcher3/util/LauncherModelHelper.java b/tests/src/com/android/launcher3/util/LauncherModelHelper.java
index 261436b..244dc26 100644
--- a/tests/src/com/android/launcher3/util/LauncherModelHelper.java
+++ b/tests/src/com/android/launcher3/util/LauncherModelHelper.java
@@ -84,6 +84,17 @@
public static final String TEST_ACTIVITY = "com.android.launcher3.tests.Activity2";
public static final String TEST_ACTIVITY2 = "com.android.launcher3.tests.Activity3";
public static final String TEST_ACTIVITY3 = "com.android.launcher3.tests.Activity4";
+ public static final String TEST_ACTIVITY4 = "com.android.launcher3.tests.Activity5";
+ public static final String TEST_ACTIVITY5 = "com.android.launcher3.tests.Activity6";
+ public static final String TEST_ACTIVITY6 = "com.android.launcher3.tests.Activity7";
+ public static final String TEST_ACTIVITY7 = "com.android.launcher3.tests.Activity8";
+ public static final String TEST_ACTIVITY8 = "com.android.launcher3.tests.Activity9";
+ public static final String TEST_ACTIVITY9 = "com.android.launcher3.tests.Activity10";
+ public static final String TEST_ACTIVITY10 = "com.android.launcher3.tests.Activity11";
+ public static final String TEST_ACTIVITY11 = "com.android.launcher3.tests.Activity12";
+ public static final String TEST_ACTIVITY12 = "com.android.launcher3.tests.Activity13";
+ public static final String TEST_ACTIVITY13 = "com.android.launcher3.tests.Activity14";
+ public static final String TEST_ACTIVITY14 = "com.android.launcher3.tests.Activity15";
// Authority for providing a test default-workspace-layout data.
private static final String TEST_PROVIDER_AUTHORITY =
diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java
index 51b7b18..dfccffc1 100644
--- a/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java
@@ -33,17 +33,17 @@
private static final IgnoreNode IGNORED_NODES_ROOT = buildIgnoreNodesTree(List.of(
CONTENT
- + "AddItemDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id"
+ + "SimpleDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id"
+ "/add_item_bottom_sheet|LinearLayout:id/add_item_bottom_sheet_content"
+ "|ScrollView:id/widget_preview_scroll_view|WidgetCell:id/widget_cell"
+ "|WidgetCellPreview:id/widget_preview_container|ImageView:id/widget_badge",
CONTENT
- + "AddItemDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id"
+ + "SimpleDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id"
+ "/add_item_bottom_sheet|LinearLayout:id/add_item_bottom_sheet_content"
+ "|ScrollView:id/widget_preview_scroll_view|WidgetCell:id/widget_cell"
+ "|WidgetCellPreview:id/widget_preview_container|WidgetCell$1|FrameLayout"
+ "|ImageView:id/icon",
- CONTENT + "AddItemDragLayer:id/add_item_drag_layer|View",
+ CONTENT + "SimpleDragLayer:id/add_item_drag_layer|View",
DRAG_LAYER
+ "AppWidgetResizeFrame|FrameLayout|ImageButton:id/widget_reconfigure_button",
DRAG_LAYER
diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/FlashDetector.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/FlashDetector.java
index e333074..fc8f818 100644
--- a/tests/src/com/android/launcher3/util/viewcapture_analysis/FlashDetector.java
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/FlashDetector.java
@@ -44,13 +44,13 @@
+ "LauncherAllAppsContainerView:id/apps_view|AllAppsRecyclerView:id"
+ "/apps_list_view|BubbleTextView:id/icon",
CONTENT
- + "AddItemDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id"
+ + "SimpleDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id"
+ "/add_item_bottom_sheet|LinearLayout:id/add_item_bottom_sheet_content"
+ "|ScrollView:id/widget_preview_scroll_view|WidgetCell:id/widget_cell"
+ "|WidgetCellPreview:id/widget_preview_container|WidgetImageView:id"
+ "/widget_preview",
CONTENT
- + "AddItemDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id"
+ + "SimpleDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id"
+ "/add_item_bottom_sheet|LinearLayout:id/add_item_bottom_sheet_content"
+ "|ScrollView:id/widget_preview_scroll_view|WidgetCell:id/widget_cell"
+ "|WidgetCellPreview:id/widget_preview_container|ImageView:id/widget_badge",
diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/PositionJumpDetector.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/PositionJumpDetector.java
index 5f2c68c..88ace68 100644
--- a/tests/src/com/android/launcher3/util/viewcapture_analysis/PositionJumpDetector.java
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/PositionJumpDetector.java
@@ -41,7 +41,7 @@
DRAG_LAYER + "AppWidgetResizeFrame",
DRAG_LAYER + "LauncherAllAppsContainerView:id/apps_view",
CONTENT
- + "AddItemDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id"
+ + "SimpleDragLayer:id/add_item_drag_layer|AddItemWidgetsBottomSheet:id"
+ "/add_item_bottom_sheet|LinearLayout:id/add_item_bottom_sheet_content",
DRAG_LAYER + "WidgetsTwoPaneSheet|SpringRelativeLayout:id/container",
DRAG_LAYER + "WidgetsFullSheet|SpringRelativeLayout:id/container",
diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java
index ba02473..b27ccbf 100644
--- a/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java
@@ -35,7 +35,7 @@
// All detectors. They will be invoked in the order listed here.
private static final AnomalyDetector[] ANOMALY_DETECTORS = {
- new AlphaJumpDetector(),
+// new AlphaJumpDetector(), // b/309014345
// new FlashDetector(), // b/309014345
new PositionJumpDetector()
};
diff --git a/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java b/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
index d9b179c..9ca2dc8 100644
--- a/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
+++ b/tests/tapl/com/android/launcher3/tapl/HomeAllApps.java
@@ -118,4 +118,17 @@
public boolean isHomeState() {
return true;
}
+
+ /** Send the "back" gesture to go to workspace. */
+ public Workspace pressBackToWorkspace() {
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+ "want to press back from all apps to workspace")) {
+ mLauncher.runToState(
+ () -> mLauncher.pressBackImpl(),
+ NORMAL_STATE_ORDINAL,
+ "pressing back");
+ return new Workspace(mLauncher);
+ }
+ }
}
diff --git a/tests/tapl/com/android/launcher3/tapl/Launchable.java b/tests/tapl/com/android/launcher3/tapl/Launchable.java
index 2512175..fe927b3 100644
--- a/tests/tapl/com/android/launcher3/tapl/Launchable.java
+++ b/tests/tapl/com/android/launcher3/tapl/Launchable.java
@@ -65,11 +65,9 @@
+ mLauncher.getVisibleBounds(mObject));
if (launcherStopsAfterLaunch()) {
- mLauncher.executeAndWaitForLauncherEvent(
+ mLauncher.executeAndWaitForLauncherStop(
() -> mLauncher.clickLauncherObject(mObject),
- event -> TestProtocol.LAUNCHER_ACTIVITY_STOPPED_MESSAGE
- .equals(event.getClassName().toString()),
- () -> "Launcher activity didn't stop", "clicking the launchable");
+ "clicking the launchable");
} else {
mLauncher.clickLauncherObject(mObject);
}
diff --git a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
index 6d58a35..184ece7 100644
--- a/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
+++ b/tests/tapl/com/android/launcher3/tapl/LaunchedAppState.java
@@ -340,4 +340,17 @@
}
}
}
+
+ /** Send the "back" gesture to go to workspace. */
+ public Workspace pressBackToWorkspace() {
+ try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
+ LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
+ "want to press back from launched app to workspace")) {
+ mLauncher.executeAndWaitForWallpaperAnimation(
+ () -> mLauncher.pressBackImpl(),
+ "pressing back"
+ );
+ return new Workspace(mLauncher);
+ }
+ }
}
diff --git a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index cbf1952..91ef472 100644
--- a/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -586,6 +586,7 @@
if (hasLauncherObject(WORKSPACE_RES_ID)) return "Workspace";
if (hasLauncherObject(APPS_RES_ID)) return "AllApps";
if (hasLauncherObject(TASKBAR_RES_ID)) return "Taskbar";
+ if (hasLauncherObject("wallpaper_carousel")) return "Launcher Settings Popup";
if (mDevice.hasObject(By.pkg(getLauncherPackageName()).depth(0))) {
return "<Launcher in invalid state>";
}
@@ -973,6 +974,14 @@
}
}
+ void executeAndWaitForLauncherStop(Runnable command, String actionName) {
+ executeAndWaitForLauncherEvent(
+ () -> command.run(),
+ event -> TestProtocol.LAUNCHER_ACTIVITY_STOPPED_MESSAGE
+ .equals(event.getClassName().toString()),
+ () -> "Launcher activity didn't stop", actionName);
+ }
+
/**
* Get the resource ID of visible floating view.
*/
@@ -1112,30 +1121,34 @@
*/
public void pressBack() {
try (Closable e = eventsCheck(); Closable c = addContextLayer("want to press back")) {
- waitForLauncherInitialized();
- final boolean launcherVisible =
- isTablet() ? isLauncherContainerVisible() : isLauncherVisible();
- boolean isThreeFingerTrackpadGesture =
- mTrackpadGestureType == TrackpadGestureType.THREE_FINGER;
- if (getNavigationModel() == NavigationModel.ZERO_BUTTON
- || isThreeFingerTrackpadGesture) {
- final Point displaySize = getRealDisplaySize();
- // TODO(b/225505986): change startY and endY back to displaySize.y / 2 once the
- // issue is solved.
- int startX = isThreeFingerTrackpadGesture ? displaySize.x / 4 : 0;
- int endX = isThreeFingerTrackpadGesture ? displaySize.x * 3 / 4 : displaySize.x / 2;
- linearGesture(startX, displaySize.y / 4, endX, displaySize.y / 4,
- 10, false, GestureScope.DONT_EXPECT_PILFER);
+ pressBackImpl();
+ }
+ }
+
+ void pressBackImpl() {
+ waitForLauncherInitialized();
+ final boolean launcherVisible =
+ isTablet() ? isLauncherContainerVisible() : isLauncherVisible();
+ boolean isThreeFingerTrackpadGesture =
+ mTrackpadGestureType == TrackpadGestureType.THREE_FINGER;
+ if (getNavigationModel() == NavigationModel.ZERO_BUTTON
+ || isThreeFingerTrackpadGesture) {
+ final Point displaySize = getRealDisplaySize();
+ // TODO(b/225505986): change startY and endY back to displaySize.y / 2 once the
+ // issue is solved.
+ int startX = isThreeFingerTrackpadGesture ? displaySize.x / 4 : 0;
+ int endX = isThreeFingerTrackpadGesture ? displaySize.x * 3 / 4 : displaySize.x / 2;
+ linearGesture(startX, displaySize.y / 4, endX, displaySize.y / 4,
+ 10, false, GestureScope.DONT_EXPECT_PILFER);
+ } else {
+ waitForNavigationUiObject("back").click();
+ }
+ if (launcherVisible) {
+ if (getContext().getApplicationInfo().isOnBackInvokedCallbackEnabled()) {
+ expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ON_BACK_INVOKED);
} else {
- waitForNavigationUiObject("back").click();
- }
- if (launcherVisible) {
- if (getContext().getApplicationInfo().isOnBackInvokedCallbackEnabled()) {
- expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_ON_BACK_INVOKED);
- } else {
- expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_KEY_BACK_DOWN);
- expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_KEY_BACK_UP);
- }
+ expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_KEY_BACK_DOWN);
+ expectEvent(TestProtocol.SEQUENCE_MAIN, EVENT_KEY_BACK_UP);
}
}
}
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
index 068482e..f383e99 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTask.java
@@ -201,11 +201,8 @@
public LaunchedAppState open() {
try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck()) {
verifyActiveContainer();
- mLauncher.executeAndWaitForLauncherEvent(
+ mLauncher.executeAndWaitForLauncherStop(
() -> mLauncher.clickLauncherObject(mTask),
- event -> TestProtocol.LAUNCHER_ACTIVITY_STOPPED_MESSAGE
- .equals(event.getClassName().toString()),
- () -> "Launcher activity didn't stop",
"clicking an overview task");
if (mOverview.getContainerType()
== LauncherInstrumentation.ContainerType.SPLIT_SCREEN_SELECT) {
diff --git a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
index 4a0131b..3d2914d 100644
--- a/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
+++ b/tests/tapl/com/android/launcher3/tapl/OverviewTaskMenu.java
@@ -20,8 +20,6 @@
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.UiObject2;
-import com.android.launcher3.testing.shared.TestProtocol;
-
/** Represents the menu of an overview task. */
public class OverviewTaskMenu {
@@ -61,12 +59,9 @@
try (LauncherInstrumentation.Closable e = mLauncher.eventsCheck();
LauncherInstrumentation.Closable c = mLauncher.addContextLayer(
"before tapping the app info menu item")) {
- mLauncher.executeAndWaitForLauncherEvent(
+ mLauncher.executeAndWaitForLauncherStop(
() -> mLauncher.clickLauncherObject(
mLauncher.findObjectInContainer(mMenu, By.text("App info"))),
- event -> TestProtocol.LAUNCHER_ACTIVITY_STOPPED_MESSAGE
- .equals(event.getClassName().toString()),
- () -> "Launcher activity didn't stop",
"tapped app info menu item");
try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index 2a2a83f..f8fa00c 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -335,7 +335,8 @@
homeAppIcon,
() -> new Point(0, 0),
() -> mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT),
- null);
+ null,
+ /* startsActivity = */ false);
try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
"dragged the app across workspace")) {
@@ -359,7 +360,8 @@
homeAppIcon,
() -> getDropPointFromDropTargetBar(mLauncher, DELETE_TARGET_TEXT_ID),
() -> mLauncher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT),
- /* expectDropEvents= */ null);
+ /* expectDropEvents= */ null,
+ /* startsActivity = */ false);
try (LauncherInstrumentation.Closable c1 = mLauncher.addContextLayer(
"dragged the app to the drop bar")) {
@@ -368,7 +370,6 @@
}
}
-
/**
* Uninstall the appIcon by dragging it to the 'uninstall' drop point of the drop_target_bar.
*
@@ -390,7 +391,8 @@
homeAppIcon,
() -> getDropPointFromDropTargetBar(launcher, UNINSTALL_TARGET_TEXT_ID),
expectLongClickEvents,
- /* expectDropEvents= */null);
+ /* expectDropEvents= */null,
+ /* startsActivity = */ false);
launcher.waitUntilLauncherObjectGone(DROP_BAR_RES_ID);
@@ -464,14 +466,25 @@
o -> new FolderIcon(mLauncher, o)).collect(Collectors.toList());
}
+ private static void sendUp(LauncherInstrumentation launcher, Point dest,
+ long downTime) {
+ launcher.sendPointer(
+ downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, dest,
+ LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
+ }
+
private static void dropDraggedIcon(LauncherInstrumentation launcher, Point dest, long downTime,
- @Nullable Runnable expectedEvents) {
- launcher.runToState(
- () -> launcher.sendPointer(
- downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, dest,
- LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER),
- NORMAL_STATE_ORDINAL,
- "sending UP event");
+ @Nullable Runnable expectedEvents, boolean startsActivity) {
+ if (startsActivity) {
+ launcher.executeAndWaitForLauncherStop(
+ () -> sendUp(launcher, dest, downTime),
+ "sending UP event");
+ } else {
+ launcher.runToState(
+ () -> sendUp(launcher, dest, downTime),
+ NORMAL_STATE_ORDINAL,
+ "sending UP event");
+ }
if (expectedEvents != null) {
expectedEvents.run();
}
@@ -488,7 +501,8 @@
LauncherInstrumentation.EVENT_START);
}
dragIconToWorkspace(
- launcher, launchable, dest, expectLongClickEvents, expectDropEvents);
+ launcher, launchable, dest, expectLongClickEvents, expectDropEvents,
+ startsActivity);
}
static void dragIconToWorkspaceCellPosition(LauncherInstrumentation launcher,
@@ -517,7 +531,8 @@
destSupplier,
/* isDecelerating= */ false,
() -> launcher.expectEvent(TestProtocol.SEQUENCE_MAIN, LONG_CLICK_EVENT),
- /* expectDropEvents= */ null);
+ /* expectDropEvents= */ null,
+ /* startsActivity = */ false);
}
static void dragIconToWorkspace(
@@ -525,9 +540,10 @@
Launchable launchable,
Supplier<Point> dest,
Runnable expectLongClickEvents,
- @Nullable Runnable expectDropEvents) {
+ @Nullable Runnable expectDropEvents,
+ boolean startsActivity) {
dragIconToWorkspace(launcher, launchable, dest, /* isDecelerating */ true,
- expectLongClickEvents, expectDropEvents);
+ expectLongClickEvents, expectDropEvents, startsActivity);
}
static void dragIconToWorkspace(
@@ -536,7 +552,8 @@
Supplier<Point> dest,
boolean isDecelerating,
Runnable expectLongClickEvents,
- @Nullable Runnable expectDropEvents) {
+ @Nullable Runnable expectDropEvents,
+ boolean startsActivity) {
try (LauncherInstrumentation.Closable ignored = launcher.addContextLayer(
"want to drag icon to workspace")) {
final long downTime = SystemClock.uptimeMillis();
@@ -568,7 +585,7 @@
launcher.movePointer(dragStart, targetDest, DEFAULT_DRAG_STEPS, isDecelerating,
downTime, SystemClock.uptimeMillis(), false,
LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
- dropDraggedIcon(launcher, targetDest, downTime, expectDropEvents);
+ dropDraggedIcon(launcher, targetDest, downTime, expectDropEvents, startsActivity);
}
}
@@ -601,7 +618,8 @@
launcher.movePointer(dragStart, targetDest, DEFAULT_DRAG_STEPS, isDecelerating,
downTime, SystemClock.uptimeMillis(), false,
LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
- dropDraggedIcon(launcher, targetDest, downTime, expectDropEvents);
+ dropDraggedIcon(launcher, targetDest, downTime, expectDropEvents,
+ /* startsActivity = */ false);
}
}
@@ -667,7 +685,8 @@
launcher.movePointer(dragStart, targetDest, DEFAULT_DRAG_STEPS, true,
downTime, SystemClock.uptimeMillis(), false,
LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
- dropDraggedIcon(launcher, targetDest, downTime, expectDropEvents);
+ dropDraggedIcon(launcher, targetDest, downTime, expectDropEvents,
+ /* startsActivity = */ false);
}
/**
diff --git a/tests/tapl/com/android/launcher3/tapl/WorkspaceDragSource.java b/tests/tapl/com/android/launcher3/tapl/WorkspaceDragSource.java
index 5a4d562..e5a2a2e 100644
--- a/tests/tapl/com/android/launcher3/tapl/WorkspaceDragSource.java
+++ b/tests/tapl/com/android/launcher3/tapl/WorkspaceDragSource.java
@@ -81,7 +81,8 @@
launchable,
dest,
launchable::addExpectedEventsForLongClick,
- /*expectDropEvents= */ null);
+ /*expectDropEvents= */ null,
+ /* startsActivity = */ false);
try (LauncherInstrumentation.Closable ignore = launcher.addContextLayer("dragged")) {
WorkspaceAppIcon appIcon =