Merge "Decrease the padding between app icon and label" into udc-qpr-dev
diff --git a/quickstep/AndroidManifest-launcher.xml b/quickstep/AndroidManifest-launcher.xml
index 7d7054f..c6e2d8c 100644
--- a/quickstep/AndroidManifest-launcher.xml
+++ b/quickstep/AndroidManifest-launcher.xml
@@ -20,7 +20,6 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.launcher3">
- <uses-sdk android:targetSdkVersion="33" android:minSdkVersion="26"/>
<!--
Manifest entries specific to Launcher3. This is merged with AndroidManifest-common.xml.
Refer comments around specific entries on how to extend individual components.
diff --git a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
index c7325ba..2b578c3 100644
--- a/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
+++ b/quickstep/src/com/android/launcher3/QuickstepTransitionManager.java
@@ -144,6 +144,7 @@
import com.android.quickstep.util.SurfaceTransaction;
import com.android.quickstep.util.SurfaceTransaction.SurfaceProperties;
import com.android.quickstep.util.SurfaceTransactionApplier;
+import com.android.quickstep.util.TaskRestartedDuringLaunchListener;
import com.android.quickstep.util.WorkspaceRevealAnim;
import com.android.quickstep.views.FloatingWidgetView;
import com.android.quickstep.views.RecentsView;
@@ -229,6 +230,7 @@
private RemoteAnimationProvider mRemoteAnimationProvider;
// Strong refs to runners which are cleared when the launcher activity is destroyed
private RemoteAnimationFactory mWallpaperOpenRunner;
+ private RemoteAnimationFactory mAppLaunchRunner;
private RemoteAnimationFactory mKeyguardGoingAwayRunner;
private RemoteAnimationFactory mWallpaperOpenTransitionRunner;
@@ -298,17 +300,23 @@
boolean fromRecents = isLaunchingFromRecents(v, null /* targets */);
RunnableList onEndCallback = new RunnableList();
- RemoteAnimationFactory delegateRunner = new AppLaunchAnimationRunner(v, onEndCallback);
+ // Handle the case where an already visible task is launched which results in no transition
+ TaskRestartedDuringLaunchListener restartedListener =
+ new TaskRestartedDuringLaunchListener();
+ restartedListener.register(onEndCallback::executeAllAndDestroy);
+ onEndCallback.add(restartedListener::unregister);
+
+ mAppLaunchRunner = new AppLaunchAnimationRunner(v, onEndCallback);
ItemInfo tag = (ItemInfo) v.getTag();
if (tag != null && tag.shouldUseBackgroundAnimation()) {
ContainerAnimationRunner containerAnimationRunner =
ContainerAnimationRunner.from(v, mStartingWindowListener, onEndCallback);
if (containerAnimationRunner != null) {
- delegateRunner = containerAnimationRunner;
+ mAppLaunchRunner = containerAnimationRunner;
}
}
RemoteAnimationRunnerCompat runner = new LauncherAnimationRunner(
- mHandler, delegateRunner, true /* startAtFrontOfQueue */);
+ mHandler, mAppLaunchRunner, true /* startAtFrontOfQueue */);
// Note that this duration is a guess as we do not know if the animation will be a
// recents launch or not for sure until we know the opening app targets.
@@ -1164,6 +1172,7 @@
// Also clear strong references to the runners registered with the remote animation
// definition so we don't have to wait for the system gc
mWallpaperOpenRunner = null;
+ mAppLaunchRunner = null;
mKeyguardGoingAwayRunner = null;
}
}
diff --git a/quickstep/src/com/android/launcher3/proxy/StartActivityParams.java b/quickstep/src/com/android/launcher3/proxy/StartActivityParams.java
index b47ef47..4d0bee6 100644
--- a/quickstep/src/com/android/launcher3/proxy/StartActivityParams.java
+++ b/quickstep/src/com/android/launcher3/proxy/StartActivityParams.java
@@ -20,7 +20,10 @@
import static android.app.PendingIntent.FLAG_ONE_SHOT;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
+import static com.android.launcher3.Utilities.allowBGLaunch;
+
import android.app.Activity;
+import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
import android.content.Context;
@@ -91,9 +94,10 @@
}
public void deliverResult(Context context, int resultCode, Intent data) {
+ ActivityOptions options = allowBGLaunch(ActivityOptions.makeBasic());
try {
if (mPICallback != null) {
- mPICallback.send(context, resultCode, data);
+ mPICallback.send(context, resultCode, data, null, null, null, options.toBundle());
}
} catch (CanceledException e) {
Log.e(TAG, "Unable to send back result", e);
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 31af1ce..43aceec 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -579,6 +579,8 @@
Executors.MAIN_EXECUTOR.getHandler(), null,
elapsedRealTime -> callbacks.executeAllAndDestroy());
options.setSplashScreenStyle(splashScreenStyle);
+ options.setPendingIntentBackgroundActivityStartMode(
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
return new ActivityOptionsWrapper(options, callbacks);
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
index 8a8e21f..1e3f4f1 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarBackground.kt
@@ -48,6 +48,8 @@
private var showingArrow: Boolean = false
private var arrowDrawable: ShapeDrawable
+ var width: Float = 0f
+
init {
paint.color = context.getColor(R.color.taskbar_background)
paint.flags = Paint.ANTI_ALIAS_FLAG
@@ -59,8 +61,11 @@
pointerSize = res.getDimension(R.dimen.bubblebar_pointer_size)
shadowAlpha =
- if (Utilities.isDarkTheme(context)) DARK_THEME_SHADOW_ALPHA
- else LIGHT_THEME_SHADOW_ALPHA
+ if (Utilities.isDarkTheme(context)) {
+ DARK_THEME_SHADOW_ALPHA
+ } else {
+ LIGHT_THEME_SHADOW_ALPHA
+ }
arrowDrawable =
ShapeDrawable(TriangleShape.create(pointerSize, pointerSize, /* pointUp= */ true))
@@ -102,7 +107,7 @@
// Draw background.
val radius = backgroundHeight / 2f
canvas.drawRoundRect(
- 0f,
+ canvas.width.toFloat() - width,
0f,
canvas.width.toFloat(),
canvas.height.toFloat(),
@@ -132,4 +137,8 @@
override fun setColorFilter(colorFilter: ColorFilter?) {
paint.colorFilter = colorFilter
}
+
+ fun setArrowAlpha(alpha: Int) {
+ arrowDrawable.paint.alpha = alpha
+ }
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
index 8d20705..58c67e3 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarView.java
@@ -15,6 +15,7 @@
*/
package com.android.launcher3.taskbar.bubbles;
+import android.animation.Animator;
import android.animation.ValueAnimator;
import android.annotation.Nullable;
import android.content.Context;
@@ -66,8 +67,8 @@
// if it's smaller than 5.
private static final int MAX_BUBBLES = 5;
private static final int ARROW_POSITION_ANIMATION_DURATION_MS = 200;
+ private static final int WIDTH_ANIMATION_DURATION_MS = 200;
- private final TaskbarActivityContext mActivityContext;
private final BubbleBarBackground mBubbleBarBackground;
// The current bounds of all the bubble bar.
@@ -90,6 +91,10 @@
private final Rect mTempRect = new Rect();
+ // An animator that represents the expansion state of the bubble bar, where 0 corresponds to the
+ // collapsed state and 1 to the fully expanded state.
+ private final ValueAnimator mWidthAnimator = ValueAnimator.ofFloat(0, 1);
+
// We don't reorder the bubbles when they are expanded as it could be jarring for the user
// this runnable will be populated with any reordering of the bubbles that should be applied
// once they are collapsed.
@@ -110,7 +115,7 @@
public BubbleBarView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
- mActivityContext = ActivityContext.lookupContext(context);
+ TaskbarActivityContext activityContext = ActivityContext.lookupContext(context);
mIconOverlapAmount = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_overlap);
mIconSpacing = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_spacing);
@@ -118,9 +123,39 @@
mBubbleElevation = getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_elevation);
setClipToPadding(false);
- mBubbleBarBackground = new BubbleBarBackground(mActivityContext,
+ mBubbleBarBackground = new BubbleBarBackground(activityContext,
getResources().getDimensionPixelSize(R.dimen.bubblebar_size));
setBackgroundDrawable(mBubbleBarBackground);
+
+ mWidthAnimator.setDuration(WIDTH_ANIMATION_DURATION_MS);
+ mWidthAnimator.addUpdateListener(animation -> {
+ updateChildrenRenderNodeProperties();
+ invalidate();
+ });
+ mWidthAnimator.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mBubbleBarBackground.showArrow(mIsBarExpanded);
+ if (!mIsBarExpanded && mReorderRunnable != null) {
+ mReorderRunnable.run();
+ mReorderRunnable = null;
+ }
+ updateWidth();
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ mBubbleBarBackground.showArrow(true);
+ }
+ });
}
@Override
@@ -146,7 +181,7 @@
return mBubbleBarBounds;
}
- // TODO: (b/273592694) animate it
+ // TODO: (b/280605790) animate it
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
if (getChildCount() + 1 > MAX_BUBBLES) {
@@ -155,27 +190,55 @@
removeViewInLayout(getChildAt(getChildCount() - 2));
}
super.addView(child, index, params);
+ updateWidth();
+ }
+
+ // TODO: (b/283309949) animate it
+ @Override
+ public void removeView(View view) {
+ super.removeView(view);
+ updateWidth();
+ }
+
+ private void updateWidth() {
+ LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
+ lp.width = (int) (mIsBarExpanded ? expandedWidth() : collapsedWidth());
+ setLayoutParams(lp);
}
/**
* Updates the z order, positions, and badge visibility of the bubble views in the bar based
* on the expanded state.
*/
- // TODO: (b/273592694) animate it
private void updateChildrenRenderNodeProperties() {
+ final float widthState = (float) mWidthAnimator.getAnimatedValue();
+ final float currentWidth = getWidth();
+ final float expandedWidth = expandedWidth();
+ final float collapsedWidth = collapsedWidth();
int bubbleCount = getChildCount();
final float ty = (mBubbleBarBounds.height() - mIconSize) / 2f;
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);
+ // the position of the bubble when the bar is fully collapsed
+ final float collapsedX = i * mIconOverlapAmount;
+
if (mIsBarExpanded) {
- final float tx = i * (mIconSize + mIconSpacing);
- bv.setTranslationX(tx);
- bv.setZ(0);
+ // where the bubble will end up when the animation ends
+ final float targetX = currentWidth - expandedWidth + expandedX;
+ bv.setTranslationX(widthState * (targetX - collapsedX) + collapsedX);
+ // if we're fully expanded, set the z level to 0
+ if (widthState == 1f) {
+ bv.setZ(0);
+ }
bv.showBadge();
} else {
+ final float targetX = currentWidth - collapsedWidth + collapsedX;
+ bv.setTranslationX(widthState * (expandedX - targetX) + targetX);
bv.setZ((MAX_BUBBLES * mBubbleElevation) - i);
- bv.setTranslationX(i * mIconOverlapAmount);
if (i > 0) {
bv.hideBadge();
} else {
@@ -183,13 +246,33 @@
}
}
}
+
+ // update the arrow position
+ final float collapsedArrowPosition = arrowPositionForSelectedWhenCollapsed();
+ 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);
+ } else {
+ final float targetPosition = currentWidth - collapsedWidth + collapsedArrowPosition;
+ final float arrowPosition =
+ targetPosition + widthState * (expandedArrowPosition - targetPosition);
+ mBubbleBarBackground.setArrowPosition(arrowPosition);
+ }
+
+ mBubbleBarBackground.setArrowAlpha((int) (255 * widthState));
+ mBubbleBarBackground.setWidth(interpolatedWidth);
}
/**
* Reorders the views to match the provided list.
*/
public void reorder(List<BubbleView> viewOrder) {
- if (isExpanded()) {
+ if (isExpanded() || mWidthAnimator.isRunning()) {
mReorderRunnable = () -> doReorder(viewOrder);
} else {
doReorder(viewOrder);
@@ -249,6 +332,16 @@
}
}
+ private float arrowPositionForSelectedWhenExpanded() {
+ final int index = indexOfChild(mSelectedBubbleView);
+ return getPaddingStart() + index * (mIconSize + mIconSpacing) + mIconSize / 2f;
+ }
+
+ private float arrowPositionForSelectedWhenCollapsed() {
+ final int index = indexOfChild(mSelectedBubbleView);
+ return getPaddingStart() + index * (mIconOverlapAmount) + mIconSize / 2f;
+ }
+
@Override
public void setOnClickListener(View.OnClickListener listener) {
mOnClickListener = listener;
@@ -266,18 +359,16 @@
/**
* Sets whether the bubble bar is expanded or collapsed.
*/
- // TODO: (b/273592694) animate it
public void setExpanded(boolean isBarExpanded) {
if (mIsBarExpanded != isBarExpanded) {
mIsBarExpanded = isBarExpanded;
updateArrowForSelected(/* shouldAnimate= */ false);
setOrUnsetClickListener();
- if (!isBarExpanded && mReorderRunnable != null) {
- mReorderRunnable.run();
- mReorderRunnable = null;
+ if (isBarExpanded) {
+ mWidthAnimator.start();
+ } else {
+ mWidthAnimator.reverse();
}
- mBubbleBarBackground.showArrow(mIsBarExpanded);
- requestLayout(); // trigger layout to reposition views & update size for expansion
}
}
@@ -288,19 +379,16 @@
return mIsBarExpanded;
}
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ private float expandedWidth() {
final int childCount = getChildCount();
- final float iconWidth = mIsBarExpanded
- ? (childCount * (mIconSize + mIconSpacing))
- : mIconSize + ((childCount - 1) * mIconOverlapAmount);
- final int totalWidth = (int) iconWidth + getPaddingStart() + getPaddingEnd();
- setMeasuredDimension(totalWidth, MeasureSpec.getSize(heightMeasureSpec));
+ final int horizontalPadding = getPaddingStart() + getPaddingEnd();
+ return childCount * (mIconSize + mIconSpacing) + horizontalPadding;
+ }
- for (int i = 0; i < childCount; i++) {
- View child = getChildAt(i);
- measureChild(child, (int) mIconSize, (int) mIconSize);
- }
+ private float collapsedWidth() {
+ final int childCount = getChildCount();
+ final int horizontalPadding = getPaddingStart() + getPaddingEnd();
+ return mIconSize + ((childCount - 1) * mIconOverlapAmount) + horizontalPadding;
}
/**
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 512d5f4..582b795 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -344,11 +344,13 @@
@Override
public RunnableList startActivitySafely(View v, Intent intent, ItemInfo item) {
- // Only pause is taskbar controller is not present
+ // Only pause is taskbar controller is not present until the transition (if it exists) ends
mHotseatPredictionController.setPauseUIUpdate(getTaskbarUIController() == null);
RunnableList result = super.startActivitySafely(v, intent, item);
- if (getTaskbarUIController() == null && result == null) {
- mHotseatPredictionController.setPauseUIUpdate(false);
+ if (result == null) {
+ if (getTaskbarUIController() == null) {
+ mHotseatPredictionController.setPauseUIUpdate(false);
+ }
} else {
result.add(() -> mHotseatPredictionController.setPauseUIUpdate(false));
}
@@ -1105,6 +1107,8 @@
activityOptions.options.setLaunchDisplayId(
(v != null && v.getDisplay() != null) ? v.getDisplay().getDisplayId()
: Display.DEFAULT_DISPLAY);
+ activityOptions.options.setPendingIntentBackgroundActivityStartMode(
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
addLaunchCookie(item, activityOptions.options);
return activityOptions;
}
@@ -1117,6 +1121,8 @@
Executors.MAIN_EXECUTOR.getHandler(), null,
elapsedRealTime -> callbacks.executeAllAndDestroy());
options.setSplashScreenStyle(splashScreenStyle);
+ options.setPendingIntentBackgroundActivityStartMode(
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
return new ActivityOptionsWrapper(options, callbacks);
}
@@ -1305,11 +1311,9 @@
: groupTask.mSplitBounds.leftTaskPercent);
}
- @Override
public boolean isCommandQueueEmpty() {
OverviewCommandHelper overviewCommandHelper = mTISBindHelper.getOverviewCommandHelper();
- return super.isCommandQueueEmpty()
- && (overviewCommandHelper == null || overviewCommandHelper.isCommandQueueEmpty());
+ return overviewCommandHelper == null || overviewCommandHelper.isCommandQueueEmpty();
}
private static final class LauncherTaskViewController extends
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index e282d1f..7cb6eb6 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -275,6 +275,8 @@
activityOptions.options.setLaunchDisplayId(
(v != null && v.getDisplay() != null) ? v.getDisplay().getDisplayId()
: Display.DEFAULT_DISPLAY);
+ activityOptions.options.setPendingIntentBackgroundActivityStartMode(
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
mHandler.postDelayed(mAnimationStartTimeoutRunnable, RECENTS_ANIMATION_TIMEOUT);
return activityOptions;
}
@@ -467,10 +469,8 @@
};
}
- @Override
public boolean isCommandQueueEmpty() {
OverviewCommandHelper overviewCommandHelper = mTISBindHelper.getOverviewCommandHelper();
- return super.isCommandQueueEmpty()
- && (overviewCommandHelper == null || overviewCommandHelper.isCommandQueueEmpty());
+ return overviewCommandHelper == null || overviewCommandHelper.isCommandQueueEmpty();
}
}
diff --git a/quickstep/src/com/android/quickstep/TaskIconCache.java b/quickstep/src/com/android/quickstep/TaskIconCache.java
index 7c05a10..164a366 100644
--- a/quickstep/src/com/android/quickstep/TaskIconCache.java
+++ b/quickstep/src/com/android/quickstep/TaskIconCache.java
@@ -15,7 +15,6 @@
*/
package com.android.quickstep;
-import static com.android.launcher3.uioverrides.QuickstepLauncher.GO_LOW_RAM_RECENTS_ENABLED;
import static com.android.launcher3.util.DisplayController.CHANGE_DENSITY;
import android.annotation.Nullable;
@@ -182,17 +181,14 @@
}
}
- // Loading content descriptions if accessibility or low RAM recents is enabled.
- if (GO_LOW_RAM_RECENTS_ENABLED || mAccessibilityManager.isEnabled()) {
- // Skip loading the content description if the activity no longer exists
- if (activityInfo == null) {
- activityInfo = PackageManagerWrapper.getInstance().getActivityInfo(
- key.getComponent(), key.userId);
- }
- if (activityInfo != null) {
- entry.contentDescription = getBadgedContentDescription(
- activityInfo, task.key.userId, task.taskDescription);
- }
+ // Skip loading the content description if the activity no longer exists
+ if (activityInfo == null) {
+ activityInfo = PackageManagerWrapper.getInstance().getActivityInfo(
+ key.getComponent(), key.userId);
+ }
+ if (activityInfo != null) {
+ entry.contentDescription = getBadgedContentDescription(
+ activityInfo, task.key.userId, task.taskDescription);
}
mIconCache.put(task.key, entry);
diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java
index bfe52dd..c9bad38 100644
--- a/quickstep/src/com/android/quickstep/TaskViewUtils.java
+++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java
@@ -473,16 +473,14 @@
throw new IllegalStateException(
"Expected task to be showing, but it is " + mode);
}
- if (change.getParent() == null) {
- throw new IllegalStateException("Initiating multi-split launch but the split"
- + "root of " + taskId + " is already visible or has broken hierarchy.");
- }
}
if (taskId == initialTaskId) {
- splitRoot1 = transitionInfo.getChange(change.getParent());
+ splitRoot1 = change.getParent() == null ? change :
+ transitionInfo.getChange(change.getParent());
}
if (taskId == secondTaskId) {
- splitRoot2 = transitionInfo.getChange(change.getParent());
+ splitRoot2 = change.getParent() == null ? change :
+ transitionInfo.getChange(change.getParent());
}
}
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index a9a57c6..7dc8347 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -86,7 +86,7 @@
}
@Override
- public boolean isCommandQueueEmpty() {
+ protected boolean isCommandQueueEmpty() {
return mActivity.isCommandQueueEmpty();
}
diff --git a/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java b/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java
new file mode 100644
index 0000000..91e8376
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/TaskRestartedDuringLaunchListener.java
@@ -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 static android.app.ActivityTaskManager.INVALID_TASK_ID;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter;
+import com.android.quickstep.RecentsModel;
+import com.android.systemui.shared.system.TaskStackChangeListener;
+import com.android.systemui.shared.system.TaskStackChangeListeners;
+
+/**
+ * This class tracks the failure of a task launch through the Launcher.startActivitySafely() call,
+ * in an edge case in which a task may already be visible on screen (ie. in PIP) and no transition
+ * will be run in WM, which results in expected callbacks to not be processed.
+ *
+ * We transiently register a task stack listener during a task launch and if the restart signal is
+ * received, then the registered callback will be notified.
+ */
+public class TaskRestartedDuringLaunchListener implements TaskStackChangeListener {
+
+ private static final String TAG = "TaskRestartedDuringLaunchListener";
+
+ private @NonNull Runnable mTaskRestartedCallback = null;
+
+ /**
+ * Registers a failure listener callback if it detects a scenario in which an app launch
+ * resulted in an already existing task to be "restarted".
+ */
+ public void register(@NonNull Runnable taskRestartedCallback) {
+ TaskStackChangeListeners.getInstance().registerTaskStackListener(this);
+ mTaskRestartedCallback = taskRestartedCallback;
+ }
+
+ /**
+ * Unregisters the failure listener.
+ */
+ public void unregister() {
+ TaskStackChangeListeners.getInstance().unregisterTaskStackListener(this);
+ mTaskRestartedCallback = null;
+ }
+
+ @Override
+ public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task,
+ boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) {
+ if (wasVisible) {
+ Log.d(TAG, "Detected activity restart during launch for task=" + task.taskId);
+ mTaskRestartedCallback.run();
+ unregister();
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index 9e9b22f..3ee9009 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -90,7 +90,7 @@
}
@Override
- public boolean isCommandQueueEmpty() {
+ protected boolean isCommandQueueEmpty() {
return mActivity.isCommandQueueEmpty();
}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index f0daf8d..421a48c 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -2374,7 +2374,7 @@
protected abstract void handleStartHome(boolean animated);
/** Returns whether the overview command helper queue is empty. */
- public abstract boolean isCommandQueueEmpty();
+ protected abstract boolean isCommandQueueEmpty();
public void reset() {
setCurrentTask(-1);
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java
index 40e3dca..5301c7c 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.java
+++ b/quickstep/src/com/android/quickstep/views/TaskView.java
@@ -842,17 +842,20 @@
// the actual overview state
failureListener.register(mActivity, mTask.key.id, () -> {
notifyTaskLaunchFailed(TAG);
- // Disable animations for now, as it is an edge case and the app usually covers
- // launcher and also any state transition animation also gets clobbered by
- // QuickstepTransitionManager.createWallpaperOpenAnimations when launcher
- // shows again
- getRecentsView().startHome(false /* animated */);
RecentsView rv = getRecentsView();
- if (rv != null && rv.mSizeStrategy.getTaskbarController() != null) {
- // LauncherTaskbarUIController depends on the launcher state when checking
- // whether to handle resume, but that can come in before startHome() changes
- // the state, so force-refresh here to ensure the taskbar is updated
- rv.mSizeStrategy.getTaskbarController().refreshResumedState();
+ if (rv != null) {
+ // Disable animations for now, as it is an edge case and the app usually
+ // covers launcher and also any state transition animation also gets
+ // clobbered by QuickstepTransitionManager.createWallpaperOpenAnimations
+ // when launcher shows again
+ rv.startHome(false /* animated */);
+ if (rv.mSizeStrategy.getTaskbarController() != null) {
+ // LauncherTaskbarUIController depends on the launcher state when
+ // checking whether to handle resume, but that can come in before
+ // startHome() changes the state, so force-refresh here to ensure the
+ // taskbar is updated
+ rv.mSizeStrategy.getTaskbarController().refreshResumedState();
+ }
}
});
}
diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java
index eb1c4d4..9db8c82 100644
--- a/src/com/android/launcher3/LauncherAppState.java
+++ b/src/com/android/launcher3/LauncherAppState.java
@@ -17,6 +17,7 @@
package com.android.launcher3;
import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED;
+import static android.content.Context.RECEIVER_EXPORTED;
import static com.android.launcher3.LauncherPrefs.ICON_STATE;
import static com.android.launcher3.LauncherPrefs.THEMED_ICONS;
@@ -26,6 +27,7 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.LauncherApps;
@@ -38,7 +40,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.graphics.IconShape;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.icons.IconProvider;
@@ -112,8 +113,9 @@
new SimpleBroadcastReceiver(mModel::onBroadcastIntent);
modelChangeReceiver.register(mContext, Intent.ACTION_LOCALE_CHANGED,
ACTION_DEVICE_POLICY_RESOURCE_UPDATED);
- if (FeatureFlags.IS_STUDIO_BUILD) {
- modelChangeReceiver.register(mContext, ACTION_FORCE_ROLOAD);
+ if (BuildConfig.IS_STUDIO_BUILD) {
+ mContext.registerReceiver(modelChangeReceiver, new IntentFilter(ACTION_FORCE_ROLOAD),
+ RECEIVER_EXPORTED);
}
mOnTerminateCallback.add(() -> mContext.unregisterReceiver(modelChangeReceiver));
diff --git a/src/com/android/launcher3/Utilities.java b/src/com/android/launcher3/Utilities.java
index 709c57c..4f5de05 100644
--- a/src/com/android/launcher3/Utilities.java
+++ b/src/com/android/launcher3/Utilities.java
@@ -24,6 +24,7 @@
import android.annotation.TargetApi;
import android.app.ActivityManager;
+import android.app.ActivityOptions;
import android.app.Person;
import android.app.WallpaperManager;
import android.content.Context;
@@ -561,6 +562,17 @@
}
/**
+ * Utility method to allow background activity launch for the provided activity options
+ */
+ public static ActivityOptions allowBGLaunch(ActivityOptions options) {
+ if (ATLEAST_U) {
+ options.setPendingIntentBackgroundActivityStartMode(
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
+ }
+ return options;
+ }
+
+ /**
* Returns the full drawable for info without any flattening or pre-processing.
*
* @param shouldThemeIcon If true, will theme icons when applicable
@@ -569,12 +581,13 @@
*/
@TargetApi(Build.VERSION_CODES.TIRAMISU)
public static Drawable getFullDrawable(Context context, ItemInfo info, int width, int height,
- boolean shouldThemeIcon, Object[] outObj) {
+ boolean shouldThemeIcon, Object[] outObj, boolean[] outIsIconThemed) {
Drawable icon = loadFullDrawableWithoutTheme(context, info, width, height, outObj);
if (ATLEAST_T && icon instanceof AdaptiveIconDrawable && shouldThemeIcon) {
AdaptiveIconDrawable aid = (AdaptiveIconDrawable) icon.mutate();
Drawable mono = aid.getMonochrome();
if (mono != null && Themes.isThemedIconEnabled(context)) {
+ outIsIconThemed[0] = true;
int[] colors = ThemedIconDrawable.getColors(context);
mono = mono.mutate();
mono.setTint(colors[1]);
@@ -635,7 +648,8 @@
* badge. When dragged from workspace or folder, it may contain app AND/OR work profile badge
**/
@TargetApi(Build.VERSION_CODES.O)
- public static Drawable getBadge(Context context, ItemInfo info, Object obj) {
+ public static Drawable getBadge(Context context, ItemInfo info, Object obj,
+ boolean isIconThemed) {
LauncherAppState appState = LauncherAppState.getInstance(context);
if (info.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
boolean iconBadged = (info instanceof ItemInfoWithIcon)
@@ -653,7 +667,8 @@
} else {
return Process.myUserHandle().equals(info.user)
? new ColorDrawable(Color.TRANSPARENT)
- : context.getDrawable(R.drawable.ic_work_app_badge);
+ : context.getDrawable(isIconThemed
+ ? R.drawable.ic_work_app_badge_themed : R.drawable.ic_work_app_badge);
}
}
diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
index a7a25f4..758bffb 100644
--- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
@@ -7,6 +7,7 @@
import static com.android.launcher3.LauncherState.NORMAL;
import static com.android.launcher3.anim.AnimatorListeners.forSuccessCallback;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_NOT_PINNABLE;
import android.appwidget.AppWidgetProviderInfo;
import android.graphics.Point;
@@ -124,12 +125,19 @@
}
}
- if ((item instanceof WorkspaceItemFactory) || (item instanceof WorkspaceItemInfo)
- || (item instanceof PendingAddItemInfo)) {
+ if (supportAddToWorkSpace(item)) {
out.add(mActions.get(ADD_TO_WORKSPACE));
}
}
+ private boolean supportAddToWorkSpace(ItemInfo item) {
+ return (item instanceof WorkspaceItemFactory)
+ || ((item instanceof WorkspaceItemInfo)
+ && (((WorkspaceItemInfo) item).runtimeStatusFlags & FLAG_NOT_PINNABLE) == 0)
+ || ((item instanceof PendingAddItemInfo)
+ && (((PendingAddItemInfo) item).runtimeStatusFlags & FLAG_NOT_PINNABLE) == 0);
+ }
+
/**
* Returns all the accessibility actions that can be handled by the host.
*/
diff --git a/src/com/android/launcher3/dragndrop/DragView.java b/src/com/android/launcher3/dragndrop/DragView.java
index c26d673..b9bb52c 100644
--- a/src/com/android/launcher3/dragndrop/DragView.java
+++ b/src/com/android/launcher3/dragndrop/DragView.java
@@ -224,11 +224,11 @@
// Load the adaptive icon on a background thread and add the view in ui thread.
MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(() -> {
Object[] outObj = new Object[1];
+ boolean[] outIsIconThemed = new boolean[1];
int w = mWidth;
int h = mHeight;
Drawable dr = Utilities.getFullDrawable(mActivity, info, w, h,
- true /* shouldThemeIcon */, outObj);
-
+ true /* shouldThemeIcon */, outObj, outIsIconThemed);
if (dr instanceof AdaptiveIconDrawable) {
int blurMargin = (int) mActivity.getResources()
.getDimension(R.dimen.blur_size_medium_outline) / 2;
@@ -237,7 +237,7 @@
bounds.inset(blurMargin, blurMargin);
// Badge is applied after icon normalization so the bounds for badge should not
// be scaled down due to icon normalization.
- mBadge = getBadge(mActivity, info, outObj[0]);
+ mBadge = getBadge(mActivity, info, outObj[0], outIsIconThemed[0]);
FastBitmapDrawable.setBadgeBounds(mBadge, bounds);
// Do not draw the background in case of folder as its translucent
diff --git a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
index e5fb015..b4a935a 100644
--- a/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
+++ b/src/com/android/launcher3/model/data/ItemInfoWithIcon.java
@@ -119,6 +119,11 @@
| FLAG_DISABLED_VERSION_LOWER;
/**
+ * Flag indicating this item can't be pinned to home screen.
+ */
+ public static final int FLAG_NOT_PINNABLE = 1 << 13;
+
+ /**
* 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/notification/NotificationInfo.java b/src/com/android/launcher3/notification/NotificationInfo.java
index bb2c37f..f4468fd 100644
--- a/src/com/android/launcher3/notification/NotificationInfo.java
+++ b/src/com/android/launcher3/notification/NotificationInfo.java
@@ -18,6 +18,7 @@
import static com.android.launcher3.AbstractFloatingView.TYPE_ACTION_POPUP;
import static com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_ALL_APPS;
+import static com.android.launcher3.Utilities.allowBGLaunch;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_NOTIFICATION_LAUNCH_TAP;
import android.app.ActivityOptions;
@@ -26,7 +27,6 @@
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
-import android.os.Bundle;
import android.service.notification.StatusBarNotification;
import android.view.View;
@@ -103,10 +103,10 @@
return;
}
final ActivityContext context = ActivityContext.lookupContext(view.getContext());
- Bundle activityOptions = ActivityOptions.makeClipRevealAnimation(
- view, 0, 0, view.getWidth(), view.getHeight()).toBundle();
+ ActivityOptions options = allowBGLaunch(ActivityOptions.makeClipRevealAnimation(
+ view, 0, 0, view.getWidth(), view.getHeight()));
try {
- intent.send(null, 0, null, null, null, null, activityOptions);
+ intent.send(null, 0, null, null, null, null, options.toBundle());
context.getStatsLogManager().logger().withItemInfo(mItemInfo)
.log(LAUNCHER_NOTIFICATION_LAUNCH_TAP);
} catch (PendingIntent.CanceledException e) {
diff --git a/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java b/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java
index 06da8c5..351ebce 100644
--- a/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java
+++ b/src/com/android/launcher3/pm/ShortcutConfigActivityInfo.java
@@ -16,8 +16,11 @@
package com.android.launcher3.pm;
+import static com.android.launcher3.Utilities.allowBGLaunch;
+
import android.annotation.TargetApi;
import android.app.Activity;
+import android.app.ActivityOptions;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
@@ -138,8 +141,10 @@
}
IntentSender is = activity.getSystemService(LauncherApps.class)
.getShortcutConfigActivityIntent(mInfo);
+ ActivityOptions options = allowBGLaunch(ActivityOptions.makeBasic());
try {
- activity.startIntentSenderForResult(is, requestCode, null, 0, 0, 0);
+ activity.startIntentSenderForResult(is, requestCode, null, 0, 0, 0,
+ options.toBundle());
return true;
} catch (IntentSender.SendIntentException e) {
Toast.makeText(activity, R.string.activity_not_found, Toast.LENGTH_SHORT).show();
diff --git a/src/com/android/launcher3/popup/RemoteActionShortcut.java b/src/com/android/launcher3/popup/RemoteActionShortcut.java
index 7c9ab87..eab0969 100644
--- a/src/com/android/launcher3/popup/RemoteActionShortcut.java
+++ b/src/com/android/launcher3/popup/RemoteActionShortcut.java
@@ -16,10 +16,12 @@
package com.android.launcher3.popup;
+import static com.android.launcher3.Utilities.allowBGLaunch;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SYSTEM_SHORTCUT_PAUSE_TAP;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import android.annotation.TargetApi;
+import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.app.RemoteAction;
import android.content.Context;
@@ -84,6 +86,8 @@
final WeakReference<BaseDraggingActivity> weakTarget = new WeakReference<>(mTarget);
final String actionIdentity = mAction.getTitle() + ", "
+ mItemInfo.getTargetComponent().getPackageName();
+
+ ActivityOptions options = allowBGLaunch(ActivityOptions.makeBasic());
try {
if (DEBUG) Log.d(TAG, "Sending action: " + actionIdentity);
mAction.getActionIntent().send(
@@ -103,7 +107,9 @@
}
}
},
- MAIN_EXECUTOR.getHandler());
+ MAIN_EXECUTOR.getHandler(),
+ null,
+ options.toBundle());
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "Remote action canceled: " + actionIdentity, e);
Toast.makeText(mTarget, mTarget.getString(
diff --git a/src/com/android/launcher3/statemanager/StatefulActivity.java b/src/com/android/launcher3/statemanager/StatefulActivity.java
index de5887f..520f33c 100644
--- a/src/com/android/launcher3/statemanager/StatefulActivity.java
+++ b/src/com/android/launcher3/statemanager/StatefulActivity.java
@@ -237,10 +237,4 @@
* @param leftOrTop if the staged split will be positioned left or top.
*/
public void enterStageSplitFromRunningApp(boolean leftOrTop) { }
-
-
- /** Returns whether the overview command helper queue is empty. */
- public boolean isCommandQueueEmpty() {
- return true;
- }
}
diff --git a/src/com/android/launcher3/views/ActivityContext.java b/src/com/android/launcher3/views/ActivityContext.java
index 67f24aa..d04f5e2 100644
--- a/src/com/android/launcher3/views/ActivityContext.java
+++ b/src/com/android/launcher3/views/ActivityContext.java
@@ -18,6 +18,7 @@
import static android.window.SplashScreen.SPLASH_SCREEN_STYLE_SOLID_COLOR;
import static com.android.launcher3.LauncherSettings.Animation.DEFAULT_NO_ICON;
+import static com.android.launcher3.Utilities.allowBGLaunch;
import static com.android.launcher3.logging.KeyboardStateManager.KeyboardState.HIDE;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_ALLAPPS_KEYBOARD_CLOSED;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_APP_LAUNCH_PENDING_INTENT;
@@ -38,7 +39,6 @@
import android.os.Bundle;
import android.os.IBinder;
import android.os.Process;
-import android.os.StrictMode;
import android.os.UserHandle;
import android.util.Log;
import android.view.Display;
@@ -414,8 +414,7 @@
}
}
ActivityOptions options =
- ActivityOptions.makeClipRevealAnimation(v, left, top, width, height);
-
+ allowBGLaunch(ActivityOptions.makeClipRevealAnimation(v, left, top, width, height));
options.setLaunchDisplayId(
(v != null && v.getDisplay() != null) ? v.getDisplay().getDisplayId()
: Display.DEFAULT_DISPLAY);
@@ -427,7 +426,7 @@
* Creates a default activity option and we do not want association with any launcher element.
*/
default ActivityOptionsWrapper makeDefaultActivityOptions(int splashScreenStyle) {
- ActivityOptions options = ActivityOptions.makeBasic();
+ ActivityOptions options = allowBGLaunch(ActivityOptions.makeBasic());
if (Utilities.ATLEAST_T) {
options.setSplashScreenStyle(splashScreenStyle);
}
diff --git a/src/com/android/launcher3/views/FloatingIconView.java b/src/com/android/launcher3/views/FloatingIconView.java
index f425821..41b98c7 100644
--- a/src/com/android/launcher3/views/FloatingIconView.java
+++ b/src/com/android/launcher3/views/FloatingIconView.java
@@ -289,12 +289,14 @@
int width = (int) pos.width();
int height = (int) pos.height();
Object[] tmpObjArray = new Object[1];
+ boolean[] outIsIconThemed = new boolean[1];
if (supportsAdaptiveIcons) {
boolean shouldThemeIcon = btvIcon instanceof FastBitmapDrawable
&& ((FastBitmapDrawable) btvIcon).isThemed();
- drawable = getFullDrawable(l, info, width, height, shouldThemeIcon, tmpObjArray);
+ drawable = getFullDrawable(
+ l, info, width, height, shouldThemeIcon, tmpObjArray, outIsIconThemed);
if (drawable instanceof AdaptiveIconDrawable) {
- badge = getBadge(l, info, tmpObjArray[0]);
+ badge = getBadge(l, info, tmpObjArray[0], outIsIconThemed[0]);
} else {
// The drawable we get back is not an adaptive icon, so we need to use the
// BubbleTextView icon that is already legacy treated.
@@ -306,7 +308,7 @@
drawable = btvIcon;
} else {
drawable = getFullDrawable(l, info, width, height, true /* shouldThemeIcon */,
- tmpObjArray);
+ tmpObjArray, outIsIconThemed);
}
}
}
diff --git a/src/com/android/launcher3/widget/LauncherWidgetHolder.java b/src/com/android/launcher3/widget/LauncherWidgetHolder.java
index 2ca825c..6acc83d 100644
--- a/src/com/android/launcher3/widget/LauncherWidgetHolder.java
+++ b/src/com/android/launcher3/widget/LauncherWidgetHolder.java
@@ -275,9 +275,15 @@
protected Bundle getConfigurationActivityOptions(@NonNull BaseDraggingActivity activity,
int widgetId) {
LauncherAppWidgetHostView view = mViews.get(widgetId);
- if (view == null) return null;
+ if (view == null) {
+ return activity.makeDefaultActivityOptions(
+ -1 /* SPLASH_SCREEN_STYLE_UNDEFINED */).toBundle();
+ }
Object tag = view.getTag();
- if (!(tag instanceof ItemInfo)) return null;
+ if (!(tag instanceof ItemInfo)) {
+ return activity.makeDefaultActivityOptions(
+ -1 /* SPLASH_SCREEN_STYLE_UNDEFINED */).toBundle();
+ }
Bundle bundle = activity.getActivityLaunchOptions(view, (ItemInfo) tag).toBundle();
bundle.putInt(KEY_SPLASH_SCREEN_STYLE, SPLASH_SCREEN_STYLE_EMPTY);
return bundle;
diff --git a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
index 8cc01ff..36e4e76 100644
--- a/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
+++ b/tests/shared/com/android/launcher3/testing/shared/TestProtocol.java
@@ -156,6 +156,7 @@
public static final String WORK_TAB_MISSING = "b/243688989";
public static final String TWO_TASKBAR_LONG_CLICKS = "b/262282528";
public static final String FLAKY_ACTIVITY_COUNT = "b/260260325";
+ public static final String ICON_MISSING = "b/282963545";
public static final String REQUEST_EMULATE_DISPLAY = "emulate-display";
public static final String REQUEST_STOP_EMULATE_DISPLAY = "stop-emulate-display";
diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
index 1262a26..7f796e7 100644
--- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
+++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java
@@ -17,6 +17,7 @@
import static androidx.test.InstrumentationRegistry.getInstrumentation;
+import static com.android.launcher3.testing.shared.TestProtocol.ICON_MISSING;
import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
@@ -603,6 +604,8 @@
protected HomeAppIcon createShortcutIfNotExist(String name, int cellX, int cellY) {
HomeAppIcon homeAppIcon = mLauncher.getWorkspace().tryGetWorkspaceAppIcon(name);
+ Log.d(ICON_MISSING, "homeAppIcon: " + homeAppIcon + " name: " + name +
+ " cell: " + cellX + ", " + cellY);
if (homeAppIcon == null) {
HomeAllApps allApps = mLauncher.getWorkspace().switchToAllApps();
allApps.freeze();
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
index 095b135..2c8acc4 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3.java
@@ -18,6 +18,8 @@
import static androidx.test.InstrumentationRegistry.getInstrumentation;
+import static com.android.launcher3.testing.shared.TestProtocol.ICON_MISSING;
+
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
@@ -579,6 +581,11 @@
@PlatinumTest(focusArea = "launcher")
public void getIconsPosition_afterIconRemoved_notContained() throws IOException {
Point[] gridPositions = getCornersAndCenterPositions();
+ StringBuilder sb = new StringBuilder();
+ for (Point p : gridPositions) {
+ sb.append(p).append(", ");
+ }
+ Log.d(ICON_MISSING, "allGridPositions: " + sb);
createShortcutIfNotExist(STORE_APP_NAME, gridPositions[0]);
createShortcutIfNotExist(MAPS_APP_NAME, gridPositions[1]);
installDummyAppAndWaitForUIUpdate();
diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java
new file mode 100644
index 0000000..e40fb79
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java
@@ -0,0 +1,91 @@
+/*
+ * 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.util.viewcapture_analysis;
+
+import static com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer.diagPathFromRoot;
+
+import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer.AnalysisNode;
+import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer.AnomalyDetector;
+
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * Anomaly detector that triggers an error when alpha of a view changes too rapidly.
+ * Invisible views are treated as if they had zero alpha.
+ */
+final class AlphaJumpDetector extends AnomalyDetector {
+ // Paths of nodes that are excluded from analysis.
+ private static final Collection<String> PATHS_TO_IGNORE = Set.of(
+ "DecorView|LinearLayout|FrameLayout:id/content|LauncherRootView:id/launcher|DragLayer"
+ + ":id/drag_layer|SearchContainerView:id/apps_view|SearchRecyclerView:id"
+ + "/search_results_list_view|SearchResultSmallIconRow",
+ "DecorView|LinearLayout|FrameLayout:id/content|LauncherRootView:id/launcher|DragLayer"
+ + ":id/drag_layer|SearchContainerView:id/apps_view|SearchRecyclerView:id"
+ + "/search_results_list_view|SearchResultIcon",
+ "DecorView|LinearLayout|FrameLayout:id/content|LauncherRootView:id/launcher|DragLayer"
+ + ":id/drag_layer|LauncherRecentsView:id/overview_panel|TaskView",
+ "DecorView|LinearLayout|FrameLayout:id/content|LauncherRootView:id/launcher|DragLayer"
+ + ":id/drag_layer|WidgetsFullSheet|SpringRelativeLayout:id/container"
+ + "|WidgetsRecyclerView:id/primary_widgets_list_view|WidgetsListHeader:id"
+ + "/widgets_list_header",
+ "DecorView|LinearLayout|FrameLayout:id/content|LauncherRootView:id/launcher|DragLayer"
+ + ":id/drag_layer|WidgetsFullSheet|SpringRelativeLayout:id/container"
+ + "|WidgetsRecyclerView:id/primary_widgets_list_view"
+ + "|StickyHeaderLayout$EmptySpaceView",
+ "DecorView|LinearLayout|FrameLayout:id/content|LauncherRootView:id/launcher|DragLayer"
+ + ":id/drag_layer|SearchContainerView:id/apps_view|AllAppsRecyclerView:id"
+ + "/apps_list_view|BubbleTextView:id/icon",
+ "DecorView|LinearLayout|FrameLayout:id/content|LauncherRootView:id/launcher|DragLayer"
+ + ":id/drag_layer|LauncherRecentsView:id/overview_panel|ClearAllButton:id"
+ + "/clear_all",
+ "DecorView|LinearLayout|FrameLayout:id/content|LauncherRootView:id/launcher|DragLayer"
+ + ":id/drag_layer|NexusOverviewActionsView:id/overview_actions_view"
+ + "|LinearLayout:id/action_buttons"
+ );
+ // Minimal increase or decrease of view's alpha between frames that triggers the error.
+ private static final float ALPHA_JUMP_THRESHOLD = 1f;
+
+ @Override
+ void initializeNode(AnalysisNode info) {
+ // If the parent view ignores alpha jumps, its descendants will too.
+ final boolean parentIgnoreAlphaJumps = info.parent != null && info.parent.ignoreAlphaJumps;
+ info.ignoreAlphaJumps = parentIgnoreAlphaJumps
+ || PATHS_TO_IGNORE.contains(diagPathFromRoot(info));
+ }
+
+ @Override
+ void detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN) {
+ // If the view was previously seen, proceed with analysis only if it was present in the
+ // view hierarchy in the previous frame.
+ if (oldInfo != null && oldInfo.frameN != frameN) return;
+
+ final AnalysisNode latestInfo = newInfo != null ? newInfo : oldInfo;
+ if (latestInfo.ignoreAlphaJumps) return;
+
+ final float oldAlpha = oldInfo != null ? oldInfo.alpha : 0;
+ final float newAlpha = newInfo != null ? newInfo.alpha : 0;
+ final float alphaDeltaAbs = Math.abs(newAlpha - oldAlpha);
+
+ if (alphaDeltaAbs >= ALPHA_JUMP_THRESHOLD) {
+ throw new AssertionError(
+ String.format(
+ "Alpha jump detected in ViewCapture data: alpha change: %s (%s -> %s)"
+ + ", threshold: %s, view: %s",
+ alphaDeltaAbs, oldAlpha, newAlpha, ALPHA_JUMP_THRESHOLD, latestInfo));
+ }
+ }
+}
diff --git a/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java b/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java
new file mode 100644
index 0000000..5a2611c
--- /dev/null
+++ b/tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java
@@ -0,0 +1,238 @@
+/*
+ * 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.util.viewcapture_analysis;
+
+import static android.view.View.VISIBLE;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.app.viewcapture.data.ExportedData;
+import com.android.app.viewcapture.data.FrameData;
+import com.android.app.viewcapture.data.ViewNode;
+import com.android.app.viewcapture.data.WindowData;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Utility that analyzes ViewCapture data and finds anomalies such as views appearing or
+ * disappearing without alpha-fading.
+ */
+public class ViewCaptureAnalyzer {
+ private static final String SCRIM_VIEW_CLASS = "com.android.launcher3.views.ScrimView";
+
+ /**
+ * Detector of one kind of anomaly.
+ */
+ abstract static class AnomalyDetector {
+ /**
+ * Initializes fields of the node that are specific to the anomaly detected by this
+ * detector.
+ */
+ abstract void initializeNode(@NonNull AnalysisNode info);
+
+ /**
+ * Detects anomalies by looking at the last occurrence of a view, and the current one.
+ * null value means that the view. 'oldInfo' and 'newInfo' cannot be both null.
+ * If an anomaly is detected, an exception will be thrown.
+ *
+ * @param oldInfo the view, as seen in the last frame that contained it in the view
+ * hierarchy before 'currentFrame'. 'null' means that the view is first seen
+ * in the 'currentFrame'.
+ * @param newInfo the view in the view hierarchy of the 'currentFrame'. 'null' means that
+ * the view is not present in the 'currentFrame', but was present in earlier
+ * frames.
+ * @param frameN number of the current frame.
+ */
+ abstract void detectAnomalies(
+ @Nullable AnalysisNode oldInfo, @Nullable AnalysisNode newInfo, int frameN);
+ }
+
+ // All detectors. They will be invoked in the order listed here.
+ private static final Iterable<AnomalyDetector> ANOMALY_DETECTORS = Arrays.asList(
+ new AlphaJumpDetector()
+ );
+
+ // A view from view capture data converted to a form that's convenient for detecting anomalies.
+ static class AnalysisNode {
+ public String className;
+ public String resourceId;
+ public AnalysisNode parent;
+
+ // Window coordinates of the view.
+ public float left;
+ public float top;
+
+ // Visible scale and alpha, build recursively from the ancestor list.
+ public float scaleX;
+ public float scaleY;
+ public float alpha;
+
+ public int frameN;
+ public ViewNode viewCaptureNode;
+
+ public boolean ignoreAlphaJumps;
+
+ @Override
+ public String toString() {
+ return String.format("window coordinates: (%s, %s), class path from the root: %s",
+ left, top, diagPathFromRoot(this));
+ }
+ }
+
+ /**
+ * Scans a view capture record and throws an error if an anomaly is found.
+ */
+ public static void assertNoAnomalies(ExportedData viewCaptureData) {
+ final int scrimClassIndex = viewCaptureData.getClassnameList().indexOf(SCRIM_VIEW_CLASS);
+
+ final int windowDataCount = viewCaptureData.getWindowDataCount();
+ for (int i = 0; i < windowDataCount; ++i) {
+ analyzeWindowData(viewCaptureData, viewCaptureData.getWindowData(i), scrimClassIndex);
+ }
+ }
+
+ private static void analyzeWindowData(ExportedData viewCaptureData, WindowData windowData,
+ int scrimClassIndex) {
+ // View hash code => Last seen node with this hash code.
+ // The view is added when we analyze the first frame where it's visible.
+ // After that, it gets updated for every frame where it's visible.
+ // As we go though frames, if a view becomes invisible, it stays in the map.
+ final Map<Integer, AnalysisNode> lastSeenNodes = new HashMap<>();
+
+ for (int frameN = 0; frameN < windowData.getFrameDataCount(); ++frameN) {
+ analyzeFrame(frameN, windowData.getFrameData(frameN), viewCaptureData, lastSeenNodes,
+ scrimClassIndex);
+ }
+ }
+
+ private static void analyzeFrame(int frameN, FrameData frame, ExportedData viewCaptureData,
+ Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex) {
+ // Analyze the node tree starting from the root.
+ analyzeView(
+ frame.getNode(),
+ /* parent = */ null,
+ frameN,
+ /* leftShift = */ 0,
+ /* topShift = */ 0,
+ viewCaptureData,
+ lastSeenNodes,
+ scrimClassIndex);
+
+ // Analyze transitions when a view visible in the last frame become invisible in the
+ // current one.
+ for (AnalysisNode info : lastSeenNodes.values()) {
+ if (info.frameN == frameN - 1) {
+ if (!info.viewCaptureNode.getWillNotDraw()) {
+ ANOMALY_DETECTORS.forEach(
+ detector -> detector.detectAnomalies(
+ /* oldInfo = */ info,
+ /* newInfo = */ null,
+ frameN));
+ }
+ }
+ }
+ }
+
+ private static void analyzeView(ViewNode viewCaptureNode, AnalysisNode parent, int frameN,
+ float leftShift, float topShift, ExportedData viewCaptureData,
+ Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex) {
+ // Skip analysis of invisible views
+ final float parentAlpha = parent != null ? parent.alpha : 1;
+ final float alpha = getVisibleAlpha(viewCaptureNode, parentAlpha);
+ if (alpha <= 0.0) return;
+
+ // Calculate analysis node parameters
+ final int hashcode = viewCaptureNode.getHashcode();
+ final int classIndex = viewCaptureNode.getClassnameIndex();
+
+ final float parentScaleX = parent != null ? parent.scaleX : 1;
+ final float parentScaleY = parent != null ? parent.scaleY : 1;
+ final float scaleX = parentScaleX * viewCaptureNode.getScaleX();
+ final float scaleY = parentScaleY * viewCaptureNode.getScaleY();
+
+ final float left = leftShift
+ + (viewCaptureNode.getLeft() + viewCaptureNode.getTranslationX()) * parentScaleX
+ + viewCaptureNode.getWidth() * (parentScaleX - scaleX) / 2;
+ final float top = topShift
+ + (viewCaptureNode.getTop() + viewCaptureNode.getTranslationY()) * parentScaleY
+ + viewCaptureNode.getHeight() * (parentScaleY - scaleY) / 2;
+
+ // Initialize new analysis node
+ final AnalysisNode newAnalysisNode = new AnalysisNode();
+ newAnalysisNode.className = viewCaptureData.getClassname(classIndex);
+ newAnalysisNode.resourceId = viewCaptureNode.getId();
+ newAnalysisNode.parent = parent;
+ newAnalysisNode.left = left;
+ newAnalysisNode.top = top;
+ newAnalysisNode.scaleX = scaleX;
+ newAnalysisNode.scaleY = scaleY;
+ newAnalysisNode.alpha = alpha;
+ newAnalysisNode.frameN = frameN;
+ newAnalysisNode.viewCaptureNode = viewCaptureNode;
+ ANOMALY_DETECTORS.forEach(detector -> detector.initializeNode(newAnalysisNode));
+
+ // Detect anomalies for the view
+ final AnalysisNode oldAnalysisNode = lastSeenNodes.get(hashcode); // may be null
+ if (frameN != 0 && !viewCaptureNode.getWillNotDraw()) {
+ ANOMALY_DETECTORS.forEach(
+ detector -> detector.detectAnomalies(oldAnalysisNode, newAnalysisNode, frameN));
+ }
+ lastSeenNodes.put(hashcode, newAnalysisNode);
+
+ // Enumerate children starting from the topmost one. Stop at ScrimView, if present.
+ final float leftShiftForChildren = left - viewCaptureNode.getScrollX();
+ final float topShiftForChildren = top - viewCaptureNode.getScrollY();
+ for (int i = viewCaptureNode.getChildrenCount() - 1; i >= 0; --i) {
+ final ViewNode child = viewCaptureNode.getChildren(i);
+
+ // Don't analyze anything under scrim view because we don't know whether it's
+ // transparent.
+ if (child.getClassnameIndex() == scrimClassIndex) break;
+
+ analyzeView(child, newAnalysisNode, frameN, leftShiftForChildren, topShiftForChildren,
+ viewCaptureData, lastSeenNodes,
+ scrimClassIndex);
+ }
+ }
+
+ private static float getVisibleAlpha(ViewNode node, float parenVisibleAlpha) {
+ return node.getVisibility() == VISIBLE
+ ? parenVisibleAlpha * Math.max(0, Math.min(node.getAlpha(), 1))
+ : 0f;
+ }
+
+ private static String classNameToSimpleName(String className) {
+ return className.substring(className.lastIndexOf(".") + 1);
+ }
+
+ static String diagPathFromRoot(AnalysisNode nodeBox) {
+ final StringBuilder path = new StringBuilder(diagPathElement(nodeBox));
+ for (AnalysisNode ancestor = nodeBox.parent; ancestor != null; ancestor = ancestor.parent) {
+ path.insert(0, diagPathElement(ancestor) + "|");
+ }
+ return path.toString();
+ }
+
+ private static String diagPathElement(AnalysisNode nodeBox) {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(classNameToSimpleName(nodeBox.className));
+ if (!"NO_ID".equals(nodeBox.resourceId)) sb.append(":" + nodeBox.resourceId);
+ return sb.toString();
+ }
+}