Merge "Animate bubble bar icon sizes and padding" into main
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index ff97b22..9051ca8 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -5,7 +5,7 @@
ktfmt = --kotlinlang-style
[Tool Paths]
-ktfmt = ${REPO_ROOT}/prebuilts/build-tools/common/framework/ktfmt.jar
+ktfmt = ${REPO_ROOT}/external/ktfmt/ktfmt.sh
[Hook Scripts]
checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --config_xml tools/checkstyle.xml --sha ${PREUPLOAD_COMMIT}
diff --git a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
index 0000c0d..9178062 100644
--- a/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
+++ b/quickstep/src/com/android/launcher3/desktop/DesktopRecentsTransitionController.kt
@@ -35,7 +35,7 @@
/** Manage recents related operations with desktop tasks */
class DesktopRecentsTransitionController(
- private val stateManager: StateManager<*>,
+ private val stateManager: StateManager<*, *>,
private val systemUiProxy: SystemUiProxy,
private val appThread: IApplicationThread,
private val depthController: DepthController?
@@ -64,7 +64,7 @@
private class RemoteDesktopLaunchTransitionRunner(
private val desktopTaskView: DesktopTaskView,
- private val stateManager: StateManager<*>,
+ private val stateManager: StateManager<*, *>,
private val depthController: DepthController?,
private val successCallback: Consumer<Boolean>?
) : RemoteTransitionStub() {
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
index 05c2f08..6b62c86 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java
@@ -62,7 +62,6 @@
import android.util.Log;
import android.view.Display;
import android.view.Gravity;
-import android.view.RoundedCorner;
import android.view.Surface;
import android.view.View;
import android.view.WindowInsets;
@@ -166,7 +165,6 @@
private final TaskbarControllers mControllers;
private final WindowManager mWindowManager;
- private final @Nullable RoundedCorner mLeftCorner, mRightCorner;
private DeviceProfile mDeviceProfile;
private WindowManager.LayoutParams mWindowLayoutParams;
private boolean mIsFullscreen;
@@ -227,16 +225,8 @@
Context c = getApplicationContext();
mWindowManager = c.getSystemService(WindowManager.class);
- boolean phoneMode = isPhoneMode();
- mLeftCorner = phoneMode
- ? null
- : display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT);
- mRightCorner = phoneMode
- ? null
- : display.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT);
-
// Inflate views.
- int taskbarLayout = DisplayController.isTransientTaskbar(this) && !phoneMode
+ int taskbarLayout = DisplayController.isTransientTaskbar(this) && !isPhoneMode()
? R.layout.transient_taskbar
: R.layout.taskbar;
mDragLayer = (TaskbarDragLayer) mLayoutInflater.inflate(taskbarLayout, null, false);
@@ -615,12 +605,9 @@
return mImeDrawsImeNavBar;
}
- public int getLeftCornerRadius() {
- return mLeftCorner == null ? 0 : mLeftCorner.getRadius();
- }
-
- public int getRightCornerRadius() {
- return mRightCorner == null ? 0 : mRightCorner.getRadius();
+ public int getCornerRadius() {
+ return isPhoneMode() ? 0 : getResources().getDimensionPixelSize(
+ R.dimen.persistent_taskbar_corner_radius);
}
public WindowManager.LayoutParams getWindowLayoutParams() {
@@ -1013,7 +1000,7 @@
return mDeviceProfile.taskbarHeight
- + Math.max(getLeftCornerRadius(), getRightCornerRadius())
+ + getCornerRadius()
+ extraHeightForTaskbarTooltips;
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
index bafd059..2737cbd 100644
--- a/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
+++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarBackgroundRenderer.kt
@@ -37,8 +37,6 @@
class TaskbarBackgroundRenderer(private val context: TaskbarActivityContext) {
private val isInSetup: Boolean = !context.isUserSetupComplete
- private val DARK_THEME_SHADOW_ALPHA = 51f
- private val LIGHT_THEME_SHADOW_ALPHA = 25f
private val maxTransientTaskbarHeight =
context.transientTaskbarDeviceProfile.taskbarHeight.toFloat()
@@ -54,6 +52,7 @@
var isAnimatingPinning = false
val paint = Paint()
+ private val strokePaint = Paint()
val lastDrawnTransientRect = RectF()
var backgroundHeight = context.deviceProfile.taskbarHeight.toFloat()
var translationYForSwipe = 0f
@@ -62,19 +61,18 @@
private val transientBackgroundBounds = context.transientTaskbarBounds
private val shadowAlpha: Float
+ private val strokeAlpha: Int
private var shadowBlur = 0f
private var keyShadowDistance = 0f
private var bottomMargin = 0
- private val fullLeftCornerRadius = context.leftCornerRadius.toFloat()
- private val fullRightCornerRadius = context.rightCornerRadius.toFloat()
- private var leftCornerRadius = fullLeftCornerRadius
- private var rightCornerRadius = fullRightCornerRadius
+ private val fullCornerRadius = context.cornerRadius.toFloat()
+ private var cornerRadius = fullCornerRadius
private var widthInsetPercentage = 0f
- private val square: Path = Path()
- private val circle: Path = Path()
- private val invertedLeftCornerPath: Path = Path()
- private val invertedRightCornerPath: Path = Path()
+ private val square = Path()
+ private val circle = Path()
+ private val invertedLeftCornerPath = Path()
+ private val invertedRightCornerPath = Path()
private var stashedHandleWidth =
context.resources.getDimensionPixelSize(R.dimen.taskbar_stashed_handle_width)
@@ -86,10 +84,18 @@
paint.color = context.getColor(R.color.taskbar_background)
paint.flags = Paint.ANTI_ALIAS_FLAG
paint.style = Paint.Style.FILL
-
- shadowAlpha =
- if (Utilities.isDarkTheme(context)) DARK_THEME_SHADOW_ALPHA
- else LIGHT_THEME_SHADOW_ALPHA
+ strokePaint.color = context.getColor(R.color.taskbar_stroke)
+ strokePaint.flags = Paint.ANTI_ALIAS_FLAG
+ strokePaint.style = Paint.Style.STROKE
+ strokePaint.strokeWidth =
+ context.resources.getDimension(R.dimen.transient_taskbar_stroke_width)
+ if (Utilities.isDarkTheme(context)) {
+ strokeAlpha = DARK_THEME_STROKE_ALPHA
+ shadowAlpha = DARK_THEME_SHADOW_ALPHA
+ } else {
+ strokeAlpha = LIGHT_THEME_STROKE_ALPHA
+ shadowAlpha = LIGHT_THEME_SHADOW_ALPHA
+ }
setCornerRoundness(DEFAULT_ROUNDNESS)
}
@@ -106,7 +112,7 @@
}
/**
- * Sets the roundness of the round corner above Taskbar. No effect on transient Taskkbar.
+ * Sets the roundness of the round corner above Taskbar. No effect on transient Taskbar.
*
* @param cornerRoundness 0 has no round corner, 1 has complete round corner.
*/
@@ -115,21 +121,18 @@
return
}
- leftCornerRadius = fullLeftCornerRadius * cornerRoundness
- rightCornerRadius = fullRightCornerRadius * cornerRoundness
+ cornerRadius = fullCornerRadius * cornerRoundness
// Create the paths for the inverted rounded corners above the taskbar. Start with a filled
// square, and then subtract out a circle from the appropriate corner.
square.reset()
- square.addRect(0f, 0f, leftCornerRadius, leftCornerRadius, Path.Direction.CW)
+ square.addRect(0f, 0f, cornerRadius, cornerRadius, Path.Direction.CW)
circle.reset()
- circle.addCircle(leftCornerRadius, 0f, leftCornerRadius, Path.Direction.CW)
+ circle.addCircle(cornerRadius, 0f, cornerRadius, Path.Direction.CW)
invertedLeftCornerPath.op(square, circle, Path.Op.DIFFERENCE)
- square.reset()
- square.addRect(0f, 0f, rightCornerRadius, rightCornerRadius, Path.Direction.CW)
circle.reset()
- circle.addCircle(0f, 0f, rightCornerRadius, Path.Direction.CW)
+ circle.addCircle(0f, 0f, cornerRadius, Path.Direction.CW)
invertedRightCornerPath.op(square, circle, Path.Op.DIFFERENCE)
}
@@ -163,10 +166,10 @@
}
// Draw the inverted rounded corners above the taskbar.
- canvas.translate(0f, -leftCornerRadius)
+ canvas.translate(0f, -cornerRadius)
canvas.drawPath(invertedLeftCornerPath, paint)
- canvas.translate(0f, leftCornerRadius)
- canvas.translate(canvas.width - rightCornerRadius, -rightCornerRadius)
+ canvas.translate(0f, cornerRadius)
+ canvas.translate(canvas.width - cornerRadius, -cornerRadius)
canvas.drawPath(invertedRightCornerPath, paint)
}
@@ -236,6 +239,7 @@
keyShadowDistance,
setColorAlphaBound(Color.BLACK, Math.round(newShadowAlpha))
)
+ strokePaint.alpha = (paint.alpha * strokeAlpha) / 255
lastDrawnTransientRect.set(
transientBackgroundBounds.left + halfWidthDelta,
@@ -247,6 +251,7 @@
lastDrawnTransientRect.inset(horizontalInset, 0f)
canvas.drawRoundRect(lastDrawnTransientRect, radius, radius, paint)
+ canvas.drawRoundRect(lastDrawnTransientRect, radius, radius, strokePaint)
}
/**
@@ -259,5 +264,9 @@
companion object {
const val DEFAULT_ROUNDNESS = 1f
+ private const val DARK_THEME_STROKE_ALPHA = 51
+ private const val LIGHT_THEME_STROKE_ALPHA = 41
+ private const val DARK_THEME_SHADOW_ALPHA = 51f
+ private const val LIGHT_THEME_SHADOW_ALPHA = 25f
}
}
diff --git a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
index 046f5b6..028df34 100644
--- a/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
+++ b/quickstep/src/com/android/launcher3/taskbar/bubbles/BubbleBarController.java
@@ -135,7 +135,6 @@
private static final Executor BUBBLE_STATE_EXECUTOR = Executors.newSingleThreadExecutor(
new SimpleThreadFactory("BubbleStateUpdates-", THREAD_PRIORITY_BACKGROUND));
- private final Executor mMainExecutor;
private final LauncherApps mLauncherApps;
private final BubbleIconFactory mIconFactory;
private final SystemUiProxy mSystemUiProxy;
@@ -198,7 +197,6 @@
if (sBubbleBarEnabled) {
mSystemUiProxy.setBubblesListener(this);
}
- mMainExecutor = MAIN_EXECUTOR;
mLauncherApps = context.getSystemService(LauncherApps.class);
mIconFactory = new BubbleIconFactory(context,
context.getResources().getDimensionPixelSize(R.dimen.bubblebar_icon_size),
@@ -241,7 +239,7 @@
private void createAndAddOverflowIfNeeded() {
if (mOverflowBubble == null) {
BubbleBarOverflow overflow = createOverflow(mContext);
- mMainExecutor.execute(() -> {
+ MAIN_EXECUTOR.execute(() -> {
// we're on the main executor now, so check that the overflow hasn't been created
// again to avoid races.
if (mOverflowBubble == null) {
@@ -303,12 +301,12 @@
}
viewUpdate.currentBubbles = currentBubbles;
}
- mMainExecutor.execute(() -> applyViewChanges(viewUpdate));
+ MAIN_EXECUTOR.execute(() -> applyViewChanges(viewUpdate));
});
} else {
// No bubbles to load, immediately apply the changes.
BUBBLE_STATE_EXECUTOR.execute(
- () -> mMainExecutor.execute(() -> applyViewChanges(viewUpdate)));
+ () -> MAIN_EXECUTOR.execute(() -> applyViewChanges(viewUpdate)));
}
}
@@ -496,7 +494,7 @@
@Override
public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) {
- mMainExecutor.execute(
+ MAIN_EXECUTOR.execute(
() -> mBubbleBarViewController.animateBubbleBarLocation(bubbleBarLocation));
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index e3a2bab..2168f7a 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -1118,7 +1118,7 @@
}
@Override
- protected void collectStateHandlers(List<StateHandler> out) {
+ public void collectStateHandlers(List<StateHandler<LauncherState>> out) {
super.collectStateHandlers(out);
out.add(getDepthController());
out.add(new RecentsViewStateController(this));
diff --git a/quickstep/src/com/android/quickstep/FallbackActivityInterface.java b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
index 9e896fd..89fbf4a 100644
--- a/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/FallbackActivityInterface.java
@@ -154,7 +154,8 @@
@Override
public void onExitOverview(RotationTouchHelper deviceState, Runnable exitRunnable) {
- final StateManager<RecentsState> stateManager = getCreatedContainer().getStateManager();
+ final StateManager<RecentsState, RecentsActivity> stateManager =
+ getCreatedContainer().getStateManager();
if (stateManager.getState() == HOME) {
exitRunnable.run();
notifyRecentsOfOrientation(deviceState);
diff --git a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
index 08c2e1c..c428827 100644
--- a/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
+++ b/quickstep/src/com/android/quickstep/LauncherActivityInterface.java
@@ -36,6 +36,7 @@
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Flags;
+import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAnimUtils;
import com.android.launcher3.LauncherInitListener;
import com.android.launcher3.LauncherState;
@@ -242,7 +243,8 @@
@Override
public void onExitOverview(RotationTouchHelper deviceState, Runnable exitRunnable) {
- final StateManager<LauncherState> stateManager = getCreatedContainer().getStateManager();
+ final StateManager<LauncherState, Launcher> stateManager =
+ getCreatedContainer().getStateManager();
stateManager.addStateListener(
new StateManager.StateListener<LauncherState>() {
@Override
diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
index 26a7c2f..7da92bc 100644
--- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
+++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java
@@ -228,8 +228,9 @@
}
BaseActivityInterface<?, T> activityInterface =
mOverviewComponentObserver.getActivityInterface();
- RecentsView visibleRecentsView = activityInterface.getVisibleRecentsView();
- RecentsView createdRecentsView;
+
+ RecentsView<?, ?> visibleRecentsView = activityInterface.getVisibleRecentsView();
+ RecentsView<?, ?> createdRecentsView;
Log.d(TAG, "executeCommand: " + cmd
+ " - visibleRecentsView: " + visibleRecentsView);
diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java
index 3df62da..13e9844 100644
--- a/quickstep/src/com/android/quickstep/RecentsActivity.java
+++ b/quickstep/src/com/android/quickstep/RecentsActivity.java
@@ -115,7 +115,7 @@
private TISBindHelper mTISBindHelper;
private @Nullable FallbackTaskbarUIController mTaskbarUIController;
- private StateManager<RecentsState> mStateManager;
+ private StateManager<RecentsState, RecentsActivity> mStateManager;
// Strong refs to runners which are cleared when the activity is destroyed
private RemoteAnimationFactory mActivityLaunchAnimationRunner;
@@ -386,6 +386,11 @@
}
}
+ @Override
+ public boolean shouldAnimateStateChange() {
+ return false;
+ }
+
/**
* Initialize/update the device profile.
*/
@@ -462,12 +467,12 @@
};
@Override
- protected void collectStateHandlers(List<StateHandler> out) {
+ public void collectStateHandlers(List<StateHandler<RecentsState>> out) {
out.add(new FallbackRecentsStateController(this));
}
@Override
- public StateManager<RecentsState> getStateManager() {
+ public StateManager<RecentsState, RecentsActivity> getStateManager() {
return mStateManager;
}
diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
index 8bcdaa3..7adce74 100644
--- a/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
+++ b/quickstep/src/com/android/quickstep/RecentsAnimationDeviceState.java
@@ -45,6 +45,7 @@
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_PINNING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED;
+import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED;
import android.app.ActivityTaskManager;
import android.content.Context;
@@ -386,7 +387,7 @@
boolean canStartWithNavHidden = (mSystemUiStateFlags & SYSUI_STATE_NAV_BAR_HIDDEN) == 0
|| (mSystemUiStateFlags & SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY) != 0
|| mRotationTouchHelper.isTaskListFrozen();
- return canStartWithNavHidden && canStartTrackpadGesture();
+ return canStartWithNavHidden && canStartAnyGesture();
}
/**
@@ -395,14 +396,24 @@
* mode.
*/
public boolean canStartTrackpadGesture() {
- return (mSystemUiStateFlags & SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED) == 0
- && (mSystemUiStateFlags & SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING) == 0
- && (mSystemUiStateFlags & SYSUI_STATE_QUICK_SETTINGS_EXPANDED) == 0
- && (mSystemUiStateFlags & SYSUI_STATE_MAGNIFICATION_OVERLAP) == 0
- && ((mSystemUiStateFlags & SYSUI_STATE_HOME_DISABLED) == 0
- || (mSystemUiStateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0)
- && (mSystemUiStateFlags & SYSUI_STATE_DEVICE_DREAMING) == 0
- && (mSystemUiStateFlags & SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION) == 0;
+ boolean trackpadGesturesEnabled =
+ (mSystemUiStateFlags & SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED) == 0;
+ return trackpadGesturesEnabled && canStartAnyGesture();
+ }
+
+ /**
+ * Common logic to determine if either trackpad or finger gesture can be started
+ */
+ private boolean canStartAnyGesture() {
+ boolean homeOrOverviewEnabled = (mSystemUiStateFlags & SYSUI_STATE_HOME_DISABLED) == 0
+ || (mSystemUiStateFlags & SYSUI_STATE_OVERVIEW_DISABLED) == 0;
+ long gestureDisablingStates = SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED
+ | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
+ | SYSUI_STATE_QUICK_SETTINGS_EXPANDED
+ | SYSUI_STATE_MAGNIFICATION_OVERLAP
+ | SYSUI_STATE_DEVICE_DREAMING
+ | SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION;
+ return (gestureDisablingStates & mSystemUiStateFlags) == 0 && homeOrOverviewEnabled;
}
/**
diff --git a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
index 485d6c4..f4a2738 100644
--- a/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
+++ b/quickstep/src/com/android/quickstep/fallback/FallbackRecentsView.java
@@ -93,7 +93,7 @@
}
@Override
- public StateManager<RecentsState> getStateManager() {
+ public StateManager<RecentsState, RecentsActivity> getStateManager() {
return mContainer.getStateManager();
}
diff --git a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
index 4f1dbbe..36ea926 100644
--- a/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
+++ b/quickstep/src/com/android/quickstep/interaction/GestureSandboxActivity.java
@@ -59,6 +59,7 @@
@Nullable private TutorialType[] mTutorialSteps;
private GestureSandboxFragment mCurrentFragment;
+ private GestureSandboxFragment mPendingFragment;
private int mCurrentStep;
private int mNumSteps;
@@ -176,16 +177,26 @@
&& getResources().getConfiguration().orientation
== ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
- showFragment(showRotationPrompt
+ GestureSandboxFragment fragment = showRotationPrompt
? new RotationPromptFragment()
- : mCurrentFragment.canRecreateFragment()
- ? mCurrentFragment.recreateFragment() : mCurrentFragment);
+ : mCurrentFragment.canRecreateFragment() || mPendingFragment == null
+ ? mCurrentFragment.recreateFragment()
+ : mPendingFragment.recreateFragment();
+ showFragment(fragment == null ? mCurrentFragment : fragment);
+
} else {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
}
private void showFragment(@NonNull GestureSandboxFragment fragment) {
+ // Store the current fragment in mPendingFragment so that it can be recreated after the
+ // new fragment is shown.
+ if (mCurrentFragment.canRecreateFragment()) {
+ mPendingFragment = mCurrentFragment;
+ } else {
+ mPendingFragment = null;
+ }
mCurrentFragment = fragment;
getSupportFragmentManager().beginTransaction()
.replace(R.id.gesture_tutorial_fragment_container, mCurrentFragment)
diff --git a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
index c9647f5..c7c04ed 100644
--- a/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
+++ b/quickstep/src/com/android/quickstep/util/AnimatorControllerWithResistance.java
@@ -39,6 +39,7 @@
import com.android.launcher3.anim.AnimatorPlaybackController;
import com.android.launcher3.anim.PendingAnimation;
import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.statemanager.StatefulActivity;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.launcher3.touch.AllAppsSwipeController;
import com.android.quickstep.DeviceConfigWrapper;
@@ -163,7 +164,8 @@
recentsOrientedState.getContainerInterface().getCreatedContainer();
if (container != null) {
RecentsView recentsView = container.getOverviewPanel();
- StateManager<LauncherState> stateManager = recentsView.getStateManager();
+ StateManager<LauncherState, StatefulActivity<LauncherState>> stateManager =
+ recentsView.getStateManager();
if (stateManager.isInStableState(LauncherState.BACKGROUND_APP)
&& stateManager.isInTransition()) {
diff --git a/quickstep/src/com/android/quickstep/util/OverviewToHomeAnim.java b/quickstep/src/com/android/quickstep/util/OverviewToHomeAnim.java
index 132d1c1..492c801 100644
--- a/quickstep/src/com/android/quickstep/util/OverviewToHomeAnim.java
+++ b/quickstep/src/com/android/quickstep/util/OverviewToHomeAnim.java
@@ -62,7 +62,7 @@
* {@link WorkspaceRevealAnim}.
*/
public void animateWithVelocity(float velocity) {
- StateManager<LauncherState> stateManager = mLauncher.getStateManager();
+ StateManager<LauncherState, Launcher> stateManager = mLauncher.getStateManager();
LauncherState startState = stateManager.getState();
if (startState != OVERVIEW) {
Log.e(TAG, "animateFromOverviewToHome: unexpected start state " + startState);
diff --git a/quickstep/src/com/android/quickstep/util/PhoneSplitToConfirmTimings.java b/quickstep/src/com/android/quickstep/util/PhoneSplitToConfirmTimings.java
index 3d9e09e..cbe0f19 100644
--- a/quickstep/src/com/android/quickstep/util/PhoneSplitToConfirmTimings.java
+++ b/quickstep/src/com/android/quickstep/util/PhoneSplitToConfirmTimings.java
@@ -27,6 +27,8 @@
public int getPlaceholderIconFadeInEnd() { return 133; }
public int getStagedRectSlideStart() { return 0; }
public int getStagedRectSlideEnd() { return 333; }
+ public int getBackingScrimFadeInStart() { return 0; }
+ public int getBackingScrimFadeInEnd() { return 266; }
public int getDuration() { return PHONE_CONFIRM_DURATION; }
}
diff --git a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
index 2a27dea..cfe5b2c 100644
--- a/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
+++ b/quickstep/src/com/android/quickstep/util/QuickstepOnboardingPrefs.java
@@ -27,6 +27,7 @@
import static com.android.launcher3.util.OnboardingPrefs.HOME_BOUNCE_SEEN;
import static com.android.launcher3.util.OnboardingPrefs.HOTSEAT_DISCOVERY_TIP_COUNT;
+import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherPrefs;
import com.android.launcher3.LauncherState;
import com.android.launcher3.Utilities;
@@ -48,7 +49,7 @@
* Sets up the initial onboarding behavior for the launcher
*/
public static void setup(QuickstepLauncher launcher) {
- StateManager<LauncherState> stateManager = launcher.getStateManager();
+ StateManager<LauncherState, Launcher> stateManager = launcher.getStateManager();
if (!HOME_BOUNCE_SEEN.get(launcher)) {
stateManager.addStateListener(new StateListener<LauncherState>() {
@Override
diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
index 8243ede..7ea04b1 100644
--- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
+++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt
@@ -281,64 +281,45 @@
}
/**
- * Creates and returns a view to fade in at .4 animation progress and adds it to the provided
- * [pendingAnimation]. Assumes that animation will be the final split placeholder launch anim.
- *
- * [secondPlaceholderEndingBounds] refers to the second placeholder view that gets added on
- * screen, not the logical second app. For landscape it's the left app and for portrait the top
- * one.
+ * Creates and returns a fullscreen scrim to fade in behind the split confirm animation, and
+ * adds it to the provided [pendingAnimation].
*/
- fun addDividerPlaceholderViewToAnim(
+ fun addScrimBehindAnim(
pendingAnimation: PendingAnimation,
container: RecentsViewContainer,
- secondPlaceholderEndingBounds: Rect,
context: Context
): View {
- val mSplitDividerPlaceholderView = View(context)
+ val scrim = View(context)
val recentsView = container.getOverviewPanel<RecentsView<*, *>>()
- val dp: com.android.launcher3.DeviceProfile = container.getDeviceProfile()
+ val dp: DeviceProfile = container.getDeviceProfile()
// Add it before/under the most recently added first floating taskView
val firstAddedSplitViewIndex: Int =
container
.getDragLayer()
.indexOfChild(recentsView.splitSelectController.firstFloatingTaskView)
- container.getDragLayer().addView(mSplitDividerPlaceholderView, firstAddedSplitViewIndex)
- val lp = mSplitDividerPlaceholderView.layoutParams as InsettableFrameLayout.LayoutParams
+ container.getDragLayer().addView(scrim, firstAddedSplitViewIndex)
+ // Make the scrim fullscreen
+ val lp = scrim.layoutParams as InsettableFrameLayout.LayoutParams
lp.topMargin = 0
+ lp.height = dp.heightPx
+ lp.width = dp.widthPx
- if (dp.isLeftRightSplit) {
- lp.height = secondPlaceholderEndingBounds.height()
- lp.width =
- container
- .asContext()
- .resources
- .getDimensionPixelSize(R.dimen.split_divider_handle_region_height)
- mSplitDividerPlaceholderView.translationX =
- secondPlaceholderEndingBounds.right - lp.width / 2f
- mSplitDividerPlaceholderView.translationY = 0f
- } else {
- lp.height =
- container
- .asContext()
- .resources
- .getDimensionPixelSize(R.dimen.split_divider_handle_region_height)
- lp.width = secondPlaceholderEndingBounds.width()
- mSplitDividerPlaceholderView.translationY =
- secondPlaceholderEndingBounds.top - lp.height / 2f
- mSplitDividerPlaceholderView.translationX = 0f
- }
-
- mSplitDividerPlaceholderView.alpha = 0f
- mSplitDividerPlaceholderView.setBackgroundColor(
+ scrim.alpha = 0f
+ scrim.setBackgroundColor(
container.asContext().resources.getColor(R.color.taskbar_background_dark)
)
- val timings = AnimUtils.getDeviceSplitToConfirmTimings(dp.isTablet)
+ val timings = AnimUtils.getDeviceSplitToConfirmTimings(dp.isTablet) as SplitToConfirmTimings
pendingAnimation.setViewAlpha(
- mSplitDividerPlaceholderView,
+ scrim,
1f,
- Interpolators.clampToProgress(timings.stagedRectScaleXInterpolator, 0.4f, 1f)
+ Interpolators.clampToProgress(
+ timings.backingScrimFadeInterpolator,
+ timings.backingScrimFadeInStartOffset,
+ timings.backingScrimFadeInEndOffset
+ )
)
- return mSplitDividerPlaceholderView
+
+ return scrim
}
/** Does not play any animation if user is not currently in split selection state. */
@@ -510,7 +491,7 @@
apps: Array<RemoteAnimationTarget>?,
wallpapers: Array<RemoteAnimationTarget>?,
nonApps: Array<RemoteAnimationTarget>?,
- stateManager: StateManager<*>,
+ stateManager: StateManager<*, *>,
depthController: DepthController?,
info: TransitionInfo?,
t: Transaction?,
@@ -589,7 +570,7 @@
@VisibleForTesting
fun composeRecentsSplitLaunchAnimator(
launchingTaskView: GroupedTaskView,
- stateManager: StateManager<*>,
+ stateManager: StateManager<*, *>,
depthController: DepthController?,
info: TransitionInfo,
t: Transaction,
@@ -617,7 +598,7 @@
apps: Array<RemoteAnimationTarget>,
wallpapers: Array<RemoteAnimationTarget>,
nonApps: Array<RemoteAnimationTarget>,
- stateManager: StateManager<*>,
+ stateManager: StateManager<*, *>,
depthController: DepthController?,
finishCallback: Runnable
) {
diff --git a/quickstep/src/com/android/quickstep/util/SplitToConfirmTimings.java b/quickstep/src/com/android/quickstep/util/SplitToConfirmTimings.java
index d1ec2b6..7557ad1 100644
--- a/quickstep/src/com/android/quickstep/util/SplitToConfirmTimings.java
+++ b/quickstep/src/com/android/quickstep/util/SplitToConfirmTimings.java
@@ -17,6 +17,7 @@
package com.android.quickstep.util;
import static com.android.app.animation.Interpolators.EMPHASIZED;
+import static com.android.app.animation.Interpolators.LINEAR;
import android.view.animation.Interpolator;
@@ -31,6 +32,8 @@
abstract public int getPlaceholderIconFadeInEnd();
abstract public int getStagedRectSlideStart();
abstract public int getStagedRectSlideEnd();
+ abstract public int getBackingScrimFadeInStart();
+ abstract public int getBackingScrimFadeInEnd();
// Common timings
public int getInstructionsFadeStart() { return 0; }
@@ -39,6 +42,7 @@
public Interpolator getStagedRectYInterpolator() { return EMPHASIZED; }
public Interpolator getStagedRectScaleXInterpolator() { return EMPHASIZED; }
public Interpolator getStagedRectScaleYInterpolator() { return EMPHASIZED; }
+ public Interpolator getBackingScrimFadeInterpolator() { return LINEAR; }
abstract public int getDuration();
@@ -48,4 +52,10 @@
public float getInstructionsFadeEndOffset() {
return (float) getInstructionsFadeEnd() / getDuration();
}
+ public float getBackingScrimFadeInStartOffset() {
+ return (float) getBackingScrimFadeInStart() / getDuration();
+ }
+ public float getBackingScrimFadeInEndOffset() {
+ return (float) getBackingScrimFadeInEnd() / getDuration();
+ }
}
diff --git a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
index 2396512..4962367 100644
--- a/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
+++ b/quickstep/src/com/android/quickstep/util/SplitToWorkspaceController.java
@@ -163,9 +163,8 @@
new RectF(firstTaskStartingBounds), firstTaskEndingBounds,
false /* fadeWithThumbnail */, true /* isStagedTask */);
- View mSplitDividerPlaceholderView = recentsView.getSplitSelectController()
- .getSplitAnimationController().addDividerPlaceholderViewToAnim(pendingAnimation,
- mLauncher, secondTaskEndingBounds, view.getContext());
+ View backingScrim = recentsView.getSplitSelectController().getSplitAnimationController()
+ .addScrimBehindAnim(pendingAnimation, mLauncher, view.getContext());
FloatingTaskView secondFloatingTaskView = FloatingTaskView.getFloatingTaskView(mLauncher,
view, bitmap, icon, secondTaskStartingBounds);
@@ -197,7 +196,7 @@
private void cleanUp() {
mLauncher.getDragLayer().removeView(firstFloatingTaskView);
mLauncher.getDragLayer().removeView(secondFloatingTaskView);
- mLauncher.getDragLayer().removeView(mSplitDividerPlaceholderView);
+ mLauncher.getDragLayer().removeView(backingScrim);
mController.getSplitAnimationController().removeSplitInstructionsView(mLauncher);
mController.resetState();
}
diff --git a/quickstep/src/com/android/quickstep/util/TabletSplitToConfirmTimings.java b/quickstep/src/com/android/quickstep/util/TabletSplitToConfirmTimings.java
index 3756b4a..d74473c 100644
--- a/quickstep/src/com/android/quickstep/util/TabletSplitToConfirmTimings.java
+++ b/quickstep/src/com/android/quickstep/util/TabletSplitToConfirmTimings.java
@@ -27,6 +27,8 @@
public int getPlaceholderIconFadeInEnd() { return 250; }
public int getStagedRectSlideStart() { return 0; }
public int getStagedRectSlideEnd() { return 500; }
+ public int getBackingScrimFadeInStart() { return 0; }
+ public int getBackingScrimFadeInEnd() { return 400; }
public int getDuration() { return TABLET_CONFIRM_DURATION; }
}
diff --git a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
index 5284b44..e48a7c6 100644
--- a/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/LauncherRecentsView.java
@@ -37,6 +37,7 @@
import androidx.annotation.Nullable;
import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherState;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.desktop.DesktopRecentsTransitionController;
@@ -104,7 +105,7 @@
}
@Override
- public StateManager<LauncherState> getStateManager() {
+ public StateManager<LauncherState, Launcher> getStateManager() {
return mContainer.getStateManager();
}
diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java
index 9096b75..4f802c9 100644
--- a/quickstep/src/com/android/quickstep/views/RecentsView.java
+++ b/quickstep/src/com/android/quickstep/views/RecentsView.java
@@ -152,6 +152,7 @@
import com.android.launcher3.statehandlers.DepthController;
import com.android.launcher3.statemanager.BaseState;
import com.android.launcher3.statemanager.StateManager;
+import com.android.launcher3.statemanager.StatefulContainer;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
import com.android.launcher3.touch.OverScroll;
@@ -738,7 +739,11 @@
private int mSplitHiddenTaskViewIndex = -1;
@Nullable
private FloatingTaskView mSecondFloatingTaskView;
- private View mSplitDividerPlaceholderView;
+ /**
+ * A fullscreen scrim that goes behind the splitscreen animation to hide color conflicts and
+ * possible flickers. Removed after tasks + divider finish animating in.
+ */
+ private View mSplitScrim;
/**
* The task to be removed and immediately re-added. Should not be added to task pool.
@@ -2487,7 +2492,9 @@
/** Returns whether user can start home based on state in {@link OverviewCommandHelper}. */
protected abstract boolean canStartHomeSafely();
- public abstract StateManager<STATE_TYPE> getStateManager();
+ /** Returns the state manager used in RecentsView **/
+ public abstract StateManager<STATE_TYPE,
+ ? extends StatefulContainer<STATE_TYPE>> getStateManager();
public void reset() {
setCurrentTask(-1);
@@ -4926,9 +4933,8 @@
mSplitSelectStateController.getActiveSplitStagePosition(), firstTaskEndingBounds,
secondTaskEndingBounds);
- mSplitDividerPlaceholderView = mSplitSelectStateController
- .getSplitAnimationController().addDividerPlaceholderViewToAnim(pendingAnimation,
- mContainer, secondTaskEndingBounds, getContext());
+ mSplitScrim = mSplitSelectStateController.getSplitAnimationController()
+ .addScrimBehindAnim(pendingAnimation, mContainer, getContext());
FloatingTaskView firstFloatingTaskView =
mSplitSelectStateController.getFirstFloatingTaskView();
firstFloatingTaskView.getBoundsOnScreen(firstTaskStartingBounds);
@@ -4983,7 +4989,7 @@
safeRemoveDragLayerView(mSplitSelectStateController.getFirstFloatingTaskView());
safeRemoveDragLayerView(mSecondFloatingTaskView);
safeRemoveDragLayerView(mSplitSelectStateController.getSplitInstructionsView());
- safeRemoveDragLayerView(mSplitDividerPlaceholderView);
+ safeRemoveDragLayerView(mSplitScrim);
mSecondFloatingTaskView = null;
mSplitSelectSource = null;
mSplitSelectStateController.getSplitAnimationController()
diff --git a/quickstep/src/com/android/quickstep/views/TaskView.kt b/quickstep/src/com/android/quickstep/views/TaskView.kt
index 5e79743..9c1aaa6 100644
--- a/quickstep/src/com/android/quickstep/views/TaskView.kt
+++ b/quickstep/src/com/android/quickstep/views/TaskView.kt
@@ -63,6 +63,7 @@
import com.android.launcher3.testing.TestLogging
import com.android.launcher3.testing.shared.TestProtocol
import com.android.launcher3.util.CancellableTask
+import com.android.launcher3.util.DisplayController
import com.android.launcher3.util.Executors
import com.android.launcher3.util.MultiPropertyFactory
import com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE
@@ -76,6 +77,7 @@
import com.android.launcher3.util.TransformingTouchDelegate
import com.android.launcher3.util.ViewPool
import com.android.launcher3.util.rects.set
+import com.android.launcher3.views.ActivityContext
import com.android.quickstep.RecentsModel
import com.android.quickstep.RemoteAnimationTargets
import com.android.quickstep.TaskAnimationManager
@@ -1574,7 +1576,20 @@
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
open fun computeWindowCornerRadius(context: Context): Float {
- return QuickStepContract.getWindowCornerRadius(context)
+ val activityContext: ActivityContext? = ActivityContext.lookupContextNoThrow(context)
+
+ // The corner radius is fixed to match when Taskbar is persistent mode
+ return if (
+ activityContext != null &&
+ activityContext.deviceProfile?.isTaskbarPresent == true &&
+ DisplayController.isTransientTaskbar(context)
+ ) {
+ context.resources
+ .getDimensionPixelSize(R.dimen.persistent_taskbar_corner_radius)
+ .toFloat()
+ } else {
+ QuickStepContract.getWindowCornerRadius(context)
+ }
}
/** Sets the progress in range [0, 1] */
diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt
new file mode 100644
index 0000000..c09dcf2
--- /dev/null
+++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/allapps/TaskbarAllAppsControllerTest.kt
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.taskbar.allapps
+
+import android.content.ComponentName
+import android.content.Intent
+import android.os.Process
+import androidx.test.annotation.UiThreadTest
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.BubbleTextView
+import com.android.launcher3.appprediction.PredictionRowView
+import com.android.launcher3.model.data.AppInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.notification.NotificationKeyData
+import com.android.launcher3.taskbar.TaskbarUnitTestRule
+import com.android.launcher3.taskbar.TaskbarUnitTestRule.InjectController
+import com.android.launcher3.taskbar.overlay.TaskbarOverlayController
+import com.android.launcher3.util.LauncherMultivalentJUnit
+import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices
+import com.android.launcher3.util.PackageUserKey
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(LauncherMultivalentJUnit::class)
+@EmulatedDevices(["pixelFoldable2023", "pixelTablet2023"])
+class TaskbarAllAppsControllerTest {
+
+ @get:Rule val taskbarUnitTestRule = TaskbarUnitTestRule()
+
+ @InjectController lateinit var allAppsController: TaskbarAllAppsController
+ @InjectController lateinit var overlayController: TaskbarOverlayController
+
+ @Test
+ @UiThreadTest
+ fun testToggle_once_showsAllApps() {
+ allAppsController.toggle()
+ assertThat(allAppsController.isOpen).isTrue()
+ }
+
+ @Test
+ @UiThreadTest
+ fun testToggle_twice_closesAllApps() {
+ allAppsController.toggle()
+ allAppsController.toggle()
+ assertThat(allAppsController.isOpen).isFalse()
+ }
+
+ @Test
+ @UiThreadTest
+ fun testToggle_taskbarRecreated_allAppsReopened() {
+ allAppsController.toggle()
+ taskbarUnitTestRule.recreateTaskbar()
+ assertThat(allAppsController.isOpen).isTrue()
+ }
+
+ @Test
+ @UiThreadTest
+ fun testSetApps_beforeOpened_cachesInfo() {
+ allAppsController.setApps(TEST_APPS, 0, emptyMap())
+ allAppsController.toggle()
+
+ val overlayContext = overlayController.requestWindow()
+ assertThat(overlayContext.appsView.appsStore.apps).isEqualTo(TEST_APPS)
+ }
+
+ @Test
+ @UiThreadTest
+ fun testSetApps_afterOpened_updatesStore() {
+ allAppsController.toggle()
+ allAppsController.setApps(TEST_APPS, 0, emptyMap())
+
+ val overlayContext = overlayController.requestWindow()
+ assertThat(overlayContext.appsView.appsStore.apps).isEqualTo(TEST_APPS)
+ }
+
+ @Test
+ @UiThreadTest
+ fun testSetPredictedApps_beforeOpened_cachesInfo() {
+ allAppsController.setPredictedApps(TEST_PREDICTED_APPS)
+ allAppsController.toggle()
+
+ val predictedApps =
+ overlayController
+ .requestWindow()
+ .appsView
+ .floatingHeaderView
+ .findFixedRowByType(PredictionRowView::class.java)
+ .predictedApps
+ assertThat(predictedApps).isEqualTo(TEST_PREDICTED_APPS)
+ }
+
+ @Test
+ @UiThreadTest
+ fun testSetPredictedApps_afterOpened_cachesInfo() {
+ allAppsController.toggle()
+ allAppsController.setPredictedApps(TEST_PREDICTED_APPS)
+
+ val predictedApps =
+ overlayController
+ .requestWindow()
+ .appsView
+ .floatingHeaderView
+ .findFixedRowByType(PredictionRowView::class.java)
+ .predictedApps
+ assertThat(predictedApps).isEqualTo(TEST_PREDICTED_APPS)
+ }
+
+ @Test
+ fun testUpdateNotificationDots_appInfo_hasDot() {
+ getInstrumentation().runOnMainSync {
+ allAppsController.setApps(TEST_APPS, 0, emptyMap())
+ allAppsController.toggle()
+ taskbarUnitTestRule.activityContext.popupDataProvider.onNotificationPosted(
+ PackageUserKey.fromItemInfo(TEST_APPS[0]),
+ NotificationKeyData("key"),
+ )
+ }
+
+ // Ensure the recycler view fully inflates before trying to grab an icon.
+ getInstrumentation().runOnMainSync {
+ val btv =
+ overlayController
+ .requestWindow()
+ .appsView
+ .activeRecyclerView
+ .findViewHolderForAdapterPosition(0)
+ ?.itemView as? BubbleTextView
+ assertThat(btv?.hasDot()).isTrue()
+ }
+ }
+
+ @Test
+ @UiThreadTest
+ fun testUpdateNotificationDots_predictedApp_hasDot() {
+ allAppsController.setPredictedApps(TEST_PREDICTED_APPS)
+ allAppsController.toggle()
+
+ taskbarUnitTestRule.activityContext.popupDataProvider.onNotificationPosted(
+ PackageUserKey.fromItemInfo(TEST_PREDICTED_APPS[0]),
+ NotificationKeyData("key"),
+ )
+
+ val predictionRowView =
+ overlayController
+ .requestWindow()
+ .appsView
+ .floatingHeaderView
+ .findFixedRowByType(PredictionRowView::class.java)
+ val btv = predictionRowView.getChildAt(0) as BubbleTextView
+ assertThat(btv.hasDot()).isTrue()
+ }
+
+ private companion object {
+ private val TEST_APPS =
+ Array(16) {
+ AppInfo(
+ ComponentName(
+ getInstrumentation().context,
+ "com.android.launcher3.tests.Activity$it",
+ ),
+ "Test App $it",
+ Process.myUserHandle(),
+ Intent(),
+ )
+ }
+
+ private val TEST_PREDICTED_APPS = TEST_APPS.take(4).map { WorkspaceItemInfo(it) }
+ }
+}
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
index 250dc7b..d40f8ab 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt
@@ -78,7 +78,7 @@
private val mockSplitSourceDrawable: Drawable = mock()
private val mockSplitSourceView: View = mock()
- private val stateManager: StateManager<*> = mock()
+ private val stateManager: StateManager<*, *> = mock()
private val depthController: DepthController = mock()
private val transitionInfo: TransitionInfo = mock()
private val transaction: Transaction = mock()
diff --git a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
index aa08ca4..bab84ef 100644
--- a/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
+++ b/quickstep/tests/multivalentTests/src/com/android/quickstep/util/SplitSelectStateControllerTest.kt
@@ -31,6 +31,7 @@
import com.android.launcher3.model.data.ItemInfo
import com.android.launcher3.statehandlers.DepthController
import com.android.launcher3.statemanager.StateManager
+import com.android.launcher3.statemanager.StatefulActivity
import com.android.launcher3.util.ComponentKey
import com.android.launcher3.util.SplitConfigurationOptions
import com.android.quickstep.RecentsModel
@@ -61,7 +62,7 @@
private val depthController: DepthController = mock()
private val statsLogManager: StatsLogManager = mock()
private val statsLogger: StatsLogger = mock()
- private val stateManager: StateManager<LauncherState> = mock()
+ private val stateManager: StateManager<LauncherState, StatefulActivity<LauncherState>> = mock()
private val handler: Handler = mock()
private val context: RecentsViewContainer = mock()
private val recentsModel: RecentsModel = mock()
diff --git a/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt b/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt
index f3115c6..04012c0 100644
--- a/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt
+++ b/quickstep/tests/src/com/android/launcher3/taskbar/FallbackTaskbarUIControllerTest.kt
@@ -37,7 +37,7 @@
lateinit var stateListener: StateManager.StateListener<RecentsState>
private val recentsActivity: RecentsActivity = mock()
- private val stateManager: StateManager<RecentsState> = mock()
+ private val stateManager: StateManager<RecentsState, RecentsActivity> = mock()
@Before
override fun setup() {
diff --git a/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt b/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt
index 5157c71..80fbce7 100644
--- a/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/RecentsAnimationDeviceStateTest.kt
@@ -9,8 +9,19 @@
import com.android.launcher3.util.DisplayController.CHANGE_ROTATION
import com.android.launcher3.util.DisplayController.Info
import com.android.launcher3.util.NavigationMode
-import com.android.launcher3.util.window.WindowManagerProxy
import com.android.quickstep.util.GestureExclusionManager
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DREAMING
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_HOME_DISABLED
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAV_BAR_HIDDEN
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED
+import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -28,7 +39,6 @@
class RecentsAnimationDeviceStateTest {
@Mock private lateinit var exclusionManager: GestureExclusionManager
- @Mock private lateinit var windowManagerProxy: WindowManagerProxy
@Mock private lateinit var info: Info
private val context = ApplicationProvider.getApplicationContext() as Context
@@ -108,4 +118,88 @@
verifyZeroInteractions(exclusionManager)
}
+
+ @Test
+ fun trackpadGesturesNotAllowedForSelectedStates() {
+ val disablingStates = GESTURE_DISABLING_SYSUI_STATES +
+ SYSUI_STATE_TOUCHPAD_GESTURES_DISABLED
+
+ allSysUiStates().forEach { state ->
+ val canStartGesture = !disablingStates.contains(state)
+ underTest.setSystemUiFlags(state)
+ assertThat(underTest.canStartTrackpadGesture()).isEqualTo(canStartGesture)
+ }
+ }
+
+ @Test
+ fun trackpadGesturesNotAllowedIfHomeAndOverviewIsDisabled() {
+ val stateToExpectedResult = mapOf(
+ SYSUI_STATE_HOME_DISABLED to true,
+ SYSUI_STATE_OVERVIEW_DISABLED to true,
+ DEFAULT_STATE
+ .enable(SYSUI_STATE_OVERVIEW_DISABLED)
+ .enable(SYSUI_STATE_HOME_DISABLED) to false
+ )
+
+ stateToExpectedResult.forEach { (state, allowed) ->
+ underTest.setSystemUiFlags(state)
+ assertThat(underTest.canStartTrackpadGesture()).isEqualTo(allowed)
+ }
+ }
+
+ @Test
+ fun systemGesturesNotAllowedForSelectedStates() {
+ val disablingStates = GESTURE_DISABLING_SYSUI_STATES + SYSUI_STATE_NAV_BAR_HIDDEN
+
+ allSysUiStates().forEach { state ->
+ val canStartGesture = !disablingStates.contains(state)
+ underTest.setSystemUiFlags(state)
+ assertThat(underTest.canStartSystemGesture()).isEqualTo(canStartGesture)
+ }
+ }
+
+ @Test
+ fun systemGesturesNotAllowedWhenGestureStateDisabledAndNavBarVisible() {
+ val stateToExpectedResult = mapOf(
+ DEFAULT_STATE
+ .enable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY)
+ .disable(SYSUI_STATE_NAV_BAR_HIDDEN) to true,
+ DEFAULT_STATE
+ .enable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY)
+ .enable(SYSUI_STATE_NAV_BAR_HIDDEN) to true,
+ DEFAULT_STATE
+ .disable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY)
+ .disable(SYSUI_STATE_NAV_BAR_HIDDEN) to true,
+ DEFAULT_STATE
+ .disable(SYSUI_STATE_ALLOW_GESTURE_IGNORING_BAR_VISIBILITY)
+ .enable(SYSUI_STATE_NAV_BAR_HIDDEN) to false,
+ )
+
+ stateToExpectedResult.forEach {(state, gestureAllowed) ->
+ underTest.setSystemUiFlags(state)
+ assertThat(underTest.canStartSystemGesture()).isEqualTo(gestureAllowed)
+ }
+ }
+
+ private fun allSysUiStates(): List<Long> {
+ // SYSUI_STATES_* are binary flags
+ return (0..SYSUI_STATES_COUNT).map { 1L shl it }
+ }
+
+ companion object {
+ private val GESTURE_DISABLING_SYSUI_STATES = listOf(
+ SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED,
+ SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING,
+ SYSUI_STATE_QUICK_SETTINGS_EXPANDED,
+ SYSUI_STATE_MAGNIFICATION_OVERLAP,
+ SYSUI_STATE_DEVICE_DREAMING,
+ SYSUI_STATE_DISABLE_GESTURE_SPLIT_INVOCATION,
+ )
+ private const val SYSUI_STATES_COUNT = 33
+ private const val DEFAULT_STATE = 0L
+ }
+
+ private fun Long.enable(state: Long) = this or state
+
+ private fun Long.disable(state: Long) = this and state.inv()
}
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index aa83c01..05724e2 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -448,6 +448,9 @@
<!-- Size of the maximum radius for the enforced rounded rectangles. -->
<dimen name="enforced_rounded_corner_max_radius">16dp</dimen>
+ <!-- Size of the radius for the rounded corners of Persistent Taskbar. -->
+ <dimen name="persistent_taskbar_corner_radius">16dp</dimen>
+
<!-- Base Swipe Detector, speed in dp/s -->
<dimen name="base_swift_detector_fling_release_velocity">1dp</dimen>
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index 6c9d4c0..4e566ab 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -312,7 +312,7 @@
private static boolean sIsNewProcess = true;
- private StateManager<LauncherState> mStateManager;
+ private StateManager<LauncherState, Launcher> mStateManager;
private static final int ON_ACTIVITY_RESULT_ANIMATION_DELAY = 500;
@@ -2775,7 +2775,7 @@
}
@Override
- protected void collectStateHandlers(List<StateHandler> out) {
+ public void collectStateHandlers(List<StateHandler<LauncherState>> out) {
out.add(getAllAppsController());
out.add(getWorkspace());
}
@@ -2996,7 +2996,7 @@
}
@Override
- public StateManager<LauncherState> getStateManager() {
+ public StateManager<LauncherState, Launcher> getStateManager() {
return mStateManager;
}
diff --git a/src/com/android/launcher3/LauncherModel.java b/src/com/android/launcher3/LauncherModel.java
index 47a7115..ca1b2a9 100644
--- a/src/com/android/launcher3/LauncherModel.java
+++ b/src/com/android/launcher3/LauncherModel.java
@@ -480,7 +480,7 @@
}
}
- if (!removedIds.isEmpty()) {
+ if (!removedIds.isEmpty() && !isAppArchived) {
taskController.deleteAndBindComponentsRemoved(
ItemInfoMatcher.ofItemIds(removedIds),
"removed because install session failed");
diff --git a/src/com/android/launcher3/LauncherState.java b/src/com/android/launcher3/LauncherState.java
index d2d56f2..102189b 100644
--- a/src/com/android/launcher3/LauncherState.java
+++ b/src/com/android/launcher3/LauncherState.java
@@ -440,7 +440,7 @@
*/
public void onBackInvoked(Launcher launcher) {
if (this != NORMAL) {
- StateManager<LauncherState> lsm = launcher.getStateManager();
+ StateManager<LauncherState, Launcher> lsm = launcher.getStateManager();
LauncherState lastState = lsm.getLastState();
lsm.goToState(lastState, forEndCallback(this::onBackAnimationCompleted));
}
@@ -461,7 +461,7 @@
*/
public void onBackProgressed(
Launcher launcher, @FloatRange(from = 0.0, to = 1.0) float backProgress) {
- StateManager<LauncherState> lsm = launcher.getStateManager();
+ StateManager<LauncherState, Launcher> lsm = launcher.getStateManager();
LauncherState toState = lsm.getLastState();
lsm.onBackProgressed(toState, backProgress);
}
@@ -471,7 +471,7 @@
* predictive back gesture.
*/
public void onBackCancelled(Launcher launcher) {
- StateManager<LauncherState> lsm = launcher.getStateManager();
+ StateManager<LauncherState, Launcher> lsm = launcher.getStateManager();
LauncherState toState = lsm.getLastState();
lsm.onBackCancelled(toState);
}
diff --git a/src/com/android/launcher3/Workspace.java b/src/com/android/launcher3/Workspace.java
index 584d089..e601a3e 100644
--- a/src/com/android/launcher3/Workspace.java
+++ b/src/com/android/launcher3/Workspace.java
@@ -526,7 +526,7 @@
}
updateChildrenLayersEnabled();
- StateManager<LauncherState> stateManager = mLauncher.getStateManager();
+ StateManager<LauncherState, Launcher> stateManager = mLauncher.getStateManager();
stateManager.addStateListener(new StateManager.StateListener<LauncherState>() {
@Override
public void onStateTransitionComplete(LauncherState finalState) {
diff --git a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
index 63a168e..1e0f289 100644
--- a/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
+++ b/src/com/android/launcher3/allapps/LauncherAllAppsContainerView.java
@@ -56,7 +56,7 @@
return false;
}
Launcher launcher = mActivityContext;
- StateManager<LauncherState> manager = launcher.getStateManager();
+ StateManager<LauncherState, Launcher> manager = launcher.getStateManager();
if (manager.isInTransition() && manager.getTargetState() != null) {
return manager.getTargetState().shouldFloatingSearchBarUsePillWhenUnfocused(launcher);
}
@@ -70,7 +70,7 @@
return super.getFloatingSearchBarRestingMarginBottom();
}
Launcher launcher = mActivityContext;
- StateManager<LauncherState> stateManager = launcher.getStateManager();
+ StateManager<LauncherState, Launcher> stateManager = launcher.getStateManager();
// We want to rest at the current state's resting position, unless we are in transition and
// the target state's resting position is higher (that way if we are closing the keyboard,
@@ -95,7 +95,7 @@
return super.getFloatingSearchBarRestingMarginStart();
}
- StateManager<LauncherState> stateManager = mActivityContext.getStateManager();
+ StateManager<LauncherState, Launcher> stateManager = mActivityContext.getStateManager();
// Special case to not expand the search bar when exiting All Apps on phones.
if (stateManager.getCurrentStableState() == LauncherState.ALL_APPS
@@ -117,7 +117,7 @@
return super.getFloatingSearchBarRestingMarginEnd();
}
- StateManager<LauncherState> stateManager = mActivityContext.getStateManager();
+ StateManager<LauncherState, Launcher> stateManager = mActivityContext.getStateManager();
// Special case to not expand the search bar when exiting All Apps on phones.
if (stateManager.getCurrentStableState() == LauncherState.ALL_APPS
diff --git a/src/com/android/launcher3/model/FirstScreenBroadcast.java b/src/com/android/launcher3/model/FirstScreenBroadcast.java
index cc20cd1..f443f81 100644
--- a/src/com/android/launcher3/model/FirstScreenBroadcast.java
+++ b/src/com/android/launcher3/model/FirstScreenBroadcast.java
@@ -96,7 +96,7 @@
.collect(groupingBy(SessionInfo::getInstallerPackageName,
mapping(SessionInfo::getAppPackageName, Collectors.toSet())))
.forEach((installer, packages) ->
- sendBroadcastToInstaller(context, installer, packages, firstScreenItems));
+ sendBroadcastToInstaller(context, installer, packages, firstScreenItems));
}
/**
diff --git a/src/com/android/launcher3/model/FirstScreenBroadcastHelper.kt b/src/com/android/launcher3/model/FirstScreenBroadcastHelper.kt
new file mode 100644
index 0000000..aa62c32
--- /dev/null
+++ b/src/com/android/launcher3/model/FirstScreenBroadcastHelper.kt
@@ -0,0 +1,387 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.launcher3.model
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageInstaller.SessionInfo
+import android.os.Process
+import android.util.Log
+import androidx.annotation.AnyThread
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.WorkerThread
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
+import com.android.launcher3.model.data.CollectionInfo
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.LauncherAppWidgetInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.pm.InstallSessionHelper
+import com.android.launcher3.util.Executors
+import com.android.launcher3.util.PackageManagerHelper
+import com.android.launcher3.util.PackageUserKey
+
+/**
+ * Helper class to send broadcasts to package installers that have:
+ * - Pending Items on first screen
+ * - Installed/Archived Items on first screen
+ * - Installed/Archived Widgets on every screen
+ *
+ * The packages are broken down by: folder items, workspace items, hotseat items, and widgets.
+ * Package installers only receive data for items that they are installing or have installed.
+ */
+object FirstScreenBroadcastHelper {
+ @VisibleForTesting const val MAX_BROADCAST_SIZE = 70
+
+ private const val TAG = "FirstScreenBroadcastHelper"
+ private const val DEBUG = true
+ private const val ACTION_FIRST_SCREEN_ACTIVE_INSTALLS =
+ "com.android.launcher3.action.FIRST_SCREEN_ACTIVE_INSTALLS"
+ // String retained as "folderItem" for back-compatibility reasons.
+ private const val PENDING_COLLECTION_ITEM_EXTRA = "folderItem"
+ private const val PENDING_WORKSPACE_ITEM_EXTRA = "workspaceItem"
+ private const val PENDING_HOTSEAT_ITEM_EXTRA = "hotseatItem"
+ private const val PENDING_WIDGET_ITEM_EXTRA = "widgetItem"
+ // Extras containing all installed items, including Archived Apps.
+ private const val INSTALLED_WORKSPACE_ITEMS_EXTRA = "workspaceInstalledItems"
+ private const val INSTALLED_HOTSEAT_ITEMS_EXTRA = "hotseatInstalledItems"
+ // This includes installed widgets on all screens, not just first.
+ private const val ALL_INSTALLED_WIDGETS_ITEM_EXTRA = "widgetInstalledItems"
+ private const val VERIFICATION_TOKEN_EXTRA = "verificationToken"
+
+ /**
+ * Return list of [FirstScreenBroadcastModel] for each installer and their
+ * installing/installed/archived items. If the FirstScreenBroadcastModel data is greater in size
+ * than [MAX_BROADCAST_SIZE], then we will truncate the data until it meets the size limit to
+ * avoid overloading the broadcast.
+ *
+ * @param packageManagerHelper helper for querying PackageManager
+ * @param firstScreenItems every ItemInfo on first screen
+ * @param userKeyToSessionMap map of pending SessionInfo's for installing items
+ * @param allWidgets list of all Widgets added to every screen
+ */
+ @WorkerThread
+ @JvmStatic
+ fun createModelsForFirstScreenBroadcast(
+ packageManagerHelper: PackageManagerHelper,
+ firstScreenItems: List<ItemInfo>,
+ userKeyToSessionMap: Map<PackageUserKey, SessionInfo>,
+ allWidgets: List<LauncherAppWidgetInfo>
+ ): List<FirstScreenBroadcastModel> {
+
+ // installers for installing items
+ val pendingItemInstallerMap: Map<String, MutableSet<String>> =
+ createPendingItemsMap(userKeyToSessionMap)
+ val installingPackages = pendingItemInstallerMap.values.flatten().toSet()
+
+ // installers for installed items on first screen
+ val installedItemInstallerMap: Map<String, MutableSet<ItemInfo>> =
+ createInstalledItemsMap(firstScreenItems, installingPackages, packageManagerHelper)
+
+ // installers for widgets on all screens
+ val allInstalledWidgetsMap: Map<String, MutableSet<LauncherAppWidgetInfo>> =
+ createAllInstalledWidgetsMap(allWidgets, installingPackages, packageManagerHelper)
+
+ val allInstallers: Set<String> =
+ pendingItemInstallerMap.keys +
+ installedItemInstallerMap.keys +
+ allInstalledWidgetsMap.keys
+ val models = mutableListOf<FirstScreenBroadcastModel>()
+ // create broadcast for each installer, with extras for each item category
+ allInstallers.forEach { installer ->
+ val installingItems = pendingItemInstallerMap[installer]
+ val broadcastModel =
+ FirstScreenBroadcastModel(installerPackage = installer).apply {
+ addPendingItems(installingItems, firstScreenItems)
+ addInstalledItems(installer, installedItemInstallerMap)
+ addAllScreenWidgets(installer, allInstalledWidgetsMap)
+ }
+ broadcastModel.truncateModelForBroadcast()
+ models.add(broadcastModel)
+ }
+ return models
+ }
+
+ /** From the model data, create Intents to send broadcasts and fire them. */
+ @WorkerThread
+ @JvmStatic
+ fun sendBroadcastsForModels(context: Context, models: List<FirstScreenBroadcastModel>) {
+ for (model in models) {
+ model.printDebugInfo()
+ val intent =
+ Intent(ACTION_FIRST_SCREEN_ACTIVE_INSTALLS)
+ .setPackage(model.installerPackage)
+ .putExtra(
+ VERIFICATION_TOKEN_EXTRA,
+ PendingIntent.getActivity(
+ context,
+ 0 /* requestCode */,
+ Intent(),
+ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
+ )
+ )
+ .putStringArrayListExtra(
+ PENDING_COLLECTION_ITEM_EXTRA,
+ ArrayList(model.pendingCollectionItems)
+ )
+ .putStringArrayListExtra(
+ PENDING_WORKSPACE_ITEM_EXTRA,
+ ArrayList(model.pendingWorkspaceItems)
+ )
+ .putStringArrayListExtra(
+ PENDING_HOTSEAT_ITEM_EXTRA,
+ ArrayList(model.pendingHotseatItems)
+ )
+ .putStringArrayListExtra(
+ PENDING_WIDGET_ITEM_EXTRA,
+ ArrayList(model.pendingWidgetItems)
+ )
+ .putStringArrayListExtra(
+ INSTALLED_WORKSPACE_ITEMS_EXTRA,
+ ArrayList(model.installedWorkspaceItems)
+ )
+ .putStringArrayListExtra(
+ INSTALLED_HOTSEAT_ITEMS_EXTRA,
+ ArrayList(model.installedHotseatItems)
+ )
+ .putStringArrayListExtra(
+ ALL_INSTALLED_WIDGETS_ITEM_EXTRA,
+ ArrayList(
+ model.firstScreenInstalledWidgets +
+ model.secondaryScreenInstalledWidgets
+ )
+ )
+ context.sendBroadcast(intent)
+ }
+ }
+
+ /** Maps Installer packages to Set of app packages from install sessions */
+ private fun createPendingItemsMap(
+ userKeyToSessionMap: Map<PackageUserKey, SessionInfo>
+ ): Map<String, MutableSet<String>> {
+ val myUser = Process.myUserHandle()
+ val result = mutableMapOf<String, MutableSet<String>>()
+ userKeyToSessionMap.forEach { entry ->
+ if (!myUser.equals(InstallSessionHelper.getUserHandle(entry.value))) return@forEach
+ val installer = entry.value.installerPackageName
+ val appPackage = entry.value.appPackageName
+ if (installer.isNullOrEmpty() || appPackage.isNullOrEmpty()) return@forEach
+ result.getOrPut(installer) { mutableSetOf() }.add(appPackage)
+ }
+ return result
+ }
+
+ /**
+ * Maps Installer packages to Set of ItemInfo from first screen. Filter out installing packages.
+ */
+ private fun createInstalledItemsMap(
+ firstScreenItems: List<ItemInfo>,
+ installingPackages: Set<String>,
+ packageManagerHelper: PackageManagerHelper
+ ): Map<String, MutableSet<ItemInfo>> {
+ val result = mutableMapOf<String, MutableSet<ItemInfo>>()
+ firstScreenItems.forEach { item ->
+ val appPackage = getPackageName(item) ?: return@forEach
+ if (installingPackages.contains(appPackage)) return@forEach
+ val installer = packageManagerHelper.getAppInstallerPackage(appPackage)
+ if (installer.isNullOrEmpty()) return@forEach
+ result.getOrPut(installer) { mutableSetOf() }.add(item)
+ }
+ return result
+ }
+
+ /**
+ * Maps Installer packages to Set of AppWidget packages installed on all screens. Filter out
+ * installing packages.
+ */
+ private fun createAllInstalledWidgetsMap(
+ allWidgets: List<LauncherAppWidgetInfo>,
+ installingPackages: Set<String>,
+ packageManagerHelper: PackageManagerHelper
+ ): Map<String, MutableSet<LauncherAppWidgetInfo>> {
+ val result = mutableMapOf<String, MutableSet<LauncherAppWidgetInfo>>()
+ allWidgets
+ .sortedBy { widget -> widget.screenId }
+ .forEach { widget ->
+ val appPackage = getPackageName(widget) ?: return@forEach
+ if (installingPackages.contains(appPackage)) return@forEach
+ val installer = packageManagerHelper.getAppInstallerPackage(appPackage)
+ if (installer.isNullOrEmpty()) return@forEach
+ result.getOrPut(installer) { mutableSetOf() }.add(widget)
+ }
+ return result
+ }
+
+ /**
+ * Add first screen Pending Items from Map to [FirstScreenBroadcastModel] for given installer
+ */
+ private fun FirstScreenBroadcastModel.addPendingItems(
+ installingItems: Set<String>?,
+ firstScreenItems: List<ItemInfo>
+ ) {
+ if (installingItems == null) return
+ for (info in firstScreenItems) {
+ addCollectionItems(info, installingItems)
+ val packageName = getPackageName(info) ?: continue
+ if (!installingItems.contains(packageName)) continue
+ when {
+ info is LauncherAppWidgetInfo -> pendingWidgetItems.add(packageName)
+ info.container == CONTAINER_HOTSEAT -> pendingHotseatItems.add(packageName)
+ info.container == CONTAINER_DESKTOP -> pendingWorkspaceItems.add(packageName)
+ }
+ }
+ }
+
+ /**
+ * Add first screen installed Items from Map to [FirstScreenBroadcastModel] for given installer
+ */
+ private fun FirstScreenBroadcastModel.addInstalledItems(
+ installer: String,
+ installedItemInstallerMap: Map<String, Set<ItemInfo>>,
+ ) {
+ installedItemInstallerMap[installer]?.forEach { info ->
+ val packageName: String = getPackageName(info) ?: return@forEach
+ when (info.container) {
+ CONTAINER_HOTSEAT -> installedHotseatItems.add(packageName)
+ CONTAINER_DESKTOP -> installedWorkspaceItems.add(packageName)
+ }
+ }
+ }
+
+ /** Add Widgets on every screen from Map to [FirstScreenBroadcastModel] for given installer */
+ private fun FirstScreenBroadcastModel.addAllScreenWidgets(
+ installer: String,
+ allInstalledWidgetsMap: Map<String, Set<LauncherAppWidgetInfo>>
+ ) {
+ allInstalledWidgetsMap[installer]?.forEach { widget ->
+ val packageName: String = getPackageName(widget) ?: return@forEach
+ if (widget.screenId == 0) {
+ firstScreenInstalledWidgets.add(packageName)
+ } else {
+ secondaryScreenInstalledWidgets.add(packageName)
+ }
+ }
+ }
+
+ private fun FirstScreenBroadcastModel.addCollectionItems(
+ info: ItemInfo,
+ installingPackages: Set<String>
+ ) {
+ if (info !is CollectionInfo) return
+ pendingCollectionItems.addAll(
+ cloneOnMainThread(info.getAppContents())
+ .mapNotNull { getPackageName(it) }
+ .filter { installingPackages.contains(it) }
+ )
+ }
+
+ /**
+ * Creates a copy of [FirstScreenBroadcastModel] with items truncated to meet
+ * [MAX_BROADCAST_SIZE] in a prioritized order.
+ */
+ @VisibleForTesting
+ fun FirstScreenBroadcastModel.truncateModelForBroadcast() {
+ val totalItemCount = getTotalItemCount()
+ if (totalItemCount <= MAX_BROADCAST_SIZE) return
+ var extraItemCount = totalItemCount - MAX_BROADCAST_SIZE
+
+ while (extraItemCount > 0) {
+ // In this order, remove items until we meet the max size limit.
+ when {
+ pendingCollectionItems.isNotEmpty() ->
+ pendingCollectionItems.apply { remove(last()) }
+ pendingHotseatItems.isNotEmpty() -> pendingHotseatItems.apply { remove(last()) }
+ installedHotseatItems.isNotEmpty() -> installedHotseatItems.apply { remove(last()) }
+ secondaryScreenInstalledWidgets.isNotEmpty() ->
+ secondaryScreenInstalledWidgets.apply { remove(last()) }
+ pendingWidgetItems.isNotEmpty() -> pendingWidgetItems.apply { remove(last()) }
+ firstScreenInstalledWidgets.isNotEmpty() ->
+ firstScreenInstalledWidgets.apply { remove(last()) }
+ pendingWorkspaceItems.isNotEmpty() -> pendingWorkspaceItems.apply { remove(last()) }
+ installedWorkspaceItems.isNotEmpty() ->
+ installedWorkspaceItems.apply { remove(last()) }
+ }
+ extraItemCount--
+ }
+ }
+
+ /** Returns count of all Items held by [FirstScreenBroadcastModel]. */
+ @VisibleForTesting
+ fun FirstScreenBroadcastModel.getTotalItemCount() =
+ pendingCollectionItems.size +
+ pendingWorkspaceItems.size +
+ pendingHotseatItems.size +
+ pendingWidgetItems.size +
+ installedWorkspaceItems.size +
+ installedHotseatItems.size +
+ firstScreenInstalledWidgets.size +
+ secondaryScreenInstalledWidgets.size
+
+ private fun FirstScreenBroadcastModel.printDebugInfo() {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "Sending First Screen Broadcast for installer=$installerPackage" +
+ ", total packages=${getTotalItemCount()}"
+ )
+ pendingCollectionItems.forEach {
+ Log.d(TAG, "$installerPackage:Pending Collection item:$it")
+ }
+ pendingWorkspaceItems.forEach {
+ Log.d(TAG, "$installerPackage:Pending Workspace item:$it")
+ }
+ pendingHotseatItems.forEach { Log.d(TAG, "$installerPackage:Pending Hotseat item:$it") }
+ pendingWidgetItems.forEach { Log.d(TAG, "$installerPackage:Pending Widget item:$it") }
+ installedWorkspaceItems.forEach {
+ Log.d(TAG, "$installerPackage:Installed Workspace item:$it")
+ }
+ installedHotseatItems.forEach {
+ Log.d(TAG, "$installerPackage:Installed Hotseat item:$it")
+ }
+ firstScreenInstalledWidgets.forEach {
+ Log.d(TAG, "$installerPackage:Installed Widget item (first screen):$it")
+ }
+ secondaryScreenInstalledWidgets.forEach {
+ Log.d(TAG, "$installerPackage:Installed Widget item (secondary screens):$it")
+ }
+ }
+ }
+
+ private fun getPackageName(info: ItemInfo): String? {
+ var packageName: String? = null
+ if (info is LauncherAppWidgetInfo) {
+ info.providerName?.let { packageName = info.providerName.packageName }
+ } else if (info.targetComponent != null) {
+ packageName = info.targetComponent?.packageName
+ }
+ return packageName
+ }
+
+ /**
+ * Clone the provided list on UI thread. This is used for [FolderInfo.getContents] which is
+ * always modified on UI thread.
+ */
+ @AnyThread
+ private fun cloneOnMainThread(list: ArrayList<WorkspaceItemInfo>): List<WorkspaceItemInfo> {
+ return try {
+ return Executors.MAIN_EXECUTOR.submit<ArrayList<WorkspaceItemInfo>> { ArrayList(list) }
+ .get()
+ } catch (e: Exception) {
+ emptyList()
+ }
+ }
+}
diff --git a/src/com/android/launcher3/model/FirstScreenBroadcastModel.kt b/src/com/android/launcher3/model/FirstScreenBroadcastModel.kt
new file mode 100644
index 0000000..ba5c526
--- /dev/null
+++ b/src/com/android/launcher3/model/FirstScreenBroadcastModel.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.model
+
+/** Data model for the information used for [FirstScreenBroadcastHelper] Broadcast Extras */
+data class FirstScreenBroadcastModel(
+ // Package name of the installer for all items
+ val installerPackage: String,
+ // Installing items in Folders
+ val pendingCollectionItems: MutableSet<String> = mutableSetOf(),
+ // Installing items on first screen
+ val pendingWorkspaceItems: MutableSet<String> = mutableSetOf(),
+ // Installing items on hotseat
+ val pendingHotseatItems: MutableSet<String> = mutableSetOf(),
+ // Installing widgets on first screen
+ val pendingWidgetItems: MutableSet<String> = mutableSetOf(),
+ // Installed/Archived Items on first screen
+ val installedWorkspaceItems: MutableSet<String> = mutableSetOf(),
+ // Installed/Archived items on hotseat
+ val installedHotseatItems: MutableSet<String> = mutableSetOf(),
+ // Installed/Archived Widgets on first screen
+ val firstScreenInstalledWidgets: MutableSet<String> = mutableSetOf(),
+ // Installed Archived Widgets on secondary screens
+ val secondaryScreenInstalledWidgets: MutableSet<String> = mutableSetOf()
+)
diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java
index 0d40a24..dc6968c 100644
--- a/src/com/android/launcher3/model/LoaderTask.java
+++ b/src/com/android/launcher3/model/LoaderTask.java
@@ -47,6 +47,7 @@
import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
+import android.provider.Settings;
import android.util.ArrayMap;
import android.util.Log;
import android.util.LongSparseArray;
@@ -195,17 +196,32 @@
}
private void sendFirstScreenActiveInstallsBroadcast() {
- ArrayList<ItemInfo> firstScreenItems = new ArrayList<>();
- ArrayList<ItemInfo> allItems = mBgDataModel.getAllWorkspaceItems();
-
// Screen set is never empty
IntArray allScreens = mBgDataModel.collectWorkspaceScreens();
final int firstScreen = allScreens.get(0);
IntSet firstScreens = IntSet.wrap(firstScreen);
+ ArrayList<ItemInfo> allItems = mBgDataModel.getAllWorkspaceItems();
+ ArrayList<ItemInfo> firstScreenItems = new ArrayList<>();
filterCurrentWorkspaceItems(firstScreens, allItems, firstScreenItems,
new ArrayList<>() /* otherScreenItems are ignored */);
- mFirstScreenBroadcast.sendBroadcasts(mApp.getContext(), firstScreenItems);
+ final int launcherBroadcastInstalledApps = Settings.Secure.getInt(
+ mApp.getContext().getContentResolver(),
+ "launcher_broadcast_installed_apps",
+ /* def= */ 0);
+ if (launcherBroadcastInstalledApps == 1) {
+ List<FirstScreenBroadcastModel> broadcastModels =
+ FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
+ mPmHelper,
+ firstScreenItems,
+ mInstallingPkgsCached,
+ mBgDataModel.appWidgets
+ );
+ logASplit("Sending first screen broadcast with additional archiving Extras");
+ FirstScreenBroadcastHelper.sendBroadcastsForModels(mApp.getContext(), broadcastModels);
+ } else {
+ mFirstScreenBroadcast.sendBroadcasts(mApp.getContext(), firstScreenItems);
+ }
}
public void run() {
@@ -249,7 +265,7 @@
mModelDelegate.workspaceLoadComplete();
// Notify the installer packages of packages with active installs on the first screen.
sendFirstScreenActiveInstallsBroadcast();
- logASplit("sendFirstScreenActiveInstallsBroadcast");
+ logASplit("sendFirstScreenBroadcast");
// Take a break
waitForIdle();
diff --git a/src/com/android/launcher3/notification/NotificationKeyData.java b/src/com/android/launcher3/notification/NotificationKeyData.java
index 4115b3d..e38824c 100644
--- a/src/com/android/launcher3/notification/NotificationKeyData.java
+++ b/src/com/android/launcher3/notification/NotificationKeyData.java
@@ -22,6 +22,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.launcher3.Utilities;
@@ -38,6 +39,11 @@
public final String[] personKeysFromNotification;
public int count;
+ @VisibleForTesting
+ public NotificationKeyData(String notificationKey) {
+ this(notificationKey, null, 1, new String[]{});
+ }
+
private NotificationKeyData(String notificationKey, String shortcutId, int count,
String[] personKeysFromNotification) {
this.notificationKey = notificationKey;
diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java
index a4ff29f..21897bf 100644
--- a/src/com/android/launcher3/provider/RestoreDbTask.java
+++ b/src/com/android/launcher3/provider/RestoreDbTask.java
@@ -418,9 +418,7 @@
DeviceGridState deviceGridState = new DeviceGridState(context);
FileLog.d(TAG, "restore initiated from backup: DeviceGridState=" + deviceGridState);
LauncherPrefs.get(context).putSync(RESTORE_DEVICE.to(deviceGridState.getDeviceType()));
- if (enableLauncherBrMetricsFixed()) {
- LauncherPrefs.get(context).putSync(IS_FIRST_LOAD_AFTER_RESTORE.to(true));
- }
+ LauncherPrefs.get(context).putSync(IS_FIRST_LOAD_AFTER_RESTORE.to(true));
}
@WorkerThread
diff --git a/src/com/android/launcher3/statemanager/BaseState.java b/src/com/android/launcher3/statemanager/BaseState.java
index a01d402..b81729a 100644
--- a/src/com/android/launcher3/statemanager/BaseState.java
+++ b/src/com/android/launcher3/statemanager/BaseState.java
@@ -21,7 +21,7 @@
import com.android.launcher3.views.ActivityContext;
/**
- * Interface representing a state of a StatefulActivity
+ * Interface representing a state of a StatefulContainer
*/
public interface BaseState<T extends BaseState> {
diff --git a/src/com/android/launcher3/statemanager/StateManager.java b/src/com/android/launcher3/statemanager/StateManager.java
index eea1a7d..ac07c0f 100644
--- a/src/com/android/launcher3/statemanager/StateManager.java
+++ b/src/com/android/launcher3/statemanager/StateManager.java
@@ -26,6 +26,7 @@
import android.animation.Animator.AnimatorListener;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
+import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
@@ -47,8 +48,11 @@
/**
* Class to manage transitions between different states for a StatefulActivity based on different
* states
+ * @param STATE_TYPE Basestate used by the state manager
+ * @param STATEFUL_CONTAINER container object used to manage state
*/
-public class StateManager<STATE_TYPE extends BaseState<STATE_TYPE>> {
+public class StateManager<STATE_TYPE extends BaseState<STATE_TYPE>,
+ STATEFUL_CONTAINER extends Context & StatefulContainer<STATE_TYPE>> {
public static final String TAG = "StateManager";
// b/279059025, b/325463989
@@ -56,7 +60,7 @@
private final AnimationState mConfig = new AnimationState();
private final Handler mUiHandler;
- private final StatefulActivity<STATE_TYPE> mActivity;
+ private final STATEFUL_CONTAINER mStatefulContainer;
private final ArrayList<StateListener<STATE_TYPE>> mListeners = new ArrayList<>();
private final STATE_TYPE mBaseState;
@@ -71,12 +75,12 @@
private STATE_TYPE mRestState;
- public StateManager(StatefulActivity<STATE_TYPE> l, STATE_TYPE baseState) {
+ public StateManager(STATEFUL_CONTAINER container, STATE_TYPE baseState) {
mUiHandler = new Handler(Looper.getMainLooper());
- mActivity = l;
+ mStatefulContainer = container;
mBaseState = baseState;
mState = mLastStableState = mCurrentStableState = baseState;
- mAtomicAnimationFactory = l.createAtomicAnimationFactory();
+ mAtomicAnimationFactory = container.createAtomicAnimationFactory();
}
public STATE_TYPE getState() {
@@ -109,10 +113,10 @@
writer.println(prefix + "\tisInTransition:" + isInTransition());
}
- public StateHandler[] getStateHandlers() {
+ public StateHandler<STATE_TYPE>[] getStateHandlers() {
if (mStateHandlers == null) {
- ArrayList<StateHandler> handlers = new ArrayList<>();
- mActivity.collectStateHandlers(handlers);
+ ArrayList<StateHandler<STATE_TYPE>> handlers = new ArrayList<>();
+ mStatefulContainer.collectStateHandlers(handlers);
mStateHandlers = handlers.toArray(new StateHandler[handlers.size()]);
}
return mStateHandlers;
@@ -130,7 +134,7 @@
* Returns true if the state changes should be animated.
*/
public boolean shouldAnimateStateChange() {
- return !mActivity.isForceInvisible() && mActivity.isStarted();
+ return mStatefulContainer.shouldAnimateStateChange();
}
/**
@@ -245,7 +249,7 @@
}
animated &= areAnimatorsEnabled();
- if (mActivity.isInState(state)) {
+ if (mStatefulContainer.isInState(state)) {
if (mConfig.currentAnimation == null) {
// Run any queued runnable
if (listener != null) {
@@ -302,8 +306,8 @@
// Since state mBaseState can be reached from multiple states, just assume that the
// transition plays in reverse and use the same duration as previous state.
mConfig.duration = state == mBaseState
- ? fromState.getTransitionDuration(mActivity, false /* isToState */)
- : state.getTransitionDuration(mActivity, true /* isToState */);
+ ? fromState.getTransitionDuration(mStatefulContainer, false /* isToState */)
+ : state.getTransitionDuration(mStatefulContainer, true /* isToState */);
prepareForAtomicAnimation(fromState, state, mConfig);
AnimatorSet animation = createAnimationToNewWorkspaceInternal(state).buildAnim();
if (listener != null) {
@@ -336,7 +340,7 @@
PendingAnimation builder = new PendingAnimation(config.duration);
prepareForAtomicAnimation(fromState, toState, config);
- for (StateHandler handler : mActivity.getStateManager().getStateHandlers()) {
+ for (StateHandler handler : mStatefulContainer.getStateManager().getStateHandlers()) {
handler.setStateWithAnimation(toState, config, builder);
}
return builder.buildAnim();
@@ -402,7 +406,7 @@
private void onStateTransitionStart(STATE_TYPE state) {
mState = state;
- mActivity.onStateSetStart(mState);
+ mStatefulContainer.onStateSetStart(mState);
if (DEBUG) {
Log.d(TAG, "onStateTransitionStart - state: " + state);
@@ -419,7 +423,7 @@
mCurrentStableState = state;
}
- mActivity.onStateSetEnd(state);
+ mStatefulContainer.onStateSetEnd(state);
if (state == mBaseState) {
setRestState(null);
}
diff --git a/src/com/android/launcher3/statemanager/StatefulActivity.java b/src/com/android/launcher3/statemanager/StatefulActivity.java
index 30ba703..28f2def 100644
--- a/src/com/android/launcher3/statemanager/StatefulActivity.java
+++ b/src/com/android/launcher3/statemanager/StatefulActivity.java
@@ -18,7 +18,6 @@
import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION;
import static android.content.pm.ActivityInfo.CONFIG_SCREEN_SIZE;
-import static com.android.launcher3.LauncherState.FLAG_CLOSE_POPUPS;
import static com.android.launcher3.LauncherState.FLAG_NON_INTERACTIVE;
import android.content.res.Configuration;
@@ -29,11 +28,9 @@
import androidx.annotation.CallSuper;
-import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BaseDraggingActivity;
import com.android.launcher3.LauncherRootView;
import com.android.launcher3.Utilities;
-import com.android.launcher3.statemanager.StateManager.AtomicAnimationFactory;
import com.android.launcher3.statemanager.StateManager.StateHandler;
import com.android.launcher3.util.window.WindowManagerProxy;
import com.android.launcher3.views.BaseDragLayer;
@@ -45,7 +42,7 @@
* @param <STATE_TYPE> Type of state object
*/
public abstract class StatefulActivity<STATE_TYPE extends BaseState<STATE_TYPE>>
- extends BaseDraggingActivity {
+ extends BaseDraggingActivity implements StatefulContainer<STATE_TYPE> {
public final Handler mHandler = new Handler();
private final Runnable mHandleDeferredResume = this::handleDeferredResume;
@@ -67,19 +64,9 @@
/**
* Create handlers to control the property changes for this activity
*/
- protected abstract void collectStateHandlers(List<StateHandler> out);
- /**
- * Returns true if the activity is in the provided state
- */
- public boolean isInState(STATE_TYPE state) {
- return getStateManager().getState() == state;
- }
-
- /**
- * Returns the state manager for this activity
- */
- public abstract StateManager<STATE_TYPE> getStateManager();
+ @Override
+ public abstract void collectStateHandlers(List<StateHandler<STATE_TYPE>> out);
protected void inflateRootView(int layoutId) {
mRootView = (LauncherRootView) LayoutInflater.from(this).inflate(layoutId, null);
@@ -106,22 +93,12 @@
if (mDeferredResumePending) {
handleDeferredResume();
}
-
- if (state.hasFlag(FLAG_CLOSE_POPUPS)) {
- AbstractFloatingView.closeAllOpenViews(this, !state.hasFlag(FLAG_NON_INTERACTIVE));
- }
+ StatefulContainer.super.onStateSetStart(state);
}
- /**
- * Called when transition to state ends
- */
- public void onStateSetEnd(STATE_TYPE state) { }
-
- /**
- * Creates a factory for atomic state animations
- */
- public AtomicAnimationFactory<STATE_TYPE> createAtomicAnimationFactory() {
- return new AtomicAnimationFactory(0);
+ @Override
+ public boolean shouldAnimateStateChange() {
+ return !isForceInvisible() && isStarted();
}
@Override
diff --git a/src/com/android/launcher3/statemanager/StatefulContainer.java b/src/com/android/launcher3/statemanager/StatefulContainer.java
new file mode 100644
index 0000000..0cf0a27
--- /dev/null
+++ b/src/com/android/launcher3/statemanager/StatefulContainer.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.statemanager;
+
+
+import static com.android.launcher3.LauncherState.FLAG_CLOSE_POPUPS;
+import static com.android.launcher3.statemanager.BaseState.FLAG_NON_INTERACTIVE;
+
+import androidx.annotation.CallSuper;
+
+import com.android.launcher3.AbstractFloatingView;
+import com.android.launcher3.views.ActivityContext;
+
+import java.util.List;
+
+/**
+ * Interface for a container that can be managed by a state manager.
+ *
+ * @param <STATE_TYPE> The type of state that the container can be in.
+ */
+public interface StatefulContainer<STATE_TYPE extends BaseState<STATE_TYPE>> extends
+ ActivityContext {
+
+ /**
+ * Creates a factory for atomic state animations
+ */
+ default StateManager.AtomicAnimationFactory<STATE_TYPE> createAtomicAnimationFactory() {
+ return new StateManager.AtomicAnimationFactory<>(0);
+ }
+
+ /**
+ * Create handlers to control the property changes for this activity
+ */
+ void collectStateHandlers(List<StateManager.StateHandler<STATE_TYPE>> out);
+
+ /**
+ * Retrieves state manager for given container
+ */
+ StateManager<STATE_TYPE, ?> getStateManager();
+
+ /**
+ * Called when transition to state ends
+ * @param state current state of State_Type
+ */
+ default void onStateSetEnd(STATE_TYPE state) { }
+
+ /**
+ * Called when transition to state starts
+ * @param state current state of State_Type
+ */
+ @CallSuper
+ default void onStateSetStart(STATE_TYPE state) {
+ if (state.hasFlag(FLAG_CLOSE_POPUPS)) {
+ AbstractFloatingView.closeAllOpenViews(this, !state.hasFlag(FLAG_NON_INTERACTIVE));
+ }
+ }
+
+ /**
+ * Returns true if the activity is in the provided state
+ * @param state current state of State_Type
+ */
+ default boolean isInState(STATE_TYPE state) {
+ return getStateManager().getState() == state;
+ }
+
+ /**
+ * Returns true if state change should transition with animation
+ */
+ boolean shouldAnimateStateChange();
+}
diff --git a/src/com/android/launcher3/util/CannedAnimationCoordinator.kt b/src/com/android/launcher3/util/CannedAnimationCoordinator.kt
index 85f81f5..749caac 100644
--- a/src/com/android/launcher3/util/CannedAnimationCoordinator.kt
+++ b/src/com/android/launcher3/util/CannedAnimationCoordinator.kt
@@ -113,10 +113,9 @@
// flags accordingly
animationController =
controller.apply {
- activity.stateManager.setCurrentAnimation(
- this,
- USER_CONTROLLED or HANDLE_STATE_APPLY
- )
+ activity
+ .stateManager
+ .setCurrentAnimation(this, USER_CONTROLLED or HANDLE_STATE_APPLY)
}
recreateAnimation(provider)
}
diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java
index f7c4df4..8c5a76e 100644
--- a/src/com/android/launcher3/util/PackageManagerHelper.java
+++ b/src/com/android/launcher3/util/PackageManagerHelper.java
@@ -16,9 +16,10 @@
package com.android.launcher3.util;
-import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
import static android.view.WindowManager.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI;
+import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
+
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
@@ -151,6 +152,18 @@
}
/**
+ * Returns the installing app package for the given package
+ */
+ public String getAppInstallerPackage(@NonNull final String packageName) {
+ try {
+ return mPm.getInstallSourceInfo(packageName).getInstallingPackageName();
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "Failed to get installer package for app package:" + packageName, e);
+ return null;
+ }
+ }
+
+ /**
* Returns the application info for the provided package or null
*/
@Nullable
diff --git a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
index 3405635..8770859 100644
--- a/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/AbstractDeviceProfileTest.kt
@@ -57,7 +57,7 @@
*
* For an implementation that mocks InvariantDeviceProfile, use [FakeInvariantDeviceProfileTest]
*/
-@AllowedDevices(allowed = [DeviceProduct.CF_PHONE, DeviceProduct.HOST_SIDE_X86_64])
+@AllowedDevices(allowed = [DeviceProduct.CF_PHONE, DeviceProduct.ROBOLECTRIC])
@IgnoreLimit(ignoreLimit = BuildConfig.IS_STUDIO_BUILD)
abstract class AbstractDeviceProfileTest {
protected val testContext: Context = InstrumentationRegistry.getInstrumentation().context
@@ -341,4 +341,9 @@
protected fun Int.dpToPx(): Int {
return ResourceUtils.pxFromDp(this.toFloat(), context!!.resources.displayMetrics)
}
+
+ protected fun String.xmlToId(): Int {
+ val context = InstrumentationRegistry.getInstrumentation().context
+ return context.resources.getIdentifier(this, "xml", context.packageName)
+ }
}
diff --git a/tests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt b/tests/multivalentTests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt
similarity index 93%
rename from tests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt
index c99da96..3dca35e 100644
--- a/tests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/responsive/AllAppsSpecsTest.kt
@@ -22,7 +22,6 @@
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.AbstractDeviceProfileTest
import com.android.launcher3.responsive.ResponsiveSpec.Companion.ResponsiveSpecType
-import com.android.launcher3.tests.R as TestR
import com.android.launcher3.util.TestResourceHelper
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -45,7 +44,7 @@
fun parseValidFile() {
val allAppsSpecs =
ResponsiveSpecsProvider.create(
- TestResourceHelper(context, TestR.xml.valid_all_apps_file),
+ TestResourceHelper(context, "valid_all_apps_file".xmlToId()),
ResponsiveSpecType.AllApps
)
@@ -114,7 +113,7 @@
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_missingTag_throwsError() {
ResponsiveSpecsProvider.create(
- TestResourceHelper(context, TestR.xml.invalid_all_apps_file_case_1),
+ TestResourceHelper(context, "invalid_all_apps_file_case_1".xmlToId()),
ResponsiveSpecType.AllApps
)
}
@@ -122,7 +121,7 @@
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_moreThanOneValuePerTag_throwsError() {
ResponsiveSpecsProvider.create(
- TestResourceHelper(context, TestR.xml.invalid_all_apps_file_case_2),
+ TestResourceHelper(context, "invalid_all_apps_file_case_2".xmlToId()),
ResponsiveSpecType.AllApps
)
}
@@ -130,7 +129,7 @@
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_valueBiggerThan1_throwsError() {
ResponsiveSpecsProvider.create(
- TestResourceHelper(context, TestR.xml.invalid_all_apps_file_case_3),
+ TestResourceHelper(context, "invalid_all_apps_file_case_3".xmlToId()),
ResponsiveSpecType.AllApps
)
}
diff --git a/tests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt b/tests/multivalentTests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt
similarity index 94%
rename from tests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt
index 1cc5ed2..8346492 100644
--- a/tests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/responsive/CalculatedAllAppsSpecTest.kt
@@ -23,7 +23,6 @@
import com.android.launcher3.AbstractDeviceProfileTest
import com.android.launcher3.responsive.ResponsiveSpec.Companion.ResponsiveSpecType
import com.android.launcher3.responsive.ResponsiveSpec.DimensionType
-import com.android.launcher3.tests.R as TestR
import com.android.launcher3.util.TestResourceHelper
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -52,7 +51,7 @@
val workspaceSpecs =
ResponsiveSpecsProvider.create(
- TestResourceHelper(context, TestR.xml.valid_workspace_file),
+ TestResourceHelper(context, "valid_workspace_file".xmlToId()),
ResponsiveSpecType.Workspace
)
val widthSpec =
@@ -62,7 +61,7 @@
val allAppsSpecs =
ResponsiveSpecsProvider.create(
- TestResourceHelper(context, TestR.xml.valid_all_apps_file),
+ TestResourceHelper(context, "valid_all_apps_file".xmlToId()),
ResponsiveSpecType.AllApps
)
diff --git a/tests/src/com/android/launcher3/responsive/CalculatedFolderSpecTest.kt b/tests/multivalentTests/src/com/android/launcher3/responsive/CalculatedFolderSpecTest.kt
similarity index 95%
rename from tests/src/com/android/launcher3/responsive/CalculatedFolderSpecTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/responsive/CalculatedFolderSpecTest.kt
index c4e2d2a..46d6cb9 100644
--- a/tests/src/com/android/launcher3/responsive/CalculatedFolderSpecTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/responsive/CalculatedFolderSpecTest.kt
@@ -23,7 +23,6 @@
import com.android.launcher3.AbstractDeviceProfileTest
import com.android.launcher3.responsive.ResponsiveSpec.Companion.ResponsiveSpecType
import com.android.launcher3.responsive.ResponsiveSpec.DimensionType
-import com.android.launcher3.tests.R
import com.android.launcher3.util.TestResourceHelper
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -47,12 +46,12 @@
val columns = 6
// Loading workspace specs
- val resourceHelperWorkspace = TestResourceHelper(context, R.xml.valid_workspace_file)
+ val resourceHelperWorkspace = TestResourceHelper(context, "valid_workspace_file".xmlToId())
val workspaceSpecs =
ResponsiveSpecsProvider.create(resourceHelperWorkspace, ResponsiveSpecType.Workspace)
// Loading folders specs
- val resourceHelperFolder = TestResourceHelper(context, R.xml.valid_folders_specs)
+ val resourceHelperFolder = TestResourceHelper(context, "valid_folders_specs".xmlToId())
val folderSpecs =
ResponsiveSpecsProvider.create(resourceHelperFolder, ResponsiveSpecType.Folder)
val specs = folderSpecs.getSpecsByAspectRatio(aspectRatio)
@@ -123,12 +122,12 @@
val rows = 5
// Loading workspace specs
- val resourceHelperWorkspace = TestResourceHelper(context, R.xml.valid_workspace_file)
+ val resourceHelperWorkspace = TestResourceHelper(context, "valid_workspace_file".xmlToId())
val workspaceSpecs =
ResponsiveSpecsProvider.create(resourceHelperWorkspace, ResponsiveSpecType.Workspace)
// Loading folders specs
- val resourceHelperFolder = TestResourceHelper(context, R.xml.valid_folders_specs)
+ val resourceHelperFolder = TestResourceHelper(context, "valid_folders_specs".xmlToId())
val folderSpecs =
ResponsiveSpecsProvider.create(resourceHelperFolder, ResponsiveSpecType.Folder)
val specs = folderSpecs.getSpecsByAspectRatio(aspectRatio)
diff --git a/tests/src/com/android/launcher3/responsive/CalculatedHotseatSpecTest.kt b/tests/multivalentTests/src/com/android/launcher3/responsive/CalculatedHotseatSpecTest.kt
similarity index 95%
rename from tests/src/com/android/launcher3/responsive/CalculatedHotseatSpecTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/responsive/CalculatedHotseatSpecTest.kt
index 1a564ac..5ab44f3 100644
--- a/tests/src/com/android/launcher3/responsive/CalculatedHotseatSpecTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/responsive/CalculatedHotseatSpecTest.kt
@@ -22,7 +22,6 @@
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.AbstractDeviceProfileTest
import com.android.launcher3.responsive.ResponsiveSpec.DimensionType
-import com.android.launcher3.tests.R as TestR
import com.android.launcher3.util.TestResourceHelper
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -47,7 +46,7 @@
val availableHeight = deviceSpec.naturalSize.second
val hotseatSpecsProvider =
- HotseatSpecsProvider.create(TestResourceHelper(context, TestR.xml.valid_hotseat_file))
+ HotseatSpecsProvider.create(TestResourceHelper(context, "valid_hotseat_file".xmlToId()))
val heightSpec =
hotseatSpecsProvider.getCalculatedSpec(
aspectRatio,
@@ -73,7 +72,7 @@
val availableHeight = deviceSpec.naturalSize.second
val hotseatSpecsProvider =
- HotseatSpecsProvider.create(TestResourceHelper(context, TestR.xml.valid_hotseat_file))
+ HotseatSpecsProvider.create(TestResourceHelper(context, "valid_hotseat_file".xmlToId()))
val heightSpec =
hotseatSpecsProvider.getCalculatedSpec(
aspectRatio,
@@ -99,7 +98,7 @@
val hotseatSpecsProvider =
HotseatSpecsProvider.create(
- TestResourceHelper(context, TestR.xml.valid_hotseat_land_file)
+ TestResourceHelper(context, "valid_hotseat_land_file".xmlToId())
)
val widthSpec =
hotseatSpecsProvider.getCalculatedSpec(aspectRatio, DimensionType.WIDTH, availableWidth)
diff --git a/tests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt b/tests/multivalentTests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt
similarity index 95%
rename from tests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt
index 0c5d347..dea98b6 100644
--- a/tests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/responsive/CalculatedWorkspaceSpecTest.kt
@@ -23,7 +23,6 @@
import com.android.launcher3.AbstractDeviceProfileTest
import com.android.launcher3.responsive.ResponsiveSpec.Companion.ResponsiveSpecType
import com.android.launcher3.responsive.ResponsiveSpec.DimensionType
-import com.android.launcher3.tests.R as TestR
import com.android.launcher3.util.TestResourceHelper
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -53,7 +52,7 @@
val workspaceSpecs =
ResponsiveSpecsProvider.create(
- TestResourceHelper(context, TestR.xml.valid_workspace_file),
+ TestResourceHelper(context, "valid_workspace_file".xmlToId()),
ResponsiveSpecType.Workspace
)
val widthSpec =
@@ -96,7 +95,7 @@
val workspaceSpecs =
ResponsiveSpecsProvider.create(
- TestResourceHelper(context, TestR.xml.valid_workspace_file),
+ TestResourceHelper(context, "valid_workspace_file".xmlToId()),
ResponsiveSpecType.Workspace
)
val widthSpec =
@@ -138,7 +137,7 @@
val availableHeight = deviceSpec.naturalSize.second - deviceSpec.statusBarNaturalPx - 640
val workspaceSpecs =
ResponsiveSpecsProvider.create(
- TestResourceHelper(context, TestR.xml.valid_workspace_unsorted_file),
+ TestResourceHelper(context, "valid_workspace_unsorted_file".xmlToId()),
ResponsiveSpecType.Workspace
)
val widthSpec =
diff --git a/tests/src/com/android/launcher3/responsive/FolderSpecTest.kt b/tests/multivalentTests/src/com/android/launcher3/responsive/FolderSpecTest.kt
similarity index 91%
rename from tests/src/com/android/launcher3/responsive/FolderSpecTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/responsive/FolderSpecTest.kt
index 5cfa49f..d2b264b 100644
--- a/tests/src/com/android/launcher3/responsive/FolderSpecTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/responsive/FolderSpecTest.kt
@@ -23,7 +23,6 @@
import com.android.launcher3.AbstractDeviceProfileTest
import com.android.launcher3.responsive.ResponsiveSpec.Companion.ResponsiveSpecType
import com.android.launcher3.responsive.ResponsiveSpec.DimensionType
-import com.android.launcher3.tests.R
import com.android.launcher3.util.TestResourceHelper
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -44,7 +43,7 @@
@Test
fun parseValidFile() {
- val resourceHelper = TestResourceHelper(context, R.xml.valid_folders_specs)
+ val resourceHelper = TestResourceHelper(context, "valid_folders_specs".xmlToId())
val folderSpecs = ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Folder)
val specs = folderSpecs.getSpecsByAspectRatio(aspectRatio)
@@ -92,25 +91,25 @@
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_missingTag_throwsError() {
- val resourceHelper = TestResourceHelper(context, R.xml.invalid_folders_specs_1)
+ val resourceHelper = TestResourceHelper(context, "invalid_folders_specs_1".xmlToId())
ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Folder)
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_moreThanOneValuePerTag_throwsError() {
- val resourceHelper = TestResourceHelper(context, R.xml.invalid_folders_specs_2)
+ val resourceHelper = TestResourceHelper(context, "invalid_folders_specs_2".xmlToId())
ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Folder)
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_valueBiggerThan1_throwsError() {
- val resourceHelper = TestResourceHelper(context, R.xml.invalid_folders_specs_3)
+ val resourceHelper = TestResourceHelper(context, "invalid_folders_specs_3".xmlToId())
ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Folder)
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_missingSpecs_throwsError() {
- val resourceHelper = TestResourceHelper(context, R.xml.invalid_folders_specs_4)
+ val resourceHelper = TestResourceHelper(context, "invalid_folders_specs_4".xmlToId())
ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Folder)
}
@@ -132,7 +131,7 @@
val calculatedWorkspaceSpec =
CalculatedResponsiveSpec(aspectRatio, availableSpace, cells, workspaceSpec)
- val resourceHelper = TestResourceHelper(context, R.xml.invalid_folders_specs_5)
+ val resourceHelper = TestResourceHelper(context, "invalid_folders_specs_5".xmlToId())
val folderSpecs = ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Folder)
folderSpecs.getCalculatedSpec(
aspectRatio,
@@ -161,7 +160,7 @@
val calculatedWorkspaceSpec =
CalculatedResponsiveSpec(aspectRatio, availableSpace, cells, workspaceSpec)
- val resourceHelper = TestResourceHelper(context, R.xml.invalid_folders_specs_5)
+ val resourceHelper = TestResourceHelper(context, "invalid_folders_specs_5".xmlToId())
val folderSpecs = ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Folder)
folderSpecs.getCalculatedSpec(
aspectRatio,
@@ -190,7 +189,7 @@
val calculatedWorkspaceSpec =
CalculatedResponsiveSpec(aspectRatio, availableSpace, cells, workspaceSpec)
- val resourceHelper = TestResourceHelper(context, R.xml.valid_folders_specs)
+ val resourceHelper = TestResourceHelper(context, "valid_folders_specs".xmlToId())
val folderSpecs = ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Folder)
val calculatedWidthSpec =
folderSpecs.getCalculatedSpec(
@@ -227,7 +226,7 @@
val calculatedWorkspaceSpec =
CalculatedResponsiveSpec(aspectRatio, availableSpace, cells, workspaceSpec)
- val resourceHelper = TestResourceHelper(context, R.xml.valid_folders_specs)
+ val resourceHelper = TestResourceHelper(context, "valid_folders_specs".xmlToId())
val folderSpecs = ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Folder)
folderSpecs.getCalculatedSpec(
aspectRatio,
@@ -256,7 +255,7 @@
val calculatedWorkspaceSpec =
CalculatedResponsiveSpec(aspectRatio, availableSpace, cells, workspaceSpec)
- val resourceHelper = TestResourceHelper(context, R.xml.valid_folders_specs)
+ val resourceHelper = TestResourceHelper(context, "valid_folders_specs".xmlToId())
val folderSpecs = ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Folder)
val calculatedHeightSpec =
folderSpecs.getCalculatedSpec(
@@ -293,7 +292,7 @@
val calculatedWorkspaceSpec =
CalculatedResponsiveSpec(aspectRatio, availableSpace, cells, workspaceSpec)
- val resourceHelper = TestResourceHelper(context, R.xml.valid_folders_specs)
+ val resourceHelper = TestResourceHelper(context, "valid_folders_specs".xmlToId())
val folderSpecs = ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Folder)
folderSpecs.getCalculatedSpec(
aspectRatio,
diff --git a/tests/src/com/android/launcher3/responsive/HotseatSpecsProviderTest.kt b/tests/multivalentTests/src/com/android/launcher3/responsive/HotseatSpecsProviderTest.kt
similarity index 93%
rename from tests/src/com/android/launcher3/responsive/HotseatSpecsProviderTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/responsive/HotseatSpecsProviderTest.kt
index 78cb1ac..58324e1 100644
--- a/tests/src/com/android/launcher3/responsive/HotseatSpecsProviderTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/responsive/HotseatSpecsProviderTest.kt
@@ -22,7 +22,6 @@
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.AbstractDeviceProfileTest
import com.android.launcher3.responsive.ResponsiveSpec.Companion.ResponsiveSpecType
-import com.android.launcher3.tests.R as TestR
import com.android.launcher3.util.TestResourceHelper
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -44,7 +43,7 @@
@Test
fun parseValidFile() {
val hotseatSpecsProvider =
- HotseatSpecsProvider.create(TestResourceHelper(context, TestR.xml.valid_hotseat_file))
+ HotseatSpecsProvider.create(TestResourceHelper(context, "valid_hotseat_file".xmlToId()))
val specs = hotseatSpecsProvider.getSpecsByAspectRatio(aspectRatio)
val expectedHeightSpecs =
@@ -76,7 +75,7 @@
fun parseValidLandscapeFile() {
val hotseatSpecsProvider =
HotseatSpecsProvider.create(
- TestResourceHelper(context, TestR.xml.valid_hotseat_land_file)
+ TestResourceHelper(context, "valid_hotseat_land_file".xmlToId())
)
val specs = hotseatSpecsProvider.getSpecsByAspectRatio(aspectRatio)
assertThat(specs.heightSpecs.size).isEqualTo(0)
@@ -107,14 +106,14 @@
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_spaceIsNotFixedSize_throwsError() {
HotseatSpecsProvider.create(
- TestResourceHelper(context, TestR.xml.invalid_hotseat_file_case_1)
+ TestResourceHelper(context, "invalid_hotseat_file_case_1".xmlToId())
)
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_invalidFixedSize_throwsError() {
HotseatSpecsProvider.create(
- TestResourceHelper(context, TestR.xml.invalid_hotseat_file_case_2)
+ TestResourceHelper(context, "invalid_hotseat_file_case_2".xmlToId())
)
}
}
diff --git a/tests/src/com/android/launcher3/responsive/ResponsiveCellSpecsProviderTest.kt b/tests/multivalentTests/src/com/android/launcher3/responsive/ResponsiveCellSpecsProviderTest.kt
similarity index 92%
rename from tests/src/com/android/launcher3/responsive/ResponsiveCellSpecsProviderTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/responsive/ResponsiveCellSpecsProviderTest.kt
index 50cd358..11161bd 100644
--- a/tests/src/com/android/launcher3/responsive/ResponsiveCellSpecsProviderTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/responsive/ResponsiveCellSpecsProviderTest.kt
@@ -22,7 +22,6 @@
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.AbstractDeviceProfileTest
import com.android.launcher3.responsive.ResponsiveSpec.Companion.ResponsiveSpecType
-import com.android.launcher3.tests.R as TestR
import com.android.launcher3.util.TestResourceHelper
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -43,7 +42,7 @@
@Test
fun parseValidFile() {
- val testResourceHelper = TestResourceHelper(context, TestR.xml.valid_cell_specs_file)
+ val testResourceHelper = TestResourceHelper(context, "valid_cell_specs_file".xmlToId())
val provider = ResponsiveCellSpecsProvider.create(testResourceHelper)
// Validate Portrait
@@ -98,21 +97,21 @@
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_IsNotFixedSizeOrMatchWorkspace_throwsError() {
ResponsiveCellSpecsProvider.create(
- TestResourceHelper(context, TestR.xml.invalid_cell_specs_1)
+ TestResourceHelper(context, "invalid_cell_specs_1".xmlToId())
)
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_dimensionTypeIsNotHeight_throwsError() {
ResponsiveCellSpecsProvider.create(
- TestResourceHelper(context, TestR.xml.invalid_cell_specs_2)
+ TestResourceHelper(context, "invalid_cell_specs_2".xmlToId())
)
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_invalidFixedSize_throwsError() {
ResponsiveCellSpecsProvider.create(
- TestResourceHelper(context, TestR.xml.invalid_cell_specs_3)
+ TestResourceHelper(context, "invalid_cell_specs_3".xmlToId())
)
}
}
diff --git a/tests/src/com/android/launcher3/responsive/ResponsiveSpecsProviderTest.kt b/tests/multivalentTests/src/com/android/launcher3/responsive/ResponsiveSpecsProviderTest.kt
similarity index 93%
rename from tests/src/com/android/launcher3/responsive/ResponsiveSpecsProviderTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/responsive/ResponsiveSpecsProviderTest.kt
index 54a1dc5..c74f7a8 100644
--- a/tests/src/com/android/launcher3/responsive/ResponsiveSpecsProviderTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/responsive/ResponsiveSpecsProviderTest.kt
@@ -23,7 +23,6 @@
import com.android.launcher3.AbstractDeviceProfileTest
import com.android.launcher3.responsive.ResponsiveSpec.Companion.ResponsiveSpecType
import com.android.launcher3.responsive.ResponsiveSpec.DimensionType
-import com.android.launcher3.tests.R
import com.android.launcher3.util.TestResourceHelper
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -44,7 +43,7 @@
@Test
fun parseValidFile() {
- val resourceHelper = TestResourceHelper(context, R.xml.valid_responsive_spec_unsorted)
+ val resourceHelper = TestResourceHelper(context, "valid_responsive_spec_unsorted".xmlToId())
val provider = ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Workspace)
// Validate Portrait
@@ -111,44 +110,44 @@
@Test(expected = IllegalStateException::class)
fun parseValidFile_invalidAspectRatio_throwsError() {
- val resourceHelper = TestResourceHelper(context, R.xml.valid_responsive_spec_unsorted)
+ val resourceHelper = TestResourceHelper(context, "valid_responsive_spec_unsorted".xmlToId())
val provider = ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Workspace)
provider.getSpecsByAspectRatio(0f)
}
@Test(expected = InvalidResponsiveGridSpec::class)
fun parseInvalidFile_missingGroups_throwsError() {
- val resourceHelper = TestResourceHelper(context, R.xml.invalid_responsive_spec_1)
+ val resourceHelper = TestResourceHelper(context, "invalid_responsive_spec_1".xmlToId())
ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Workspace)
}
@Test(expected = InvalidResponsiveGridSpec::class)
fun parseInvalidFile_partialGroups_throwsError() {
- val resourceHelper = TestResourceHelper(context, R.xml.invalid_responsive_spec_2)
+ val resourceHelper = TestResourceHelper(context, "invalid_responsive_spec_2".xmlToId())
ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Workspace)
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_invalidAspectRatio_throwsError() {
- val resourceHelper = TestResourceHelper(context, R.xml.invalid_responsive_spec_3)
+ val resourceHelper = TestResourceHelper(context, "invalid_responsive_spec_3".xmlToId())
ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Workspace)
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_invalidRemainderSpace_throwsError() {
- val resourceHelper = TestResourceHelper(context, R.xml.invalid_responsive_spec_4)
+ val resourceHelper = TestResourceHelper(context, "invalid_responsive_spec_4".xmlToId())
ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Workspace)
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_invalidAvailableSpace_throwsError() {
- val resourceHelper = TestResourceHelper(context, R.xml.invalid_responsive_spec_5)
+ val resourceHelper = TestResourceHelper(context, "invalid_responsive_spec_5".xmlToId())
ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Workspace)
}
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_invalidFixedSize_throwsError() {
- val resourceHelper = TestResourceHelper(context, R.xml.invalid_responsive_spec_6)
+ val resourceHelper = TestResourceHelper(context, "invalid_responsive_spec_6".xmlToId())
ResponsiveSpecsProvider.create(resourceHelper, ResponsiveSpecType.Workspace)
}
diff --git a/tests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt b/tests/multivalentTests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt
similarity index 94%
rename from tests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt
rename to tests/multivalentTests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt
index 17b0ee4..bc133ba 100644
--- a/tests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt
+++ b/tests/multivalentTests/src/com/android/launcher3/responsive/WorkspaceSpecsTest.kt
@@ -22,7 +22,6 @@
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.AbstractDeviceProfileTest
import com.android.launcher3.responsive.ResponsiveSpec.Companion.ResponsiveSpecType
-import com.android.launcher3.tests.R as TestR
import com.android.launcher3.util.TestResourceHelper
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -45,7 +44,7 @@
fun parseValidFile() {
val workspaceSpecs =
ResponsiveSpecsProvider.create(
- TestResourceHelper(context, TestR.xml.valid_workspace_file),
+ TestResourceHelper(context, "valid_workspace_file".xmlToId()),
ResponsiveSpecType.Workspace
)
@@ -169,7 +168,7 @@
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_missingTag_throwsError() {
ResponsiveSpecsProvider.create(
- TestResourceHelper(context, TestR.xml.invalid_workspace_file_case_1),
+ TestResourceHelper(context, "invalid_workspace_file_case_1".xmlToId()),
ResponsiveSpecType.Workspace
)
}
@@ -177,7 +176,7 @@
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_moreThanOneValuePerTag_throwsError() {
ResponsiveSpecsProvider.create(
- TestResourceHelper(context, TestR.xml.invalid_workspace_file_case_2),
+ TestResourceHelper(context, "invalid_workspace_file_case_2".xmlToId()),
ResponsiveSpecType.Workspace
)
}
@@ -185,7 +184,7 @@
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_valueBiggerThan1_throwsError() {
ResponsiveSpecsProvider.create(
- TestResourceHelper(context, TestR.xml.invalid_workspace_file_case_3),
+ TestResourceHelper(context, "invalid_workspace_file_case_3".xmlToId()),
ResponsiveSpecType.Workspace
)
}
@@ -193,7 +192,7 @@
@Test(expected = IllegalStateException::class)
fun parseInvalidFile_matchWorkspace_true_throwsError() {
ResponsiveSpecsProvider.create(
- TestResourceHelper(context, TestR.xml.invalid_workspace_file_case_4),
+ TestResourceHelper(context, "invalid_workspace_file_case_4".xmlToId()),
ResponsiveSpecType.Workspace
)
}
diff --git a/tests/multivalentTests/src/com/android/launcher3/util/TestResourceHelper.kt b/tests/multivalentTests/src/com/android/launcher3/util/TestResourceHelper.kt
new file mode 100644
index 0000000..d95c6f8
--- /dev/null
+++ b/tests/multivalentTests/src/com/android/launcher3/util/TestResourceHelper.kt
@@ -0,0 +1,59 @@
+/*
+ * 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
+
+import android.content.Context
+import android.content.res.TypedArray
+import android.util.AttributeSet
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.launcher3.R
+import kotlin.IntArray
+
+class TestResourceHelper(private val context: Context, specsFileId: Int) :
+ ResourceHelper(context, specsFileId) {
+
+ val responsiveStyleables = listOf(
+ R.styleable.SizeSpec,
+ R.styleable.WorkspaceSpec,
+ R.styleable.FolderSpec,
+ R.styleable.AllAppsSpec,
+ R.styleable.ResponsiveSpecGroup
+ )
+
+ override fun obtainStyledAttributes(attrs: AttributeSet, styleId: IntArray): TypedArray {
+ val clone =
+ if (responsiveStyleables.any { styleId.contentEquals(it) }) {
+ convertStyleId(styleId)
+ } else {
+ styleId.clone()
+ }
+
+ return context.obtainStyledAttributes(attrs, clone)
+ }
+
+ private fun convertStyleId(styleableArr: IntArray): IntArray {
+ val targetContextRes = getInstrumentation().targetContext.resources
+ val context = getInstrumentation().context
+ return styleableArr
+ .map { attrId -> targetContextRes.getResourceName(attrId).split(":").last() }
+ .map { attrName ->
+ // Get required attr from context instead of targetContext
+ context.resources.getIdentifier(attrName, null, context.packageName)
+ }
+ .toIntArray()
+ }
+}
diff --git a/tests/src/com/android/launcher3/model/FirstScreenBroadcastHelperTest.kt b/tests/src/com/android/launcher3/model/FirstScreenBroadcastHelperTest.kt
new file mode 100644
index 0000000..aadf72e
--- /dev/null
+++ b/tests/src/com/android/launcher3/model/FirstScreenBroadcastHelperTest.kt
@@ -0,0 +1,386 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.launcher3.model
+
+import android.app.PendingIntent
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.PackageInstaller.SessionInfo
+import android.os.UserHandle
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
+import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
+import com.android.launcher3.model.FirstScreenBroadcastHelper.MAX_BROADCAST_SIZE
+import com.android.launcher3.model.FirstScreenBroadcastHelper.getTotalItemCount
+import com.android.launcher3.model.FirstScreenBroadcastHelper.truncateModelForBroadcast
+import com.android.launcher3.model.data.FolderInfo
+import com.android.launcher3.model.data.LauncherAppWidgetInfo
+import com.android.launcher3.model.data.WorkspaceItemInfo
+import com.android.launcher3.util.PackageManagerHelper
+import com.android.launcher3.util.PackageUserKey
+import junit.framework.Assert.assertEquals
+import org.junit.Test
+import org.mockito.ArgumentCaptor
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+class FirstScreenBroadcastHelperTest {
+ private val context = spy(InstrumentationRegistry.getInstrumentation().targetContext)
+ private val mockPmHelper = mock<PackageManagerHelper>()
+ private val expectedAppPackage = "appPackageExpected"
+ private val expectedComponentName = ComponentName(expectedAppPackage, "expectedClass")
+ private val expectedInstallerPackage = "installerPackage"
+ private val expectedIntent =
+ Intent().apply {
+ component = expectedComponentName
+ setPackage(expectedAppPackage)
+ }
+ private val unexpectedAppPackage = "appPackageUnexpected"
+ private val unexpectedComponentName = ComponentName(expectedAppPackage, "unexpectedClass")
+ private val firstScreenItems =
+ listOf(
+ WorkspaceItemInfo().apply {
+ container = CONTAINER_DESKTOP
+ intent = expectedIntent
+ },
+ WorkspaceItemInfo().apply {
+ container = CONTAINER_HOTSEAT
+ intent = expectedIntent
+ },
+ LauncherAppWidgetInfo().apply { providerName = expectedComponentName }
+ )
+
+ @Test
+ fun `Broadcast Models are created with Pending Items from first screen`() {
+ // Given
+ val sessionInfoExpected =
+ SessionInfo().apply {
+ installerPackageName = expectedInstallerPackage
+ appPackageName = expectedAppPackage
+ }
+ val sessionInfoUnexpected =
+ SessionInfo().apply {
+ installerPackageName = expectedInstallerPackage
+ appPackageName = unexpectedAppPackage
+ }
+ val sessionInfoMap: HashMap<PackageUserKey, SessionInfo> =
+ hashMapOf(
+ PackageUserKey(unexpectedAppPackage, UserHandle(0)) to sessionInfoExpected,
+ PackageUserKey(expectedAppPackage, UserHandle(0)) to sessionInfoUnexpected
+ )
+
+ // When
+ val actualResult =
+ FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
+ packageManagerHelper = mockPmHelper,
+ firstScreenItems = firstScreenItems,
+ userKeyToSessionMap = sessionInfoMap,
+ allWidgets = listOf()
+ )
+
+ // Then
+ val expectedResult =
+ listOf(
+ FirstScreenBroadcastModel(
+ installerPackage = expectedInstallerPackage,
+ pendingWorkspaceItems = mutableSetOf(expectedAppPackage),
+ pendingHotseatItems = mutableSetOf(expectedAppPackage),
+ pendingWidgetItems = mutableSetOf(expectedAppPackage)
+ )
+ )
+
+ assertEquals(expectedResult, actualResult)
+ }
+
+ @Test
+ fun `Broadcast Models are created with Installed Items from first screen`() {
+ // Given
+ whenever(mockPmHelper.getAppInstallerPackage(expectedAppPackage))
+ .thenReturn(expectedInstallerPackage)
+
+ // When
+ val actualResult =
+ FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
+ packageManagerHelper = mockPmHelper,
+ firstScreenItems = firstScreenItems,
+ userKeyToSessionMap = hashMapOf(),
+ allWidgets =
+ listOf(
+ LauncherAppWidgetInfo().apply {
+ providerName = expectedComponentName
+ screenId = 0
+ }
+ )
+ )
+
+ // Then
+ val expectedResult =
+ listOf(
+ FirstScreenBroadcastModel(
+ installerPackage = expectedInstallerPackage,
+ installedHotseatItems = mutableSetOf(expectedAppPackage),
+ installedWorkspaceItems = mutableSetOf(expectedAppPackage),
+ firstScreenInstalledWidgets = mutableSetOf(expectedAppPackage)
+ )
+ )
+ assertEquals(expectedResult, actualResult)
+ }
+
+ @Test
+ fun `Broadcast Models are created with Installed Widgets from every screen`() {
+ // Given
+ val expectedAppPackage2 = "appPackageExpected2"
+ val expectedComponentName2 = ComponentName(expectedAppPackage2, "expectedClass2")
+ whenever(mockPmHelper.getAppInstallerPackage(expectedAppPackage))
+ .thenReturn(expectedInstallerPackage)
+ whenever(mockPmHelper.getAppInstallerPackage(expectedAppPackage2))
+ .thenReturn(expectedInstallerPackage)
+
+ // When
+ val actualResult =
+ FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
+ packageManagerHelper = mockPmHelper,
+ firstScreenItems = listOf(),
+ userKeyToSessionMap = hashMapOf(),
+ allWidgets =
+ listOf(
+ LauncherAppWidgetInfo().apply {
+ providerName = expectedComponentName
+ screenId = 0
+ },
+ LauncherAppWidgetInfo().apply {
+ providerName = expectedComponentName2
+ screenId = 1
+ },
+ LauncherAppWidgetInfo().apply {
+ providerName = unexpectedComponentName
+ screenId = 0
+ }
+ )
+ )
+
+ // Then
+ val expectedResult =
+ listOf(
+ FirstScreenBroadcastModel(
+ installerPackage = expectedInstallerPackage,
+ installedHotseatItems = mutableSetOf(),
+ installedWorkspaceItems = mutableSetOf(),
+ firstScreenInstalledWidgets = mutableSetOf(expectedAppPackage),
+ secondaryScreenInstalledWidgets = mutableSetOf(expectedAppPackage2)
+ )
+ )
+ assertEquals(expectedResult, actualResult)
+ }
+
+ @Test
+ fun `Broadcast Models are created with Pending Items in Collections from the first screen`() {
+ // Given
+ val sessionInfoExpected =
+ SessionInfo().apply {
+ installerPackageName = expectedInstallerPackage
+ appPackageName = expectedAppPackage
+ }
+ val sessionInfoUnexpected =
+ SessionInfo().apply {
+ installerPackageName = expectedInstallerPackage
+ appPackageName = unexpectedAppPackage
+ }
+ val sessionInfoMap: HashMap<PackageUserKey, SessionInfo> =
+ hashMapOf(
+ PackageUserKey(unexpectedAppPackage, UserHandle(0)) to sessionInfoExpected,
+ PackageUserKey(expectedAppPackage, UserHandle(0)) to sessionInfoUnexpected,
+ )
+ val expectedItemInfo = WorkspaceItemInfo().apply { intent = expectedIntent }
+ val expectedFolderInfo = FolderInfo().apply { add(expectedItemInfo) }
+ val firstScreenItems = listOf(expectedFolderInfo)
+
+ // When
+ val actualResult =
+ FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast(
+ packageManagerHelper = mockPmHelper,
+ firstScreenItems = firstScreenItems,
+ userKeyToSessionMap = sessionInfoMap,
+ allWidgets = listOf()
+ )
+
+ // Then
+ val expectedResult =
+ listOf(
+ FirstScreenBroadcastModel(
+ installerPackage = expectedInstallerPackage,
+ pendingCollectionItems = mutableSetOf(expectedAppPackage)
+ )
+ )
+ assertEquals(expectedResult, actualResult)
+ }
+
+ @Test
+ fun `Models with too many items get truncated to max Broadcast size`() {
+ // given
+ val broadcastModel =
+ FirstScreenBroadcastModel(
+ installerPackage = expectedInstallerPackage,
+ pendingCollectionItems =
+ mutableSetOf<String>().apply { repeat(20) { add(it.toString()) } },
+ pendingWorkspaceItems =
+ mutableSetOf<String>().apply { repeat(20) { add(it.toString()) } },
+ pendingHotseatItems =
+ mutableSetOf<String>().apply { repeat(20) { add(it.toString()) } },
+ pendingWidgetItems =
+ mutableSetOf<String>().apply { repeat(20) { add(it.toString()) } },
+ installedWorkspaceItems =
+ mutableSetOf<String>().apply { repeat(20) { add(it.toString()) } },
+ installedHotseatItems =
+ mutableSetOf<String>().apply { repeat(20) { add(it.toString()) } },
+ firstScreenInstalledWidgets =
+ mutableSetOf<String>().apply { repeat(20) { add(it.toString()) } },
+ secondaryScreenInstalledWidgets =
+ mutableSetOf<String>().apply { repeat(20) { add(it.toString()) } }
+ )
+
+ // When
+ broadcastModel.truncateModelForBroadcast()
+
+ // Then
+ assertEquals(MAX_BROADCAST_SIZE, broadcastModel.getTotalItemCount())
+ }
+
+ @Test
+ fun `Broadcast truncates installed Hotseat items before other installed items`() {
+ // Given
+ val broadcastModel =
+ FirstScreenBroadcastModel(
+ installerPackage = expectedInstallerPackage,
+ installedWorkspaceItems =
+ mutableSetOf<String>().apply { repeat(50) { add(it.toString()) } },
+ firstScreenInstalledWidgets =
+ mutableSetOf<String>().apply { repeat(10) { add(it.toString()) } },
+ secondaryScreenInstalledWidgets =
+ mutableSetOf<String>().apply { repeat(10) { add((it + 10).toString()) } },
+ installedHotseatItems =
+ mutableSetOf<String>().apply { repeat(10) { add(it.toString()) } },
+ )
+
+ // When
+ broadcastModel.truncateModelForBroadcast()
+
+ // Then
+ assertEquals(MAX_BROADCAST_SIZE, broadcastModel.getTotalItemCount())
+ assertEquals(50, broadcastModel.installedWorkspaceItems.size)
+ assertEquals(10, broadcastModel.firstScreenInstalledWidgets.size)
+ assertEquals(10, broadcastModel.secondaryScreenInstalledWidgets.size)
+ assertEquals(0, broadcastModel.installedHotseatItems.size)
+ }
+
+ @Test
+ fun `Broadcast truncates Widgets before the rest of the first screen items`() {
+ // Given
+ val broadcastModel =
+ FirstScreenBroadcastModel(
+ installerPackage = expectedInstallerPackage,
+ installedWorkspaceItems =
+ mutableSetOf<String>().apply { repeat(70) { add(it.toString()) } },
+ firstScreenInstalledWidgets =
+ mutableSetOf<String>().apply { repeat(20) { add(it.toString()) } },
+ secondaryScreenInstalledWidgets =
+ mutableSetOf<String>().apply { repeat(20) { add(it.toString()) } },
+ )
+
+ // When
+ broadcastModel.truncateModelForBroadcast()
+
+ // Then
+ assertEquals(MAX_BROADCAST_SIZE, broadcastModel.getTotalItemCount())
+ assertEquals(70, broadcastModel.installedWorkspaceItems.size)
+ assertEquals(0, broadcastModel.firstScreenInstalledWidgets.size)
+ assertEquals(0, broadcastModel.secondaryScreenInstalledWidgets.size)
+ }
+
+ @Test
+ fun `Broadcasts are correctly formed with Extras for each Installer`() {
+ // Given
+ val broadcastModels: List<FirstScreenBroadcastModel> =
+ listOf(
+ FirstScreenBroadcastModel(
+ installerPackage = expectedInstallerPackage,
+ pendingCollectionItems = mutableSetOf("pendingCollectionItem"),
+ pendingWorkspaceItems = mutableSetOf("pendingWorkspaceItem"),
+ pendingHotseatItems = mutableSetOf("pendingHotseatItems"),
+ pendingWidgetItems = mutableSetOf("pendingWidgetItems"),
+ installedWorkspaceItems = mutableSetOf("installedWorkspaceItems"),
+ installedHotseatItems = mutableSetOf("installedHotseatItems"),
+ firstScreenInstalledWidgets = mutableSetOf("firstScreenInstalledWidgetItems"),
+ secondaryScreenInstalledWidgets = mutableSetOf("secondaryInstalledWidgetItems")
+ )
+ )
+ val expectedPendingIntent =
+ PendingIntent.getActivity(
+ context,
+ 0 /* requestCode */,
+ Intent(),
+ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ // When
+ FirstScreenBroadcastHelper.sendBroadcastsForModels(context, broadcastModels)
+
+ // Then
+ val argumentCaptor = ArgumentCaptor.forClass(Intent::class.java)
+ verify(context).sendBroadcast(argumentCaptor.capture())
+
+ assertEquals(
+ "com.android.launcher3.action.FIRST_SCREEN_ACTIVE_INSTALLS",
+ argumentCaptor.value.action
+ )
+ assertEquals(expectedInstallerPackage, argumentCaptor.value.`package`)
+ assertEquals(
+ expectedPendingIntent,
+ argumentCaptor.value.getParcelableExtra("verificationToken")
+ )
+ assertEquals(
+ arrayListOf("pendingCollectionItem"),
+ argumentCaptor.value.getStringArrayListExtra("folderItem")
+ )
+ assertEquals(
+ arrayListOf("pendingWorkspaceItem"),
+ argumentCaptor.value.getStringArrayListExtra("workspaceItem")
+ )
+ assertEquals(
+ arrayListOf("pendingHotseatItems"),
+ argumentCaptor.value.getStringArrayListExtra("hotseatItem")
+ )
+ assertEquals(
+ arrayListOf("pendingWidgetItems"),
+ argumentCaptor.value.getStringArrayListExtra("widgetItem")
+ )
+ assertEquals(
+ arrayListOf("installedWorkspaceItems"),
+ argumentCaptor.value.getStringArrayListExtra("workspaceInstalledItems")
+ )
+ assertEquals(
+ arrayListOf("installedHotseatItems"),
+ argumentCaptor.value.getStringArrayListExtra("hotseatInstalledItems")
+ )
+ assertEquals(
+ arrayListOf("firstScreenInstalledWidgetItems", "secondaryInstalledWidgetItems"),
+ argumentCaptor.value.getStringArrayListExtra("widgetInstalledItems")
+ )
+ }
+}
diff --git a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3Test.java b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3Test.java
index 4a67600..342eedf 100644
--- a/tests/src/com/android/launcher3/ui/TaplTestsLauncher3Test.java
+++ b/tests/src/com/android/launcher3/ui/TaplTestsLauncher3Test.java
@@ -21,9 +21,8 @@
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
-import com.android.launcher3.Launcher;
-import com.android.launcher3.util.rule.ScreenRecordRule.KeepRecordOnSuccess;
import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
+import com.android.launcher3.Launcher;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -32,7 +31,6 @@
@RunWith(AndroidJUnit4.class)
public class TaplTestsLauncher3Test extends AbstractLauncherUiTest<Launcher> {
- @KeepRecordOnSuccess
@ScreenRecord // b/322823478
@Test
public void testDevicePressMenu() throws Exception {
diff --git a/tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java b/tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java
index 69b42cb..b2e413d 100644
--- a/tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java
+++ b/tests/src/com/android/launcher3/ui/TaplWorkProfileTest.java
@@ -45,7 +45,6 @@
import com.android.launcher3.allapps.WorkProfileManager;
import com.android.launcher3.tapl.LauncherInstrumentation;
import com.android.launcher3.util.TestUtil;
-import com.android.launcher3.util.rule.ScreenRecordRule.KeepRecordOnSuccess;
import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
import com.android.launcher3.util.rule.TestStabilityRule.Stability;
@@ -196,7 +195,6 @@
}
- @KeepRecordOnSuccess
@ScreenRecord // b/322823478
@Test
public void testEdu() {
diff --git a/tests/src/com/android/launcher3/util/TestResourceHelper.kt b/tests/src/com/android/launcher3/util/TestResourceHelper.kt
deleted file mode 100644
index b4d3ba8..0000000
--- a/tests/src/com/android/launcher3/util/TestResourceHelper.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * 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
-
-import android.content.Context
-import android.content.res.TypedArray
-import android.util.AttributeSet
-import com.android.launcher3.R
-import com.android.launcher3.tests.R as TestR
-import kotlin.IntArray
-
-class TestResourceHelper(private val context: Context, specsFileId: Int) :
- ResourceHelper(context, specsFileId) {
- override fun obtainStyledAttributes(attrs: AttributeSet, styleId: IntArray): TypedArray {
- val clone =
- when {
- styleId.contentEquals(R.styleable.SizeSpec) -> TestR.styleable.SizeSpec
- styleId.contentEquals(R.styleable.WorkspaceSpec) -> TestR.styleable.WorkspaceSpec
- styleId.contentEquals(R.styleable.FolderSpec) -> TestR.styleable.FolderSpec
- styleId.contentEquals(R.styleable.AllAppsSpec) -> TestR.styleable.AllAppsSpec
- styleId.contentEquals(R.styleable.ResponsiveSpecGroup) ->
- TestR.styleable.ResponsiveSpecGroup
- else -> styleId.clone()
- }
-
- return context.obtainStyledAttributes(attrs, clone)
- }
-}
diff --git a/tests/src/com/android/launcher3/util/rule/ScreenRecordRule.java b/tests/src/com/android/launcher3/util/rule/ScreenRecordRule.java
index ff96afb..7a5cf2c 100644
--- a/tests/src/com/android/launcher3/util/rule/ScreenRecordRule.java
+++ b/tests/src/com/android/launcher3/util/rule/ScreenRecordRule.java
@@ -49,9 +49,6 @@
return base;
}
- final Boolean keepRecordOnSuccess = description.getAnnotation(KeepRecordOnSuccess.class)
- != null;
-
return new Statement() {
@Override
public void evaluate() throws Throwable {
@@ -73,7 +70,7 @@
device.executeShellCommand("kill -INT " + screenRecordPid);
Log.e(TAG, "Screenrecord captured at: " + outputFile);
output.close();
- if (success && !keepRecordOnSuccess) {
+ if (success) {
automation.executeShellCommand("rm " + outputFile);
}
}
@@ -88,14 +85,4 @@
@Target(ElementType.METHOD)
public @interface ScreenRecord {
}
-
-
- /**
- * Interface to indicate that we should keep the screen record even if the test succeeds, use
- * sparingly since it uses a lof of memory.
- */
- @Retention(RetentionPolicy.RUNTIME)
- @Target(ElementType.METHOD)
- public @interface KeepRecordOnSuccess {
- }
}
diff --git a/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java b/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java
index 4be46ab..c58d16e 100644
--- a/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java
+++ b/tests/tapl/com/android/launcher3/tapl/SearchResultFromQsb.java
@@ -17,10 +17,12 @@
import static com.android.launcher3.testing.shared.TestProtocol.NORMAL_STATE_ORDINAL;
-import android.text.TextUtils;
+import android.graphics.Rect;
import android.widget.TextView;
import androidx.test.uiautomator.By;
+import androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.Direction;
import androidx.test.uiautomator.UiObject2;
import java.util.ArrayList;
@@ -34,10 +36,12 @@
// This particular ID change should happen with caution
private static final String SEARCH_CONTAINER_RES_ID = "search_results_list_view";
protected final LauncherInstrumentation mLauncher;
+ private final UiObject2 mSearchContainer;
SearchResultFromQsb(LauncherInstrumentation launcher) {
mLauncher = launcher;
mLauncher.waitForLauncherObject("search_container_all_apps");
+ mSearchContainer = mLauncher.waitForSystemLauncherObject(SEARCH_CONTAINER_RES_ID);
}
/** Find the app from search results with app name. */
@@ -52,18 +56,9 @@
/** Find the web suggestion from search suggestion's title text */
public SearchWebSuggestion findWebSuggestion(String text) {
- ArrayList<UiObject2> webSuggestions =
- new ArrayList<>(mLauncher.waitForObjectsInContainer(
- mLauncher.waitForSystemLauncherObject(SEARCH_CONTAINER_RES_ID),
- By.clazz(TextView.class)));
- for (UiObject2 uiObject: webSuggestions) {
- String currentString = uiObject.getText();
- if (currentString.equals(text)) {
- return createWebSuggestion(uiObject);
- }
- }
- mLauncher.fail("Web suggestion title: " + text + " not found");
- return null;
+ UiObject2 webSuggestion = mLauncher.waitForObjectInContainer(mSearchContainer,
+ getSearchResultSelector(text));
+ return createWebSuggestion(webSuggestion);
}
protected SearchWebSuggestion createWebSuggestion(UiObject2 webSuggestion) {
@@ -73,13 +68,25 @@
/** Find the total amount of views being displayed and return the size */
public int getSearchResultItemSize() {
ArrayList<UiObject2> searchResultItems =
- new ArrayList<>(mLauncher.waitForObjectsInContainer(
- mLauncher.waitForSystemLauncherObject(SEARCH_CONTAINER_RES_ID),
+ new ArrayList<>(mLauncher.waitForObjectsInContainer(mSearchContainer,
By.clazz(TextView.class)));
return searchResultItems.size();
}
/**
+ * Scroll down to make next page search results rendered.
+ */
+ public void getNextPageSearchResults() {
+ final int searchContainerHeight = mLauncher.getVisibleBounds(mSearchContainer).height();
+ // Start scrolling from center of the screen to top of the screen.
+ mLauncher.scroll(mSearchContainer,
+ Direction.DOWN,
+ new Rect(0, 0, 0, searchContainerHeight / 2),
+ /* steps= */ 10,
+ /*slowDown= */ false);
+ }
+
+ /**
* Taps outside bottom sheet to dismiss and return to workspace. Available on tablets only.
* @param tapRight Tap on the right of bottom sheet if true, or left otherwise.
*/
@@ -121,25 +128,13 @@
/** Verify a tile is present by checking its title and subtitle. */
public void verifyTileIsPresent(String title, String subtitle) {
- ArrayList<UiObject2> searchResults =
- new ArrayList<>(mLauncher.waitForObjectsInContainer(
- mLauncher.waitForSystemLauncherObject(SEARCH_CONTAINER_RES_ID),
- By.clazz(TextView.class)));
- boolean foundTitle = false;
- boolean foundSubtitle = false;
- for (UiObject2 uiObject: searchResults) {
- String currentString = uiObject.getText();
- if (TextUtils.equals(currentString, title)) {
- foundTitle = true;
- } else if (TextUtils.equals(currentString, subtitle)) {
- foundSubtitle = true;
- }
- }
- if (!foundTitle) {
- mLauncher.fail("Tile not found for title: " + title);
- }
- if (!foundSubtitle) {
- mLauncher.fail("Tile not found for subtitle: " + subtitle);
- }
+ mLauncher.waitForObjectInContainer(mSearchContainer,
+ getSearchResultSelector(title));
+ mLauncher.waitForObjectInContainer(mSearchContainer,
+ getSearchResultSelector(subtitle));
+ }
+
+ private BySelector getSearchResultSelector(String text) {
+ return By.clazz(TextView.class).text(text);
}
}
diff --git a/tests/tapl/com/android/launcher3/tapl/Workspace.java b/tests/tapl/com/android/launcher3/tapl/Workspace.java
index 4fa93ef..9ac6768 100644
--- a/tests/tapl/com/android/launcher3/tapl/Workspace.java
+++ b/tests/tapl/com/android/launcher3/tapl/Workspace.java
@@ -444,6 +444,8 @@
Runnable expectLongClickEvents) {
try (LauncherInstrumentation.Closable c = launcher.addContextLayer(
"uninstalling app icon")) {
+
+ final String appNameToUninstall = homeAppIcon.getAppName();
dragIconToWorkspace(
launcher,
homeAppIcon,
@@ -468,7 +470,10 @@
try (LauncherInstrumentation.Closable c1 = launcher.addContextLayer(
"uninstalled app by dragging to the drop bar")) {
- return new Workspace(launcher);
+ final Workspace newWorkspace = new Workspace(launcher);
+ launcher.waitUntilLauncherObjectGone(
+ AppIcon.getAppIconSelector(appNameToUninstall));
+ return newWorkspace;
}
}
}