Snap for 12667701 from 15d689df48 to 25Q1-release

Change-Id: Ib4f2bf4328cbf5eb15e0168f9f634124acada522
This commit is contained in:
Android Build Coastguard Worker
2024-11-16 23:06:12 +00:00
26 changed files with 723 additions and 331 deletions
@@ -217,6 +217,7 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener
public static final int CONTENT_ALPHA_DURATION = 217;
public static final int TRANSIENT_TASKBAR_TRANSITION_DURATION = 417;
public static final int PINNED_TASKBAR_TRANSITION_DURATION = 600;
public static final int TASKBAR_TO_APP_DURATION = 600;
// TODO(b/236145847): Tune TASKBAR_TO_HOME_DURATION to 383 after conflict with unlock animation
// is solved.
@@ -1745,8 +1746,21 @@ public class QuickstepTransitionManager implements OnDeviceProfileChangeListener
return new AnimatorBackState(rectFSpringAnim, anim);
}
public static int getTaskbarToHomeDuration() {
if (enableScalingRevealHomeAnimation()) {
/** Get animation duration for taskbar for going to home. */
public static int getTaskbarToHomeDuration(boolean isPinnedTaskbar) {
return getTaskbarToHomeDuration(false, isPinnedTaskbar);
}
/**
* Get animation duration for taskbar for going to home.
*
* @param shouldOverrideToFastAnimation should overwrite scaling reveal home animation duration
*/
public static int getTaskbarToHomeDuration(boolean shouldOverrideToFastAnimation,
boolean isPinnedTaskbar) {
if (isPinnedTaskbar) {
return PINNED_TASKBAR_TRANSITION_DURATION;
} else if (enableScalingRevealHomeAnimation() && !shouldOverrideToFastAnimation) {
return TASKBAR_TO_HOME_DURATION_SLOW;
} else {
return TASKBAR_TO_HOME_DURATION_FAST;
@@ -488,6 +488,15 @@ public class DesktopVisibilityController {
}
});
}
public void onEnterDesktopModeTransitionStarted(int transitionDuration) {
}
@Override
public void onExitDesktopModeTransitionStarted(int transitionDuration) {
}
}
/** A listener for Taskbar in Desktop Mode. */
@@ -212,8 +212,12 @@ public class LauncherTaskbarUIController extends TaskbarUIController {
}
private int getTaskbarAnimationDuration(boolean isVisible) {
if (isVisible && !mLauncher.getPredictiveBackToHomeInProgress()) {
return getTaskbarToHomeDuration();
// fast animation duration since we will not be playing workspace reveal animation.
boolean shouldOverrideToFastAnimation =
!isHotseatIconOnTopWhenAligned() || mLauncher.getPredictiveBackToHomeInProgress();
boolean isPinnedTaskbar = DisplayController.isPinnedTaskbar(mLauncher);
if (isVisible || isPinnedTaskbar) {
return getTaskbarToHomeDuration(shouldOverrideToFastAnimation, isPinnedTaskbar);
} else {
return DisplayController.isTransientTaskbar(mLauncher)
? TRANSIENT_TASKBAR_TRANSITION_DURATION
@@ -17,6 +17,7 @@ package com.android.launcher3.taskbar;
import static com.android.app.animation.Interpolators.EMPHASIZED;
import static com.android.app.animation.Interpolators.FINAL_FRAME;
import static com.android.app.animation.Interpolators.INSTANT;
import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
import static com.android.launcher3.Hotseat.ALPHA_CHANNEL_TASKBAR_ALIGNMENT;
import static com.android.launcher3.Hotseat.ALPHA_CHANNEL_TASKBAR_STASH;
@@ -222,7 +223,9 @@ public class TaskbarLauncherStateController {
updateStateForFlag(FLAG_LAUNCHER_IN_STATE_TRANSITION, true);
if (!mShouldDelayLauncherStateAnim) {
if (toState == LauncherState.NORMAL) {
applyState(QuickstepTransitionManager.getTaskbarToHomeDuration());
applyState(QuickstepTransitionManager.getTaskbarToHomeDuration(
DisplayController.isPinnedTaskbar(
mControllers.taskbarActivityContext)));
} else {
applyState();
}
@@ -459,9 +462,12 @@ public class TaskbarLauncherStateController {
private Animator onStateChangeApplied(int changedFlags, long duration, boolean start) {
final boolean isInLauncher = isInLauncher();
final boolean isInOverview = mControllers.uiController.isInOverviewUi();
final boolean isIconAlignedWithHotseat = isIconAlignedWithHotseat();
final float toAlignment = isIconAlignedWithHotseat ? 1 : 0;
boolean handleOpenFloatingViews = false;
boolean isPinnedTaskbar = DisplayController.isPinnedTaskbar(
mControllers.taskbarActivityContext);
if (DEBUG) {
Log.d(TAG, "onStateChangeApplied - isInLauncher: " + isInLauncher
+ ", mLauncherState: " + mLauncherState
@@ -573,10 +579,17 @@ public class TaskbarLauncherStateController {
}
float backgroundAlpha = isInLauncher && isTaskbarAlignedWithHotseat() ? 0 : 1;
AnimatedFloat taskbarBgOffset =
mControllers.taskbarDragLayerController.getTaskbarBackgroundOffset();
boolean showTaskbar = !isInLauncher || isInOverview;
float taskbarBgOffsetEnd = showTaskbar ? 0f : 1f;
float taskbarBgOffsetStart = showTaskbar ? 1f : 0f;
// Don't animate if background has reached desired value.
if (mTaskbarBackgroundAlpha.isAnimating()
|| mTaskbarBackgroundAlpha.value != backgroundAlpha) {
|| mTaskbarBackgroundAlpha.value != backgroundAlpha
|| taskbarBgOffset.isAnimatingToValue(taskbarBgOffsetStart)
|| taskbarBgOffset.value != taskbarBgOffsetEnd) {
mTaskbarBackgroundAlpha.cancelAnimation();
if (DEBUG) {
Log.d(TAG, "onStateChangeApplied - taskbarBackgroundAlpha - "
@@ -587,25 +600,35 @@ public class TaskbarLauncherStateController {
boolean isInLauncherIconNotAligned = isInLauncher && !isIconAlignedWithHotseat;
boolean notInLauncherIconNotAligned = !isInLauncher && !isIconAlignedWithHotseat;
boolean isInLauncherIconIsAligned = isInLauncher && isIconAlignedWithHotseat;
// When Hotseat icons are not on top don't change duration or add start delay.
// This will keep the duration in sync for icon alignment and background fade in/out.
// For example, launching app from launcher all apps.
boolean isHotseatIconOnTopWhenAligned =
mControllers.uiController.isHotseatIconOnTopWhenAligned();
float startDelay = 0;
// We want to delay the background from fading in so that the icons have time to move
// into the bounds of the background before it appears.
if (isInLauncherIconNotAligned) {
startDelay = duration * TASKBAR_BG_ALPHA_LAUNCHER_NOT_ALIGNED_DELAY_MULT;
} else if (notInLauncherIconNotAligned) {
} else if (notInLauncherIconNotAligned && isHotseatIconOnTopWhenAligned) {
startDelay = duration * TASKBAR_BG_ALPHA_NOT_LAUNCHER_NOT_ALIGNED_DELAY_MULT;
}
float newDuration = duration - startDelay;
if (isInLauncherIconIsAligned) {
if (isInLauncherIconIsAligned && isHotseatIconOnTopWhenAligned) {
// Make the background fade out faster so that it is gone by the time the
// icons move outside of the bounds of the background.
newDuration = duration * TASKBAR_BG_ALPHA_LAUNCHER_IS_ALIGNED_DURATION_MULT;
}
Animator taskbarBackgroundAlpha = mTaskbarBackgroundAlpha
.animateToValue(backgroundAlpha)
.setDuration((long) newDuration);
taskbarBackgroundAlpha.setStartDelay((long) startDelay);
Animator taskbarBackgroundAlpha = mTaskbarBackgroundAlpha.animateToValue(
backgroundAlpha);
if (isPinnedTaskbar) {
setupPinnedTaskbarAnimation(animatorSet, showTaskbar, taskbarBgOffset,
taskbarBgOffsetStart, taskbarBgOffsetEnd, duration, taskbarBackgroundAlpha);
} else {
taskbarBackgroundAlpha.setDuration((long) newDuration);
taskbarBackgroundAlpha.setStartDelay((long) startDelay);
}
animatorSet.play(taskbarBackgroundAlpha);
}
@@ -671,15 +694,18 @@ public class TaskbarLauncherStateController {
+ mIconAlignment.value
+ " -> " + toAlignment + ": " + duration);
}
if (hasAnyFlag(FLAG_TASKBAR_HIDDEN)) {
iconAlignAnim.setInterpolator(FINAL_FRAME);
} else {
animatorSet.play(iconAlignAnim);
if (!isPinnedTaskbar) {
if (hasAnyFlag(FLAG_TASKBAR_HIDDEN)) {
iconAlignAnim.setInterpolator(FINAL_FRAME);
} else {
animatorSet.play(iconAlignAnim);
}
}
}
Interpolator interpolator = enableScalingRevealHomeAnimation()
Interpolator interpolator = enableScalingRevealHomeAnimation() && !isPinnedTaskbar
? ScalingWorkspaceRevealAnim.SCALE_INTERPOLATOR : EMPHASIZED;
animatorSet.setInterpolator(interpolator);
if (start) {
@@ -688,6 +714,49 @@ public class TaskbarLauncherStateController {
return animatorSet;
}
private void setupPinnedTaskbarAnimation(AnimatorSet animatorSet, boolean showTaskbar,
AnimatedFloat taskbarBgOffset, float taskbarBgOffsetStart, float taskbarBgOffsetEnd,
long duration, Animator taskbarBackgroundAlpha) {
float targetAlpha = !showTaskbar ? 1 : 0;
mLauncher.getHotseat().setIconsAlpha(targetAlpha, ALPHA_CHANNEL_TASKBAR_ALIGNMENT);
if (mIsQsbInline) {
mLauncher.getHotseat().setQsbAlpha(targetAlpha,
ALPHA_CHANNEL_TASKBAR_ALIGNMENT);
}
if ((taskbarBgOffset.value != taskbarBgOffsetEnd && !taskbarBgOffset.isAnimating())
|| taskbarBgOffset.isAnimatingToValue(taskbarBgOffsetStart)) {
taskbarBgOffset.cancelAnimation();
Animator taskbarIconAlpha = mTaskbarAlphaForHome.animateToValue(
showTaskbar ? 1f : 0f);
AnimatedFloat taskbarIconTranslationYForHome =
mControllers.taskbarViewController.mTaskbarIconTranslationYForHome;
ObjectAnimator taskbarBackgroundOffset = taskbarBgOffset.animateToValue(
taskbarBgOffsetStart,
taskbarBgOffsetEnd);
ObjectAnimator taskbarIconsYTranslation = null;
float taskbarHeight =
mControllers.taskbarActivityContext.getDeviceProfile().taskbarHeight;
if (showTaskbar) {
taskbarIconsYTranslation = taskbarIconTranslationYForHome.animateToValue(
taskbarHeight, 0);
} else {
taskbarIconsYTranslation = taskbarIconTranslationYForHome.animateToValue(0,
taskbarHeight);
}
taskbarIconAlpha.setDuration(duration);
taskbarIconsYTranslation.setDuration(duration);
taskbarBackgroundOffset.setDuration(duration);
animatorSet.play(taskbarIconAlpha);
animatorSet.play(taskbarIconsYTranslation);
animatorSet.play(taskbarBackgroundOffset);
}
taskbarBackgroundAlpha.setInterpolator(showTaskbar ? INSTANT : FINAL_FRAME);
taskbarBackgroundAlpha.setDuration(duration);
}
/**
* Whether the taskbar is aligned with the hotseat in the current/target launcher state.
*
@@ -940,7 +1009,12 @@ public class TaskbarLauncherStateController {
@Override
public void onRecentsAnimationFinished(RecentsAnimationController controller) {
endGestureStateOverride(!controller.getFinishTargetIsLauncher(), false /*canceled*/);
endGestureStateOverride(!controller.getFinishTargetIsLauncher(),
controller.getLauncherIsVisibleAtFinish(), false /*canceled*/);
}
private void endGestureStateOverride(boolean finishedToApp, boolean canceled) {
endGestureStateOverride(finishedToApp, finishedToApp, canceled);
}
/**
@@ -950,10 +1024,13 @@ public class TaskbarLauncherStateController {
*
* @param finishedToApp {@code true} if the recents animation finished to showing an app and
* not workspace or overview
* @param canceled {@code true} if the recents animation was canceled instead of finishing
* to completion
* @param launcherIsVisible {code true} if launcher is visible at finish
* @param canceled {@code true} if the recents animation was canceled instead of
* finishing
* to completion
*/
private void endGestureStateOverride(boolean finishedToApp, boolean canceled) {
private void endGestureStateOverride(boolean finishedToApp, boolean launcherIsVisible,
boolean canceled) {
mCallbacks.removeListener(this);
mTaskBarRecentsAnimationListener = null;
((RecentsView) mLauncher.getOverviewPanel()).setTaskLaunchListener(null);
@@ -962,17 +1039,27 @@ public class TaskbarLauncherStateController {
mSkipNextRecentsAnimEnd = false;
return;
}
updateStateForUserFinishedToApp(finishedToApp);
updateStateForUserFinishedToApp(finishedToApp, launcherIsVisible);
}
}
/**
* Updates the visible state immediately to ensure a seamless handoff.
* @param finishedToApp True iff user is in an app.
* @see #updateStateForUserFinishedToApp(boolean, boolean)
*/
private void updateStateForUserFinishedToApp(boolean finishedToApp) {
updateStateForUserFinishedToApp(finishedToApp, !finishedToApp);
}
/**
* Updates the visible state immediately to ensure a seamless handoff.
*
* @param finishedToApp True iff user is in an app.
* @param launcherIsVisible True iff launcher is still visible (ie. transparent app)
*/
private void updateStateForUserFinishedToApp(boolean finishedToApp,
boolean launcherIsVisible) {
// Update the visible state immediately to ensure a seamless handoff
boolean launcherVisible = !finishedToApp;
boolean launcherVisible = !finishedToApp || launcherIsVisible;
updateStateForFlag(FLAG_TRANSITION_TO_VISIBLE, false);
updateStateForFlag(FLAG_VISIBLE, launcherVisible);
applyState();
@@ -981,7 +1068,7 @@ public class TaskbarLauncherStateController {
if (DEBUG) {
Log.d(TAG, "endGestureStateOverride - FLAG_IN_APP: " + finishedToApp);
}
controller.updateStateForFlag(FLAG_IN_APP, finishedToApp);
controller.updateStateForFlag(FLAG_IN_APP, finishedToApp && !launcherIsVisible);
controller.applyState();
}
@@ -23,6 +23,7 @@ import static com.android.app.animation.Interpolators.INSTANT;
import static com.android.app.animation.Interpolators.LINEAR;
import static com.android.internal.jank.InteractionJankMonitor.Configuration;
import static com.android.launcher3.Flags.enableScalingRevealHomeAnimation;
import static com.android.launcher3.QuickstepTransitionManager.PINNED_TASKBAR_TRANSITION_DURATION;
import static com.android.launcher3.config.FeatureFlags.ENABLE_TASKBAR_NAVBAR_UNIFICATION;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TRANSIENT_TASKBAR_HIDE;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TRANSIENT_TASKBAR_SHOW;
@@ -398,6 +399,9 @@ public class TaskbarStashController implements TaskbarControllers.LoggableTaskba
* Returns how long the stash/unstash animation should play.
*/
public long getStashDuration() {
if (DisplayController.isPinnedTaskbar(mActivity)) {
return PINNED_TASKBAR_TRANSITION_DURATION;
}
return DisplayController.isTransientTaskbar(mActivity)
? TRANSIENT_TASKBAR_STASH_DURATION
: TASKBAR_STASH_DURATION;
@@ -120,7 +120,7 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar
private final TaskbarView mTaskbarView;
private final MultiValueAlpha mTaskbarIconAlpha;
private final AnimatedFloat mTaskbarIconScaleForStash = new AnimatedFloat(this::updateScale);
private final AnimatedFloat mTaskbarIconTranslationYForHome = new AnimatedFloat(
public final AnimatedFloat mTaskbarIconTranslationYForHome = new AnimatedFloat(
this::updateTranslationY);
private final AnimatedFloat mTaskbarIconTranslationYForStash = new AnimatedFloat(
this::updateTranslationY);
@@ -796,6 +796,8 @@ public class TaskbarViewController implements TaskbarControllers.LoggableTaskbar
*/
private AnimatorPlaybackController createIconAlignmentController(DeviceProfile launcherDp) {
PendingAnimation setter = new PendingAnimation(100);
// icon alignment not needed for pinned taskbar.
if (DisplayController.isPinnedTaskbar(mActivity)) return setter.createPlaybackController();
mOnControllerPreCreateCallback.run();
DeviceProfile taskbarDp = mActivity.getDeviceProfile();
Rect hotseatPadding = launcherDp.getHotseatLayoutPadding(mActivity);
@@ -95,6 +95,7 @@ public class QuickstepAtomicAnimationFactory extends
public void prepareForAtomicAnimation(LauncherState fromState, LauncherState toState,
StateAnimationConfig config) {
RecentsView overview = mContainer.getOverviewPanel();
boolean isPinnedTaskbar = DisplayController.isPinnedTaskbar(mContainer);
if ((fromState == OVERVIEW || fromState == OVERVIEW_SPLIT_SELECT) && toState == NORMAL) {
overview.switchToScreenshot(() ->
overview.finishRecentsAnimation(true /* toRecents */, null));
@@ -109,7 +110,8 @@ public class QuickstepAtomicAnimationFactory extends
// We sync the scrim fade with the taskbar animation duration to avoid any flickers for
// taskbar icons disappearing before hotseat icons show up.
float scrimUpperBoundFromSplit =
QuickstepTransitionManager.getTaskbarToHomeDuration() / (float) config.duration;
QuickstepTransitionManager.getTaskbarToHomeDuration(isPinnedTaskbar)
/ (float) config.duration;
scrimUpperBoundFromSplit = Math.min(scrimUpperBoundFromSplit, 1f);
config.setInterpolator(ANIM_OVERVIEW_ACTIONS_FADE, clampToProgress(LINEAR, 0, 0.25f));
config.setInterpolator(ANIM_SCRIM_FADE,
@@ -139,7 +141,8 @@ public class QuickstepAtomicAnimationFactory extends
// Sync scroll so that it ends before or at the same time as the taskbar animation.
if (mContainer.getDeviceProfile().isTaskbarPresent) {
config.duration = Math.min(
config.duration, QuickstepTransitionManager.getTaskbarToHomeDuration());
config.duration,
QuickstepTransitionManager.getTaskbarToHomeDuration(isPinnedTaskbar));
}
overview.snapToPage(DEFAULT_PAGE, Math.toIntExact(config.duration));
} else {
@@ -100,6 +100,7 @@ import com.android.internal.jank.Cuj;
import com.android.internal.util.LatencyTracker;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.QuickstepTransitionManager;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.anim.AnimationSuccessListener;
@@ -1373,8 +1374,9 @@ public abstract class AbsSwipeUpHandler<
mInputConsumerProxy.enable();
}
if (endTarget == HOME) {
boolean isPinnedTaskbar = DisplayController.isPinnedTaskbar(mContext);
duration = mContainer != null && mContainer.getDeviceProfile().isTaskbarPresent
? StaggeredWorkspaceAnim.DURATION_TASKBAR_MS
? QuickstepTransitionManager.getTaskbarToHomeDuration(isPinnedTaskbar)
: StaggeredWorkspaceAnim.DURATION_MS;
ContextualEduStatsManager.INSTANCE.get(mContext).updateEduStats(
mGestureState.isTrackpadGesture(), GestureType.HOME);
@@ -53,6 +53,8 @@ public class RecentsAnimationController {
private boolean mFinishRequested = false;
// Only valid when mFinishRequested == true.
private boolean mFinishTargetIsLauncher;
// Only valid when mFinishRequested == true
private boolean mLauncherIsVisibleAtFinish;
private RunnableList mPendingFinishCallbacks = new RunnableList();
public RecentsAnimationController(RecentsAnimationControllerCompat controller,
@@ -116,14 +118,28 @@ public class RecentsAnimationController {
finishController(toRecents, onFinishComplete, sendUserLeaveHint);
}
@UiThread
public void finish(boolean toRecents, boolean launcherIsVisibleAtFinish,
Runnable onFinishComplete, boolean sendUserLeaveHint) {
Preconditions.assertUIThread();
finishController(toRecents, launcherIsVisibleAtFinish, onFinishComplete, sendUserLeaveHint,
false);
}
@UiThread
public void finishController(boolean toRecents, Runnable callback, boolean sendUserLeaveHint) {
finishController(toRecents, callback, sendUserLeaveHint, false /* forceFinish */);
finishController(toRecents, false, callback, sendUserLeaveHint, false /* forceFinish */);
}
@UiThread
public void finishController(boolean toRecents, Runnable callback, boolean sendUserLeaveHint,
boolean forceFinish) {
finishController(toRecents, toRecents, callback, sendUserLeaveHint, forceFinish);
}
@UiThread
public void finishController(boolean toRecents, boolean launcherIsVisibleAtFinish,
Runnable callback, boolean sendUserLeaveHint, boolean forceFinish) {
mPendingFinishCallbacks.add(callback);
if (!forceFinish && mFinishRequested) {
// If finish has already been requested, then add the callback to the pending list.
@@ -135,6 +151,7 @@ public class RecentsAnimationController {
// Finish not yet requested
mFinishRequested = true;
mFinishTargetIsLauncher = toRecents;
mLauncherIsVisibleAtFinish = launcherIsVisibleAtFinish;
mOnFinishedListener.accept(this);
Runnable finishCb = () -> {
mController.finish(toRecents, sendUserLeaveHint, new IResultReceiver.Stub() {
@@ -211,6 +228,14 @@ public class RecentsAnimationController {
return mFinishTargetIsLauncher;
}
/**
* RecentsAnimationListeners can check this in onRecentsAnimationFinished() to determine whether
* the animation was finished to launcher vs an app.
*/
public boolean getLauncherIsVisibleAtFinish() {
return mLauncherIsVisibleAtFinish;
}
public void dump(String prefix, PrintWriter pw) {
pw.println(prefix + "RecentsAnimationController:");
@@ -49,6 +49,7 @@ import com.android.launcher3.celllayout.CellLayoutLayoutParams;
import com.android.launcher3.statehandlers.DepthController;
import com.android.launcher3.states.StateAnimationConfig;
import com.android.launcher3.uioverrides.QuickstepLauncher;
import com.android.launcher3.util.DisplayController;
import com.android.launcher3.util.DynamicResource;
import com.android.quickstep.views.RecentsView;
import com.android.systemui.plugins.ResourceProvider;
@@ -63,8 +64,7 @@ public class StaggeredWorkspaceAnim {
private static final int APP_CLOSE_ROW_START_DELAY_MS = 10;
// Should be used for animations running alongside this StaggeredWorkspaceAnim.
public static final int DURATION_MS = 250;
public static final int DURATION_TASKBAR_MS =
QuickstepTransitionManager.getTaskbarToHomeDuration();
private final int mTaskbarDurationInMs;
private static final float MAX_VELOCITY_PX_PER_S = 22f;
@@ -81,6 +81,8 @@ public class StaggeredWorkspaceAnim {
public StaggeredWorkspaceAnim(QuickstepLauncher launcher, float velocity,
boolean animateOverviewScrim, @Nullable View ignoredView, boolean staggerWorkspace) {
mTaskbarDurationInMs = QuickstepTransitionManager.getTaskbarToHomeDuration(
DisplayController.isPinnedTaskbar(launcher));
prepareToAnimate(launcher, animateOverviewScrim);
mIgnoredView = ignoredView;
@@ -93,7 +95,7 @@ public class StaggeredWorkspaceAnim {
.getDimensionPixelSize(R.dimen.swipe_up_max_workspace_trans_y);
DeviceProfile grid = launcher.getDeviceProfile();
long duration = grid.isTaskbarPresent ? DURATION_TASKBAR_MS : DURATION_MS;
long duration = grid.isTaskbarPresent ? mTaskbarDurationInMs : DURATION_MS;
if (staggerWorkspace) {
Workspace<?> workspace = launcher.getWorkspace();
Hotseat hotseat = launcher.getHotseat();
@@ -5854,15 +5854,22 @@ public abstract class RecentsView<
* Finish recents animation.
*/
public void finishRecentsAnimation(boolean toRecents, @Nullable Runnable onFinishComplete) {
finishRecentsAnimation(toRecents, true /* shouldPip */, onFinishComplete);
finishRecentsAnimation(toRecents, false, true /* shouldPip */, onFinishComplete);
}
/**
* Finish recents animation.
*/
public void finishRecentsAnimation(boolean toRecents, boolean shouldPip,
@Nullable Runnable onFinishComplete) {
finishRecentsAnimation(toRecents, shouldPip, false, onFinishComplete);
}
/**
* NOTE: Whatever value gets passed through to the toRecents param may need to also be set on
* {@link #mRecentsAnimationController#setWillFinishToHome}.
*/
public void finishRecentsAnimation(boolean toRecents, boolean shouldPip,
@Nullable Runnable onFinishComplete) {
boolean allAppTargetsAreTranslucent, @Nullable Runnable onFinishComplete) {
Log.d(TAG, "finishRecentsAnimation - mRecentsAnimationController: "
+ mRecentsAnimationController);
// TODO(b/197232424#comment#10) Move this back into onRecentsAnimationComplete(). Maybe?
@@ -5894,7 +5901,7 @@ public abstract class RecentsView<
tx, null /* overlay */);
}
}
mRecentsAnimationController.finish(toRecents, () -> {
mRecentsAnimationController.finish(toRecents, allAppTargetsAreTranslucent, () -> {
if (onFinishComplete != null) {
onFinishComplete.run();
}
@@ -21,6 +21,7 @@ import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.launcher3.LauncherPrefs.Companion.TASKBAR_PINNING
import com.android.launcher3.QuickstepTransitionManager.PINNED_TASKBAR_TRANSITION_DURATION
import com.android.launcher3.R
import com.android.launcher3.taskbar.StashedHandleViewController.ALPHA_INDEX_STASHED
import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_EDU_OPEN
@@ -158,7 +159,7 @@ class TaskbarStashControllerTest {
@Test
@TaskbarMode(PINNED)
fun testGetStashDuration_pinnedMode() {
assertThat(stashController.stashDuration).isEqualTo(TASKBAR_STASH_DURATION)
assertThat(stashController.stashDuration).isEqualTo(PINNED_TASKBAR_TRANSITION_DURATION)
}
@Test
@@ -17,12 +17,14 @@
package com.android.launcher3.model;
import static com.android.launcher3.Flags.enableSmartspaceRemovalToggle;
import static com.android.launcher3.Flags.oneGridSpecs;
import static com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME;
import static com.android.launcher3.LauncherSettings.Favorites.TMP_TABLE;
import static com.android.launcher3.Utilities.SHOULD_SHOW_FIRST_PAGE_WIDGET;
import static com.android.launcher3.model.LoaderTask.SMARTSPACE_ON_HOME_SCREEN;
import static com.android.launcher3.provider.LauncherDbUtils.copyTable;
import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
import static com.android.launcher3.provider.LauncherDbUtils.shiftTableByXCells;
import android.content.ComponentName;
import android.content.ContentValues;
@@ -130,6 +132,20 @@ public class GridSizeMigrationDBController {
// Only use this strategy when comparing the previous grid to the new grid and the
// columns are the same and the destination has more rows
copyTable(source, TABLE_NAME, target.getWritableDatabase(), TABLE_NAME, context);
if (oneGridSpecs()) {
DbReader destReader = new DbReader(
target.getWritableDatabase(), TABLE_NAME, context);
boolean shouldShiftCells = shouldShiftCells(destReader, srcDeviceState.getRows());
if (shouldShiftCells) {
shiftTableByXCells(
target.getWritableDatabase(),
(destDeviceState.getRows() - srcDeviceState.getRows()),
TABLE_NAME);
}
}
// Save current configuration, so that the migration does not run again.
destDeviceState.writeToPrefs(context);
return true;
}
@@ -427,17 +443,22 @@ public class GridSizeMigrationDBController {
}
}
static void copyCurrentGridToNewGrid(
@NonNull Context context,
@NonNull DeviceGridState destDeviceState,
@NonNull DatabaseHelper target,
@NonNull SQLiteDatabase source) {
// Only use this strategy when comparing the previous grid to the new grid and the
// columns are the same and the destination has more rows
copyTable(source, TABLE_NAME, target.getWritableDatabase(), TABLE_NAME, context);
destDeviceState.writeToPrefs(context);
private static boolean shouldShiftCells(final DbReader destReader, final int srcGridRowCount) {
List<DbEntry> workspaceItems = destReader.loadAllWorkspaceEntries();
int firstPageItemsRowPosSum = workspaceItems.stream()
.filter(entry -> entry.screenId == 0)
.mapToInt(entry -> entry.cellY).sum();
int firstPageWorkspaceItemsCount = (int) workspaceItems.stream()
.filter(entry -> entry.screenId == 0).count();
if (firstPageWorkspaceItemsCount == 0) {
return false;
}
float srcGridMidPoint = srcGridRowCount / 2f;
float firstPageItemPosAvg = (float) firstPageItemsRowPosSum / firstPageWorkspaceItemsCount;
return (firstPageItemPosAvg >= srcGridMidPoint);
}
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static class DbReader {
@@ -21,15 +21,20 @@ import android.graphics.Point
import android.util.Log
import androidx.annotation.VisibleForTesting
import com.android.launcher3.Flags
import com.android.launcher3.Flags.oneGridSpecs
import com.android.launcher3.LauncherPrefs
import com.android.launcher3.LauncherPrefs.Companion.get
import com.android.launcher3.LauncherPrefs.Companion.getPrefs
import com.android.launcher3.LauncherSettings
import com.android.launcher3.LauncherSettings.Favorites.TABLE_NAME
import com.android.launcher3.LauncherSettings.Favorites.TMP_TABLE
import com.android.launcher3.Utilities
import com.android.launcher3.config.FeatureFlags
import com.android.launcher3.model.GridSizeMigrationDBController.DbReader
import com.android.launcher3.provider.LauncherDbUtils
import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction
import com.android.launcher3.provider.LauncherDbUtils.copyTable
import com.android.launcher3.provider.LauncherDbUtils.dropTable
import com.android.launcher3.provider.LauncherDbUtils.shiftTableByXCells
import com.android.launcher3.util.CellAndSpan
import com.android.launcher3.util.GridOccupancy
import com.android.launcher3.util.IntArray
@@ -59,27 +64,30 @@ class GridSizeMigrationLogic {
// amount of rows we simply copy over the source grid to the destination grid, rather
// than undergoing the general grid migration.
if (shouldMigrateToStrictlyTallerGrid(isDestNewDb, srcDeviceState, destDeviceState)) {
GridSizeMigrationDBController.copyCurrentGridToNewGrid(
context,
destDeviceState,
target,
source,
)
copyTable(source, TABLE_NAME, target.writableDatabase, TABLE_NAME, context)
if (oneGridSpecs()) {
val destReader = DbReader(target.writableDatabase, TABLE_NAME, context)
val shouldShiftCells = shouldShiftCells(destReader, srcDeviceState.rows)
if (shouldShiftCells) {
shiftTableByXCells(
target.writableDatabase,
(destDeviceState.rows - srcDeviceState.rows),
TABLE_NAME,
)
}
}
// Save current configuration, so that the migration does not run again.
destDeviceState.writeToPrefs(context)
return
}
LauncherDbUtils.copyTable(
source,
LauncherSettings.Favorites.TABLE_NAME,
target.writableDatabase,
LauncherSettings.Favorites.TMP_TABLE,
context,
)
copyTable(source, TABLE_NAME, target.writableDatabase, TMP_TABLE, context)
val migrationStartTime = System.currentTimeMillis()
try {
SQLiteTransaction(target.writableDatabase).use { t ->
val srcReader = DbReader(t.db, LauncherSettings.Favorites.TMP_TABLE, context)
val destReader = DbReader(t.db, LauncherSettings.Favorites.TABLE_NAME, context)
val srcReader = DbReader(t.db, TMP_TABLE, context)
val destReader = DbReader(t.db, TABLE_NAME, context)
val targetSize = Point(destDeviceState.columns, destDeviceState.rows)
@@ -95,7 +103,7 @@ class GridSizeMigrationLogic {
// Migrate workspace.
migrateWorkspace(srcReader, destReader, target, targetSize, idsInUse)
LauncherDbUtils.dropTable(t.db, LauncherSettings.Favorites.TMP_TABLE)
dropTable(t.db, TMP_TABLE)
t.commit()
}
} catch (e: Exception) {
@@ -112,6 +120,19 @@ class GridSizeMigrationLogic {
}
}
private fun shouldShiftCells(destReader: DbReader, srcGridRowCount: Int): Boolean {
val workspaceItems = destReader.loadAllWorkspaceEntries()
val firstPageItemsRowPosSum =
workspaceItems.sumOf { entry -> if (entry.screenId == 0) entry.cellY else 0 }
val firstPageWorkspaceItemsCount = workspaceItems.count { entry -> entry.screenId == 0 }
if (firstPageWorkspaceItemsCount == 0) {
return false
}
val srcGridMidPoint = srcGridRowCount / 2f
val firstPageItemPosAvg = firstPageItemsRowPosSum / firstPageWorkspaceItemsCount.toFloat()
return (firstPageItemPosAvg >= srcGridMidPoint)
}
/** Handles hotseat migration. */
@VisibleForTesting
fun migrateHotseat(
@@ -131,6 +131,11 @@ object LauncherDbUtils {
}
}
@JvmStatic
fun shiftTableByXCells(db: SQLiteDatabase, x: Int, toTable: String) {
db.run { execSQL("UPDATE $toTable SET cellY = cellY + $x") }
}
/**
* Migrates the legacy shortcuts to deep shortcuts pinned under Launcher. Removes any invalid
* shortcut or any shortcut which requires some permission to launch
@@ -18,18 +18,23 @@ package com.android.launcher3.recyclerview
import android.content.Context
import android.util.Log
import android.view.InflateException
import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.Companion.PROTECTED
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.android.launcher3.BubbleTextView
import com.android.launcher3.BuildConfig
import com.android.launcher3.allapps.BaseAllAppsAdapter
import com.android.launcher3.config.FeatureFlags
import com.android.launcher3.util.CancellableTask
import com.android.launcher3.util.Executors.MAIN_EXECUTOR
import com.android.launcher3.util.Executors.VIEW_PREINFLATION_EXECUTOR
import com.android.launcher3.util.Themes
import com.android.launcher3.views.ActivityContext
import com.android.launcher3.views.ActivityContext.ActivityContextDelegate
import java.lang.IllegalStateException
const val PREINFLATE_ICONS_ROW_COUNT = 4
const val EXTRA_ICONS_COUNT = 2
@@ -39,10 +44,11 @@ const val EXTRA_ICONS_COUNT = 2
* [RecyclerView]. The view inflation will happen on background thread and inflated [ViewHolder]s
* will be added to [RecycledViewPool] on main thread.
*/
class AllAppsRecyclerViewPool<T> : RecycledViewPool() {
class AllAppsRecyclerViewPool<T> : RecycledViewPool() where T : Context, T : ActivityContext {
var hasWorkProfile = false
private var mCancellableTask: CancellableTask<List<ViewHolder>>? = null
@VisibleForTesting(otherwise = PROTECTED)
var mCancellableTask: CancellableTask<List<ViewHolder>>? = null
companion object {
private const val TAG = "AllAppsRecyclerViewPool"
@@ -53,7 +59,7 @@ class AllAppsRecyclerViewPool<T> : RecycledViewPool() {
/**
* Preinflate app icons. If all apps RV cannot be scrolled down, we don't need to preinflate.
*/
fun <T> preInflateAllAppsViewHolders(context: T) where T : Context, T : ActivityContext {
fun preInflateAllAppsViewHolders(context: T) {
val appsView = context.appsView ?: return
val activeRv: RecyclerView = appsView.activeRecyclerView ?: return
val preInflateCount = getPreinflateCount(context)
@@ -97,36 +103,65 @@ class AllAppsRecyclerViewPool<T> : RecycledViewPool() {
override fun getLayoutManager(): RecyclerView.LayoutManager? = null
}
preInflateAllAppsViewHolders(
adapter,
BaseAllAppsAdapter.VIEW_TYPE_ICON,
activeRv,
preInflateCount,
) {
getPreinflateCount(context)
}
}
@VisibleForTesting(otherwise = PROTECTED)
fun preInflateAllAppsViewHolders(
adapter: RecyclerView.Adapter<*>,
viewType: Int,
activeRv: RecyclerView,
preInflationCount: Int,
preInflationCountProvider: () -> Int,
) {
if (preInflationCount <= 0) {
return
}
mCancellableTask?.cancel()
var task: CancellableTask<List<ViewHolder>>? = null
task =
CancellableTask(
{
val list: ArrayList<ViewHolder> = ArrayList()
for (i in 0 until preInflateCount) {
for (i in 0 until preInflationCount) {
if (task?.canceled == true) {
break
}
// If activeRv's layout manager has been reset to null on main thread, skip
// the preinflation as we cannot generate correct LayoutParams
if (activeRv.layoutManager == null) {
list.clear()
break
}
try {
list.add(adapter.createViewHolder(activeRv, viewType))
} catch (e: InflateException) {
list.clear()
// It's still possible for UI thread to set activeRv's layout manager to
// null and we should break the loop and cancel the preinflation.
break
}
list.add(
adapter.createViewHolder(activeRv, BaseAllAppsAdapter.VIEW_TYPE_ICON)
)
}
list
},
MAIN_EXECUTOR,
{ viewHolders ->
for (i in 0 until minOf(viewHolders.size, getPreinflateCount(context))) {
// Run preInflationCountProvider again as the needed VH might have changed
val newPreInflationCount = preInflationCountProvider.invoke()
for (i in 0 until minOf(viewHolders.size, newPreInflationCount)) {
putRecycledView(viewHolders[i])
}
},
)
mCancellableTask = task
VIEW_PREINFLATION_EXECUTOR.submit(mCancellableTask)
VIEW_PREINFLATION_EXECUTOR.execute(mCancellableTask)
}
/**
@@ -143,10 +178,11 @@ class AllAppsRecyclerViewPool<T> : RecycledViewPool() {
* app icons plus [EXTRA_ICONS_COUNT] is the magic minimal count of app icons to preinflate to
* suffice fast scrolling.
*
* Note that we need to preinfate extra app icons in size of one all apps pages, so that opening
* all apps don't need to inflate app icons.
* Note that if [FeatureFlags.ALL_APPS_GONE_VISIBILITY] is enabled, we need to preinfate extra
* app icons in size of one all apps pages, so that opening all apps don't need to inflate app
* icons.
*/
fun <T> getPreinflateCount(context: T): Int where T : Context, T : ActivityContext {
fun getPreinflateCount(context: T): Int {
var targetPreinflateCount =
PREINFLATE_ICONS_ROW_COUNT * context.deviceProfile.numShownAllAppsColumns +
EXTRA_ICONS_COUNT
-1
View File
@@ -63,7 +63,6 @@ filegroup {
"src/com/android/launcher3/dragging/TaplDragTest.java",
"src/com/android/launcher3/dragging/TaplUninstallRemoveTest.java",
"src/com/android/launcher3/ui/TaplTestsLauncher3Test.java",
"src/com/android/launcher3/ui/widget/TaplWidgetPickerTest.java",
"src/com/android/launcher3/ui/workspace/TaplWorkspaceTest.java",
],
}
@@ -0,0 +1,120 @@
/*
* 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.recyclerview
import android.content.Context
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.LayoutManager
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.launcher3.util.Executors
import com.android.launcher3.views.ActivityContext
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.Mock
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidJUnit4::class)
class AllAppsRecyclerViewPoolTest<T> where T : Context, T : ActivityContext {
private lateinit var underTest: AllAppsRecyclerViewPool<T>
private lateinit var adapter: RecyclerView.Adapter<*>
@Mock private lateinit var parent: RecyclerView
@Mock private lateinit var itemView: View
@Mock private lateinit var layoutManager: LayoutManager
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
underTest = spy(AllAppsRecyclerViewPool())
adapter =
object : RecyclerView.Adapter<ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
object : ViewHolder(itemView) {}
override fun getItemCount() = 0
override fun onBindViewHolder(holder: ViewHolder, position: Int) {}
}
underTest.setMaxRecycledViews(VIEW_TYPE, 20)
`when`(parent.layoutManager).thenReturn(layoutManager)
}
@Test
fun preinflate_success() {
underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 10) { 10 }
awaitTasksCompleted()
assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(10)
}
@Test
fun preinflate_not_triggered() {
underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 0) { 0 }
awaitTasksCompleted()
assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
}
@Test
fun preinflate_cancel_before_runOnMainThread() {
underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 10) { 10 }
assertThat(underTest.mCancellableTask!!.canceled).isFalse()
underTest.clear()
awaitTasksCompleted()
verify(underTest, never()).putRecycledView(any(ViewHolder::class.java))
assertThat(underTest.mCancellableTask!!.canceled).isTrue()
assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
}
@Test
fun preinflate_cancel_after_run() {
underTest.preInflateAllAppsViewHolders(adapter, VIEW_TYPE, parent, 10) { 10 }
assertThat(underTest.mCancellableTask!!.canceled).isFalse()
awaitTasksCompleted()
underTest.clear()
verify(underTest, times(10)).putRecycledView(any(ViewHolder::class.java))
assertThat(underTest.mCancellableTask!!.canceled).isTrue()
assertThat(underTest.getRecycledViewCount(VIEW_TYPE)).isEqualTo(0)
}
private fun awaitTasksCompleted() {
Executors.VIEW_PREINFLATION_EXECUTOR.submit<Any> { null }.get()
Executors.MAIN_EXECUTOR.submit<Any> { null }.get()
}
companion object {
private const val VIEW_TYPE: Int = 4
}
}
@@ -21,11 +21,8 @@ import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static org.junit.Assert.assertTrue;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Process;
import android.system.OsConstants;
import android.util.Log;
@@ -53,7 +50,6 @@ import org.junit.rules.TestRule;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
@@ -216,38 +212,6 @@ public abstract class AbstractLauncherUiTest<LAUNCHER_TYPE extends Launcher>
}, mLauncher, timeout);
}
/**
* Broadcast receiver which blocks until the result is received.
*/
public class BlockingBroadcastReceiver extends BroadcastReceiver {
private final CountDownLatch latch = new CountDownLatch(1);
private Intent mIntent;
public BlockingBroadcastReceiver(String action) {
mTargetContext.registerReceiver(this, new IntentFilter(action),
Context.RECEIVER_EXPORTED/*UNAUDITED*/);
}
@Override
public void onReceive(Context context, Intent intent) {
mIntent = intent;
latch.countDown();
}
public Intent blockingGetIntent() throws InterruptedException {
assertTrue("Timed Out", latch.await(DEFAULT_BROADCAST_TIMEOUT_SECS, TimeUnit.SECONDS));
mTargetContext.unregisterReceiver(this);
return mIntent;
}
public Intent blockingGetExtraIntent() throws InterruptedException {
Intent intent = blockingGetIntent();
return intent == null ? null : (Intent) intent.getParcelableExtra(
Intent.EXTRA_INTENT);
}
}
public static void startAppFast(String packageName) {
startIntent(
getInstrumentation().getContext().getPackageManager().getLaunchIntentForPackage(
@@ -1,17 +1,17 @@
/*
* Copyright (C) 2017 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
* 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
* 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.
* 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.ui.widget;
@@ -24,23 +24,34 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import android.appwidget.AppWidgetManager;
import android.content.Context;
import android.content.Intent;
import android.os.Process;
import android.view.View;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.Launcher;
import com.android.launcher3.celllayout.FavoriteItemsTransaction;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.testcomponent.WidgetConfigActivity;
import com.android.launcher3.ui.AbstractLauncherUiTest;
import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
import com.android.launcher3.ui.TestViewHelpers;
import com.android.launcher3.util.BaseLauncherActivityTest;
import com.android.launcher3.util.BlockingBroadcastReceiver;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.Wait;
import com.android.launcher3.util.rule.ShellCommandRule;
import com.android.launcher3.views.OptionsPopupView;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.PendingAddWidgetInfo;
import com.android.launcher3.widget.WidgetCell;
import com.android.launcher3.widget.picker.WidgetsFullSheet;
import com.android.launcher3.widget.picker.WidgetsListAdapter;
import com.android.launcher3.widget.picker.WidgetsRecyclerView;
import org.junit.Before;
import org.junit.Rule;
@@ -52,7 +63,7 @@ import org.junit.runner.RunWith;
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
public class TaplAddConfigWidgetTest extends AbstractLauncherUiTest<Launcher> {
public class AddConfigWidgetTest extends BaseLauncherActivityTest<Launcher> {
@Rule
public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
@@ -62,12 +73,10 @@ public class TaplAddConfigWidgetTest extends AbstractLauncherUiTest<Launcher> {
private int mWidgetId;
@Override
@Before
public void setUp() throws Exception {
super.setUp();
mWidgetInfo = TestViewHelpers.findWidgetProvider(true /* hasConfigureScreen */);
mAppWidgetManager = AppWidgetManager.getInstance(mTargetContext);
mAppWidgetManager = AppWidgetManager.getInstance(targetContext());
}
@Test
@@ -82,80 +91,86 @@ public class TaplAddConfigWidgetTest extends AbstractLauncherUiTest<Launcher> {
runTest(false);
}
/**
* @param acceptConfig accept the config activity
*/
private void runTest(boolean acceptConfig) throws Throwable {
commitTransactionAndLoadHome(new FavoriteItemsTransaction(mTargetContext));
new FavoriteItemsTransaction(targetContext()).commit();
loadLauncherSync();
// Drag widget to homescreen
// Add widget to homescreen
WidgetConfigStartupMonitor monitor = new WidgetConfigStartupMonitor();
mLauncher.getWorkspace()
.openAllWidgets()
.getWidget(mWidgetInfo.getLabel())
.dragToWorkspace(true, false);
executeOnLauncher(OptionsPopupView::openWidgets);
uiDevice.waitForIdle();
// Select the widget header
Context testContext = getInstrumentation().getContext();
String packageName = testContext.getPackageName();
executeOnLauncher(l -> {
WidgetsRecyclerView wrv = WidgetsFullSheet.getWidgetsView(l);
WidgetsListAdapter adapter = (WidgetsListAdapter) wrv.getAdapter();
int pos = adapter.getItems().indexOf(
adapter.getItems().stream()
.filter(entry -> packageName.equals(entry.mPkgItem.packageName))
.findFirst()
.get());
wrv.getLayoutManager().scrollToPosition(pos);
adapter.onHeaderClicked(true, new PackageUserKey(packageName, Process.myUserHandle()));
});
uiDevice.waitForIdle();
View widgetView = getOnceNotNull("Widget not found", l -> searchView(l.getDragLayer(), v ->
v instanceof WidgetCell
&& v.getTag() instanceof PendingAddWidgetInfo pawi
&& mWidgetInfo.provider.equals(pawi.componentName)));
addToWorkspace(widgetView);
// Widget id for which the config activity was opened
mWidgetId = monitor.getWidgetId();
// Verify that the widget id is valid and bound
assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
setResult(acceptConfig);
setResultAndWaitForAnimation(acceptConfig);
if (acceptConfig) {
Wait.atMost("", new WidgetSearchCondition(), mLauncher);
getOnceNotNull("Widget was not added", l -> {
// Close the resize frame before searching for widget
AbstractFloatingView.closeAllOpenViews(l);
return l.getWorkspace().getFirstMatch(new WidgetSearchCondition());
});
assertNotNull(mAppWidgetManager.getAppWidgetInfo(mWidgetId));
} else {
// Verify that the widget id is deleted.
Wait.atMost("", () -> mAppWidgetManager.getAppWidgetInfo(mWidgetId) == null,
mLauncher);
Wait.atMost("", () -> mAppWidgetManager.getAppWidgetInfo(mWidgetId) == null);
}
}
private static void setResult(boolean success) {
private void setResult(boolean success) {
getInstrumentation().getTargetContext().sendBroadcast(
WidgetConfigActivity.getCommandIntent(WidgetConfigActivity.class,
success ? "clickOK" : "clickCancel"));
}
private void setResultAndWaitForAnimation(boolean success) {
if (mLauncher.isLauncher3()) {
setResult(success);
} else {
mLauncher.executeAndWaitForWallpaperAnimation(
() -> setResult(success),
"setting widget coinfig result");
}
uiDevice.waitForIdle();
}
/**
* Condition for searching widget id
*/
private class WidgetSearchCondition implements Wait.Condition, ItemOperator {
@Override
public boolean isTrue() throws Throwable {
return mMainThreadExecutor.submit(() -> {
Launcher l = Launcher.ACTIVITY_TRACKER.getCreatedContext();
return l != null && l.getWorkspace().getFirstMatch(this) != null;
}).get();
}
private class WidgetSearchCondition implements ItemOperator {
@Override
public boolean evaluate(ItemInfo info, View view) {
return info instanceof LauncherAppWidgetInfo
&& ((LauncherAppWidgetInfo) info).providerName.getClassName().equals(
mWidgetInfo.provider.getClassName())
&& ((LauncherAppWidgetInfo) info).appWidgetId == mWidgetId;
return info instanceof LauncherAppWidgetInfo lawi
&& lawi.providerName.equals(mWidgetInfo.provider)
&& lawi.appWidgetId == mWidgetId;
}
}
/**
* Broadcast receiver for receiving widget config activity status.
*/
private class WidgetConfigStartupMonitor extends BlockingBroadcastReceiver {
private static class WidgetConfigStartupMonitor extends BlockingBroadcastReceiver {
public WidgetConfigStartupMonitor() {
WidgetConfigStartupMonitor() {
super(WidgetConfigActivity.class.getName());
}
@@ -1,17 +1,17 @@
/*
* Copyright (C) 2017 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
* 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
* 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.
* 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.ui.widget;
@@ -22,12 +22,12 @@ import static com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_PROVID
import static com.android.launcher3.model.data.LauncherAppWidgetInfo.FLAG_RESTORE_STARTED;
import static com.android.launcher3.provider.LauncherDbUtils.itemIdMatch;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import static com.android.launcher3.util.TestUtil.getOnUiThread;
import static com.android.launcher3.util.Wait.atMost;
import static com.android.launcher3.util.WidgetUtils.createWidgetInfo;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
@@ -36,6 +36,7 @@ import android.content.pm.PackageInstaller.SessionParams;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Bundle;
import android.text.TextUtils;
import android.widget.RemoteViews;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -49,13 +50,12 @@ import com.android.launcher3.R;
import com.android.launcher3.celllayout.FavoriteItemsTransaction;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.pm.InstallSessionHelper;
import com.android.launcher3.tapl.Widget;
import com.android.launcher3.tapl.Workspace;
import com.android.launcher3.ui.AbstractLauncherUiTest;
import com.android.launcher3.ui.TestViewHelpers;
import com.android.launcher3.util.TestUtil;
import com.android.launcher3.util.BaseLauncherActivityTest;
import com.android.launcher3.util.rule.ShellCommandRule;
import com.android.launcher3.widget.LauncherAppWidgetHostView;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.PendingAppWidgetHostView;
import com.android.launcher3.widget.WidgetManagerHelper;
import org.junit.After;
@@ -67,6 +67,7 @@ import org.junit.runner.RunWith;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* Tests for bind widget flow.
@@ -75,7 +76,7 @@ import java.util.function.Consumer;
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
public class TaplBindWidgetTest extends AbstractLauncherUiTest<Launcher> {
public class BindWidgetTest extends BaseLauncherActivityTest<Launcher> {
@Rule
public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
@@ -87,11 +88,9 @@ public class TaplBindWidgetTest extends AbstractLauncherUiTest<Launcher> {
private LauncherModel mModel;
@Override
@Before
public void setUp() throws Exception {
super.setUp();
mModel = LauncherAppState.getInstance(mTargetContext).getModel();
mModel = LauncherAppState.getInstance(targetContext()).getModel();
}
@After
@@ -101,7 +100,7 @@ public class TaplBindWidgetTest extends AbstractLauncherUiTest<Launcher> {
}
if (mSessionId > -1) {
mTargetContext.getPackageManager().getPackageInstaller().abandonSession(mSessionId);
targetContext().getPackageManager().getPackageInstaller().abandonSession(mSessionId);
}
}
@@ -122,13 +121,12 @@ public class TaplBindWidgetTest extends AbstractLauncherUiTest<Launcher> {
LauncherAppWidgetProviderInfo info = addWidgetToScreen(false, false,
item -> item.appWidgetId = -33);
final Workspace workspace = mLauncher.getWorkspace();
// Item deleted from db
mCursor = queryItem();
assertEquals(0, mCursor.getCount());
// The view does not exist
assertTrue("Widget exists", workspace.tryGetWidget(info.label, 0) == null);
verifyItemEventuallyNull("Widget exists", widgetProvider(info));
}
@Test
@@ -154,18 +152,19 @@ public class TaplBindWidgetTest extends AbstractLauncherUiTest<Launcher> {
// Widget has a valid Id now.
assertEquals(0, mCursor.getInt(mCursor.getColumnIndex(LauncherSettings.Favorites.RESTORED))
& FLAG_ID_NOT_VALID);
assertNotNull(AppWidgetManager.getInstance(mTargetContext)
assertNotNull(AppWidgetManager.getInstance(targetContext())
.getAppWidgetInfo(mCursor.getInt(mCursor.getColumnIndex(
LauncherSettings.Favorites.APPWIDGET_ID))));
// send OPTION_APPWIDGET_RESTORE_COMPLETED
int appWidgetId = mCursor.getInt(
mCursor.getColumnIndex(LauncherSettings.Favorites.APPWIDGET_ID));
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mTargetContext);
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(targetContext());
Bundle b = new Bundle();
b.putBoolean(WidgetManagerHelper.WIDGET_OPTION_RESTORE_COMPLETED, true);
RemoteViews remoteViews = new RemoteViews(mTargetPackage, R.layout.appwidget_not_ready);
RemoteViews remoteViews = new RemoteViews(
targetContext().getPackageName(), R.layout.appwidget_not_ready);
appWidgetManager.updateAppWidgetOptions(appWidgetId, b);
appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
@@ -175,15 +174,14 @@ public class TaplBindWidgetTest extends AbstractLauncherUiTest<Launcher> {
WidgetManagerHelper.WIDGET_OPTION_RESTORE_COMPLETED));
executeOnLauncher(l -> l.getAppWidgetHolder().startListening());
verifyWidgetPresent(info);
assertNull(mLauncher.getWorkspace().tryGetPendingWidget(100));
verifyItemEventuallyNull("Pending widget exists", pendingWidgetProvider());
}
@Test
public void testPendingWidget_notRestored_removed() {
addPendingItemToScreen(getInvalidWidgetInfo(), FLAG_ID_NOT_VALID | FLAG_PROVIDER_NOT_READY);
assertTrue("Pending widget exists",
mLauncher.getWorkspace().tryGetPendingWidget(0) == null);
verifyItemEventuallyNull("Pending widget exists", pendingWidgetProvider());
// Item deleted from db
mCursor = queryItem();
assertEquals(0, mCursor.getCount());
@@ -216,7 +214,7 @@ public class TaplBindWidgetTest extends AbstractLauncherUiTest<Launcher> {
// Create an active installer session
SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
params.setAppPackageName(item.providerName.getPackageName());
PackageInstaller installer = mTargetContext.getPackageManager().getPackageInstaller();
PackageInstaller installer = targetContext().getPackageManager().getPackageInstaller();
mSessionId = installer.createSession(params);
addPendingItemToScreen(item, FLAG_ID_NOT_VALID | FLAG_PROVIDER_NOT_READY);
@@ -234,36 +232,47 @@ public class TaplBindWidgetTest extends AbstractLauncherUiTest<Launcher> {
}
private void verifyWidgetPresent(LauncherAppWidgetProviderInfo info) {
final Widget widget = mLauncher.getWorkspace().tryGetWidget(info.label,
TestUtil.DEFAULT_UI_TIMEOUT);
assertTrue("Widget is not present",
widget != null);
getOnceNotNull("Widget is not present", widgetProvider(info));
}
private void verifyPendingWidgetPresent() {
final Widget widget = mLauncher.getWorkspace().tryGetPendingWidget(
TestUtil.DEFAULT_UI_TIMEOUT);
assertTrue("Pending widget is not present",
widget != null);
getOnceNotNull("Widget is not present", pendingWidgetProvider());
}
private Function<Launcher, Object> pendingWidgetProvider() {
return l -> l.getWorkspace().getFirstMatch(
(item, view) -> view instanceof PendingAppWidgetHostView);
}
private Function<Launcher, Object> widgetProvider(LauncherAppWidgetProviderInfo info) {
return l -> l.getWorkspace().getFirstMatch((item, view) ->
view instanceof LauncherAppWidgetHostView
&& TextUtils.equals(info.label, view.getContentDescription()));
}
private void verifyItemEventuallyNull(String message, Function<Launcher, Object> provider) {
atMost(message, () -> getFromLauncher(provider) == null);
}
private void addPendingItemToScreen(LauncherAppWidgetInfo item, int restoreStatus) {
item.restoreStatus = restoreStatus;
item.screenId = FIRST_SCREEN_ID;
commitTransactionAndLoadHome(
new FavoriteItemsTransaction(mTargetContext).addItem(() -> item));
new FavoriteItemsTransaction(targetContext()).addItem(() -> item).commit();
loadLauncherSync();
}
private LauncherAppWidgetProviderInfo addWidgetToScreen(boolean hasConfigureScreen,
boolean bindWidget, Consumer<LauncherAppWidgetInfo> itemOverride) {
LauncherAppWidgetProviderInfo info = TestViewHelpers.findWidgetProvider(hasConfigureScreen);
commitTransactionAndLoadHome(new FavoriteItemsTransaction(mTargetContext)
new FavoriteItemsTransaction(targetContext())
.addItem(() -> {
LauncherAppWidgetInfo item = createWidgetInfo(info, mTargetContext, bindWidget);
LauncherAppWidgetInfo item =
createWidgetInfo(info, targetContext(), bindWidget);
item.screenId = FIRST_SCREEN_ID;
itemOverride.accept(item);
return item;
}));
}).commit();
loadLauncherSync();
return info;
}
@@ -277,13 +286,13 @@ public class TaplBindWidgetTest extends AbstractLauncherUiTest<Launcher> {
Set<String> activePackage = getOnUiThread(() -> {
Set<String> packages = new HashSet<>();
InstallSessionHelper.INSTANCE.get(mTargetContext).getActiveSessions()
InstallSessionHelper.INSTANCE.get(targetContext()).getActiveSessions()
.keySet().forEach(packageUserKey -> packages.add(packageUserKey.mPackageName));
return packages;
});
while (true) {
try {
mTargetContext.getPackageManager().getPackageInfo(
targetContext().getPackageManager().getPackageInfo(
pkg, PackageManager.GET_UNINSTALLED_PACKAGES);
} catch (Exception e) {
if (!activePackage.contains(pkg)) {
@@ -1,34 +1,41 @@
/*
* Copyright (C) 2017 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
* 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
* 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.
* 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.ui.widget;
import static android.app.PendingIntent.FLAG_MUTABLE;
import static android.app.PendingIntent.FLAG_ONE_SHOT;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static java.util.regex.Pattern.CASE_INSENSITIVE;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.view.View;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
import androidx.test.uiautomator.By;
import androidx.test.uiautomator.BySelector;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherSettings.Favorites;
@@ -37,14 +44,13 @@ 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.shortcuts.ShortcutKey;
import com.android.launcher3.tapl.AddToHomeScreenPrompt;
import com.android.launcher3.testcomponent.AppWidgetNoConfig;
import com.android.launcher3.testcomponent.AppWidgetWithConfig;
import com.android.launcher3.testcomponent.RequestPinItemActivity;
import com.android.launcher3.ui.AbstractLauncherUiTest;
import com.android.launcher3.util.BaseLauncherActivityTest;
import com.android.launcher3.util.BlockingBroadcastReceiver;
import com.android.launcher3.util.LauncherBindableItemsContainer.ItemOperator;
import com.android.launcher3.util.Wait;
import com.android.launcher3.util.Wait.Condition;
import com.android.launcher3.util.TestUtil;
import com.android.launcher3.util.rule.ShellCommandRule;
import org.junit.Before;
@@ -53,25 +59,27 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.UUID;
import java.util.regex.Pattern;
/**
* Test to verify pin item request flow.
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
public class TaplRequestPinItemTest extends AbstractLauncherUiTest<Launcher> {
public class RequestPinItemTest extends BaseLauncherActivityTest<Launcher> {
@Rule
public ShellCommandRule mGrantWidgetRule = ShellCommandRule.grantWidgetBind();
@Rule
public ShellCommandRule mDefaultLauncherRule = ShellCommandRule.setDefaultLauncher();
private String mCallbackAction;
private String mShortcutId;
private int mAppWidgetId;
@Override
@Before
public void setUp() throws Exception {
super.setUp();
mCallbackAction = UUID.randomUUID().toString();
mShortcutId = UUID.randomUUID().toString();
}
@@ -81,9 +89,9 @@ public class TaplRequestPinItemTest extends AbstractLauncherUiTest<Launcher> {
@Test
public void testPinWidgetNoConfig() throws Throwable {
runTest("pinWidgetNoConfig", true, (info, view) -> info instanceof LauncherAppWidgetInfo &&
((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId &&
((LauncherAppWidgetInfo) info).providerName.getClassName()
runTest("pinWidgetNoConfig", true, (info, view) -> info instanceof LauncherAppWidgetInfo
&& ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId
&& ((LauncherAppWidgetInfo) info).providerName.getClassName()
.equals(AppWidgetNoConfig.class.getName()));
}
@@ -94,18 +102,18 @@ public class TaplRequestPinItemTest extends AbstractLauncherUiTest<Launcher> {
RequestPinItemActivity.class, "setRemoteViewColor").putExtra(
RequestPinItemActivity.EXTRA_PARAM + "0", Color.RED);
runTest("pinWidgetNoConfig", true, (info, view) -> info instanceof LauncherAppWidgetInfo &&
((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId &&
((LauncherAppWidgetInfo) info).providerName.getClassName()
runTest("pinWidgetNoConfig", true, (info, view) -> info instanceof LauncherAppWidgetInfo
&& ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId
&& ((LauncherAppWidgetInfo) info).providerName.getClassName()
.equals(AppWidgetNoConfig.class.getName()), command);
}
@Test
public void testPinWidgetWithConfig() throws Throwable {
runTest("pinWidgetWithConfig", true,
(info, view) -> info instanceof LauncherAppWidgetInfo &&
((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId &&
((LauncherAppWidgetInfo) info).providerName.getClassName()
(info, view) -> info instanceof LauncherAppWidgetInfo
&& ((LauncherAppWidgetInfo) info).appWidgetId == mAppWidgetId
&& ((LauncherAppWidgetInfo) info).providerName.getClassName()
.equals(AppWidgetWithConfig.class.getName()));
}
@@ -119,47 +127,48 @@ public class TaplRequestPinItemTest extends AbstractLauncherUiTest<Launcher> {
runTest("pinShortcut", false, new ItemOperator() {
@Override
public boolean evaluate(ItemInfo info, View view) {
return info instanceof WorkspaceItemInfo &&
info.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT &&
ShortcutKey.fromItemInfo(info).getId().equals(mShortcutId);
return info instanceof WorkspaceItemInfo
&& info.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT
&& ShortcutKey.fromItemInfo(info).getId().equals(mShortcutId);
}
}, command);
}
private void runTest(String activityMethod, boolean isWidget, ItemOperator itemMatcher,
Intent... commandIntents) throws Throwable {
commitTransactionAndLoadHome(new FavoriteItemsTransaction(mTargetContext));
new FavoriteItemsTransaction(targetContext()).commit();
loadLauncherSync();
// Open Pin item activity
BlockingBroadcastReceiver openMonitor = new BlockingBroadcastReceiver(
RequestPinItemActivity.class.getName());
mLauncher.
getWorkspace().
switchToAllApps().
getAppIcon("Test Pin Item").
launch(getAppPackageName());
Context testContext = getInstrumentation().getContext();
startAppFast(
testContext.getPackageName(),
new Intent(testContext, RequestPinItemActivity.class));
assertNotNull(openMonitor.blockingGetExtraIntent());
// Set callback
PendingIntent callback = PendingIntent.getBroadcast(mTargetContext, 0,
new Intent(mCallbackAction).setPackage(mTargetContext.getPackageName()),
PendingIntent callback = PendingIntent.getBroadcast(targetContext(), 0,
new Intent(mCallbackAction).setPackage(targetContext().getPackageName()),
FLAG_ONE_SHOT | FLAG_MUTABLE);
mTargetContext.sendBroadcast(RequestPinItemActivity.getCommandIntent(
targetContext().sendBroadcast(RequestPinItemActivity.getCommandIntent(
RequestPinItemActivity.class, "setCallback").putExtra(
RequestPinItemActivity.EXTRA_PARAM + "0", callback));
for (Intent command : commandIntents) {
mTargetContext.sendBroadcast(command);
targetContext().sendBroadcast(command);
}
// call the requested method to start the flow
mTargetContext.sendBroadcast(RequestPinItemActivity.getCommandIntent(
targetContext().sendBroadcast(RequestPinItemActivity.getCommandIntent(
RequestPinItemActivity.class, activityMethod));
final AddToHomeScreenPrompt addToHomeScreenPrompt = mLauncher.getAddToHomeScreenPrompt();
// Accept confirmation:
BlockingBroadcastReceiver resultReceiver = new BlockingBroadcastReceiver(mCallbackAction);
addToHomeScreenPrompt.addAutomatically();
BySelector selector = By.text(Pattern.compile("^Add to home screen$", CASE_INSENSITIVE))
.pkg(targetContext().getPackageName());
uiDevice.wait(device -> device.findObject(selector), TestUtil.DEFAULT_UI_TIMEOUT).click();
Intent result = resultReceiver.blockingGetIntent();
assertNotNull(result);
mAppWidgetId = result.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
@@ -167,28 +176,9 @@ public class TaplRequestPinItemTest extends AbstractLauncherUiTest<Launcher> {
assertNotSame(-1, mAppWidgetId);
}
// Go back to home
mLauncher.goHome();
Wait.atMost("", new ItemSearchCondition(itemMatcher), mLauncher);
}
/**
* Condition for for an item
*/
private class ItemSearchCondition implements Condition {
private final ItemOperator mOp;
ItemSearchCondition(ItemOperator op) {
mOp = op;
}
@Override
public boolean isTrue() throws Throwable {
return mMainThreadExecutor.submit(() -> {
Launcher l = Launcher.ACTIVITY_TRACKER.getCreatedContext();
return l != null && l.getWorkspace().getFirstMatch(mOp) != null;
}).get();
}
// Reload activity, so that the activity is focused
closeCurrentActivity();
loadLauncherSync();
getOnceNotNull("", l -> l.getWorkspace().getFirstMatch(itemMatcher));
}
}
@@ -22,24 +22,30 @@ import static org.junit.Assert.assertTrue;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.Launcher;
import com.android.launcher3.tapl.Widgets;
import com.android.launcher3.ui.AbstractLauncherUiTest;
import com.android.launcher3.ui.PortraitLandscapeRunner.PortraitLandscape;
import com.android.launcher3.util.BaseLauncherActivityTest;
import com.android.launcher3.util.rule.ScreenRecordRule;
import com.android.launcher3.util.rule.ScreenRecordRule.ScreenRecord;
import com.android.launcher3.views.OptionsPopupView;
import com.android.launcher3.widget.picker.WidgetsFullSheet;
import com.android.launcher3.widget.picker.WidgetsRecyclerView;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
/**
* This test run in both Out of process (Oop) and in-process (Ipc).
* Make sure the basic interactions with the WidgetPicker works.
*/
@MediumTest
@RunWith(AndroidJUnit4.class)
public class TaplWidgetPickerTest extends AbstractLauncherUiTest<Launcher> {
public class WidgetPickerTest extends BaseLauncherActivityTest<Launcher> {
@Rule
public TestRule screenRecordRule = new ScreenRecordRule();
private WidgetsRecyclerView getWidgetsView(Launcher launcher) {
return WidgetsFullSheet.getWidgetsView(launcher);
@@ -56,30 +62,21 @@ public class TaplWidgetPickerTest extends AbstractLauncherUiTest<Launcher> {
@ScreenRecord
@PortraitLandscape
public void testWidgets() {
mLauncher.goHome();
loadLauncherSync();
// Test opening widgets.
executeOnLauncher(launcher ->
assertTrue("Widgets is initially opened", getWidgetsView(launcher) == null));
Widgets widgets = mLauncher.getWorkspace().openAllWidgets();
assertNotNull("openAllWidgets() returned null", widgets);
widgets = mLauncher.getAllWidgets();
assertNotNull("openAllWidgets() returned null",
getFromLauncher(OptionsPopupView::openWidgets));
WidgetsRecyclerView widgets = getFromLauncher(this::getWidgetsView);
assertNotNull("getAllWidgets() returned null", widgets);
executeOnLauncher(launcher ->
assertTrue("Widgets is not shown", getWidgetsView(launcher).isShown()));
executeOnLauncher(launcher -> assertTrue("Widgets is not shown", widgets.isShown()));
executeOnLauncher(launcher -> assertEquals("Widgets is scrolled upon opening",
0, getWidgetsScroll(launcher)));
// Test flinging widgets.
widgets.flingForward();
Integer flingForwardY = getFromLauncher(launcher -> getWidgetsScroll(launcher));
executeOnLauncher(launcher -> assertTrue("Flinging forward didn't scroll widgets",
flingForwardY > 0));
executeOnLauncher(AbstractFloatingView::closeAllOpenViews);
uiDevice.waitForIdle();
widgets.flingBackward();
executeOnLauncher(launcher -> assertTrue("Flinging backward didn't scroll widgets",
getWidgetsScroll(launcher) < flingForwardY));
mLauncher.goHome();
waitForLauncherCondition("Widgets were not closed",
launcher -> getWidgetsView(launcher) == null);
}
@@ -15,8 +15,6 @@
*/
package com.android.launcher3.ui.workspace;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.android.launcher3.AbstractFloatingView.TYPE_ACTION_POPUP;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.TestConstants.AppNames.TEST_APP_NAME;
@@ -29,11 +27,9 @@ import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.net.Uri;
import android.view.View;
import android.view.ViewGroup;
import androidx.test.filters.LargeTest;
import androidx.test.uiautomator.UiDevice;
import com.android.launcher3.AbstractFloatingView;
import com.android.launcher3.BubbleTextView;
@@ -49,9 +45,6 @@ import com.android.launcher3.util.TestUtil;
import org.junit.Test;
import java.util.ArrayDeque;
import java.util.Queue;
/**
* Tests for theme icon support in Launcher
*
@@ -137,27 +130,10 @@ public class ThemeIconsTest extends BaseLauncherActivityTest<Launcher> {
} catch (Exception e) {
throw new RuntimeException(e);
}
// Find the app icon
Queue<View> viewQueue = new ArrayDeque<>();
viewQueue.add(parent);
BubbleTextView icon = null;
while (!viewQueue.isEmpty()) {
View view = viewQueue.poll();
if (view instanceof ViewGroup) {
parent = (ViewGroup) view;
for (int i = parent.getChildCount() - 1; i >= 0; i--) {
viewQueue.add(parent.getChildAt(i));
}
} else if (view instanceof BubbleTextView btv) {
if (btv.getContentDescription() != null
&& title.equals(btv.getContentDescription().toString())) {
icon = btv;
break;
}
}
}
return icon;
return (BubbleTextView) searchView(parent, v ->
v instanceof BubbleTextView btv
&& btv.getContentDescription() != null
&& title.equals(btv.getContentDescription().toString()));
}
private BubbleTextView verifyIconTheme(String title, ViewGroup parent, boolean isThemed) {
@@ -193,11 +169,4 @@ public class ThemeIconsTest extends BaseLauncherActivityTest<Launcher> {
rv.getLayoutManager().scrollToPosition(pos);
});
}
private void addToWorkspace(View btv) {
TestUtil.runOnExecutorSync(MAIN_EXECUTOR, () ->
btv.getAccessibilityDelegate().performAccessibilityAction(
btv, com.android.launcher3.R.id.action_add_to_workspace, null));
UiDevice.getInstance(getInstrumentation()).waitForIdle();
}
}
@@ -22,6 +22,9 @@ import android.view.InputDevice
import android.view.KeyCharacterMap
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.core.view.children
import androidx.lifecycle.Lifecycle.State.RESUMED
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ActivityScenario.ActivityAction
@@ -30,11 +33,13 @@ import androidx.test.uiautomator.UiDevice
import com.android.launcher3.Launcher
import com.android.launcher3.LauncherAppState
import com.android.launcher3.LauncherState
import com.android.launcher3.R
import com.android.launcher3.allapps.AllAppsStore.DEFER_UPDATES_TEST
import com.android.launcher3.tapl.TestHelpers
import com.android.launcher3.util.ModelTestExtensions.loadModelSync
import com.android.launcher3.util.Wait.atMost
import java.util.function.Function
import java.util.function.Predicate
import java.util.function.Supplier
import org.junit.After
@@ -56,6 +61,8 @@ open class BaseLauncherActivityTest<LAUNCHER_TYPE : Launcher> {
)
.also { currentScenario = it }
@JvmField val uiDevice = UiDevice.getInstance(getInstrumentation())
@After
fun closeCurrentActivity() {
currentScenario?.close()
@@ -136,18 +143,43 @@ open class BaseLauncherActivityTest<LAUNCHER_TYPE : Launcher> {
event.recycle()
}
fun startAppFast(packageName: String) {
val intent = targetContext().packageManager.getLaunchIntentForPackage(packageName)!!
@JvmOverloads
fun startAppFast(
packageName: String,
intent: Intent = targetContext().packageManager.getLaunchIntentForPackage(packageName)!!,
) {
intent.addCategory(Intent.CATEGORY_LAUNCHER)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
targetContext().startActivity(intent)
UiDevice.getInstance(getInstrumentation()).waitForIdle()
uiDevice.waitForIdle()
}
fun freezeAllApps() = executeOnLauncher {
it.appsView.appsStore.enableDeferUpdates(DEFER_UPDATES_TEST)
}
fun executeShellCommand(cmd: String) =
UiDevice.getInstance(getInstrumentation()).executeShellCommand(cmd)
fun executeShellCommand(cmd: String) = uiDevice.executeShellCommand(cmd)
fun addToWorkspace(view: View) {
TestUtil.runOnExecutorSync(Executors.MAIN_EXECUTOR) {
view.accessibilityDelegate.performAccessibilityAction(
view,
R.id.action_add_to_workspace,
null,
)
}
UiDevice.getInstance(getInstrumentation()).waitForIdle()
}
fun ViewGroup.searchView(filter: Predicate<View>): View? {
if (filter.test(this)) return this
for (child in children) {
if (filter.test(child)) return child
if (child is ViewGroup)
child.searchView(filter)?.let {
return it
}
}
return null
}
}
@@ -0,0 +1,54 @@
/*
* 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.util
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Parcelable
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit.SECONDS
private const val DEFAULT_BROADCAST_TIMEOUT_SECS: Long = 10
/** Broadcast receiver which blocks until the result is received. */
open class BlockingBroadcastReceiver(action: String) : BroadcastReceiver() {
val value = CompletableFuture<Intent>()
init {
getInstrumentation()
.targetContext
.registerReceiver(this, IntentFilter(action), Context.RECEIVER_EXPORTED)
}
override fun onReceive(context: Context, intent: Intent) {
value.complete(intent)
}
@Throws(InterruptedException::class)
fun blockingGetIntent(): Intent =
value.get(DEFAULT_BROADCAST_TIMEOUT_SECS, SECONDS).also {
getInstrumentation().targetContext.unregisterReceiver(this)
}
@Throws(InterruptedException::class)
fun blockingGetExtraIntent(): Intent? =
blockingGetIntent().getParcelableExtra<Parcelable>(Intent.EXTRA_INTENT) as Intent?
}