Merge "Do not enter stage split from keyboard shortcuts if the user is already in split" into main
diff --git a/Android.bp b/Android.bp
index 75902c8..39b1ae0 100644
--- a/Android.bp
+++ b/Android.bp
@@ -175,6 +175,7 @@
"androidx.preference_preference",
"androidx.slice_slice-view",
"androidx.cardview_cardview",
+ "androidx.window_window",
"com.google.android.material_material",
"iconloader_base",
"view_capture",
diff --git a/AndroidManifest-common.xml b/AndroidManifest-common.xml
index 7e824ec..a31ee80 100644
--- a/AndroidManifest-common.xml
+++ b/AndroidManifest-common.xml
@@ -184,5 +184,9 @@
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />
+
+ <property
+ android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
+ android:value="true" />
</application>
</manifest>
diff --git a/OWNERS b/OWNERS
index b8aae78..4409b33 100644
--- a/OWNERS
+++ b/OWNERS
@@ -25,5 +25,10 @@
andonian@google.com
sihua@google.com
+# Multitasking eng team
+tracyzhou@google.com
+peanutbutter@google.com
+jeremysim@google.com
+
per-file FeatureFlags.java, globs = set noparent
per-file FeatureFlags.java = sunnygoyal@google.com, winsonc@google.com, adamcohen@google.com, hyunyoungs@google.com, captaincole@google.com
diff --git a/aconfig/launcher.aconfig b/aconfig/launcher.aconfig
index 98dd4d6..defb0e6 100644
--- a/aconfig/launcher.aconfig
+++ b/aconfig/launcher.aconfig
@@ -85,6 +85,13 @@
}
flag {
+ name: "enable_two_pane_launcher_settings"
+ namespace: "launcher"
+ description: "Enables two panel settings when on large enough displays"
+ bug: "204463748"
+}
+
+flag {
name: "enable_shortcut_dont_suggest_app"
namespace: "launcher"
description: "Enables don't suggest app shortcut for suggested apps"
@@ -126,3 +133,10 @@
description: "Enables asnc inflation of workspace icons"
bug: "318539160"
}
+
+flag {
+ name: "enable_unfold_state_animation"
+ namespace: "launcher"
+ description: "Tie unfold animation with state animation"
+ bug: "297057373"
+}
diff --git a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
index f012197..e680ea9 100644
--- a/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
+++ b/quickstep/src/com/android/launcher3/appprediction/PredictionRowView.java
@@ -72,6 +72,8 @@
private boolean mPredictionsEnabled = false;
+ private boolean mPredictionUiUpdatePaused = false;
+
public PredictionRowView(@NonNull Context context) {
this(context, null);
}
@@ -193,7 +195,18 @@
applyPredictionApps();
}
+ /** Pause the prediction row UI update */
+ public void setPredictionUiUpdatePaused(boolean predictionUiUpdatePaused) {
+ mPredictionUiUpdatePaused = predictionUiUpdatePaused;
+ if (!mPredictionUiUpdatePaused) {
+ applyPredictionApps();
+ }
+ }
+
private void applyPredictionApps() {
+ if (mPredictionUiUpdatePaused) {
+ return;
+ }
if (getChildCount() != mNumPredictedAppsPerRow) {
while (getChildCount() > mNumPredictedAppsPerRow) {
removeViewAt(0);
diff --git a/quickstep/src/com/android/launcher3/proxy/ProxyActivityStarter.java b/quickstep/src/com/android/launcher3/proxy/ProxyActivityStarter.java
index 6d90b035..212a5ff 100644
--- a/quickstep/src/com/android/launcher3/proxy/ProxyActivityStarter.java
+++ b/quickstep/src/com/android/launcher3/proxy/ProxyActivityStarter.java
@@ -53,13 +53,10 @@
try {
if (mParams.intent != null) {
- startActivityForResult(mParams.intent, mParams.requestCode, mParams.options);
+ startActivity();
return;
} else if (mParams.intentSender != null) {
- startIntentSenderForResult(mParams.intentSender, mParams.requestCode,
- mParams.fillInIntent, mParams.flagsMask, mParams.flagsValues,
- mParams.extraFlags,
- mParams.options);
+ startIntentSender();
return;
}
} catch (NullPointerException | ActivityNotFoundException | SecurityException
@@ -83,4 +80,26 @@
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
}
+
+ private void startActivity() throws SendIntentException {
+ if (mParams.requireActivityResult) {
+ startActivityForResult(mParams.intent, mParams.requestCode, mParams.options);
+ } else {
+ startActivity(mParams.intent, mParams.options);
+ finishAndRemoveTask();
+ }
+ }
+
+ private void startIntentSender() throws SendIntentException {
+ if (mParams.requireActivityResult) {
+ startIntentSenderForResult(mParams.intentSender, mParams.requestCode,
+ mParams.fillInIntent, mParams.flagsMask, mParams.flagsValues,
+ mParams.extraFlags,
+ mParams.options);
+ } else {
+ startIntentSender(mParams.intentSender, mParams.fillInIntent, mParams.flagsMask,
+ mParams.flagsValues, mParams.extraFlags, mParams.options);
+ finishAndRemoveTask();
+ }
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
index 33641a4..7c7c426 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java
@@ -22,6 +22,7 @@
import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
import static com.android.launcher3.BaseActivity.EVENT_DESTROYED;
+import static com.android.launcher3.Flags.enableUnfoldStateAnimation;
import static com.android.launcher3.LauncherState.OVERVIEW;
import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
import static com.android.launcher3.config.FeatureFlags.enableTaskbarNoRecreate;
@@ -405,8 +406,12 @@
*/
private UnfoldTransitionProgressProvider getUnfoldTransitionProgressProviderForActivity(
StatefulActivity activity) {
- if (activity instanceof QuickstepLauncher) {
- return ((QuickstepLauncher) activity).getUnfoldTransitionProgressProvider();
+ if (!enableUnfoldStateAnimation()) {
+ if (activity instanceof QuickstepLauncher ql) {
+ return ql.getUnfoldTransitionProgressProvider();
+ }
+ } else {
+ return SystemUiProxy.INSTANCE.get(mContext).getUnfoldTransitionProvider();
}
return null;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
index 74517a8..666d98e 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarView.java
@@ -51,6 +51,7 @@
import com.android.launcher3.Utilities;
import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.folder.FolderIcon;
+import com.android.launcher3.folder.PreviewBackground;
import com.android.launcher3.icons.ThemedIconDrawable;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
@@ -618,9 +619,11 @@
super.onDraw(canvas);
if (mLeaveBehindFolderIcon != null) {
canvas.save();
- canvas.translate(mLeaveBehindFolderIcon.getLeft(), mLeaveBehindFolderIcon.getTop());
- mLeaveBehindFolderIcon.getFolderBackground().drawLeaveBehind(canvas,
- mFolderLeaveBehindColor);
+ canvas.translate(
+ mLeaveBehindFolderIcon.getLeft() + mLeaveBehindFolderIcon.getTranslationX(),
+ mLeaveBehindFolderIcon.getTop());
+ PreviewBackground previewBackground = mLeaveBehindFolderIcon.getFolderBackground();
+ previewBackground.drawLeaveBehind(canvas, mFolderLeaveBehindColor);
canvas.restore();
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
index 1e3f4f1..aa2b29d 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
@@ -50,6 +50,22 @@
var width: Float = 0f
+ /**
+ * Set whether the drawable is anchored to the left or right edge of the container.
+ *
+ * When `anchorLeft` is set to `true`, drawable left edge aligns up with the container left
+ * edge. Drawable can be drawn outside container bounds on the right edge. When it is set to
+ * `false` (the default), drawable right edge aligns up with the container right edge. Drawable
+ * can be drawn outside container bounds on the left edge.
+ */
+ var anchorLeft: Boolean = false
+ set(value) {
+ if (field != value) {
+ field = value
+ invalidateSelf()
+ }
+ }
+
init {
paint.color = context.getColor(R.color.taskbar_background)
paint.flags = Paint.ANTI_ALIAS_FLAG
@@ -106,15 +122,9 @@
// Draw background.
val radius = backgroundHeight / 2f
- canvas.drawRoundRect(
- canvas.width.toFloat() - width,
- 0f,
- canvas.width.toFloat(),
- canvas.height.toFloat(),
- radius,
- radius,
- paint
- )
+ val left = if (anchorLeft) 0f else canvas.width.toFloat() - width
+ val right = if (anchorLeft) width else canvas.width.toFloat()
+ canvas.drawRoundRect(left, 0f, right, canvas.height.toFloat(), radius, radius, paint)
if (showingArrow) {
// Draw arrow.
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index ec9f4e5..5ca2991 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -197,6 +197,16 @@
updateChildrenRenderNodeProperties();
}
+ @Override
+ public void onRtlPropertiesChanged(int layoutDirection) {
+ // TODO(b/273310265): set this based on bubble bar position and not LTR or RTL
+ mBubbleBarBackground.setAnchorLeft(layoutDirection == LAYOUT_DIRECTION_RTL);
+ }
+
+ private boolean isOnLeft() {
+ return getLayoutDirection() == LAYOUT_DIRECTION_RTL;
+ }
+
/**
* Updates the bounds with translation that may have been applied and returns the result.
*/
@@ -275,18 +285,31 @@
int bubbleCount = getChildCount();
final float ty = (mBubbleBarBounds.height() - mIconSize) / 2f;
final boolean animate = getVisibility() == VISIBLE;
+ final boolean onLeft = isOnLeft();
for (int i = 0; i < bubbleCount; i++) {
BubbleView bv = (BubbleView) getChildAt(i);
bv.setTranslationY(ty);
// the position of the bubble when the bar is fully expanded
- final float expandedX = i * (mIconSize + mIconSpacing);
+ final float expandedX;
// the position of the bubble when the bar is fully collapsed
- final float collapsedX = i == 0 ? 0 : mIconOverlapAmount;
+ final float collapsedX;
+ if (onLeft) {
+ // If bar is on the left, bubbles are ordered right to left
+ expandedX = (bubbleCount - i - 1) * (mIconSize + mIconSpacing);
+ // Shift the first bubble only if there are more bubbles in addition to overflow
+ collapsedX = i == 0 && bubbleCount > 2 ? mIconOverlapAmount : 0;
+ } else {
+ // Bubbles ordered left to right, don't move the first bubble
+ expandedX = i * (mIconSize + mIconSpacing);
+ collapsedX = i == 0 ? 0 : mIconOverlapAmount;
+ }
if (mIsBarExpanded) {
+ // If bar is on the right, account for bubble bar expanding and shifting left
+ final float expandedBarShift = onLeft ? 0 : currentWidth - expandedWidth;
// where the bubble will end up when the animation ends
- final float targetX = currentWidth - expandedWidth + expandedX;
+ final float targetX = expandedX + expandedBarShift;
bv.setTranslationX(widthState * (targetX - collapsedX) + collapsedX);
// if we're fully expanded, set the z level to 0 or to bubble elevation if dragged
if (widthState == 1f) {
@@ -296,7 +319,9 @@
bv.setBehindStack(false, animate);
bv.setAlpha(1);
} else {
- final float targetX = currentWidth - collapsedWidth + collapsedX;
+ // If bar is on the right, account for bubble bar expanding and shifting left
+ final float collapsedBarShift = onLeft ? 0 : currentWidth - collapsedWidth;
+ final float targetX = collapsedX + collapsedBarShift;
bv.setTranslationX(widthState * (expandedX - targetX) + targetX);
bv.setZ((MAX_BUBBLES * mBubbleElevation) - i);
// If we're not the first bubble we're behind the stack
@@ -318,18 +343,22 @@
final float expandedArrowPosition = arrowPositionForSelectedWhenExpanded();
final float interpolatedWidth =
widthState * (expandedWidth - collapsedWidth) + collapsedWidth;
- if (mIsBarExpanded) {
- // when the bar is expanding, the selected bubble is always the first, so the arrow
- // always shifts with the interpolated width.
- final float arrowPosition = currentWidth - interpolatedWidth + collapsedArrowPosition;
- mBubbleBarBackground.setArrowPosition(arrowPosition);
+ final float arrowPosition;
+ if (onLeft) {
+ float interpolatedShift = (expandedArrowPosition - collapsedArrowPosition) * widthState;
+ arrowPosition = collapsedArrowPosition + interpolatedShift;
} else {
- final float targetPosition = currentWidth - collapsedWidth + collapsedArrowPosition;
- final float arrowPosition =
- targetPosition + widthState * (expandedArrowPosition - targetPosition);
- mBubbleBarBackground.setArrowPosition(arrowPosition);
+ if (mIsBarExpanded) {
+ // when the bar is expanding, the selected bubble is always the first, so the arrow
+ // always shifts with the interpolated width.
+ arrowPosition = currentWidth - interpolatedWidth + collapsedArrowPosition;
+ } else {
+ final float targetPosition = currentWidth - collapsedWidth + collapsedArrowPosition;
+ arrowPosition =
+ targetPosition + widthState * (expandedArrowPosition - targetPosition);
+ }
}
-
+ mBubbleBarBackground.setArrowPosition(arrowPosition);
mBubbleBarBackground.setArrowAlpha((int) (255 * widthState));
mBubbleBarBackground.setWidth(interpolatedWidth);
}
@@ -394,9 +423,8 @@
Log.w(TAG, "trying to update selection arrow without a selected view!");
return;
}
- final int index = indexOfChild(mSelectedBubbleView);
// Find the center of the bubble when it's expanded, set the arrow position to it.
- final float tx = getPaddingStart() + index * (mIconSize + mIconSpacing) + mIconSize / 2f;
+ final float tx = arrowPositionForSelectedWhenExpanded();
if (shouldAnimate) {
final float currentArrowPosition = mBubbleBarBackground.getArrowPositionX();
@@ -416,12 +444,27 @@
private float arrowPositionForSelectedWhenExpanded() {
final int index = indexOfChild(mSelectedBubbleView);
- return getPaddingStart() + index * (mIconSize + mIconSpacing) + mIconSize / 2f;
+ final int bubblePosition;
+ if (isOnLeft()) {
+ // Bubble positions are reversed. First bubble is on the right.
+ bubblePosition = getChildCount() - index - 1;
+ } else {
+ bubblePosition = index;
+ }
+ return getPaddingStart() + bubblePosition * (mIconSize + mIconSpacing) + mIconSize / 2f;
}
private float arrowPositionForSelectedWhenCollapsed() {
final int index = indexOfChild(mSelectedBubbleView);
- return getPaddingStart() + index * (mIconOverlapAmount) + mIconSize / 2f;
+ final int bubblePosition;
+ if (isOnLeft()) {
+ // Bubble positions are reversed. First bubble may be shifted, if there are more
+ // bubbles than the current bubble and overflow.
+ bubblePosition = index == 0 && getChildCount() > 2 ? 1 : 0;
+ } else {
+ bubblePosition = index;
+ }
+ return getPaddingStart() + bubblePosition * (mIconOverlapAmount) + mIconSize / 2f;
}
@Override
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
index 065dd58..6bb7b04 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarViewController.java
@@ -21,6 +21,7 @@
import android.graphics.Point;
import android.graphics.Rect;
import android.util.Log;
+import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
@@ -289,7 +290,8 @@
*/
public void addBubble(BubbleBarItem b) {
if (b != null) {
- mBarView.addView(b.getView(), 0, new FrameLayout.LayoutParams(mIconSize, mIconSize));
+ mBarView.addView(b.getView(), 0,
+ new FrameLayout.LayoutParams(mIconSize, mIconSize, Gravity.LEFT));
b.getView().setOnClickListener(mBubbleClickListener);
mBubbleDragController.setupBubbleView(b.getView());
} else {
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/OWNERS b/quickstep/src/com/android/launcher3/taskbar/bubbles/OWNERS
index 7af0389..edabae2 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/OWNERS
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/OWNERS
@@ -1 +1,3 @@
+atsjenk@google.com
+liranb@google.com
madym@google.com
diff --git a/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java b/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
index 3ebc8ed..5c4eb9d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/ApiWrapper.java
@@ -138,6 +138,11 @@
|| Flags.enablePrivateSpaceInstallShortcut())) {
StartActivityParams params = new StartActivityParams((PendingIntent) null, 0);
params.intentSender = launcherApps.getAppMarketActivityIntent(packageName, user);
+ ActivityOptions options = ActivityOptions.makeBasic()
+ .setPendingIntentBackgroundActivityStartMode(ActivityOptions
+ .MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
+ params.options = options.toBundle();
+ params.requireActivityResult = false;
return ProxyActivityStarter.getLaunchIntent(context, params);
} else {
return new Intent(Intent.ACTION_VIEW)
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index c3bcde0..2e8e613 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -21,6 +21,7 @@
import static android.view.accessibility.AccessibilityEvent.TYPE_VIEW_FOCUSED;
import static com.android.app.animation.Interpolators.EMPHASIZED;
+import static com.android.launcher3.Flags.enableUnfoldStateAnimation;
import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.PENDING_SPLIT_SELECT_INFO;
import static com.android.launcher3.LauncherConstants.SavedInstanceKeys.RUNTIME_STATE;
import static com.android.launcher3.LauncherSettings.Animation.DEFAULT_NO_ICON;
@@ -170,6 +171,8 @@
import com.android.quickstep.util.SplitToWorkspaceController;
import com.android.quickstep.util.SplitWithKeyboardShortcutController;
import com.android.quickstep.util.TISBindHelper;
+import com.android.quickstep.util.unfold.LauncherUnfoldTransitionController;
+import com.android.quickstep.util.unfold.ProxyUnfoldTransitionProvider;
import com.android.quickstep.views.FloatingTaskView;
import com.android.quickstep.views.OverviewActionsView;
import com.android.quickstep.views.RecentsView;
@@ -367,11 +370,19 @@
mHotseatPredictionController.setPauseUIUpdate(getTaskbarUIController() == null);
Log.d("b/318394698", "startActivitySafely being run, getTaskbarUIController is: "
+ getTaskbarUIController());
+ PredictionRowView<?> predictionRowView =
+ getAppsView().getFloatingHeaderView().findFixedRowByType(PredictionRowView.class);
+ // Pause the prediction row updates until the transition (if it exists) ends.
+ predictionRowView.setPredictionUiUpdatePaused(true);
RunnableList result = super.startActivitySafely(v, intent, item);
if (result == null) {
mHotseatPredictionController.setPauseUIUpdate(false);
+ predictionRowView.setPredictionUiUpdatePaused(false);
} else {
- result.add(() -> mHotseatPredictionController.setPauseUIUpdate(false));
+ result.add(() -> {
+ mHotseatPredictionController.setPauseUIUpdate(false);
+ predictionRowView.setPredictionUiUpdatePaused(false);
+ });
}
return result;
}
@@ -468,7 +479,7 @@
@Override
public void bindExtraContainerItems(FixedContainerItems item) {
- Log.d(TAG, "Bind extra container items");
+ Log.d(TAG, "Bind extra container items. ContainerId = " + item.containerId);
if (item.containerId == Favorites.CONTAINER_PREDICTION) {
mAllAppsPredictions = item;
PredictionRowView<?> predictionRowView =
@@ -957,9 +968,17 @@
}
private void initUnfoldTransitionProgressProvider() {
- final UnfoldTransitionConfig config = new ResourceUnfoldTransitionConfig();
- if (config.isEnabled()) {
- initRemotelyCalculatedUnfoldAnimation(config);
+ if (!enableUnfoldStateAnimation()) {
+ final UnfoldTransitionConfig config = new ResourceUnfoldTransitionConfig();
+ if (config.isEnabled()) {
+ initRemotelyCalculatedUnfoldAnimation(config);
+ }
+ } else {
+ ProxyUnfoldTransitionProvider provider =
+ SystemUiProxy.INSTANCE.get(this).getUnfoldTransitionProvider();
+ if (provider != null) {
+ new LauncherUnfoldTransitionController(this, provider);
+ }
}
}
diff --git a/quickstep/src/com/android/quickstep/SystemUiProxy.java b/quickstep/src/com/android/quickstep/SystemUiProxy.java
index 723af43..56a4024 100644
--- a/quickstep/src/com/android/quickstep/SystemUiProxy.java
+++ b/quickstep/src/com/android/quickstep/SystemUiProxy.java
@@ -17,6 +17,7 @@
import static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE;
+import static com.android.launcher3.Flags.enableUnfoldStateAnimation;
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;
@@ -67,6 +68,7 @@
import com.android.launcher3.util.Preconditions;
import com.android.quickstep.util.ActiveGestureLog;
import com.android.quickstep.util.AssistUtils;
+import com.android.quickstep.util.unfold.ProxyUnfoldTransitionProvider;
import com.android.systemui.shared.recents.ISystemUiProxy;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.shared.system.RecentsAnimationControllerCompat;
@@ -74,6 +76,7 @@
import com.android.systemui.shared.system.smartspace.ILauncherUnlockAnimationController;
import com.android.systemui.shared.system.smartspace.ISysuiUnlockAnimationController;
import com.android.systemui.shared.system.smartspace.SmartspaceState;
+import com.android.systemui.unfold.config.ResourceUnfoldTransitionConfig;
import com.android.systemui.unfold.progress.IUnfoldAnimation;
import com.android.systemui.unfold.progress.IUnfoldTransitionListener;
import com.android.wm.shell.back.IBackAnimation;
@@ -177,7 +180,10 @@
*/
private final PendingIntent mRecentsPendingIntent;
- public SystemUiProxy(Context context) {
+ @Nullable
+ private final ProxyUnfoldTransitionProvider mUnfoldTransitionProvider;
+
+ private SystemUiProxy(Context context) {
mContext = context;
mAsyncHandler = new Handler(UI_HELPER_EXECUTOR.getLooper(), this::handleMessageAsync);
final Intent baseIntent = new Intent().setPackage(mContext.getPackageName());
@@ -187,6 +193,10 @@
mRecentsPendingIntent = PendingIntent.getActivity(mContext, 0, baseIntent,
PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT
| Intent.FILL_IN_COMPONENT, options.toBundle());
+
+ mUnfoldTransitionProvider =
+ (enableUnfoldStateAnimation() && new ResourceUnfoldTransitionConfig().isEnabled())
+ ? new ProxyUnfoldTransitionProvider() : null;
}
@Override
@@ -251,7 +261,7 @@
mRecentTasks = recentTasks;
mBackAnimation = backAnimation;
mDesktopMode = desktopMode;
- mUnfoldAnimation = unfoldAnimation;
+ mUnfoldAnimation = enableUnfoldStateAnimation() ? null : unfoldAnimation;
mDragAndDrop = dragAndDrop;
linkToDeath();
// re-attach the listeners once missing due to setProxy has not been initialized yet.
@@ -272,6 +282,19 @@
setAssistantOverridesRequested(
AssistUtils.newInstance(mContext).getSysUiAssistOverrideInvocationTypes());
mStateChangeCallbacks.forEach(Runnable::run);
+
+ if (mUnfoldTransitionProvider != null) {
+ if (unfoldAnimation != null) {
+ try {
+ unfoldAnimation.setListener(mUnfoldTransitionProvider);
+ mUnfoldTransitionProvider.setActive(true);
+ } catch (RemoteException e) {
+ // Ignore
+ }
+ } else {
+ mUnfoldTransitionProvider.setActive(false);
+ }
+ }
}
/**
@@ -1451,6 +1474,11 @@
}
}
+ @Nullable
+ public ProxyUnfoldTransitionProvider getUnfoldTransitionProvider() {
+ return mUnfoldTransitionProvider;
+ }
+
//
// Recents
//
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.java b/quickstep/src/com/android/quickstep/TaskIconCache.java
index 1b3f598..4e6b23f 100644
--- a/quickstep/src/com/android/quickstep/TaskIconCache.java
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.java
@@ -191,8 +191,7 @@
entry.contentDescription = getBadgedContentDescription(
activityInfo, task.key.userId, task.taskDescription);
if (enableOverviewIconMenu()) {
- entry.title = Utilities.trim(
- activityInfo.applicationInfo.loadLabel(mContext.getPackageManager()));
+ entry.title = Utilities.trim(activityInfo.loadLabel(mContext.getPackageManager()));
}
}
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 647ff90..ce6ddd8 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -77,7 +77,6 @@
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.MotionEvent;
-import android.view.SurfaceControl;
import android.view.accessibility.AccessibilityManager;
import androidx.annotation.BinderThread;
@@ -303,15 +302,6 @@
});
}
- @Override
- public void onNavigationBarSurface(SurfaceControl surface) {
- // TODO: implement
- if (surface != null) {
- surface.release();
- surface = null;
- }
- }
-
@BinderThread
public void onSystemUiStateChanged(int stateFlags) {
MAIN_EXECUTOR.execute(() -> executeForTouchInteractionService(tis -> {
diff --git a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
index 28efc97..50a5a83 100644
--- a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
@@ -16,6 +16,7 @@
package com.android.quickstep.util;
+import static com.android.launcher3.LauncherSettings.Favorites.ITEM_TYPE_APP_PAIR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import static com.android.quickstep.views.DesktopTaskView.isDesktopModeSupported;
@@ -43,6 +44,7 @@
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.icons.IconCache;
+import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.quickstep.views.FloatingTaskView;
@@ -123,7 +125,12 @@
intent = appInfo.intent;
user = appInfo.user;
bitmapInfo = appInfo.bitmap;
+ } else if (tag instanceof FolderInfo fi && fi.itemType == ITEM_TYPE_APP_PAIR) {
+ // Prompt the user to select something else by wiggling the instructions view
+ mController.getSplitInstructionsView().goBoing();
+ return true;
} else {
+ // Use Launcher's default click handler
return false;
}
diff --git a/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt b/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt
new file mode 100644
index 0000000..54d317d
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/unfold/LauncherUnfoldTransitionController.kt
@@ -0,0 +1,129 @@
+/*
+ * 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.unfold
+
+import android.app.Activity
+import android.os.Trace
+import android.view.Surface
+import com.android.launcher3.Alarm
+import com.android.launcher3.DeviceProfile
+import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener
+import com.android.launcher3.Launcher
+import com.android.launcher3.anim.PendingAnimation
+import com.android.launcher3.config.FeatureFlags
+import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+
+/** Controls animations that are happening during unfolding foldable devices */
+class LauncherUnfoldTransitionController(
+ private val launcher: Launcher,
+ private val progressProvider: ProxyUnfoldTransitionProvider
+) : OnDeviceProfileChangeListener, ActivityLifecycleCallbacksAdapter, TransitionProgressListener {
+
+ private var isTablet: Boolean? = null
+ private var hasUnfoldTransitionStarted = false
+ private val timeoutAlarm =
+ Alarm().apply {
+ setOnAlarmListener {
+ onTransitionFinished()
+ Trace.endAsyncSection("$TAG#startedPreemptively", 0)
+ }
+ }
+
+ init {
+ launcher.addOnDeviceProfileChangeListener(this)
+ launcher.registerActivityLifecycleCallbacks(this)
+ }
+
+ override fun onActivityPaused(activity: Activity) {
+ progressProvider.removeCallback(this)
+ }
+
+ override fun onActivityResumed(activity: Activity) {
+ progressProvider.addCallback(this)
+ }
+
+ override fun onDeviceProfileChanged(dp: DeviceProfile) {
+ if (!FeatureFlags.PREEMPTIVE_UNFOLD_ANIMATION_START.get()) {
+ return
+ }
+
+ if (isTablet != null && dp.isTablet != isTablet) {
+ // We should preemptively start the animation only if:
+ // - We changed to the unfolded screen
+ // - SystemUI IPC connection is alive, so we won't end up in a situation that we won't
+ // receive transition progress events from SystemUI later because there was no
+ // IPC connection established (e.g. because of SystemUI crash)
+ // - SystemUI has not already sent unfold animation progress events. This might happen
+ // if Launcher was not open during unfold, in this case we receive the configuration
+ // change only after we went back to home screen and we don't want to start the
+ // animation in this case.
+ if (dp.isTablet && progressProvider.isActive && !hasUnfoldTransitionStarted) {
+ // Preemptively start the unfold animation to make sure that we have drawn
+ // the first frame of the animation before the screen gets unblocked
+ onTransitionStarted()
+ Trace.beginAsyncSection("$TAG#startedPreemptively", 0)
+ timeoutAlarm.setAlarm(PREEMPTIVE_UNFOLD_TIMEOUT_MS)
+ }
+ if (!dp.isTablet) {
+ // Reset unfold transition status when folded
+ hasUnfoldTransitionStarted = false
+ }
+ }
+
+ isTablet = dp.isTablet
+ }
+
+ override fun onTransitionStarted() {
+ hasUnfoldTransitionStarted = true
+ launcher.animationCoordinator.setAnimation(
+ provider = this,
+ factory = this::onPrepareUnfoldAnimation,
+ duration =
+ 1000L // The expected duration for the animation. Then only comes to play if we have
+ // to run the animation ourselves in case sysui misses the end signal
+ )
+ timeoutAlarm.cancelAlarm()
+ }
+
+ override fun onTransitionProgress(progress: Float) {
+ hasUnfoldTransitionStarted = true
+ launcher.animationCoordinator.getPlaybackController(this)?.setPlayFraction(progress)
+ }
+
+ override fun onTransitionFinished() {
+ // Run the animation to end the animation in case it is not already at end progress. It
+ // will scale the duration to the remaining progress
+ launcher.animationCoordinator.getPlaybackController(this)?.start()
+ timeoutAlarm.cancelAlarm()
+ }
+
+ private fun onPrepareUnfoldAnimation(anim: PendingAnimation) {
+ val dp = launcher.deviceProfile
+ val rotation = dp.displayInfo.rotation
+ val isVertical = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180
+ UnfoldAnimationBuilder.buildUnfoldAnimation(
+ launcher,
+ isVertical,
+ dp.displayInfo.currentSize,
+ anim
+ )
+ }
+
+ companion object {
+ private const val TAG = "LauncherUnfoldTransitionController"
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/unfold/ProxyUnfoldTransitionProvider.kt b/quickstep/src/com/android/quickstep/util/unfold/ProxyUnfoldTransitionProvider.kt
new file mode 100644
index 0000000..83c7f72
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/unfold/ProxyUnfoldTransitionProvider.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.unfold
+
+import androidx.annotation.AnyThread
+import androidx.annotation.FloatRange
+import com.android.launcher3.util.Executors.MAIN_EXECUTOR
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider
+import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
+import com.android.systemui.unfold.progress.IUnfoldTransitionListener
+import com.android.systemui.unfold.progress.UnfoldRemoteFilter
+
+/** Receives unfold events from remote senders (System UI). */
+class ProxyUnfoldTransitionProvider :
+ UnfoldTransitionProgressProvider, IUnfoldTransitionListener.Stub() {
+
+ private val listeners: MutableSet<TransitionProgressListener> = mutableSetOf()
+ private val delegate = UnfoldRemoteFilter(ProcessedProgressListener())
+
+ private var transitionStarted = false
+ var isActive = false
+ set(value) {
+ field = value
+ if (!value) {
+ // Finish any active transition
+ onTransitionFinished()
+ }
+ }
+
+ @AnyThread
+ override fun onTransitionStarted() {
+ MAIN_EXECUTOR.execute(delegate::onTransitionStarted)
+ }
+
+ @AnyThread
+ override fun onTransitionProgress(progress: Float) {
+ MAIN_EXECUTOR.execute { delegate.onTransitionProgress(progress) }
+ }
+
+ @AnyThread
+ override fun onTransitionFinished() {
+ MAIN_EXECUTOR.execute(delegate::onTransitionFinished)
+ }
+
+ override fun addCallback(listener: TransitionProgressListener) {
+ listeners += listener
+ if (transitionStarted) {
+ // Update the listener in case there was is an active transition
+ listener.onTransitionStarted()
+ }
+ }
+
+ override fun removeCallback(listener: TransitionProgressListener) {
+ listeners -= listener
+ if (transitionStarted) {
+ // Finish the transition if it was already running
+ listener.onTransitionFinished()
+ }
+ }
+
+ override fun destroy() {
+ listeners.clear()
+ }
+
+ private inner class ProcessedProgressListener : TransitionProgressListener {
+ override fun onTransitionStarted() {
+ if (!transitionStarted) {
+ transitionStarted = true
+ listeners.forEach(TransitionProgressListener::onTransitionStarted)
+ }
+ }
+
+ override fun onTransitionProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) {
+ listeners.forEach { it.onTransitionProgress(progress) }
+ }
+
+ override fun onTransitionFinished() {
+ if (transitionStarted) {
+ transitionStarted = false
+ listeners.forEach(TransitionProgressListener::onTransitionFinished)
+ }
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/unfold/UnfoldAnimationBuilder.kt b/quickstep/src/com/android/quickstep/util/unfold/UnfoldAnimationBuilder.kt
new file mode 100644
index 0000000..d2c4728
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/unfold/UnfoldAnimationBuilder.kt
@@ -0,0 +1,167 @@
+/*
+ * 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.unfold
+
+import android.graphics.Point
+import android.view.ViewGroup
+import com.android.app.animation.Interpolators.LINEAR
+import com.android.app.animation.Interpolators.clampToProgress
+import com.android.launcher3.CellLayout
+import com.android.launcher3.Launcher
+import com.android.launcher3.LauncherAnimUtils.HOTSEAT_SCALE_PROPERTY_FACTORY
+import com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_UNFOLD_ANIMATION
+import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X
+import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y
+import com.android.launcher3.LauncherAnimUtils.WORKSPACE_SCALE_PROPERTY_FACTORY
+import com.android.launcher3.Workspace
+import com.android.launcher3.anim.PendingAnimation
+import com.android.launcher3.util.HorizontalInsettableView
+
+private typealias ViewGroupAction = (ViewGroup, Boolean) -> Unit
+
+object UnfoldAnimationBuilder {
+
+ private val CLIP_CHILDREN: ViewGroupAction = ViewGroup::setClipChildren
+ private val CLIP_TO_PADDING: ViewGroupAction = ViewGroup::setClipToPadding
+
+ data class RestoreInfo(val action: ViewGroupAction, var target: ViewGroup, var value: Boolean)
+
+ // Percentage of the width of the quick search bar that will be reduced
+ // from the both sides of the bar when progress is 0
+ private const val MAX_WIDTH_INSET_FRACTION = 0.04f
+
+ // Scale factor for the whole workspace and hotseat
+ private const val SCALE_LAUNCHER_FROM = 0.92f
+
+ // Translation factor for all the items on the homescreen
+ private const val TRANSLATION_PERCENTAGE = 0.08f
+
+ private fun setClipChildren(
+ target: ViewGroup,
+ value: Boolean,
+ restoreList: MutableList<RestoreInfo>
+ ) {
+ val originalValue = target.clipChildren
+ if (originalValue != value) {
+ target.clipChildren = value
+ restoreList.add(RestoreInfo(CLIP_CHILDREN, target, originalValue))
+ }
+ }
+
+ private fun setClipToPadding(
+ target: ViewGroup,
+ value: Boolean,
+ restoreList: MutableList<RestoreInfo>
+ ) {
+ val originalValue = target.clipToPadding
+ if (originalValue != value) {
+ target.clipToPadding = value
+ restoreList.add(RestoreInfo(CLIP_TO_PADDING, target, originalValue))
+ }
+ }
+
+ private fun addChildrenAnimation(
+ itemsContainer: ViewGroup,
+ isVerticalFold: Boolean,
+ screenSize: Point,
+ anim: PendingAnimation
+ ) {
+ val tempLocation = IntArray(2)
+ for (i in 0 until itemsContainer.childCount) {
+ val child = itemsContainer.getChildAt(i)
+
+ child.getLocationOnScreen(tempLocation)
+ if (isVerticalFold) {
+ val viewCenterX = tempLocation[0] + child.width / 2
+ val distanceFromScreenCenterToViewCenter = screenSize.x / 2 - viewCenterX
+ anim.addFloat(
+ child,
+ VIEW_TRANSLATE_X,
+ distanceFromScreenCenterToViewCenter * TRANSLATION_PERCENTAGE,
+ 0f,
+ LINEAR
+ )
+ } else {
+ val viewCenterY = tempLocation[1] + child.height / 2
+ val distanceFromScreenCenterToViewCenter = screenSize.y / 2 - viewCenterY
+ anim.addFloat(
+ child,
+ VIEW_TRANSLATE_Y,
+ distanceFromScreenCenterToViewCenter * TRANSLATION_PERCENTAGE,
+ 0f,
+ LINEAR
+ )
+ }
+ }
+ }
+
+ /**
+ * Builds an animation for the unfold experience and adds it to the provided PendingAnimation
+ */
+ fun buildUnfoldAnimation(
+ launcher: Launcher,
+ isVerticalFold: Boolean,
+ screenSize: Point,
+ anim: PendingAnimation
+ ) {
+ val restoreList = ArrayList<RestoreInfo>()
+ val registerViews: (CellLayout) -> Unit = { cellLayout ->
+ setClipChildren(cellLayout, false, restoreList)
+ setClipToPadding(cellLayout, false, restoreList)
+ addChildrenAnimation(cellLayout.shortcutsAndWidgets, isVerticalFold, screenSize, anim)
+ }
+
+ val workspace: Workspace<*> = launcher.workspace
+ val hotseat = launcher.hotseat
+
+ // Animation icons from workspace for all orientations
+ workspace.forEachVisiblePage { registerViews(it as CellLayout) }
+ setClipChildren(workspace, false, restoreList)
+ setClipToPadding(workspace, true, restoreList)
+
+ // Workspace scale
+ launcher.workspace.setPivotToScaleWithSelf(launcher.hotseat)
+ val interpolator = clampToProgress(LINEAR, 0f, 1f)
+ anim.addFloat(
+ workspace,
+ WORKSPACE_SCALE_PROPERTY_FACTORY[SCALE_INDEX_UNFOLD_ANIMATION],
+ SCALE_LAUNCHER_FROM,
+ 1f,
+ interpolator
+ )
+ anim.addFloat(
+ hotseat,
+ HOTSEAT_SCALE_PROPERTY_FACTORY[SCALE_INDEX_UNFOLD_ANIMATION],
+ SCALE_LAUNCHER_FROM,
+ 1f,
+ interpolator
+ )
+
+ if (isVerticalFold) {
+ if (hotseat.qsb is HorizontalInsettableView) {
+ anim.addFloat(
+ hotseat.qsb as HorizontalInsettableView,
+ HorizontalInsettableView.HORIZONTAL_INSETS,
+ MAX_WIDTH_INSET_FRACTION,
+ 0f,
+ LINEAR
+ )
+ }
+ registerViews(hotseat)
+ }
+ anim.addEndListener { restoreList.forEach { it.action(it.target, it.value) } }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
index f6501f1..f39a901 100644
--- a/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
+++ b/quickstep/src/com/android/quickstep/views/SplitInstructionsView.java
@@ -16,6 +16,8 @@
package com.android.quickstep.views;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.os.Bundle;
import android.util.AttributeSet;
@@ -26,9 +28,15 @@
import android.widget.TextView;
import androidx.annotation.Nullable;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+import com.android.app.animation.Interpolators;
import com.android.launcher3.LauncherState;
import com.android.launcher3.R;
+import com.android.launcher3.Utilities;
+import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.statemanager.StatefulActivity;
@@ -40,7 +48,11 @@
* Appears and disappears concurrently with a FloatingTaskView.
*/
public class SplitInstructionsView extends LinearLayout {
+ private static final int BOUNCE_DURATION = 250;
+ private static final float BOUNCE_HEIGHT = 20;
+
private final StatefulActivity mLauncher;
+ public boolean mIsCurrentlyAnimating = false;
public static final FloatProperty<SplitInstructionsView> UNFOLD =
new FloatProperty<>("SplitInstructionsUnfold") {
@@ -55,6 +67,19 @@
}
};
+ public static final FloatProperty<SplitInstructionsView> TRANSLATE_Y =
+ new FloatProperty<>("SplitInstructionsTranslateY") {
+ @Override
+ public void setValue(SplitInstructionsView splitInstructionsView, float v) {
+ splitInstructionsView.setTranslationY(v);
+ }
+
+ @Override
+ public Float get(SplitInstructionsView splitInstructionsView) {
+ return splitInstructionsView.getTranslationY();
+ }
+ };
+
public SplitInstructionsView(Context context) {
this(context, null);
}
@@ -143,4 +168,42 @@
getMeasuredWidth()
);
}
+
+ /**
+ * Draws attention to the split instructions view by bouncing it up and down.
+ */
+ public void goBoing() {
+ if (mIsCurrentlyAnimating) {
+ return;
+ }
+
+ float restingY = getTranslationY();
+ float bounceToY = restingY - Utilities.dpToPx(BOUNCE_HEIGHT);
+ PendingAnimation anim = new PendingAnimation(BOUNCE_DURATION);
+ // Animate the view lifting up to a higher position
+ anim.addFloat(this, TRANSLATE_Y, restingY, bounceToY, Interpolators.STANDARD);
+
+ anim.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mIsCurrentlyAnimating = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ // Create a low stiffness, medium bounce spring centering at the rest position
+ SpringForce spring = new SpringForce(restingY)
+ .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY)
+ .setStiffness(SpringForce.STIFFNESS_LOW);
+ // Animate the view getting pulled back to rest position by the spring
+ SpringAnimation springAnim = new SpringAnimation(SplitInstructionsView.this,
+ DynamicAnimation.TRANSLATION_Y).setSpring(spring).setStartValue(bounceToY);
+
+ springAnim.addEndListener((a, b, c, d) -> mIsCurrentlyAnimating = false);
+ springAnim.start();
+ }
+ });
+
+ anim.buildAnim().start();
+ }
}
diff --git a/res/layout/widgets_two_pane_sheet_recyclerview.xml b/res/layout/widgets_two_pane_sheet_recyclerview.xml
index c9c855c..8b48abb 100644
--- a/res/layout/widgets_two_pane_sheet_recyclerview.xml
+++ b/res/layout/widgets_two_pane_sheet_recyclerview.xml
@@ -45,24 +45,22 @@
android:background="?attr/widgetPickerPrimarySurfaceColor"
android:clipToPadding="false"
android:elevation="0.1dp"
- android:paddingBottom="8dp"
+ android:paddingBottom="16dp"
android:paddingHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
launcher:layout_sticky="true">
<include layout="@layout/widgets_search_bar" />
</FrameLayout>
- <LinearLayout
+ <FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/suggestions_header"
- android:layout_marginTop="8dp"
android:layout_marginHorizontal="@dimen/widget_list_horizontal_margin_two_pane"
android:paddingBottom="16dp"
- android:orientation="horizontal"
android:background="?attr/widgetPickerPrimarySurfaceColor"
launcher:layout_sticky="true">
- </LinearLayout>
+ </FrameLayout>
</com.android.launcher3.views.StickyHeaderLayout>
</FrameLayout>
</merge>
\ No newline at end of file
diff --git a/res/values/config.xml b/res/values/config.xml
index 2a8ec28..5bdd7ebb 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -252,6 +252,9 @@
<!-- Used for custom widgets -->
<array name="custom_widget_providers"/>
+ <!-- Embed parameters -->
+ <dimen name="activity_split_ratio" format="float">0.5</dimen>
+ <integer name="min_width_split">720</integer>
<!-- Skip "Install to private" long-press shortcut packages name -->
<string-array name="skip_private_profile_shortcut_packages" translatable="false">
diff --git a/res/xml/split_configuration.xml b/res/xml/split_configuration.xml
new file mode 100644
index 0000000..531fef8
--- /dev/null
+++ b/res/xml/split_configuration.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources
+ xmlns:window="http://schemas.android.com/apk/res-auto">
+ <!-- Automatically split the following activity pairs. -->
+ <SplitPairRule
+ window:splitRatio="@dimen/activity_split_ratio"
+ window:splitLayoutDirection="locale"
+ window:splitMinWidthDp="@integer/min_width_split"
+ window:splitMaxAspectRatioInPortrait="alwaysAllow"
+ window:finishPrimaryWithSecondary="never"
+ window:finishSecondaryWithPrimary="always"
+ window:clearTop="false">
+ <SplitPairFilter
+ window:primaryActivityName="com.android.launcher3.settings.SettingsActivity"
+ window:secondaryActivityName="com.android.launcher3.settings.SettingsActivity"/>
+
+ </SplitPairRule>
+</resources>
\ No newline at end of file
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 826eeb2..39b8de1 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -152,6 +152,7 @@
import androidx.annotation.StringRes;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
+import androidx.window.embedding.RuleController;
import com.android.launcher3.DropTarget.DragObject;
import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
@@ -580,6 +581,11 @@
}
setTitle(R.string.home_screen);
mStartupLatencyLogger.logEnd(LAUNCHER_LATENCY_STARTUP_ACTIVITY_ON_CREATE);
+
+ if (com.android.launcher3.Flags.enableTwoPaneLauncherSettings()) {
+ RuleController.getInstance(this).setRules(
+ RuleController.parseRules(this, R.xml.split_configuration));
+ }
}
protected ModelCallbacks createModelCallbacks() {
diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java
index aee511c..1c46dac 100644
--- a/src/com/android/launcher3/allapps/PrivateProfileManager.java
+++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java
@@ -21,6 +21,7 @@
import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_HEADER;
import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_NOTHING;
import static com.android.launcher3.model.BgDataModel.Callbacks.FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_PRIVATE_SPACE_INSTALL_APP;
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.SettingsCache.PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI;
@@ -105,6 +106,7 @@
itemInfo.bitmap = bitmapInfo;
itemInfo.contentDescription = context.getResources().getString(
com.android.launcher3.R.string.ps_add_button_content_description);
+ itemInfo.runtimeStatusFlags |= FLAG_PRIVATE_SPACE_INSTALL_APP;
BaseAllAppsAdapter.AdapterItem item = new BaseAllAppsAdapter.AdapterItem(VIEW_TYPE_ICON);
item.itemInfo = itemInfo;
diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java
index 45ff33b..f9b7495 100644
--- a/src/com/android/launcher3/logging/StatsLogManager.java
+++ b/src/com/android/launcher3/logging/StatsLogManager.java
@@ -713,6 +713,9 @@
@UiEvent(doc = "User tapped on install to private space system shortcut.")
LAUNCHER_PRIVATE_SPACE_INSTALL_SYSTEM_SHORTCUT_TAP(1565),
+ @UiEvent(doc = "User tapped private space install app button.")
+ LAUNCHER_PRIVATE_SPACE_INSTALL_APP_BUTTON_TAP(1605),
+
@UiEvent(doc = "User attempted to create split screen with a widget")
LAUNCHER_SPLIT_WIDGET_ATTEMPT(1604)
diff --git a/src/com/android/launcher3/model/PackageUpdatedTask.java b/src/com/android/launcher3/model/PackageUpdatedTask.java
index 069e96b..529a8f9 100644
--- a/src/com/android/launcher3/model/PackageUpdatedTask.java
+++ b/src/com/android/launcher3/model/PackageUpdatedTask.java
@@ -353,9 +353,17 @@
}
if (!removedPackages.isEmpty() || !removedComponents.isEmpty()) {
- Predicate<ItemInfo> removeMatch = ItemInfoMatcher.ofPackages(removedPackages, mUser)
- .or(ItemInfoMatcher.ofComponents(removedComponents, mUser))
- .and(ItemInfoMatcher.ofItemIds(forceKeepShortcuts).negate());
+ // This predicate is used to mark an ItemInfo for removal if its package or component
+ // is marked for removal.
+ Predicate<ItemInfo> removeAppMatch =
+ ItemInfoMatcher.ofPackages(removedPackages, mUser)
+ .or(ItemInfoMatcher.ofComponents(removedComponents, mUser))
+ .and(ItemInfoMatcher.ofItemIds(forceKeepShortcuts).negate());
+ // This predicate is used to mark an app pair for removal if it contains an app marked
+ // for removal.
+ Predicate<ItemInfo> removeAppPairMatch =
+ ItemInfoMatcher.forAppPairMatch(removeAppMatch);
+ Predicate<ItemInfo> removeMatch = removeAppMatch.or(removeAppPairMatch);
deleteAndBindComponentsRemoved(removeMatch,
"removed because the corresponding package or component is removed. "
+ "mOp=" + mOp + " removedPackages=" + removedPackages.stream().collect(
diff --git a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
index 58b12b1..c8ab09c 100644
--- a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
+++ b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
@@ -122,6 +122,11 @@
public static final int FLAG_ARCHIVED = 1 << 14;
/**
+ * Flag indicating it's the Private Space Install App icon.
+ */
+ public static final int FLAG_PRIVATE_SPACE_INSTALL_APP = 1 << 15;
+
+ /**
* Status associated with the system state of the underlying item. This is calculated every
* time a new info is created and not persisted on the disk.
*/
diff --git a/src/com/android/launcher3/touch/ItemClickHandler.java b/src/com/android/launcher3/touch/ItemClickHandler.java
index ff8b381..369008d 100644
--- a/src/com/android/launcher3/touch/ItemClickHandler.java
+++ b/src/com/android/launcher3/touch/ItemClickHandler.java
@@ -19,6 +19,7 @@
import static com.android.launcher3.LauncherConstants.ActivityCodes.REQUEST_BIND_PENDING_APPWIDGET;
import static com.android.launcher3.LauncherConstants.ActivityCodes.REQUEST_RECONFIGURE_APPWIDGET;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_OPEN;
+import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_INSTALL_APP_BUTTON_TAP;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_BY_PUBLISHER;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_QUIET_USER;
@@ -352,6 +353,12 @@
appInfo.getTargetComponent().getPackageName(), Process.myUserHandle());
} else {
intent = item.getIntent();
+ if (item instanceof AppInfo
+ && (((ItemInfoWithIcon) item).runtimeStatusFlags
+ & ItemInfoWithIcon.FLAG_PRIVATE_SPACE_INSTALL_APP) != 0) {
+ launcher.getStatsLogManager().logger().log(
+ LAUNCHER_PRIVATE_SPACE_INSTALL_APP_BUTTON_TAP);
+ }
}
if (intent == null) {
throw new IllegalArgumentException("Input must have a valid intent");
diff --git a/src/com/android/launcher3/util/ItemInfoMatcher.java b/src/com/android/launcher3/util/ItemInfoMatcher.java
index b6af314..3074111 100644
--- a/src/com/android/launcher3/util/ItemInfoMatcher.java
+++ b/src/com/android/launcher3/util/ItemInfoMatcher.java
@@ -70,6 +70,15 @@
}
/**
+ * Returns a matcher for items within app pairs.
+ */
+ public static Predicate<ItemInfo> forAppPairMatch(Predicate<ItemInfo> childOperator) {
+ Predicate<ItemInfo> isAppPair = info ->
+ info instanceof FolderInfo fi && fi.itemType == Favorites.ITEM_TYPE_APP_PAIR;
+ return isAppPair.and(forFolderMatch(childOperator));
+ }
+
+ /**
* Returns a matcher for items with provided ids
*/
public static Predicate<ItemInfo> ofItemIds(IntSet ids) {
diff --git a/src/com/android/launcher3/util/StartActivityParams.java b/src/com/android/launcher3/util/StartActivityParams.java
index b48562f..d66b0a0 100644
--- a/src/com/android/launcher3/util/StartActivityParams.java
+++ b/src/com/android/launcher3/util/StartActivityParams.java
@@ -52,6 +52,7 @@
public int flagsValues;
public int extraFlags;
public Bundle options;
+ public boolean requireActivityResult = true;
public StartActivityParams(Activity activity, int requestCode) {
this(activity.createPendingResult(requestCode, new Intent(),
@@ -74,6 +75,7 @@
flagsValues = parcel.readInt();
extraFlags = parcel.readInt();
options = parcel.readBundle();
+ requireActivityResult = parcel.readInt() != 0;
}
@@ -94,6 +96,7 @@
parcel.writeInt(flagsValues);
parcel.writeInt(extraFlags);
parcel.writeBundle(options);
+ parcel.writeInt(requireActivityResult ? 1 : 0);
}
/** Perform the operation on the pendingIntent. */
diff --git a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
index 54c9324..6656237 100644
--- a/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
+++ b/src/com/android/launcher3/widget/picker/WidgetsTwoPaneSheet.java
@@ -56,7 +56,7 @@
private static final int MAXIMUM_WIDTH_LEFT_PANE_FOLDABLE_DP = 395;
private static final String SUGGESTIONS_PACKAGE_NAME = "widgets_list_suggestions_entry";
- private LinearLayout mSuggestedWidgetsContainer;
+ private FrameLayout mSuggestedWidgetsContainer;
private WidgetsListHeader mSuggestedWidgetsHeader;
private LinearLayout mRightPane;
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 2596b75..daace8e 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -24,7 +24,7 @@
<application android:debuggable="true">
<uses-library android:name="android.test.runner" />
- <receiver android:name="com.android.launcher3.compat.PromiseIconUiTest$UnarchiveBroadcastReceiver"
+ <receiver android:name="com.android.launcher3.compat.TaplPromiseIconUiTest$UnarchiveBroadcastReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
diff --git a/tests/multivalentTests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java b/tests/multivalentTests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
index f68e12c..fef93b7 100644
--- a/tests/multivalentTests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
+++ b/tests/multivalentTests/tapl/com/android/launcher3/tapl/LauncherInstrumentation.java
@@ -1771,6 +1771,7 @@
}
endTime = movePointer(
start, end, steps, false, downTime, downTime, slowDown, gestureScope);
+ } finally {
if (mTrackpadGestureType != TrackpadGestureType.NONE) {
for (int i = mPointerCount; i >= 2; i--) {
sendPointer(downTime, downTime,
@@ -1778,7 +1779,6 @@
start, gestureScope);
}
}
- } finally {
sendPointer(downTime, endTime, MotionEvent.ACTION_UP, end, gestureScope);
}
}
@@ -2055,11 +2055,14 @@
final long downTime = SystemClock.uptimeMillis();
sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, targetCenter,
GestureScope.DONT_EXPECT_PILFER);
- expectEvent(TestProtocol.SEQUENCE_MAIN, longClickEvent);
- final UiObject2 result = waitForLauncherObject(resName);
- sendPointer(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, targetCenter,
- GestureScope.DONT_EXPECT_PILFER);
- return result;
+ try {
+ expectEvent(TestProtocol.SEQUENCE_MAIN, longClickEvent);
+ final UiObject2 result = waitForLauncherObject(resName);
+ return result;
+ } finally {
+ sendPointer(downTime, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, targetCenter,
+ GestureScope.DONT_EXPECT_PILFER);
+ }
}
@NonNull
@@ -2070,12 +2073,15 @@
sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, targetCenter,
GestureScope.DONT_EXPECT_PILFER, InputDevice.SOURCE_MOUSE,
/* isRightClick= */ true);
- expectEvent(TestProtocol.SEQUENCE_MAIN, rightClickEvent);
- final UiObject2 result = waitForLauncherObject(resName);
- sendPointer(downTime, SystemClock.uptimeMillis(), ACTION_UP, targetCenter,
- GestureScope.DONT_EXPECT_PILFER, InputDevice.SOURCE_MOUSE,
- /* isRightClick= */ true);
- return result;
+ try {
+ expectEvent(TestProtocol.SEQUENCE_MAIN, rightClickEvent);
+ final UiObject2 result = waitForLauncherObject(resName);
+ return result;
+ } finally {
+ sendPointer(downTime, SystemClock.uptimeMillis(), ACTION_UP, targetCenter,
+ GestureScope.DONT_EXPECT_PILFER, InputDevice.SOURCE_MOUSE,
+ /* isRightClick= */ true);
+ }
}
private static int getSystemIntegerRes(Context context, String resName) {
diff --git a/tests/multivalentTests/tapl/com/android/launcher3/tapl/WidgetResizeFrame.java b/tests/multivalentTests/tapl/com/android/launcher3/tapl/WidgetResizeFrame.java
index ec1cbd8..d0573e0 100644
--- a/tests/multivalentTests/tapl/com/android/launcher3/tapl/WidgetResizeFrame.java
+++ b/tests/multivalentTests/tapl/com/android/launcher3/tapl/WidgetResizeFrame.java
@@ -16,6 +16,7 @@
package com.android.launcher3.tapl;
import static com.android.launcher3.tapl.Launchable.DEFAULT_DRAG_STEPS;
+
import static org.junit.Assert.assertTrue;
import android.graphics.Point;
@@ -61,11 +62,14 @@
final long downTime = SystemClock.uptimeMillis();
mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_DOWN, targetStart,
LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
- mLauncher.movePointer(targetStart, targetDest, DEFAULT_DRAG_STEPS,
- true, downTime, downTime, true,
- LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
- mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_UP, targetDest,
- LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
+ try {
+ mLauncher.movePointer(targetStart, targetDest, DEFAULT_DRAG_STEPS,
+ true, downTime, downTime, true,
+ LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
+ } finally {
+ mLauncher.sendPointer(downTime, downTime, MotionEvent.ACTION_UP, targetDest,
+ LauncherInstrumentation.GestureScope.DONT_EXPECT_PILFER);
+ }
try (LauncherInstrumentation.Closable c2 = mLauncher.addContextLayer(
"want to return resized widget resize frame")) {