diff --git a/quickstep/robolectric_tests/src/com/android/quickstep/util/RecentsOrientedStateTest.java b/quickstep/robolectric_tests/src/com/android/quickstep/util/RecentsOrientedStateTest.java new file mode 100644 index 0000000000..656379f10f --- /dev/null +++ b/quickstep/robolectric_tests/src/com/android/quickstep/util/RecentsOrientedStateTest.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.quickstep.util; + +import static android.view.Surface.ROTATION_0; +import static android.view.Surface.ROTATION_180; +import static android.view.Surface.ROTATION_90; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +import android.content.Context; + +import com.android.quickstep.FallbackActivityInterface; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.LooperMode; +import org.robolectric.annotation.LooperMode.Mode; + +/** + * Tests for {@link RecentsOrientedState} + */ +@RunWith(RobolectricTestRunner.class) +@LooperMode(Mode.PAUSED) +public class RecentsOrientedStateTest { + + private RecentsOrientedState mR1, mR2; + + @Before + public void setup() { + Context context = RuntimeEnvironment.application; + mR1 = new RecentsOrientedState(context, FallbackActivityInterface.INSTANCE, i -> { }); + mR2 = new RecentsOrientedState(context, FallbackActivityInterface.INSTANCE, i -> { }); + assertEquals(mR1.getStateId(), mR2.getStateId()); + } + + @Test + public void stateId_changesWithFlags() { + mR1.setGestureActive(true); + mR2.setGestureActive(false); + assertNotEquals(mR1.getStateId(), mR2.getStateId()); + + mR2.setGestureActive(true); + assertEquals(mR1.getStateId(), mR2.getStateId()); + } + + @Test + public void stateId_changesWithRecentsRotation() { + mR1.setRecentsRotation(ROTATION_90); + mR2.setRecentsRotation(ROTATION_180); + assertNotEquals(mR1.getStateId(), mR2.getStateId()); + + mR2.setRecentsRotation(ROTATION_90); + assertEquals(mR1.getStateId(), mR2.getStateId()); + } + + @Test + public void stateId_changesWithDisplayRotation() { + mR1.update(ROTATION_0, ROTATION_90); + mR2.update(ROTATION_0, ROTATION_180); + assertNotEquals(mR1.getStateId(), mR2.getStateId()); + + mR2.update(ROTATION_90, ROTATION_90); + assertNotEquals(mR1.getStateId(), mR2.getStateId()); + + mR2.update(ROTATION_90, ROTATION_0); + assertNotEquals(mR1.getStateId(), mR2.getStateId()); + + mR2.update(ROTATION_0, ROTATION_90); + assertEquals(mR1.getStateId(), mR2.getStateId()); + } +} diff --git a/quickstep/robolectric_tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java b/quickstep/robolectric_tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java index 6c88e55225..688f323d75 100644 --- a/quickstep/robolectric_tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java +++ b/quickstep/robolectric_tests/src/com/android/quickstep/util/TaskViewSimulatorTest.java @@ -148,7 +148,7 @@ public class TaskViewSimulatorTest { if (mAppRotation < 0) { mAppRotation = launcherRotation; } - tvs.setLayoutRotation(launcherRotation, mAppRotation); + tvs.getOrientationState().update(launcherRotation, mAppRotation); if (mAppInsets == null) { mAppInsets = new Rect(mLauncherInsets); } diff --git a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java index 456727c069..c724318c2d 100644 --- a/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java +++ b/quickstep/src/com/android/launcher3/BaseQuickstepLauncher.java @@ -326,6 +326,8 @@ public abstract class BaseQuickstepLauncher extends Launcher @Override public void onAttachedToWindow() { super.onAttachedToWindow(); - InteractionJankMonitorWrapper.init(getWindow().getDecorView()); + if (Utilities.ATLEAST_R) { + InteractionJankMonitorWrapper.init(getWindow().getDecorView()); + } } } diff --git a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java index 5c8463c2a7..034d51ff38 100644 --- a/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java +++ b/quickstep/src/com/android/launcher3/LauncherAnimationRunner.java @@ -17,6 +17,7 @@ package com.android.launcher3; import static com.android.launcher3.Utilities.postAsyncCallback; import static com.android.launcher3.util.DisplayController.getSingleFrameMs; +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.systemui.shared.recents.utilities.Utilities.postAtFrontOfQueueAsynchronously; import android.animation.Animator; @@ -62,7 +63,7 @@ public abstract class LauncherAnimationRunner implements RemoteAnimationRunnerCo Runnable r = () -> { finishExistingAnimation(); mAnimationResult = new AnimationResult(() -> { - runnable.run(); + UI_HELPER_EXECUTOR.execute(runnable); mAnimationResult = null; }); onCreateAnimation(appTargets, wallpaperTargets, mAnimationResult); diff --git a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java index d3b7e22299..4953138bee 100644 --- a/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java +++ b/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java @@ -251,7 +251,10 @@ public class HotseatPredictionController implements DragController.DragListener, * Sets or updates the predicted items */ public void setPredictedItems(FixedContainerItems items) { - if (!mLauncher.isWorkspaceLoading() + boolean shouldIgnoreVisibility = mLauncher.isWorkspaceLoading() + || mPredictedItems.equals(items.items) + || mHotseat.getShortcutsAndWidgets().getChildCount() < mHotSeatItemsCount; + if (!shouldIgnoreVisibility && mHotseat.isShown() && mHotseat.getWindowVisibility() == View.VISIBLE) { mHotseat.setOnVisibilityAggregatedCallback((isVisible) -> { diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java index 5181a86960..f73e2f2f0a 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NavBarToHomeTouchController.java @@ -17,6 +17,8 @@ package com.android.launcher3.uioverrides.touchcontrollers; import static com.android.launcher3.AbstractFloatingView.TYPE_ALL; import static com.android.launcher3.AbstractFloatingView.TYPE_ALL_APPS_EDU; +import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS; +import static com.android.launcher3.LauncherAnimUtils.newCancelListener; import static com.android.launcher3.LauncherState.ALL_APPS; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.allapps.AllAppsTransitionController.ALL_APPS_PROGRESS; @@ -24,7 +26,6 @@ import static com.android.launcher3.anim.Interpolators.DEACCEL_3; import static com.android.launcher3.config.FeatureFlags.ENABLE_ALL_APPS_EDU; import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_HOME_GESTURE; -import static com.android.launcher3.touch.AbstractStateChangeTouchController.SUCCESS_TRANSITION_PROGRESS; import static com.android.systemui.shared.system.ActivityManagerWrapper.CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; import android.animation.ValueAnimator; @@ -186,8 +187,8 @@ public class NavBarToHomeTouchController implements TouchController, if (topView != null) { topView.addHintCloseAnim(mPullbackDistance, PULLBACK_INTERPOLATOR, builder); } - mCurrentAnimation = builder.createPlaybackController() - .setOnCancelRunnable(this::clearState); + mCurrentAnimation = builder.createPlaybackController(); + mCurrentAnimation.getTarget().addListener(newCancelListener(this::clearState)); } private void clearState() { diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java index 2a3bdbfcb8..702c519ace 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonNavbarToOverviewTouchController.java @@ -16,6 +16,7 @@ package com.android.launcher3.uioverrides.touchcontrollers; +import static com.android.launcher3.LauncherAnimUtils.newCancelListener; import static com.android.launcher3.LauncherState.ALL_APPS; import static com.android.launcher3.LauncherState.HINT_STATE; import static com.android.launcher3.LauncherState.NORMAL; @@ -191,15 +192,17 @@ public class NoButtonNavbarToOverviewTouchController extends PortraitStatesTouch return; } mNormalToHintOverviewScrimAnimator = null; - mCurrentAnimation.dispatchOnCancelWithoutCancelRunnable(() -> { + mCurrentAnimation.getTarget().addListener(newCancelListener(() -> mLauncher.getStateManager().goToState(OVERVIEW, true, () -> { mOverviewResistYAnim = AnimatorControllerWithResistance .createRecentsResistanceFromOverviewAnim(mLauncher, null) .createPlaybackController(); mReachedOverview = true; maybeSwipeInteractionToOverviewComplete(); - }); - }); + }))); + + mCurrentAnimation.getTarget().removeListener(mClearStateOnCancelListener); + mCurrentAnimation.dispatchOnCancel(); mStartedOverview = true; VibratorWrapper.INSTANCE.get(mLauncher).vibrate(OVERVIEW_HAPTIC); } diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java index f378848a71..df433f8cc0 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/NoButtonQuickSwitchTouchController.java @@ -15,6 +15,7 @@ */ package com.android.launcher3.uioverrides.touchcontrollers; +import static com.android.launcher3.LauncherAnimUtils.newCancelListener; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.LauncherState.OVERVIEW; import static com.android.launcher3.LauncherState.OVERVIEW_BUTTONS; @@ -44,6 +45,7 @@ import static com.android.quickstep.views.RecentsView.TASK_SECONDARY_TRANSLATION import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_OVERVIEW_DISABLED; import android.animation.Animator; +import android.animation.Animator.AnimatorListener; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.graphics.PointF; @@ -92,6 +94,8 @@ public class NoButtonQuickSwitchTouchController implements TouchController, private final MotionPauseDetector mMotionPauseDetector; private final float mMotionPauseMinDisplacement; private final LauncherRecentsView mRecentsView; + protected final AnimatorListener mClearStateOnCancelListener = + newCancelListener(this::clearState); private boolean mNoIntercept; private LauncherState mStartState; @@ -204,8 +208,8 @@ public class NoButtonQuickSwitchTouchController implements TouchController, config.duration = (long) (Math.max(mXRange, mYRange) * 2); config.animFlags = config.animFlags | SKIP_OVERVIEW; mNonOverviewAnim = mLauncher.getStateManager() - .createAnimationToNewWorkspace(toState, config) - .setOnCancelRunnable(this::clearState); + .createAnimationToNewWorkspace(toState, config); + mNonOverviewAnim.getTarget().addListener(mClearStateOnCancelListener); } private void setupOverviewAnimators() { @@ -379,7 +383,8 @@ public class NoButtonQuickSwitchTouchController implements TouchController, if (canceled) { // Let the state manager know that the animation didn't go to the target state, // but don't clean up yet (we already clean up when the animation completes). - mNonOverviewAnim.dispatchOnCancelWithoutCancelRunnable(); + mNonOverviewAnim.getTarget().removeListener(mClearStateOnCancelListener); + mNonOverviewAnim.dispatchOnCancel(); } float startProgress = mNonOverviewAnim.getProgressFraction(); float endProgress = canceled ? 0 : 1; diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java index 037d9889fd..3c9b808f2f 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/PortraitStatesTouchController.java @@ -245,29 +245,29 @@ public class PortraitStatesTouchController extends AbstractStateChangeTouchContr config.animFlags = animFlags; config.duration = maxAccuracy; - cancelPendingAnim(); + if (mCurrentAnimation != null) { + mCurrentAnimation.getTarget().removeListener(mClearStateOnCancelListener); + mCurrentAnimation.dispatchOnCancel(); + } + mGoingBetweenStates = true; if (mFromState == OVERVIEW && mToState == NORMAL && mOverviewPortraitStateTouchHelper.shouldSwipeDownReturnToApp()) { // Reset the state manager, when changing the interaction mode mLauncher.getStateManager().goToState(OVERVIEW, false /* animate */); - mPendingAnimation = mOverviewPortraitStateTouchHelper - .createSwipeDownToTaskAppAnimation(maxAccuracy, Interpolators.LINEAR); - Runnable onCancelRunnable = () -> { - cancelPendingAnim(); - clearState(); - }; - mCurrentAnimation = mPendingAnimation.createPlaybackController() - .setOnCancelRunnable(onCancelRunnable); + mGoingBetweenStates = false; + mCurrentAnimation = mOverviewPortraitStateTouchHelper + .createSwipeDownToTaskAppAnimation(maxAccuracy, Interpolators.LINEAR) + .createPlaybackController(); mLauncher.getStateManager().setCurrentUserControlledAnimation(mCurrentAnimation); RecentsView recentsView = mLauncher.getOverviewPanel(); totalShift = LayoutUtils.getShelfTrackingDistance(mLauncher, mLauncher.getDeviceProfile(), recentsView.getPagedOrientationHandler()); } else { mCurrentAnimation = mLauncher.getStateManager() - .createAnimationToNewWorkspace(mToState, config) - .setOnCancelRunnable(this::clearState); + .createAnimationToNewWorkspace(mToState, config); } + mCurrentAnimation.getTarget().addListener(mClearStateOnCancelListener); if (totalShift == 0) { totalShift = Math.signum(mFromState.ordinal - mToState.ordinal) @@ -276,13 +276,6 @@ public class PortraitStatesTouchController extends AbstractStateChangeTouchContr return 1 / totalShift; } - private void cancelPendingAnim() { - if (mPendingAnimation != null) { - mPendingAnimation.finish(false); - mPendingAnimation = null; - } - } - @Override protected void updateSwipeCompleteAnimation(ValueAnimator animator, long expectedDuration, LauncherState targetState, float velocity, boolean isFling) { diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java index d39b6f5d97..fc9e1bbfe5 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/QuickSwitchTouchController.java @@ -106,8 +106,8 @@ public class QuickSwitchTouchController extends AbstractStateChangeTouchControll setupInterpolators(config); config.duration = (long) (getShiftRange() * 2); mCurrentAnimation = mLauncher.getStateManager() - .createAnimationToNewWorkspace(mToState, config) - .setOnCancelRunnable(this::clearState); + .createAnimationToNewWorkspace(mToState, config); + mCurrentAnimation.getTarget().addListener(mClearStateOnCancelListener); mCurrentAnimation.getAnimationPlayer().addUpdateListener(valueAnimator -> updateFullscreenProgress((Float) valueAnimator.getAnimatedValue())); return 1 / getShiftRange(); diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java index 186caf6dc1..97296957fe 100644 --- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java +++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchController.java @@ -16,6 +16,7 @@ package com.android.launcher3.uioverrides.touchcontrollers; import static com.android.launcher3.AbstractFloatingView.TYPE_ACCESSIBLE; +import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS; import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH; import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_NEGATIVE; import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_POSITIVE; @@ -50,16 +51,12 @@ public abstract class TaskViewTouchController extends AnimatorListenerAdapter implements TouchController, SingleAxisSwipeDetector.Listener { - // Progress after which the transition is assumed to be a success in case user does not fling - public static final float SUCCESS_TRANSITION_PROGRESS = 0.5f; - protected final T mActivity; private final SingleAxisSwipeDetector mDetector; private final RecentsView mRecentsView; private final int[] mTempCords = new int[2]; private final boolean mIsRtl; - private PendingAnimation mPendingAnimation; private AnimatorPlaybackController mCurrentAnimation; private boolean mCurrentAnimationIsGoingUp; @@ -200,10 +197,8 @@ public abstract class TaskViewTouchController } if (mCurrentAnimation != null) { mCurrentAnimation.setPlayFraction(0); - } - if (mPendingAnimation != null) { - mPendingAnimation.finish(false); - mPendingAnimation = null; + mCurrentAnimation.getTarget().removeListener(this); + mCurrentAnimation.dispatchOnCancel(); } PagedOrientationHandler orientationHandler = mRecentsView.getPagedOrientationHandler(); @@ -216,15 +211,16 @@ public abstract class TaskViewTouchController // The interpolator controlling the most prominent visual movement. We use this to determine // whether we passed SUCCESS_TRANSITION_PROGRESS. final Interpolator currentInterpolator; + PendingAnimation pa; if (goingUp) { currentInterpolator = Interpolators.LINEAR; - mPendingAnimation = mRecentsView.createTaskDismissAnimation(mTaskBeingDragged, + pa = mRecentsView.createTaskDismissAnimation(mTaskBeingDragged, true /* animateTaskView */, true /* removeTask */, maxDuration); mEndDisplacement = -secondaryTaskDimension; } else { currentInterpolator = Interpolators.ZOOM_IN; - mPendingAnimation = mRecentsView.createTaskLaunchAnimation( + pa = mRecentsView.createTaskLaunchAnimation( mTaskBeingDragged, maxDuration, currentInterpolator); // Since the thumbnail is what is filling the screen, based the end displacement on it. @@ -234,12 +230,8 @@ public abstract class TaskViewTouchController mEndDisplacement = secondaryLayerDimension - mTempCords[1]; } mEndDisplacement *= verticalFactor; + mCurrentAnimation = pa.createPlaybackController(); - if (mCurrentAnimation != null) { - mCurrentAnimation.setOnCancelRunnable(null); - } - mCurrentAnimation = mPendingAnimation.createPlaybackController() - .setOnCancelRunnable(this::clearState); // Setting this interpolator doesn't affect the visual motion, but is used to determine // whether we successfully reached the target state in onDragEnd(). mCurrentAnimation.getTarget().setInterpolator(currentInterpolator); @@ -303,27 +295,15 @@ public abstract class TaskViewTouchController animationDuration *= LauncherAnimUtils.blockedFlingDurationFactor(velocity); } - mCurrentAnimation.setEndAction(() -> onCurrentAnimationEnd(goingToEnd)); + mCurrentAnimation.setEndAction(this::clearState); mCurrentAnimation.startWithVelocity(mActivity, goingToEnd, velocity, mEndDisplacement, animationDuration); } - private void onCurrentAnimationEnd(boolean wasSuccess) { - if (mPendingAnimation != null) { - mPendingAnimation.finish(wasSuccess); - mPendingAnimation = null; - } - clearState(); - } - private void clearState() { mDetector.finishedScrolling(); mDetector.setDetectableScrollConditions(0, false); mTaskBeingDragged = null; mCurrentAnimation = null; - if (mPendingAnimation != null) { - mPendingAnimation.finish(false); - mPendingAnimation = null; - } } } diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java index 62f60a726c..c59c045991 100644 --- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java +++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java @@ -124,7 +124,7 @@ public abstract class AbsSwipeUpHandler, Q extends RecentsAnimationCallbacks.RecentsAnimationListener { private static final String TAG = "AbsSwipeUpHandler"; - private static final String[] STATE_NAMES = DEBUG_STATES ? new String[16] : null; + private static final String[] STATE_NAMES = DEBUG_STATES ? new String[17] : null; protected final BaseActivityInterface mActivityInterface; protected final InputConsumerProxy mInputConsumerProxy; @@ -186,6 +186,8 @@ public abstract class AbsSwipeUpHandler, Q extends getFlagForIndex(14, "STATE_START_NEW_TASK"); private static final int STATE_CURRENT_TASK_FINISHED = getFlagForIndex(15, "STATE_CURRENT_TASK_FINISHED"); + private static final int STATE_FINISH_WITH_NO_END = + getFlagForIndex(16, "STATE_FINISH_WITH_NO_END"); private static final int LAUNCHER_UI_STATES = STATE_LAUNCHER_PRESENT | STATE_LAUNCHER_DRAWN | STATE_LAUNCHER_STARTED; @@ -307,6 +309,8 @@ public abstract class AbsSwipeUpHandler, Q extends this::invalidateHandlerWithLauncher); mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED | STATE_RESUME_LAST_TASK, this::notifyTransitionCancelled); + mStateCallback.runOnceAtState(STATE_HANDLER_INVALIDATED | STATE_FINISH_WITH_NO_END, + this::notifyTransitionCancelled); if (!ENABLE_QUICKSTEP_LIVE_TILE.get()) { mStateCallback.addChangeListener(STATE_APP_CONTROLLER_RECEIVED | STATE_LAUNCHER_PRESENT @@ -379,7 +383,7 @@ public abstract class AbsSwipeUpHandler, Q extends if (mStateCallback.hasStates(STATE_HANDLER_INVALIDATED)) { return; } - mTaskViewSimulator.setRecentsRotation(mActivity.getDisplay().getRotation()); + mTaskViewSimulator.setOrientationState(mRecentsView.getPagedViewOrientedState()); // If we've already ended the gesture and are going home, don't prepare recents UI, // as that will set the state as BACKGROUND_APP, overriding the animation to NORMAL. @@ -684,6 +688,7 @@ public abstract class AbsSwipeUpHandler, Q extends dp.updateInsets(targets.homeContentInsets); dp.updateIsSeascape(mContext); initTransitionEndpoints(dp); + mTaskViewSimulator.getOrientationState().setMultiWindowMode(dp.isMultiWindowMode); } // Notify when the animation starts @@ -1018,7 +1023,7 @@ public abstract class AbsSwipeUpHandler, Q extends protected abstract HomeAnimationFactory createHomeAnimationFactory(long duration); - private TaskStackChangeListener mActivityRestartListener = new TaskStackChangeListener() { + private final TaskStackChangeListener mActivityRestartListener = new TaskStackChangeListener() { @Override public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { @@ -1254,6 +1259,7 @@ public abstract class AbsSwipeUpHandler, Q extends if (mGestureState.getEndTarget() != null && !mGestureState.isRunningAnimationToLauncher()) { cancelCurrentAnimation(); } else { + mStateCallback.setStateOnUiThread(STATE_FINISH_WITH_NO_END); reset(); } } @@ -1457,13 +1463,33 @@ public abstract class AbsSwipeUpHandler, Q extends protected abstract void finishRecentsControllerToHome(Runnable callback); + private final TaskStackChangeListener mLiveTileRestartListener = new TaskStackChangeListener() { + @Override + public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, + boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { + if (mRecentsAnimationTargets.hasTask(task.taskId)) { + launchOtherTaskInLiveTileMode(task.taskId, mRecentsAnimationTargets.apps); + } + ActivityManagerWrapper.getInstance().unregisterTaskStackListener( + mLiveTileRestartListener); + } + }; + private void setupLauncherUiAfterSwipeUpToRecentsAnimation() { endLauncherTransitionController(); mActivityInterface.onSwipeUpToRecentsComplete(); mRecentsView.onSwipeUpAnimationSuccess(); if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { mTaskAnimationManager.setLaunchOtherTaskInLiveTileModeHandler( - this::launchOtherTaskInLiveTileMode); + appearedTaskTarget -> { + RemoteAnimationTargetCompat[] apps = Arrays.copyOf( + mRecentsAnimationTargets.apps, + mRecentsAnimationTargets.apps.length + 1); + apps[apps.length - 1] = appearedTaskTarget; + launchOtherTaskInLiveTileMode(appearedTaskTarget.taskId, apps); + }); + ActivityManagerWrapper.getInstance().registerTaskStackListener( + mLiveTileRestartListener); } SystemUiProxy.INSTANCE.get(mContext).onOverviewShown(false, TAG); @@ -1471,17 +1497,12 @@ public abstract class AbsSwipeUpHandler, Q extends reset(); } - private void launchOtherTaskInLiveTileMode(RemoteAnimationTargetCompat appearedTaskTarget) { - TaskView taskView = mRecentsView.getTaskView(appearedTaskTarget.taskId); + private void launchOtherTaskInLiveTileMode(int taskId, RemoteAnimationTargetCompat[] apps) { + TaskView taskView = mRecentsView.getTaskView(taskId); if (taskView == null) { return; } - RemoteAnimationTargetCompat[] apps = Arrays.copyOf( - mRecentsAnimationTargets.apps, - mRecentsAnimationTargets.apps.length + 1); - apps[apps.length - 1] = appearedTaskTarget; - AnimatorSet anim = new AnimatorSet(); TaskViewUtils.composeRecentsLaunchAnimator( anim, taskView, apps, diff --git a/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java b/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java index 55f542461c..efd4530c14 100644 --- a/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java +++ b/quickstep/src/com/android/quickstep/AppToOverviewAnimationProvider.java @@ -132,9 +132,8 @@ final class AppToOverviewAnimationProvider> extend TaskViewSimulator tsv = new TaskViewSimulator(mActivity, mRecentsView.getSizeStrategy()); tsv.setDp(mActivity.getDeviceProfile()); + tsv.setOrientationState(mRecentsView.getPagedViewOrientedState()); tsv.setPreview(runningTaskTarget); - tsv.setLayoutRotation(mRecentsView.getPagedViewOrientedState().getTouchRotation(), - mRecentsView.getPagedViewOrientedState().getDisplayRotation()); TransformParams params = new TransformParams() .setTargetSet(targets) diff --git a/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java index 718c5baa2c..da0a66431e 100644 --- a/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java +++ b/quickstep/src/com/android/quickstep/RecentsAnimationTargets.java @@ -41,4 +41,13 @@ public class RecentsAnimationTargets extends RemoteAnimationTargets { public boolean hasTargets() { return unfilteredApps.length != 0; } + + public boolean hasTask(int taskId) { + for (RemoteAnimationTargetCompat target : unfilteredApps) { + if (target.taskId == taskId) { + return true; + } + } + return false; + } } diff --git a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java index 8f7ec3bfa0..4bb1bb565e 100644 --- a/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java +++ b/quickstep/src/com/android/quickstep/SwipeUpAnimationLogic.java @@ -79,7 +79,7 @@ public abstract class SwipeUpAnimationLogic { mTaskViewSimulator = new TaskViewSimulator(context, gestureState.getActivityInterface()); mTransformParams = transformParams; - mTaskViewSimulator.setLayoutRotation( + mTaskViewSimulator.getOrientationState().update( mDeviceState.getRotationTouchHelper().getCurrentActiveRotation(), mDeviceState.getRotationTouchHelper().getDisplayRotation()); mTaskViewSimulator.setDrawsBelowRecents(true); diff --git a/quickstep/src/com/android/quickstep/TaskViewUtils.java b/quickstep/src/com/android/quickstep/TaskViewUtils.java index ed761cf640..2aed76afcb 100644 --- a/quickstep/src/com/android/quickstep/TaskViewUtils.java +++ b/quickstep/src/com/android/quickstep/TaskViewUtils.java @@ -179,16 +179,17 @@ public final class TaskViewUtils { Context context = v.getContext(); DeviceProfile dp = BaseActivity.fromContext(context).getDeviceProfile(); - // RecentsView never updates the display rotation until swipe-up so the value may be stale. - // Use the display value instead. - int displayRotation = DisplayController.getDefaultDisplay(context).getInfo().rotation; - TaskViewSimulator topMostSimulator = null; if (tsv == null && targets.apps.length > 0) { tsv = new TaskViewSimulator(context, recentsView.getSizeStrategy()); tsv.setDp(dp); - tsv.setLayoutRotation(displayRotation, displayRotation); + + // RecentsView never updates the display rotation until swipe-up so the value may + // be stale. Use the display value instead. + int displayRotation = DisplayController.getDefaultDisplay(context).getInfo().rotation; + tsv.getOrientationState().update(displayRotation, displayRotation); + tsv.setPreview(targets.apps[targets.apps.length - 1]); tsv.fullScreenProgress.value = 0; tsv.recentsViewScale.value = 1; diff --git a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java index bb84380edd..e273aeb81f 100644 --- a/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java +++ b/quickstep/src/com/android/quickstep/util/RecentsOrientedState.java @@ -72,7 +72,6 @@ public final class RecentsOrientedState implements SharedPreferences.OnSharedPre private static final String TAG = "RecentsOrientedState"; private static final boolean DEBUG = false; - private static final String DELIMITER_DOT = "\\."; private ContentObserver mSystemAutoRotateObserver = newContentObserver(new Handler(), t -> updateAutoRotateSetting()); @@ -129,6 +128,9 @@ public final class RecentsOrientedState implements SharedPreferences.OnSharedPre private int mFlags; private int mPreviousRotation = ROTATION_0; + // Combined int which encodes the full state. + private int mStateId = 0; + /** * @param rotationChangeListener Callback for receiving rotation events when rotation watcher * is enabled @@ -169,7 +171,7 @@ public final class RecentsOrientedState implements SharedPreferences.OnSharedPre */ public boolean setRecentsRotation(@SurfaceRotation int recentsRotation) { mRecentsRotation = recentsRotation; - return update(mTouchRotation, mDisplayRotation); + return updateHandler(); } /** @@ -183,8 +185,7 @@ public final class RecentsOrientedState implements SharedPreferences.OnSharedPre * Sets if the swipe up gesture is currently running or not */ public boolean setGestureActive(boolean isGestureActive) { - setFlag(FLAG_SWIPE_UP_NOT_RUNNING, !isGestureActive); - return update(mTouchRotation, mDisplayRotation); + return setFlag(FLAG_SWIPE_UP_NOT_RUNNING, !isGestureActive); } /** @@ -201,14 +202,13 @@ public final class RecentsOrientedState implements SharedPreferences.OnSharedPre mDisplayRotation = displayRotation; mTouchRotation = touchRotation; mPreviousRotation = touchRotation; + return updateHandler(); + } - PagedOrientationHandler oldHandler = mOrientationHandler; + private boolean updateHandler() { if (mRecentsActivityRotation == mTouchRotation || (canRecentsActivityRotate() && (mFlags & FLAG_SWIPE_UP_NOT_RUNNING) != 0)) { mOrientationHandler = PagedOrientationHandler.PORTRAIT; - if (DEBUG) { - Log.d(TAG, "current RecentsOrientedState: " + this); - } } else if (mTouchRotation == ROTATION_90) { mOrientationHandler = PagedOrientationHandler.LANDSCAPE; } else if (mTouchRotation == ROTATION_270) { @@ -219,19 +219,26 @@ public final class RecentsOrientedState implements SharedPreferences.OnSharedPre if (DEBUG) { Log.d(TAG, "current RecentsOrientedState: " + this); } - return oldHandler != mOrientationHandler; + + int oldStateId = mStateId; + // Each SurfaceRotation value takes two bits + mStateId = (((((mFlags << 2) + | mDisplayRotation) << 2) + | mTouchRotation) << 3) + | (mRecentsRotation < 0 ? 7 : mRecentsRotation); + return mStateId != oldStateId; } @SurfaceRotation private int inferRecentsActivityRotation(@SurfaceRotation int displayRotation) { if (isRecentsActivityRotationAllowed()) { - return mRecentsRotation < ROTATION_0 ? displayRotation : mRecentsRotation; + return mRecentsRotation < 0 ? displayRotation : mRecentsRotation; } else { return ROTATION_0; } } - private void setFlag(int mask, boolean enabled) { + private boolean setFlag(int mask, boolean enabled) { boolean wasRotationEnabled = !TestProtocol.sDisableSensorRotation && (mFlags & VALUE_ROTATION_WATCHER_ENABLED) == VALUE_ROTATION_WATCHER_ENABLED && !canRecentsActivityRotate(); @@ -253,6 +260,7 @@ public final class RecentsOrientedState implements SharedPreferences.OnSharedPre } }); } + return updateHandler(); } @Override @@ -327,6 +335,13 @@ public final class RecentsOrientedState implements SharedPreferences.OnSharedPre return mRecentsActivityRotation; } + /** + * Returns an id that can be used to tracking internal changes + */ + public int getStateId() { + return mStateId; + } + public boolean isMultipleOrientationSupportedByDevice() { return (mFlags & MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE) == MASK_MULTIPLE_ORIENTATION_SUPPORTED_BY_DEVICE; @@ -509,14 +524,15 @@ public final class RecentsOrientedState implements SharedPreferences.OnSharedPre public String toString() { boolean systemRotationOn = (mFlags & FLAG_SYSTEM_ROTATION_ALLOWED) != 0; return "[" - + "this=" + extractObjectNameAndAddress(super.toString()) - + " mOrientationHandler=" + - extractObjectNameAndAddress(mOrientationHandler.toString()) + + "this=" + nameAndAddress(this) + + " mOrientationHandler=" + nameAndAddress(mOrientationHandler) + " mDisplayRotation=" + mDisplayRotation + " mTouchRotation=" + mTouchRotation + " mRecentsActivityRotation=" + mRecentsActivityRotation + + " mRecentsRotation=" + mRecentsRotation + " isRecentsActivityRotationAllowed=" + isRecentsActivityRotationAllowed() + " mSystemRotation=" + systemRotationOn + + " mStateId=" + mStateId + " mFlags=" + mFlags + "]"; } @@ -533,13 +549,7 @@ public final class RecentsOrientedState implements SharedPreferences.OnSharedPre : idp.portraitProfile; } - /** - * String conversion for only the helpful parts of {@link Object#toString()} method - * @param stringToExtract "foo.bar.baz.MyObject@1234" - * @return "MyObject@1234" - */ - private static String extractObjectNameAndAddress(String stringToExtract) { - int index = stringToExtract.lastIndexOf(DELIMITER_DOT); - return index >= 0 ? stringToExtract.substring(index) : ""; + private static String nameAndAddress(Object obj) { + return obj.getClass().getSimpleName() + "@" + obj.hashCode(); } } diff --git a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java index 61ba411363..2f4bb8ed9d 100644 --- a/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java +++ b/quickstep/src/com/android/quickstep/util/TaskViewSimulator.java @@ -23,7 +23,6 @@ import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_ import android.animation.TimeInterpolator; import android.content.Context; -import android.content.res.Configuration; import android.graphics.Matrix; import android.graphics.Point; import android.graphics.PointF; @@ -31,6 +30,8 @@ import android.graphics.Rect; import android.graphics.RectF; import android.util.IntProperty; +import androidx.annotation.NonNull; + import com.android.launcher3.DeviceProfile; import com.android.launcher3.Utilities; import com.android.launcher3.anim.PendingAnimation; @@ -67,10 +68,12 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { private final RectF mTempRectF = new RectF(); private final float[] mTempPoint = new float[2]; - private final RecentsOrientedState mOrientationState; private final Context mContext; private final BaseActivityInterface mSizeStrategy; + @NonNull + private RecentsOrientedState mOrientationState; + private final Rect mTaskRect = new Rect(); private boolean mDrawsBelowRecents; private final PointF mPivot = new PointF(); @@ -100,6 +103,7 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { // Cached calculations private boolean mLayoutValid = false; private boolean mScrollValid = false; + private int mOrientationStateId; public TaskViewSimulator(Context context, BaseActivityInterface sizeStrategy) { mContext = context; @@ -107,8 +111,8 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { mOrientationState = new RecentsOrientedState(context, sizeStrategy, i -> { }); mOrientationState.setGestureActive(true); - mCurrentFullscreenParams = new FullscreenDrawParams(context); + mOrientationStateId = mOrientationState.getStateId(); } /** @@ -116,23 +120,14 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { */ public void setDp(DeviceProfile dp) { mDp = dp; - mOrientationState.setMultiWindowMode(mDp.isMultiWindowMode); mLayoutValid = false; } /** - * @see com.android.quickstep.views.RecentsView#setLayoutRotation(int, int) + * Sets the orientation state used for this animation */ - public void setLayoutRotation(int touchRotation, int displayRotation) { - mOrientationState.update(touchRotation, displayRotation); - mLayoutValid = false; - } - - /** - * @see com.android.quickstep.views.RecentsView#onConfigurationChanged(Configuration) - */ - public void setRecentsRotation(int recentsRotation) { - mOrientationState.setRecentsRotation(recentsRotation); + public void setOrientationState(@NonNull RecentsOrientedState orientationState) { + mOrientationState = orientationState; mLayoutValid = false; } @@ -251,8 +246,9 @@ public class TaskViewSimulator implements TransformParams.BuilderProxy { if (mDp == null || mThumbnailPosition.isEmpty()) { return; } - if (!mLayoutValid) { + if (!mLayoutValid || mOrientationStateId != mOrientationState.getStateId()) { mLayoutValid = true; + mOrientationStateId = mOrientationState.getStateId(); getFullScreenScale(); mThumbnailData.rotation = mOrientationState.getDisplayRotation(); diff --git a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java index 95bde801bf..6630aede3d 100644 --- a/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java +++ b/quickstep/src/com/android/quickstep/views/DigitalWellBeingToast.java @@ -17,6 +17,8 @@ package com.android.quickstep.views; import static android.provider.Settings.ACTION_APP_USAGE_SETTINGS; +import static android.view.Gravity.BOTTOM; +import static android.view.Gravity.CENTER_HORIZONTAL; import static com.android.launcher3.Utilities.prefixTextWithIcon; import static com.android.launcher3.util.Executors.THREAD_POOL_EXECUTOR; @@ -27,6 +29,7 @@ import android.content.ActivityNotFoundException; import android.content.Intent; import android.content.pm.LauncherApps; import android.content.pm.LauncherApps.AppUsageLimit; +import android.graphics.Outline; import android.icu.text.MeasureFormat; import android.icu.text.MeasureFormat.FormatWidth; import android.icu.util.Measure; @@ -35,6 +38,9 @@ import android.os.Build; import android.os.UserHandle; import android.util.Log; import android.view.View; +import android.view.ViewGroup; +import android.view.ViewOutlineProvider; +import android.widget.FrameLayout; import android.widget.TextView; import androidx.annotation.StringRes; @@ -61,6 +67,10 @@ public final class DigitalWellBeingToast { private Task mTask; private boolean mHasLimit; private long mAppRemainingTimeMs; + private View mBanner; + private ViewOutlineProvider mOldBannerOutlineProvider; + private float mBannerOffsetPercentage; + private float mBannerAlpha = 1f; public DigitalWellBeingToast(BaseDraggingActivity activity, TaskView taskView) { mActivity = activity; @@ -68,18 +78,10 @@ public final class DigitalWellBeingToast { mLauncherApps = activity.getSystemService(LauncherApps.class); } - private void setTaskFooter(View view) { - View oldFooter = mTaskView.setFooter(TaskView.INDEX_DIGITAL_WELLBEING_TOAST, view); - if (oldFooter != null) { - oldFooter.setOnClickListener(null); - mActivity.getViewCache().recycleView(R.layout.digital_wellbeing_toast, oldFooter); - } - } - private void setNoLimit() { mHasLimit = false; mTaskView.setContentDescription(mTask.titleDescription); - setTaskFooter(null); + replaceBanner(null); mAppRemainingTimeMs = 0; } @@ -90,7 +92,7 @@ public final class DigitalWellBeingToast { mActivity, mTaskView); toast.setText(prefixTextWithIcon(mActivity, R.drawable.ic_hourglass_top, getText())); toast.setOnClickListener(this::openAppUsageSettings); - setTaskFooter(toast); + replaceBanner(toast); mTaskView.setContentDescription( getContentDescriptionForTask(mTask, appUsageLimitTimeMs, appRemainingTimeMs)); @@ -233,4 +235,64 @@ public final class DigitalWellBeingToast { getText(appRemainingTimeMs)) : task.titleDescription; } + + private void replaceBanner(View view) { + resetOldBanner(); + setBanner(view); + } + + private void resetOldBanner() { + if (mBanner != null) { + mBanner.setOutlineProvider(mOldBannerOutlineProvider); + mTaskView.removeView(mBanner); + mBanner.setOnClickListener(null); + mActivity.getViewCache().recycleView(R.layout.digital_wellbeing_toast, mBanner); + } + } + + private void setBanner(View view) { + mBanner = view; + if (view != null) { + setupAndAddBanner(); + setBannerOutline(); + } + } + + private void setupAndAddBanner() { + FrameLayout.LayoutParams layoutParams = + (FrameLayout.LayoutParams) mBanner.getLayoutParams(); + layoutParams.gravity = BOTTOM | CENTER_HORIZONTAL; + layoutParams.bottomMargin = ((ViewGroup.MarginLayoutParams) + mTaskView.getThumbnail().getLayoutParams()).bottomMargin; + mBanner.setTranslationY(mBannerOffsetPercentage * mBanner.getHeight()); + mBanner.setAlpha(mBannerAlpha); + mTaskView.addView(mBanner); + } + + private void setBannerOutline() { + mOldBannerOutlineProvider = mBanner.getOutlineProvider(); + mBanner.setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + mOldBannerOutlineProvider.getOutline(view, outline); + outline.offset(0, -Math.round(view.getTranslationY())); + } + }); + mBanner.setClipToOutline(true); + } + + void updateBannerOffset(float offsetPercentage) { + if (mBanner != null && mBannerOffsetPercentage != offsetPercentage) { + mBannerOffsetPercentage = offsetPercentage; + mBanner.setTranslationY(offsetPercentage * mBanner.getHeight()); + mBanner.invalidateOutline(); + } + } + + void updateBannerAlpha(float alpha) { + if (mBanner != null && mBannerAlpha != alpha) { + mBannerAlpha = alpha; + mBanner.setAlpha(alpha); + } + } } diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index f35eb1fa5d..f281296ee2 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -22,6 +22,7 @@ import static android.view.View.MeasureSpec.makeMeasureSpec; import static com.android.launcher3.BaseActivity.STATE_HANDLER_INVISIBILITY_FLAGS; import static com.android.launcher3.InvariantDeviceProfile.CHANGE_FLAG_ICON_PARAMS; +import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS; import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA; import static com.android.launcher3.LauncherState.BACKGROUND_APP; import static com.android.launcher3.LauncherState.OVERVIEW_MODAL_TASK; @@ -39,7 +40,6 @@ import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCH import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_DISMISS_SWIPE_UP; import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_TASK_LAUNCH_SWIPE_DOWN; import static com.android.launcher3.statehandlers.DepthController.DEPTH; -import static com.android.launcher3.uioverrides.touchcontrollers.TaskViewTouchController.SUCCESS_TRANSITION_PROGRESS; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; import static com.android.launcher3.util.SystemUiController.UI_STATE_OVERVIEW; import static com.android.quickstep.TaskUtils.checkCurrentOrManagedUserId; @@ -89,6 +89,7 @@ import android.widget.ListView; import androidx.annotation.Nullable; import com.android.launcher3.BaseActivity; +import com.android.launcher3.BaseActivity.MultiWindowModeChangedListener; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Insettable; import com.android.launcher3.InvariantDeviceProfile; @@ -99,7 +100,6 @@ import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimationSuccessListener; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.PendingAnimation; -import com.android.launcher3.anim.PendingAnimation.EndState; import com.android.launcher3.anim.SpringProperty; import com.android.launcher3.compat.AccessibilityManagerCompat; import com.android.launcher3.config.FeatureFlags; @@ -240,7 +240,7 @@ public abstract class RecentsView extends PagedView } }; - protected RecentsOrientedState mOrientationState; + protected final RecentsOrientedState mOrientationState; protected final BaseActivityInterface mSizeStrategy; protected RecentsAnimationController mRecentsAnimationController; protected SurfaceTransactionApplier mSyncTransactionApplier; @@ -409,17 +409,18 @@ public abstract class RecentsView extends PagedView private int mTaskViewStartIndex = 0; private OverviewActionsView mActionsView; - private BaseActivity.MultiWindowModeChangedListener mMultiWindowModeChangedListener = - (inMultiWindowMode) -> { - if (mOrientationState != null) { + private MultiWindowModeChangedListener mMultiWindowModeChangedListener = + new MultiWindowModeChangedListener() { + @Override + public void onMultiWindowModeChanged(boolean inMultiWindowMode) { mOrientationState.setMultiWindowMode(inMultiWindowMode); setLayoutRotation(mOrientationState.getTouchRotation(), mOrientationState.getDisplayRotation()); updateChildTaskOrientations(); - } - if (!inMultiWindowMode && mOverviewStateEnabled) { - // TODO: Re-enable layout transitions for addition of the unpinned task - reloadIfNeeded(); + if (!inMultiWindowMode && mOverviewStateEnabled) { + // TODO: Re-enable layout transitions for addition of the unpinned task + reloadIfNeeded(); + } } }; @@ -477,9 +478,7 @@ public abstract class RecentsView extends PagedView mLiveTileTaskViewSimulator = new TaskViewSimulator(getContext(), getSizeStrategy()); mLiveTileTaskViewSimulator.recentsViewScale.value = 1; - mLiveTileTaskViewSimulator.setLayoutRotation(getPagedViewOrientedState().getTouchRotation(), - getPagedViewOrientedState().getDisplayRotation()); - mLiveTileTaskViewSimulator.setRecentsRotation(rotation); + mLiveTileTaskViewSimulator.setOrientationState(mOrientationState); mLiveTileTaskViewSimulator.setDrawsBelowRecents(true); } @@ -772,7 +771,7 @@ public abstract class RecentsView extends PagedView protected void applyLoadPlan(ArrayList tasks) { if (mPendingAnimation != null) { - mPendingAnimation.addEndListener((endState) -> applyLoadPlan(tasks)); + mPendingAnimation.addEndListener(success -> applyLoadPlan(tasks)); return; } @@ -1464,19 +1463,10 @@ public abstract class RecentsView extends PagedView verticalFactor * secondaryTaskDimension).setDuration(duration), LINEAR, sp); } - private void removeTask(TaskView taskView, int index, EndState endState) { - if (taskView.getTask() != null) { - UI_HELPER_EXECUTOR.execute(() -> - ActivityManagerWrapper.getInstance().removeTask(taskView.getTask().key.id)); - mActivity.getStatsLogManager().logger().withItemInfo(taskView.getItemInfo()) - .log(LAUNCHER_TASK_DISMISS_SWIPE_UP); - } - } - public PendingAnimation createTaskDismissAnimation(TaskView taskView, boolean animateTaskView, boolean shouldRemoveTask, long duration) { if (mPendingAnimation != null) { - mPendingAnimation.finish(false); + mPendingAnimation.createPlaybackController().dispatchOnCancel(); } PendingAnimation anim = new PendingAnimation(duration); @@ -1559,22 +1549,27 @@ public abstract class RecentsView extends PagedView } mPendingAnimation = anim; - mPendingAnimation.addEndListener(new Consumer() { + mPendingAnimation.addEndListener(new Consumer() { @Override - public void accept(EndState endState) { - if (ENABLE_QUICKSTEP_LIVE_TILE.get() && - taskView.isRunningTask() && endState.isSuccess) { - finishRecentsAnimation(true /* toHome */, () -> onEnd(endState)); + public void accept(Boolean success) { + if (ENABLE_QUICKSTEP_LIVE_TILE.get() && taskView.isRunningTask() && success) { + finishRecentsAnimation(true /* toHome */, () -> onEnd(success)); } else { - onEnd(endState); + onEnd(success); } } @SuppressWarnings("WrongCall") - private void onEnd(EndState endState) { - if (endState.isSuccess) { + private void onEnd(boolean success) { + if (success) { if (shouldRemoveTask) { - removeTask(taskView, draggedIndex, endState); + if (taskView.getTask() != null) { + UI_HELPER_EXECUTOR.execute(() -> ActivityManagerWrapper.getInstance() + .removeTask(taskView.getTask().key.id)); + mActivity.getStatsLogManager().logger() + .withItemInfo(taskView.getItemInfo()) + .log(LAUNCHER_TASK_DISMISS_SWIPE_UP); + } } int pageToSnapTo = mCurrentPage; @@ -1613,8 +1608,8 @@ public abstract class RecentsView extends PagedView } mPendingAnimation = anim; - mPendingAnimation.addEndListener((endState) -> { - if (endState.isSuccess) { + mPendingAnimation.addEndListener(isSuccess -> { + if (isSuccess) { // Remove all the task views now UI_HELPER_EXECUTOR.execute( ActivityManagerWrapper.getInstance()::removeAllRecentTasks); @@ -1642,7 +1637,6 @@ public abstract class RecentsView extends PagedView protected void runDismissAnimation(PendingAnimation pendingAnim) { AnimatorPlaybackController controller = pendingAnim.createPlaybackController(); controller.dispatchOnStart(); - controller.setEndAction(() -> pendingAnim.finish(true)); controller.getAnimationPlayer().setInterpolator(FAST_OUT_SLOW_IN); controller.start(); } @@ -1769,7 +1763,7 @@ public abstract class RecentsView extends PagedView if (mOrientationState.setRecentsRotation(rotation)) { updateOrientationHandler(); } - mLiveTileTaskViewSimulator.setRecentsRotation(rotation); + // If overview is in modal state when rotate, reset it to overview state without running // animation. if (mActivity.isInState(OVERVIEW_MODAL_TASK)) { @@ -1804,8 +1798,6 @@ public abstract class RecentsView extends PagedView requestLayout(); // Reapply the current page to update page scrolls. setCurrentPage(mCurrentPage); - mLiveTileTaskViewSimulator.setLayoutRotation(getPagedViewOrientedState().getTouchRotation(), - getPagedViewOrientedState().getDisplayRotation()); } public RecentsOrientedState getPagedViewOrientedState() { @@ -2186,8 +2178,8 @@ public abstract class RecentsView extends PagedView mLiveTileTaskViewSimulator.addOverviewToAppAnim(mPendingAnimation, interpolator); mPendingAnimation.addOnFrameCallback(this::redrawLiveTile); } - mPendingAnimation.addEndListener((endState) -> { - if (endState.isSuccess) { + mPendingAnimation.addEndListener(isSuccess -> { + if (isSuccess) { Consumer onLaunchResult = (result) -> { onTaskLaunchAnimationEnd(result); if (!result) { diff --git a/quickstep/src/com/android/quickstep/views/TaskView.java b/quickstep/src/com/android/quickstep/views/TaskView.java index 2db3b66df2..8440391f4a 100644 --- a/quickstep/src/com/android/quickstep/views/TaskView.java +++ b/quickstep/src/com/android/quickstep/views/TaskView.java @@ -42,7 +42,6 @@ import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; -import android.animation.ValueAnimator; import android.app.ActivityOptions; import android.content.Context; import android.content.Intent; @@ -72,7 +71,6 @@ import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.Interpolators; -import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.popup.SystemShortcut; import com.android.launcher3.statemanager.StatefulActivity; @@ -194,12 +192,6 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable { private boolean mEndQuickswitchCuj; - // Order in which the footers appear. Lower order appear below higher order. - public static final int INDEX_DIGITAL_WELLBEING_TOAST = 0; - private final FooterWrapper[] mFooters = new FooterWrapper[2]; - private float mFooterVerticalOffset = 0; - private float mFooterAlpha = 1; - private int mStackHeight; private View mContextualChipWrapper; private View mContextualChip; private final float[] mIconCenterCoords = new float[2]; @@ -340,8 +332,7 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable { if (mContextualChipWrapper != null) { mContextualChipWrapper.setAlpha(comp(modalness)); } - - updateFooterVerticalOffset(mFooterVerticalOffset); + mDigitalWellBeingToast.updateBannerOffset(modalness); } public TaskMenuView getMenuView() { @@ -378,11 +369,9 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable { } public AnimatorPlaybackController createLaunchAnimationForRunningTask() { - final PendingAnimation pendingAnimation = getRecentsView().createTaskLaunchAnimation( - this, RECENTS_LAUNCH_DURATION, TOUCH_RESPONSE_INTERPOLATOR); - AnimatorPlaybackController currentAnimation = pendingAnimation.createPlaybackController(); - currentAnimation.setEndAction(() -> pendingAnimation.finish(true)); - return currentAnimation; + return getRecentsView().createTaskLaunchAnimation( + this, RECENTS_LAUNCH_DURATION, TOUCH_RESPONSE_INTERPOLATOR) + .createPlaybackController(); } public void launchTask(boolean animate) { @@ -560,8 +549,12 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable { .getInterpolation(progress); mIconView.setScaleX(scale); mIconView.setScaleY(scale); - - updateFooterVerticalOffset(1.0f - scale); + if (mContextualChip != null && mContextualChipWrapper != null) { + mContextualChipWrapper.setAlpha(scale); + mContextualChip.setScaleX(scale); + mContextualChip.setScaleY(scale); + } + mDigitalWellBeingToast.updateBannerOffset(1f - scale); } public void setIconScaleAnimStartProgress(float startProgress) { @@ -633,12 +626,9 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable { mSnapshotView.setDimAlpha(curveInterpolation * MAX_PAGE_SCRIM_ALPHA); setCurveScale(curveScaleForCurveInterpolation); - mFooterAlpha = Utilities.boundToRange(1.0f - 2 * scrollState.linearInterpolation, 0f, 1f); - for (FooterWrapper footer : mFooters) { - if (footer != null) { - footer.mView.setAlpha(mFooterAlpha); - } - } + float dwbBannerAlpha = Utilities.boundToRange(1.0f - 2 * scrollState.linearInterpolation, + 0f, 1f); + mDigitalWellBeingToast.updateBannerAlpha(dwbBannerAlpha); if (mMenuView != null) { PagedOrientationHandler pagedOrientationHandler = getPagedOrientationHandler(); @@ -650,57 +640,6 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable { } } - /** - * Sets the footer at the specific index and returns the previously set footer. - */ - public View setFooter(int index, View view) { - View oldFooter = null; - - // If the footer are is already collapsed, do not animate entry - boolean shouldAnimateEntry = mFooterVerticalOffset <= 0; - - if (mFooters[index] != null) { - oldFooter = mFooters[index].mView; - mFooters[index].release(); - removeView(oldFooter); - - // If we are replacing an existing footer, do not animate entry - shouldAnimateEntry = false; - } - if (view != null) { - int indexToAdd = getChildCount(); - for (int i = index - 1; i >= 0; i--) { - if (mFooters[i] != null) { - indexToAdd = indexOfChild(mFooters[i].mView); - break; - } - } - - addView(view, indexToAdd); - LayoutParams layoutParams = (LayoutParams) view.getLayoutParams(); - layoutParams.gravity = BOTTOM | CENTER_HORIZONTAL; - layoutParams.bottomMargin = - ((MarginLayoutParams) mSnapshotView.getLayoutParams()).bottomMargin; - view.setAlpha(mFooterAlpha); - mFooters[index] = new FooterWrapper(view); - if (shouldAnimateEntry) { - mFooters[index].animateEntry(); - } - } else { - mFooters[index] = null; - } - - mStackHeight = 0; - for (FooterWrapper footer : mFooters) { - if (footer != null) { - footer.setVerticalShift(mStackHeight); - mStackHeight += footer.mExpectedHeight; - } - } - - return oldFooter; - } - /** * Sets the contextual chip. * @@ -774,24 +713,6 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable { SYSTEM_GESTURE_EXCLUSION_RECT.get(0).set(0, 0, getWidth(), getHeight()); setSystemGestureExclusionRects(SYSTEM_GESTURE_EXCLUSION_RECT); } - - mStackHeight = 0; - for (FooterWrapper footer : mFooters) { - if (footer != null) { - mStackHeight += footer.mView.getHeight(); - } - } - updateFooterVerticalOffset(0); - } - - private void updateFooterVerticalOffset(float offset) { - mFooterVerticalOffset = offset; - - for (FooterWrapper footer : mFooters) { - if (footer != null) { - footer.updateFooterOffset(); - } - } } public static float getCurveScaleForInterpolation(float linearInterpolation) { @@ -854,71 +775,6 @@ public class TaskView extends FrameLayout implements PageCallbacks, Reusable { } } - private class FooterWrapper extends ViewOutlineProvider { - - final View mView; - final ViewOutlineProvider mOldOutlineProvider; - final ViewOutlineProvider mDelegate; - - final int mExpectedHeight; - final int mOldPaddingBottom; - - int mAnimationOffset = 0; - int mEntryAnimationOffset = 0; - - public FooterWrapper(View view) { - mView = view; - mOldOutlineProvider = view.getOutlineProvider(); - mDelegate = mOldOutlineProvider == null - ? ViewOutlineProvider.BACKGROUND : mOldOutlineProvider; - - mExpectedHeight = getExpectedViewHeight(view); - mOldPaddingBottom = view.getPaddingBottom(); - - if (mOldOutlineProvider != null) { - view.setOutlineProvider(this); - view.setClipToOutline(true); - } - } - - public void setVerticalShift(int shift) { - mView.setPadding(mView.getPaddingLeft(), mView.getPaddingTop(), - mView.getPaddingRight(), mOldPaddingBottom + shift); - } - - @Override - public void getOutline(View view, Outline outline) { - mDelegate.getOutline(view, outline); - outline.offset(0, -mAnimationOffset - mEntryAnimationOffset); - } - - void updateFooterOffset() { - float offset = Utilities.or(mFooterVerticalOffset, mModalness); - mAnimationOffset = Math.round(mStackHeight * offset); - mView.setTranslationY(mAnimationOffset + mEntryAnimationOffset - + mCurrentFullscreenParams.mCurrentDrawnInsets.bottom - + mCurrentFullscreenParams.mCurrentDrawnInsets.top); - mView.invalidateOutline(); - } - - void release() { - mView.setOutlineProvider(mOldOutlineProvider); - setVerticalShift(0); - } - - void animateEntry() { - ValueAnimator animator = ValueAnimator.ofFloat(0, 1); - animator.addUpdateListener(anim -> { - float factor = 1 - anim.getAnimatedFraction(); - int totalShift = mExpectedHeight + mView.getPaddingBottom() - mOldPaddingBottom; - mEntryAnimationOffset = Math.round(factor * totalShift); - updateFooterOffset(); - }); - animator.setDuration(100); - animator.start(); - } - } - private int getExpectedViewHeight(View view) { int expectedHeight; int h = view.getLayoutParams().height; diff --git a/res/layout/search_result_hero_app.xml b/res/layout/search_result_hero_app.xml deleted file mode 100644 index bd0e42b6a9..0000000000 --- a/res/layout/search_result_hero_app.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/res/layout/search_result_icon_row.xml b/res/layout/search_result_icon_row.xml index ef3c8b2857..81c23e4c90 100644 --- a/res/layout/search_result_icon_row.xml +++ b/res/layout/search_result_icon_row.xml @@ -1,4 +1,4 @@ - + android:padding="@dimen/dynamic_grid_edge_margin"> + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/search_result_people_item.xml b/res/layout/search_result_people_item.xml index a603941dfa..7526f6f10f 100644 --- a/res/layout/search_result_people_item.xml +++ b/res/layout/search_result_people_item.xml @@ -29,6 +29,7 @@ android:layout_width="0dp" android:textColor="?android:attr/textColorPrimary" android:id="@+id/title" + android:textSize="@dimen/search_hero_title_size" android:layout_height="wrap_content" android:layout_weight="1" /> diff --git a/res/layout/search_result_settings_row.xml b/res/layout/search_result_settings_row.xml index 19daf346e2..22c08bf58f 100644 --- a/res/layout/search_result_settings_row.xml +++ b/res/layout/search_result_settings_row.xml @@ -18,40 +18,43 @@ android:background="?android:attr/selectableItemBackground" android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="vertical" + android:orientation="horizontal" android:gravity="center_vertical" - android:padding="4dp" - android:minHeight="48dp" - android:textColor="?android:attr/textColorPrimary" - android:textSize="14sp"> + android:padding="@dimen/dynamic_grid_cell_padding_x" + android:textColor="?android:attr/textColorPrimary"> - + + - - - - + android:layout_weight="1"> + + + + \ No newline at end of file diff --git a/res/layout/search_result_slice.xml b/res/layout/search_result_slice.xml index ea1d49a56d..24d75e9eb6 100644 --- a/res/layout/search_result_slice.xml +++ b/res/layout/search_result_slice.xml @@ -1,5 +1,4 @@ - - - \ No newline at end of file + android:layout_height="wrap_content"> + + + + + + + + + + diff --git a/res/layout/search_result_suggest.xml b/res/layout/search_result_suggest.xml index 1d8c8039e6..01e25d5893 100644 --- a/res/layout/search_result_suggest.xml +++ b/res/layout/search_result_suggest.xml @@ -12,7 +12,7 @@ See the License for the specific language governing permissions and limitations under the License. --> - + android:drawablePadding="@dimen/dynamic_grid_icon_drawable_padding"> - \ No newline at end of file + \ No newline at end of file diff --git a/res/layout/search_result_widget_live.xml b/res/layout/search_result_widget_live.xml new file mode 100644 index 0000000000..0dd8a06c9a --- /dev/null +++ b/res/layout/search_result_widget_live.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/res/layout/search_result_widget_preview.xml b/res/layout/search_result_widget_preview.xml new file mode 100644 index 0000000000..942b199b7f --- /dev/null +++ b/res/layout/search_result_widget_preview.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/res/values/attrs.xml b/res/values/attrs.xml index c3ad1eb823..96c30b5f03 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -1,5 +1,4 @@ - - 24dp + + 16sp + 15sp + 12sp + 36dp + 16dp + 4dp + diff --git a/robolectric_tests/Android.mk b/robolectric_tests/Android.mk index 3fa9b0ab63..836ded5d25 100644 --- a/robolectric_tests/Android.mk +++ b/robolectric_tests/Android.mk @@ -59,4 +59,4 @@ LOCAL_INSTRUMENT_SOURCE_DIRS := packages/apps/Launcher3/src LOCAL_ROBOTEST_TIMEOUT := 36000 -include prebuilts/misc/common/robolectric/4.3.1/run_robotests.mk +include prebuilts/misc/common/robolectric/4.4/run_robotests.mk diff --git a/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java b/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java index b7ba10696f..2a94d9bc73 100644 --- a/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java +++ b/robolectric_tests/src/com/android/launcher3/folder/FolderNameProviderTest.java @@ -25,16 +25,20 @@ import android.os.UserHandle; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; +import com.android.launcher3.util.Executors; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.LooperMode; +import org.robolectric.annotation.LooperMode.Mode; import java.util.ArrayList; @RunWith(RobolectricTestRunner.class) +@LooperMode(Mode.PAUSED) public final class FolderNameProviderTest { private Context mContext; private WorkspaceItemInfo mItem1; @@ -58,18 +62,20 @@ public final class FolderNameProviderTest { } @Test - public void getSuggestedFolderName_workAssignedToEnd() { + public void getSuggestedFolderName_workAssignedToEnd() throws Exception { ArrayList list = new ArrayList<>(); list.add(mItem1); list.add(mItem2); FolderNameInfos nameInfos = new FolderNameInfos(); - new FolderNameProvider().getSuggestedFolderName(mContext, list, nameInfos); + Executors.MODEL_EXECUTOR.submit(() -> + new FolderNameProvider().getSuggestedFolderName(mContext, list, nameInfos)).get(); assertEquals("Work", nameInfos.getLabels()[0]); nameInfos.setLabel(0, "candidate1", 1.0f); nameInfos.setLabel(1, "candidate2", 1.0f); nameInfos.setLabel(2, "candidate3", 1.0f); - new FolderNameProvider().getSuggestedFolderName(mContext, list, nameInfos); + Executors.MODEL_EXECUTOR.submit(() -> + new FolderNameProvider().getSuggestedFolderName(mContext, list, nameInfos)).get(); assertEquals("Work", nameInfos.getLabels()[3]); assertTrue(nameInfos.hasSuggestions()); assertTrue(nameInfos.hasPrimary()); diff --git a/robolectric_tests/src/com/android/launcher3/logging/FileLogTest.java b/robolectric_tests/src/com/android/launcher3/logging/FileLogTest.java index c892618a9d..01b23ba966 100644 --- a/robolectric_tests/src/com/android/launcher3/logging/FileLogTest.java +++ b/robolectric_tests/src/com/android/launcher3/logging/FileLogTest.java @@ -9,8 +9,8 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; -import org.robolectric.Shadows; -import org.robolectric.util.Scheduler; +import org.robolectric.annotation.LooperMode; +import org.robolectric.annotation.LooperMode.Mode; import java.io.File; import java.io.PrintWriter; @@ -21,11 +21,10 @@ import java.util.Calendar; * Tests for {@link FileLog} */ @RunWith(RobolectricTestRunner.class) +@LooperMode(Mode.PAUSED) public class FileLogTest { private File mTempDir; - private boolean mTestActive; - @Before public void setUp() { int count = 0; @@ -35,14 +34,6 @@ public class FileLogTest { } while (!mTempDir.mkdir()); FileLog.setDir(mTempDir); - - mTestActive = true; - Scheduler scheduler = Shadows.shadowOf(FileLog.getHandler().getLooper()).getScheduler(); - new Thread(() -> { - while (mTestActive) { - scheduler.advanceToLastPostedRunnable(); - } - }).start(); } @After @@ -52,8 +43,6 @@ public class FileLogTest { new File(mTempDir, "log-" + i).delete(); } mTempDir.delete(); - - mTestActive = false; } @Test diff --git a/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java b/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java index 87fe3c086b..aab6c25f1c 100644 --- a/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java +++ b/robolectric_tests/src/com/android/launcher3/model/ModelMultiCallbacksTest.java @@ -30,6 +30,7 @@ import com.android.launcher3.PagedView; import com.android.launcher3.model.BgDataModel.Callbacks; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.ItemInfo; +import com.android.launcher3.shadows.ShadowLooperExecutor; import com.android.launcher3.util.Executors; import com.android.launcher3.util.LauncherLayoutBuilder; import com.android.launcher3.util.LauncherModelHelper; @@ -43,8 +44,8 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.LooperMode; import org.robolectric.annotation.LooperMode.Mode; +import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowPackageManager; -import org.robolectric.util.ReflectionHelpers; import java.util.ArrayList; import java.util.Arrays; @@ -74,7 +75,8 @@ public class ModelMultiCallbacksTest { // Since robolectric tests run on main thread, we run the loader-UI calls on a temp thread, // so that we can wait appropriately for the loader to complete. mTempMainExecutor = new LooperExecutor(createAndStartNewForegroundLooper("tempMain")); - ReflectionHelpers.setField(mModelHelper.getModel(), "mMainExecutor", mTempMainExecutor); + ShadowLooperExecutor sle = Shadow.extract(Executors.MAIN_EXECUTOR); + sle.setHandler(mTempMainExecutor.getHandler()); } @Test diff --git a/robolectric_tests/src/com/android/launcher3/secondarydisplay/SDWorkModeTest.java b/robolectric_tests/src/com/android/launcher3/secondarydisplay/SDWorkModeTest.java index baae2a6a80..e3694aeddc 100644 --- a/robolectric_tests/src/com/android/launcher3/secondarydisplay/SDWorkModeTest.java +++ b/robolectric_tests/src/com/android/launcher3/secondarydisplay/SDWorkModeTest.java @@ -59,8 +59,6 @@ public class SDWorkModeTest { private InvariantDeviceProfile mIdp; private LauncherModelHelper mModelHelper; - private LauncherLayoutBuilder mLayoutBuilder; - @Before public void setup() throws Exception { mModelHelper = new LauncherModelHelper(); @@ -70,7 +68,6 @@ public class SDWorkModeTest { Settings.Global.WINDOW_ANIMATION_SCALE, 0); mModelHelper.installApp(TEST_PACKAGE); - mLayoutBuilder = new LauncherLayoutBuilder(); } @Test @@ -86,7 +83,7 @@ public class SDWorkModeTest { public void testAllAppsList_workProfile() throws Exception { ShadowUserManager sum = Shadow.extract(mTargetContext.getSystemService(UserManager.class)); sum.addUser(SYSTEM_USER, "me", FLAG_SYSTEM); - sum.addUser(WORK_PROFILE_ID, "work", FLAG_PROFILE); + sum.addProfile(SYSTEM_USER, WORK_PROFILE_ID, "work", FLAG_PROFILE); SecondaryDisplayLauncher launcher = loadLauncher(); launcher.showAppDrawer(true); diff --git a/robolectric_tests/src/com/android/launcher3/shadows/ShadowLooperExecutor.java b/robolectric_tests/src/com/android/launcher3/shadows/ShadowLooperExecutor.java index a3b7dc706f..57eda7eae8 100644 --- a/robolectric_tests/src/com/android/launcher3/shadows/ShadowLooperExecutor.java +++ b/robolectric_tests/src/com/android/launcher3/shadows/ShadowLooperExecutor.java @@ -23,6 +23,8 @@ import static org.robolectric.util.ReflectionHelpers.setField; import android.os.Handler; +import androidx.annotation.Nullable; + import com.android.launcher3.util.LooperExecutor; import org.robolectric.annotation.Implementation; @@ -37,8 +39,13 @@ public class ShadowLooperExecutor { @RealObject private LooperExecutor mRealExecutor; + private Handler mOverriddenHandler; + @Implementation protected Handler getHandler() { + if (mOverriddenHandler != null) { + return mOverriddenHandler; + } Handler handler = directlyOn(mRealExecutor, LooperExecutor.class, "getHandler"); Thread thread = handler.getLooper().getThread(); if (!thread.isAlive()) { @@ -49,4 +56,8 @@ public class ShadowLooperExecutor { } return directlyOn(mRealExecutor, LooperExecutor.class, "getHandler"); } + + public void setHandler(@Nullable Handler handler) { + mOverriddenHandler = handler; + } } diff --git a/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java b/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java index f2b3071c95..849f98b8ff 100644 --- a/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java +++ b/robolectric_tests/src/com/android/launcher3/util/LauncherModelHelper.java @@ -48,10 +48,12 @@ import com.android.launcher3.model.BgDataModel.Callbacks; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.pm.UserCache; +import com.android.launcher3.shadows.ShadowLooperExecutor; import org.mockito.ArgumentCaptor; import org.robolectric.Robolectric; import org.robolectric.RuntimeEnvironment; +import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowContentResolver; import org.robolectric.shadows.ShadowPackageManager; import org.robolectric.util.ReflectionHelpers; @@ -403,14 +405,16 @@ public class LauncherModelHelper { public void loadModelSync() throws ExecutionException, InterruptedException { // Since robolectric tests run on main thread, we run the loader-UI calls on a temp thread, // so that we can wait appropriately for the loader to complete. - ReflectionHelpers.setField(getModel(), "mMainExecutor", Executors.UI_HELPER_EXECUTOR); + ShadowLooperExecutor sle = Shadow.extract(Executors.MAIN_EXECUTOR); + sle.setHandler(Executors.UI_HELPER_EXECUTOR.getHandler()); Callbacks mockCb = mock(Callbacks.class); getModel().addCallbacksAndLoad(mockCb); Executors.MODEL_EXECUTOR.submit(() -> { }).get(); Executors.UI_HELPER_EXECUTOR.submit(() -> { }).get(); - ReflectionHelpers.setField(getModel(), "mMainExecutor", Executors.MAIN_EXECUTOR); + + sle.setHandler(null); getModel().removeCallbacks(mockCb); } diff --git a/src/com/android/launcher3/AppWidgetResizeFrame.java b/src/com/android/launcher3/AppWidgetResizeFrame.java index 168d9c479c..bc3e341806 100644 --- a/src/com/android/launcher3/AppWidgetResizeFrame.java +++ b/src/com/android/launcher3/AppWidgetResizeFrame.java @@ -36,7 +36,7 @@ public class AppWidgetResizeFrame extends AbstractFloatingView implements View.O private static final Rect sTmpRect = new Rect(); // Represents the cell size on the grid in the two orientations. - private static final MainThreadInitializedObject CELL_SIZE = + public static final MainThreadInitializedObject CELL_SIZE = new MainThreadInitializedObject<>(c -> { InvariantDeviceProfile inv = LauncherAppState.getIDP(c); return new Point[] {inv.landscapeProfile.getCellSize(), diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java index 5cd63721d3..3eb52adf1a 100644 --- a/src/com/android/launcher3/BubbleTextView.java +++ b/src/com/android/launcher3/BubbleTextView.java @@ -171,6 +171,8 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, private IconLoadRequest mIconLoadRequest; + private boolean mEnableIconUpdateAnimation = false; + public BubbleTextView(Context context) { this(context, null, 0); } @@ -697,6 +699,10 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, applyCompoundDrawables(icon); } + protected boolean iconUpdateAnimationEnabled() { + return mEnableIconUpdateAnimation; + } + protected void applyCompoundDrawables(Drawable icon) { // If we had already set an icon before, disable relayout as the icon size is the // same as before. @@ -706,13 +712,10 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, updateIcon(icon); - ItemInfo itemInfo = (ItemInfo) getTag(); - // If the current icon is a placeholder color, animate its update. if (mIcon != null && mIcon instanceof PlaceHolderIconDrawable - && (itemInfo == null - || itemInfo.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT)) { + && iconUpdateAnimationEnabled()) { animateIconUpdate((PlaceHolderIconDrawable) mIcon, icon); } @@ -734,6 +737,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, if (getTag() == info) { mIconLoadRequest = null; mDisableRelayout = true; + mEnableIconUpdateAnimation = true; // Optimization: Starting in N, pre-uploads the bitmap to RenderThread. info.bitmap.icon.prepareToDraw(); @@ -750,6 +754,7 @@ public class BubbleTextView extends TextView implements ItemInfoUpdateReceiver, } mDisableRelayout = false; + mEnableIconUpdateAnimation = false; } } diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index 59365a55d4..058eca8293 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -111,6 +111,7 @@ import com.android.launcher3.allapps.AllAppsContainerView; import com.android.launcher3.allapps.AllAppsStore; import com.android.launcher3.allapps.AllAppsTransitionController; import com.android.launcher3.allapps.DiscoveryBounce; +import com.android.launcher3.allapps.search.LiveSearchManager; import com.android.launcher3.anim.PropertyListBuilder; import com.android.launcher3.compat.AccessibilityManagerCompat; import com.android.launcher3.config.FeatureFlags; @@ -276,6 +277,8 @@ public class Launcher extends StatefulActivity implements Launche private LifecycleRegistry mLifecycleRegistry; + private LiveSearchManager mLiveSearchManager; + @Thunk Workspace mWorkspace; @Thunk @@ -389,6 +392,8 @@ public class Launcher extends StatefulActivity implements Launche mIconCache = app.getIconCache(); mAccessibilityDelegate = new LauncherAccessibilityDelegate(this); + mLiveSearchManager = new LiveSearchManager(this); + mDragController = new DragController(this); mAllAppsController = new AllAppsTransitionController(this); mStateManager = new StateManager<>(this, NORMAL); @@ -492,6 +497,10 @@ public class Launcher extends StatefulActivity implements Launche return mLifecycleRegistry; } + public LiveSearchManager getLiveSearchManager() { + return mLiveSearchManager; + } + protected LauncherOverlayManager getDefaultOverlay() { return new LauncherOverlayManager() { }; } @@ -1583,6 +1592,7 @@ public class Launcher extends StatefulActivity implements Launche mAppTransitionManager.unregisterRemoteAnimations(); mUserChangedCallbackCloseable.close(); mLifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED); + mLiveSearchManager.stop(); } public LauncherAccessibilityDelegate getAccessibilityDelegate() { @@ -2489,6 +2499,7 @@ public class Launcher extends StatefulActivity implements Launche @Override public void bindAllApplications(AppInfo[] apps, int flags) { mAppsView.getAppsStore().setApps(apps, flags); + PopupContainerWithArrow.dismissInvalidPopup(this); } /** @@ -2520,6 +2531,7 @@ public class Launcher extends StatefulActivity implements Launche public void bindWorkspaceItemsChanged(List updated) { if (!updated.isEmpty()) { mWorkspace.updateShortcuts(updated); + PopupContainerWithArrow.dismissInvalidPopup(this); } } @@ -2544,6 +2556,7 @@ public class Launcher extends StatefulActivity implements Launche public void bindWorkspaceComponentsRemoved(final ItemInfoMatcher matcher) { mWorkspace.removeItemsByMatcher(matcher); mDragController.onAppsRemoved(matcher); + PopupContainerWithArrow.dismissInvalidPopup(this); } @Override diff --git a/src/com/android/launcher3/LauncherAnimUtils.java b/src/com/android/launcher3/LauncherAnimUtils.java index d9cf7f19b3..803f8d2c3d 100644 --- a/src/com/android/launcher3/LauncherAnimUtils.java +++ b/src/com/android/launcher3/LauncherAnimUtils.java @@ -16,6 +16,9 @@ package com.android.launcher3; +import android.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.animation.AnimatorListenerAdapter; import android.graphics.drawable.Drawable; import android.util.FloatProperty; import android.util.IntProperty; @@ -29,8 +32,8 @@ public class LauncherAnimUtils { */ public static final int SPRING_LOADED_EXIT_DELAY = 500; - // The progress of an animation to all apps must be at least this far along to snap to all apps. - public static final float MIN_PROGRESS_TO_ALL_APPS = 0.5f; + // Progress after which the transition is assumed to be a success + public static final float SUCCESS_TRANSITION_PROGRESS = 0.5f; public static final IntProperty DRAWABLE_ALPHA = new IntProperty("drawableAlpha") { @@ -131,4 +134,23 @@ public class LauncherAnimUtils { return view.getAlpha(); } }; + + /** + * Utility method to create an {@link AnimatorListener} which executes a callback on animation + * cancel. + */ + public static AnimatorListener newCancelListener(Runnable callback) { + return new AnimatorListenerAdapter() { + + boolean mDispatched = false; + + @Override + public void onAnimationCancel(Animator animation) { + if (!mDispatched) { + mDispatched = true; + callback.run(); + } + } + }; + } } diff --git a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java index f7731915e3..3c88288160 100644 --- a/src/com/android/launcher3/allapps/AllAppsGridAdapter.java +++ b/src/com/android/launcher3/allapps/AllAppsGridAdapter.java @@ -37,7 +37,6 @@ import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.core.view.accessibility.AccessibilityRecordCompat; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import androidx.slice.widget.SliceView; import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.BubbleTextView; @@ -47,7 +46,7 @@ import com.android.launcher3.allapps.search.SearchSectionInfo; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.util.PackageManagerHelper; -import com.android.launcher3.views.SearchSliceWrapper; +import com.android.launcher3.views.SearchResultWidget; import com.android.systemui.plugins.shared.SearchTarget; import java.util.List; @@ -75,8 +74,6 @@ public class AllAppsGridAdapter extends public static final int VIEW_TYPE_SEARCH_CORPUS_TITLE = 1 << 5; - public static final int VIEW_TYPE_SEARCH_HERO_APP = 1 << 6; - public static final int VIEW_TYPE_SEARCH_ROW_WITH_BUTTON = 1 << 7; public static final int VIEW_TYPE_SEARCH_ROW = 1 << 8; @@ -93,6 +90,10 @@ public class AllAppsGridAdapter extends public static final int VIEW_TYPE_SEARCH_ICON = 1 << 14; + public static final int VIEW_TYPE_SEARCH_WIDGET_LIVE = 1 << 15; + + public static final int VIEW_TYPE_SEARCH_WIDGET_PREVIEW = 1 << 16; + // Common view type masks public static final int VIEW_TYPE_MASK_DIVIDER = VIEW_TYPE_ALL_APPS_DIVIDER; public static final int VIEW_TYPE_MASK_ICON = VIEW_TYPE_ICON | VIEW_TYPE_SEARCH_ICON; @@ -178,7 +179,6 @@ public class AllAppsGridAdapter extends boolean isCountedForAccessibility() { return viewType == VIEW_TYPE_ICON - || viewType == VIEW_TYPE_SEARCH_HERO_APP || viewType == VIEW_TYPE_SEARCH_ROW_WITH_BUTTON || viewType == VIEW_TYPE_SEARCH_SLICE || viewType == VIEW_TYPE_SEARCH_ROW @@ -289,7 +289,8 @@ public class AllAppsGridAdapter extends int viewType = mApps.getAdapterItems().get(position).viewType; if (isIconViewType(viewType)) { return 1 * SPAN_MULTIPLIER; - } else if (viewType == VIEW_TYPE_SEARCH_THUMBNAIL) { + } else if (viewType == VIEW_TYPE_SEARCH_THUMBNAIL + || viewType == VIEW_TYPE_SEARCH_WIDGET_PREVIEW) { return mAppsPerRow; } else { // Section breaks span the full width @@ -411,9 +412,6 @@ public class AllAppsGridAdapter extends case VIEW_TYPE_SEARCH_CORPUS_TITLE: return new ViewHolder( mLayoutInflater.inflate(R.layout.search_section_title, parent, false)); - case VIEW_TYPE_SEARCH_HERO_APP: - return new ViewHolder(mLayoutInflater.inflate( - R.layout.search_result_hero_app, parent, false)); case VIEW_TYPE_SEARCH_ROW_WITH_BUTTON: return new ViewHolder(mLayoutInflater.inflate( R.layout.search_result_play_item, parent, false)); @@ -435,6 +433,12 @@ public class AllAppsGridAdapter extends case VIEW_TYPE_SEARCH_SUGGEST: return new ViewHolder(mLayoutInflater.inflate( R.layout.search_result_suggest, parent, false)); + case VIEW_TYPE_SEARCH_WIDGET_LIVE: + return new ViewHolder(mLayoutInflater.inflate( + R.layout.search_result_widget_live, parent, false)); + case VIEW_TYPE_SEARCH_WIDGET_PREVIEW: + return new ViewHolder(mLayoutInflater.inflate( + R.layout.search_result_widget_preview, parent, false)); default: throw new RuntimeException("Unexpected view type"); } @@ -467,24 +471,18 @@ public class AllAppsGridAdapter extends } else { searchView.setVisibility(View.GONE); } - break; - case VIEW_TYPE_SEARCH_SLICE: - SliceView sliceView = (SliceView) holder.itemView; - SearchAdapterItem slicePayload = (SearchAdapterItem) mApps.getAdapterItems().get( - position); - SearchTarget searchTarget = slicePayload.getSearchTarget(); - sliceView.setTag(new SearchSliceWrapper(mLauncher, sliceView, searchTarget)); - break; case VIEW_TYPE_SEARCH_CORPUS_TITLE: case VIEW_TYPE_SEARCH_ROW_WITH_BUTTON: - case VIEW_TYPE_SEARCH_HERO_APP: + case VIEW_TYPE_SEARCH_SLICE: case VIEW_TYPE_SEARCH_ROW: case VIEW_TYPE_SEARCH_ICON: case VIEW_TYPE_SEARCH_ICON_ROW: case VIEW_TYPE_SEARCH_PEOPLE: case VIEW_TYPE_SEARCH_THUMBNAIL: case VIEW_TYPE_SEARCH_SUGGEST: + case VIEW_TYPE_SEARCH_WIDGET_LIVE: + case VIEW_TYPE_SEARCH_WIDGET_PREVIEW: SearchAdapterItem item = (SearchAdapterItem) mApps.getAdapterItems().get(position); SearchTargetHandler payloadResultView = (SearchTargetHandler) holder.itemView; @@ -503,12 +501,8 @@ public class AllAppsGridAdapter extends if (holder.itemView instanceof AllAppsSectionDecorator.SelfDecoratingView) { ((AllAppsSectionDecorator.SelfDecoratingView) holder.itemView).removeDecoration(); } - if (holder.itemView instanceof SliceView) { - SliceView sliceView = (SliceView) holder.itemView; - if (sliceView.getTag() instanceof SearchSliceWrapper) { - ((SearchSliceWrapper) sliceView.getTag()).destroy(); - } - sliceView.setTag(null); + if (holder.itemView instanceof SearchResultWidget) { + ((SearchResultWidget) holder.itemView).removeListener(); } } diff --git a/src/com/android/launcher3/allapps/AllAppsInsetTransitionController.java b/src/com/android/launcher3/allapps/AllAppsInsetTransitionController.java index 08d573f08b..93da1c0dd6 100644 --- a/src/com/android/launcher3/allapps/AllAppsInsetTransitionController.java +++ b/src/com/android/launcher3/allapps/AllAppsInsetTransitionController.java @@ -15,6 +15,7 @@ */ package com.android.launcher3.allapps; +import android.annotation.TargetApi; import android.graphics.Insets; import android.os.Build; import android.util.Log; @@ -26,9 +27,8 @@ import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; -import androidx.core.os.BuildCompat; +import com.android.launcher3.Utilities; import com.android.launcher3.util.UiThreadHelper; /** @@ -57,7 +57,8 @@ public class AllAppsInsetTransitionController { // Only purpose of these states is to keep track of fast fling transition enum State { - RESET, DRAG_START_BOTTOM, FLING_END_TOP, + RESET, DRAG_START_BOTTOM, DRAG_START_BOTTOM_IME_CANCELLED, + FLING_END_TOP, FLING_END_TOP_IME_CANCELLED, DRAG_START_TOP, FLING_END_BOTTOM } private State mState; @@ -68,7 +69,7 @@ public class AllAppsInsetTransitionController { } public void hide() { - if (!BuildCompat.isAtLeastR()) return; + if (!Utilities.ATLEAST_R) return; WindowInsets insets = mApps.getRootWindowInsets(); if (insets == null) return; @@ -89,9 +90,9 @@ public class AllAppsInsetTransitionController { * * @param progress value between 0..1 */ - @RequiresApi(api = Build.VERSION_CODES.R) + @TargetApi(Build.VERSION_CODES.R) public void onDragStart(float progress) { - if (!BuildCompat.isAtLeastR()) return; + if (!Utilities.ATLEAST_R) return; // Until getRootWindowInsets().isVisible(...) method returns correct value, // only support InsetController based IME transition during swipe up and @@ -149,13 +150,12 @@ public class AllAppsInsetTransitionController { + " mAnimationController=" + mAnimationController); } if (mState == State.DRAG_START_BOTTOM) { - mApps.getWindowInsetsController().show(WindowInsets.Type.ime()); + mState = State.DRAG_START_BOTTOM_IME_CANCELLED; } mAnimationController = null; if (controller != null) { controller.finish(true); } - } }); } @@ -164,7 +164,7 @@ public class AllAppsInsetTransitionController { * If IME bounds after touch sequence finishes, call finish. */ private boolean handleFinishOnFling(WindowInsetsAnimationController controller) { - if (!BuildCompat.isAtLeastR()) return false; + if (!Utilities.ATLEAST_R) return false; if (mState == State.FLING_END_TOP) { controller.finish(true); @@ -181,9 +181,9 @@ public class AllAppsInsetTransitionController { * * @param progress value between 0..1 */ - @RequiresApi(api = 30) + @TargetApi(Build.VERSION_CODES.R) public void setProgress(float progress) { - if (!BuildCompat.isAtLeastR()) return; + if (!Utilities.ATLEAST_R) return; // progress that equals to 0 or 1 is error prone. Do not use them. // Instead use onDragStart and onAnimationEnd if (mAnimationController == null || progress <= 0f || progress >= 1f) return; @@ -200,7 +200,7 @@ public class AllAppsInsetTransitionController { final int end = mShownAtDown ? mHiddenInsetBottom : mShownInsetBottom; inset = Math.max(inset, mHiddenInsetBottom); inset = Math.min(inset, mShownInsetBottom); - if (DEBUG || false) { + if (DEBUG && false) { Log.d(TAG, "updateInset mCurrent=" + mCurrent + " mDown=" + mDown + " hidden=" + mHiddenInsetBottom + " shown=" + mShownInsetBottom @@ -218,7 +218,7 @@ public class AllAppsInsetTransitionController { * * @param progress value between 0..1 */ - @RequiresApi(api = 30) + @TargetApi(Build.VERSION_CODES.R) public void onAnimationEnd(float progress) { if (DEBUG) { Log.d(TAG, "onAnimationEnd progress=" + progress @@ -228,8 +228,14 @@ public class AllAppsInsetTransitionController { // only called when launcher restarting. UiThreadHelper.hideKeyboardAsync(mApps.getContext(), mApps.getWindowToken()); } + setState(false, true, progress); + + if (mAnimationController == null) { + if (mState == State.FLING_END_TOP_IME_CANCELLED) { + mApps.getWindowInsetsController().show(WindowInsets.Type.ime()); + } return; } @@ -268,8 +274,12 @@ public class AllAppsInsetTransitionController { } else if (end) { if (Float.compare(progress, 1f) == 0 && mState == State.DRAG_START_TOP) { state = State.FLING_END_BOTTOM; - } else if (Float.compare(progress, 0f) == 0 && mState == State.DRAG_START_BOTTOM) { - state = State.FLING_END_TOP; + } else if (Float.compare(progress, 0f) == 0) { + if (mState == State.DRAG_START_BOTTOM) { + state = State.FLING_END_TOP; + } else if (mState == State.DRAG_START_BOTTOM_IME_CANCELLED) { + state = State.FLING_END_TOP_IME_CANCELLED; + } } } if (DEBUG) { diff --git a/src/com/android/launcher3/allapps/AllAppsSectionDecorator.java b/src/com/android/launcher3/allapps/AllAppsSectionDecorator.java index 3c81811e16..1d31975114 100644 --- a/src/com/android/launcher3/allapps/AllAppsSectionDecorator.java +++ b/src/com/android/launcher3/allapps/AllAppsSectionDecorator.java @@ -98,7 +98,8 @@ public class AllAppsSectionDecorator extends RecyclerView.ItemDecoration { mAppsView.getActiveRecyclerView().getLayoutManager(); if (layoutManager.findFirstVisibleItemPosition() <= index && index < parent.getChildCount()) { - decorationHandler.onFocusDraw(c, parent.getChildAt(index)); + RecyclerView.ViewHolder vh = parent.findViewHolderForAdapterPosition(index); + if (vh != null) decorationHandler.onFocusDraw(c, vh.itemView); } } decorationHandler.reset(); diff --git a/src/com/android/launcher3/allapps/AllAppsTransitionController.java b/src/com/android/launcher3/allapps/AllAppsTransitionController.java index 0c488a609b..f9ab196792 100644 --- a/src/com/android/launcher3/allapps/AllAppsTransitionController.java +++ b/src/com/android/launcher3/allapps/AllAppsTransitionController.java @@ -264,11 +264,15 @@ public class AllAppsTransitionController implements StateHandler, if (FeatureFlags.ENABLE_DEVICE_SEARCH.get() && BuildCompat.isAtLeastR()) { mInsetController.onAnimationEnd(mProgress); if (Float.compare(mProgress, 0f) == 0) { + mLauncher.getLiveSearchManager().start(); EditText editText = mAppsView.getSearchUiManager().getEditText(); if (editText != null) { editText.requestFocus(); } } + else { + mLauncher.getLiveSearchManager().stop(); + } // TODO: should make the controller hide synchronously } } diff --git a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java index 2450787e49..f235199d12 100644 --- a/src/com/android/launcher3/allapps/AlphabeticalAppsList.java +++ b/src/com/android/launcher3/allapps/AlphabeticalAppsList.java @@ -132,6 +132,9 @@ public class AlphabeticalAppsList implements AllAppsStore.OnUpdateListener { * Returns the child adapter item with IME launch focus. */ public AdapterItem getFocusedChild() { + if (mAdapterItems.size() == 0 || getFocusedChildIndex() != -1) { + return null; + } return mAdapterItems.get(getFocusedChildIndex()); } diff --git a/src/com/android/launcher3/allapps/search/LiveSearchManager.java b/src/com/android/launcher3/allapps/search/LiveSearchManager.java new file mode 100644 index 0000000000..ec33908c75 --- /dev/null +++ b/src/com/android/launcher3/allapps/search/LiveSearchManager.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.allapps.search; + +import static com.android.launcher3.widget.WidgetHostViewLoader.getDefaultOptionsForWidget; + +import android.appwidget.AppWidgetHost; +import android.appwidget.AppWidgetHostView; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.os.UserHandle; + +import androidx.lifecycle.LiveData; +import androidx.slice.Slice; +import androidx.slice.widget.SliceLiveData; + +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.util.ComponentKey; +import com.android.launcher3.widget.PendingAddWidgetInfo; + +import java.util.HashMap; + +/** + * Manages Lifecycle for Live search results + */ +public class LiveSearchManager { + + public static final int SEARCH_APPWIDGET_HOST_ID = 2048; + + private final Launcher mLauncher; + private final AppWidgetManager mAppWidgetManger; + private final HashMap mWidgetPlaceholders = + new HashMap<>(); + private final HashMap> mUriSliceMap = new HashMap<>(); + private SearchWidgetHost mSearchWidgetHost; + + public LiveSearchManager(Launcher launcher) { + mLauncher = launcher; + mAppWidgetManger = AppWidgetManager.getInstance(launcher); + } + + /** + * Creates new {@link AppWidgetHostView} from {@link AppWidgetProviderInfo}. Caches views for + * quicker result within the same search session + */ + public SearchWidgetInfoContainer getPlaceHolderWidget(AppWidgetProviderInfo providerInfo) { + if (mSearchWidgetHost == null) { + throw new RuntimeException("AppWidgetHost has not been created yet"); + } + + ComponentName provider = providerInfo.provider; + UserHandle userHandle = providerInfo.getProfile(); + + ComponentKey key = new ComponentKey(provider, userHandle); + SearchWidgetInfoContainer view = mWidgetPlaceholders.getOrDefault(key, null); + if (mWidgetPlaceholders.containsKey(key)) { + return mWidgetPlaceholders.get(key); + } + LauncherAppWidgetProviderInfo pinfo = LauncherAppWidgetProviderInfo.fromProviderInfo( + mLauncher, providerInfo); + PendingAddWidgetInfo pendingAddWidgetInfo = new PendingAddWidgetInfo(pinfo); + + Bundle options = getDefaultOptionsForWidget(mLauncher, pendingAddWidgetInfo); + int appWidgetId = mSearchWidgetHost.allocateAppWidgetId(); + boolean success = mAppWidgetManger.bindAppWidgetIdIfAllowed(appWidgetId, userHandle, + provider, options); + if (!success) { + mWidgetPlaceholders.put(key, null); + return null; + } + + view = (SearchWidgetInfoContainer) mSearchWidgetHost.createView(mLauncher, appWidgetId, + providerInfo); + view.setTag(pendingAddWidgetInfo); + mWidgetPlaceholders.put(key, view); + return view; + } + + /** + * Creates {@link LiveData} from Slice Uri. Caches created live data to be reused + * within the same search session. Removes previous observers when new SliceView request a + * live data for observation. + */ + public LiveData getSliceForUri(Uri sliceUri) { + LiveData sliceLiveData = mUriSliceMap.getOrDefault(sliceUri, null); + if (sliceLiveData == null) { + sliceLiveData = SliceLiveData.fromUri(mLauncher, sliceUri); + mUriSliceMap.put(sliceUri, sliceLiveData); + } + return sliceLiveData; + } + + /** + * Start search session + */ + public void start() { + stop(); + mSearchWidgetHost = new SearchWidgetHost(mLauncher); + mSearchWidgetHost.startListening(); + } + + /** + * Stop search session + */ + public void stop() { + if (mSearchWidgetHost != null) { + mSearchWidgetHost.stopListening(); + mSearchWidgetHost.deleteHost(); + for (SearchWidgetInfoContainer placeholder : mWidgetPlaceholders.values()) { + placeholder.clearListeners(); + } + mWidgetPlaceholders.clear(); + mSearchWidgetHost = null; + } + for (LiveData liveData : mUriSliceMap.values()) { + liveData.removeObservers(mLauncher); + } + mUriSliceMap.clear(); + } + + static class SearchWidgetHost extends AppWidgetHost { + SearchWidgetHost(Context context) { + super(context, SEARCH_APPWIDGET_HOST_ID); + } + + @Override + protected AppWidgetHostView onCreateView(Context context, int appWidgetId, + AppWidgetProviderInfo appWidget) { + return new SearchWidgetInfoContainer(context); + } + } +} diff --git a/src/com/android/launcher3/allapps/search/SearchWidgetInfoContainer.java b/src/com/android/launcher3/allapps/search/SearchWidgetInfoContainer.java new file mode 100644 index 0000000000..b5c22688e5 --- /dev/null +++ b/src/com/android/launcher3/allapps/search/SearchWidgetInfoContainer.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.launcher3.allapps.search; + +import android.appwidget.AppWidgetHostView; +import android.appwidget.AppWidgetProviderInfo; +import android.content.Context; +import android.widget.RemoteViews; + +import java.util.ArrayList; +import java.util.List; + +/** + * A placeholder {@link AppWidgetHostView} used for managing widget search results + */ +public class SearchWidgetInfoContainer extends AppWidgetHostView { + private int mAppWidgetId; + private AppWidgetProviderInfo mProviderInfo; + private RemoteViews mViews; + private List mListeners = new ArrayList<>(); + + public SearchWidgetInfoContainer(Context context) { + super(context); + } + + @Override + public void setAppWidget(int appWidgetId, AppWidgetProviderInfo info) { + mAppWidgetId = appWidgetId; + mProviderInfo = info; + for (AppWidgetHostView listener : mListeners) { + listener.setAppWidget(mAppWidgetId, mProviderInfo); + } + } + + @Override + public void updateAppWidget(RemoteViews remoteViews) { + mViews = remoteViews; + for (AppWidgetHostView listener : mListeners) { + listener.updateAppWidget(remoteViews); + } + } + + /** + * Create a live {@link AppWidgetHostView} from placeholder + */ + public void attachWidget(AppWidgetHostView hv) { + hv.setTag(getTag()); + hv.setAppWidget(mAppWidgetId, mProviderInfo); + hv.updateAppWidget(mViews); + mListeners.add(hv); + } + + /** + * stops AppWidgetHostView from getting updates + */ + public void detachWidget(AppWidgetHostView hostView) { + mListeners.remove(hostView); + } + + /** + * Removes all AppWidgetHost update listeners + */ + public void clearListeners() { + mListeners.clear(); + } +} diff --git a/src/com/android/launcher3/anim/AnimatorPlaybackController.java b/src/com/android/launcher3/anim/AnimatorPlaybackController.java index dcdfb6e51a..edaf51dbc6 100644 --- a/src/com/android/launcher3/anim/AnimatorPlaybackController.java +++ b/src/com/android/launcher3/anim/AnimatorPlaybackController.java @@ -29,8 +29,6 @@ import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.content.Context; -import androidx.annotation.Nullable; - import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -72,7 +70,6 @@ public class AnimatorPlaybackController implements ValueAnimator.AnimatorUpdateL private Runnable mEndAction; protected boolean mTargetCancelled = false; - protected Runnable mOnCancelRunnable; /** package private */ AnimatorPlaybackController(AnimatorSet anim, long duration, ArrayList childAnims) { @@ -88,16 +85,11 @@ public class AnimatorPlaybackController implements ValueAnimator.AnimatorUpdateL @Override public void onAnimationCancel(Animator animation) { mTargetCancelled = true; - if (mOnCancelRunnable != null) { - mOnCancelRunnable.run(); - mOnCancelRunnable = null; - } } @Override public void onAnimationEnd(Animator animation) { mTargetCancelled = false; - mOnCancelRunnable = null; } @Override @@ -269,33 +261,6 @@ public class AnimatorPlaybackController implements ValueAnimator.AnimatorUpdateL } } - /** @see #dispatchOnCancelWithoutCancelRunnable(Runnable) */ - public void dispatchOnCancelWithoutCancelRunnable() { - dispatchOnCancelWithoutCancelRunnable(null); - } - - /** - * Sets mOnCancelRunnable = null before dispatching the cancel and restoring the runnable. This - * is intended to be used only if you need to cancel but want to defer cleaning up yourself. - * @param callback An optional callback to run after dispatching the cancel but before resetting - * the onCancelRunnable. - */ - public void dispatchOnCancelWithoutCancelRunnable(@Nullable Runnable callback) { - Runnable onCancel = mOnCancelRunnable; - setOnCancelRunnable(null); - dispatchOnCancel(); - if (callback != null) { - callback.run(); - } - setOnCancelRunnable(onCancel); - } - - - public AnimatorPlaybackController setOnCancelRunnable(Runnable runnable) { - mOnCancelRunnable = runnable; - return this; - } - public void dispatchOnStart() { callListenerCommandRecursively(mAnim, AnimatorListener::onAnimationStart); } diff --git a/src/com/android/launcher3/anim/PendingAnimation.java b/src/com/android/launcher3/anim/PendingAnimation.java index 6dd316ed60..4e90c9e67f 100644 --- a/src/com/android/launcher3/anim/PendingAnimation.java +++ b/src/com/android/launcher3/anim/PendingAnimation.java @@ -15,10 +15,12 @@ */ package com.android.launcher3.anim; +import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS; import static com.android.launcher3.anim.AnimatorPlaybackController.addAnimationHoldersRecur; import android.animation.Animator; import android.animation.Animator.AnimatorListener; +import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; @@ -43,8 +45,6 @@ import java.util.function.Consumer; */ public class PendingAnimation implements PropertySetter { - private final ArrayList> mEndListeners = new ArrayList<>(); - private final ArrayList mAnimHolders = new ArrayList<>(); private final AnimatorSet mAnim; private final long mDuration; @@ -73,13 +73,6 @@ public class PendingAnimation implements PropertySetter { addAnimationHoldersRecur(a, mDuration, springProperty, mAnimHolders); } - public void finish(boolean isSuccess) { - for (Consumer listeners : mEndListeners) { - listeners.accept(new EndState(isSuccess)); - } - mEndListeners.clear(); - } - @Override public void setViewAlpha(View view, float alpha, TimeInterpolator interpolator) { if (view == null || view.getAlpha() == alpha) { @@ -163,19 +156,38 @@ public class PendingAnimation implements PropertySetter { } /** - * Add a listener of receiving the end state. - * Note that the listeners are called as a result of calling {@link #finish(boolean)} - * and not automatically + * Add a listener of receiving the success/failure callback in the end. */ - public void addEndListener(Consumer listener) { - mEndListeners.add(listener); + public void addEndListener(Consumer listener) { + if (mProgressAnimator == null) { + mProgressAnimator = ValueAnimator.ofFloat(0, 1); + } + mProgressAnimator.addListener(new EndStateCallbackWrapper(listener)); } - public static class EndState { - public boolean isSuccess; + private static class EndStateCallbackWrapper extends AnimatorListenerAdapter { - public EndState(boolean isSuccess) { - this.isSuccess = isSuccess; + private final Consumer mListener; + private boolean mCalled = false; + + EndStateCallbackWrapper(Consumer listener) { + mListener = listener; + } + + @Override + public void onAnimationCancel(Animator animation) { + if (!mCalled) { + mCalled = true; + mListener.accept(false); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + if (!mCalled) { + ValueAnimator anim = (ValueAnimator) animation; + mListener.accept(anim.getAnimatedFraction() > SUCCESS_TRANSITION_PROGRESS); + } } } } diff --git a/src/com/android/launcher3/logging/StatsLogManager.java b/src/com/android/launcher3/logging/StatsLogManager.java index 7e90769143..0b445bca4e 100644 --- a/src/com/android/launcher3/logging/StatsLogManager.java +++ b/src/com/android/launcher3/logging/StatsLogManager.java @@ -297,12 +297,6 @@ public class StatsLogManager implements ResourceBasedOverride { @UiEvent(doc = "User tapped on image content in Overview Select mode.") LAUNCHER_SELECT_MODE_IMAGE(627), - @UiEvent(doc = "A folder was replaced by a single item") - LAUNCHER_FOLDER_CONVERTED_TO_ICON(628), - - @UiEvent(doc = "A hotseat prediction item was pinned") - LAUNCHER_HOTSEAT_PREDICTION_PINNED(629), - @UiEvent(doc = "Activity to add external item was started") LAUNCHER_ADD_EXTERNAL_ITEM_START(641), @@ -318,6 +312,12 @@ public class StatsLogManager implements ResourceBasedOverride { @UiEvent(doc = "Item was dragged in external item addition flow") LAUNCHER_ADD_EXTERNAL_ITEM_DRAGGED(645), + @UiEvent(doc = "A folder was replaced by a single item") + LAUNCHER_FOLDER_CONVERTED_TO_ICON(646), + + @UiEvent(doc = "A hotseat prediction item was pinned") + LAUNCHER_HOTSEAT_PREDICTION_PINNED(647), + @UiEvent(doc = "Undo event was tapped.") LAUNCHER_UNDO(648), diff --git a/src/com/android/launcher3/popup/PopupContainerWithArrow.java b/src/com/android/launcher3/popup/PopupContainerWithArrow.java index 6d92b8b627..59930ff850 100644 --- a/src/com/android/launcher3/popup/PopupContainerWithArrow.java +++ b/src/com/android/launcher3/popup/PopupContainerWithArrow.java @@ -599,6 +599,17 @@ public class PopupContainerWithArrow extends Arr } } + /** + * Dismisses the popup if it is no longer valid + */ + public static void dismissInvalidPopup(BaseDraggingActivity activity) { + PopupContainerWithArrow popup = getOpen(activity); + if (popup != null && (!popup.mOriginalIcon.isAttachedToWindow() + || !canShow(popup.mOriginalIcon, (ItemInfo) popup.mOriginalIcon.getTag()))) { + popup.animateClose(); + } + } + /** * Handler to control drag-and-drop for popup items */ diff --git a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java index 2b04365ddc..2adf8ce25a 100644 --- a/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java +++ b/src/com/android/launcher3/secondarydisplay/SecondaryDisplayLauncher.java @@ -39,6 +39,7 @@ import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.LauncherAppWidgetInfo; import com.android.launcher3.model.data.PromiseAppInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; +import com.android.launcher3.popup.PopupContainerWithArrow; import com.android.launcher3.popup.PopupDataProvider; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.IntArray; @@ -296,6 +297,7 @@ public class SecondaryDisplayLauncher extends BaseDraggingActivity @Override public void bindAllApplications(AppInfo[] apps, int flags) { mAppsView.getAppsStore().setApps(apps, flags); + PopupContainerWithArrow.dismissInvalidPopup(this); } public PopupDataProvider getPopupDataProvider() { diff --git a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java index 57c0ad92af..a9d0e614e4 100644 --- a/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java +++ b/src/com/android/launcher3/touch/AbstractStateChangeTouchController.java @@ -15,7 +15,8 @@ */ package com.android.launcher3.touch; -import static com.android.launcher3.LauncherAnimUtils.MIN_PROGRESS_TO_ALL_APPS; +import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS; +import static com.android.launcher3.LauncherAnimUtils.newCancelListener; import static com.android.launcher3.LauncherState.ALL_APPS; import static com.android.launcher3.LauncherState.NORMAL; import static com.android.launcher3.LauncherState.OVERVIEW; @@ -32,6 +33,7 @@ import static com.android.launcher3.states.StateAnimationConfig.PLAY_NON_ATOMIC; import static com.android.launcher3.util.DisplayController.getSingleFrameMs; import android.animation.Animator; +import android.animation.Animator.AnimatorListener; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ValueAnimator; @@ -48,7 +50,6 @@ import com.android.launcher3.LauncherState; import com.android.launcher3.Utilities; import com.android.launcher3.anim.AnimationSuccessListener; import com.android.launcher3.anim.AnimatorPlaybackController; -import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.logger.LauncherAtom; import com.android.launcher3.logging.StatsLogManager; @@ -64,9 +65,6 @@ import com.android.launcher3.util.TouchController; public abstract class AbstractStateChangeTouchController implements TouchController, SingleAxisSwipeDetector.Listener { - // Progress after which the transition is assumed to be a success in case user does not fling - public static final float SUCCESS_TRANSITION_PROGRESS = 0.5f; - /** * Play an atomic recents animation when the progress from NORMAL to OVERVIEW reaches this. */ @@ -77,6 +75,9 @@ public abstract class AbstractStateChangeTouchController protected final SingleAxisSwipeDetector mDetector; protected final SingleAxisSwipeDetector.Direction mSwipeDirection; + protected final AnimatorListener mClearStateOnCancelListener = + newCancelListener(this::clearState); + private boolean mNoIntercept; private boolean mIsLogContainerSet; protected int mStartContainerType; @@ -85,7 +86,7 @@ public abstract class AbstractStateChangeTouchController protected LauncherState mFromState; protected LauncherState mToState; protected AnimatorPlaybackController mCurrentAnimation; - protected PendingAnimation mPendingAnimation; + protected boolean mGoingBetweenStates = true; private float mStartProgress; // Ratio of transition process [0, 1] to drag displacement (px) @@ -209,7 +210,7 @@ public abstract class AbstractStateChangeTouchController mStartProgress = 0; mPassedOverviewAtomicThreshold = false; if (mCurrentAnimation != null) { - mCurrentAnimation.setOnCancelRunnable(null); + mCurrentAnimation.getTarget().removeListener(mClearStateOnCancelListener); } int animComponents = goingBetweenNormalAndOverview(mFromState, mToState) ? PLAY_NON_ATOMIC : ANIM_ALL_COMPONENTS; @@ -237,7 +238,7 @@ public abstract class AbstractStateChangeTouchController LauncherState toState) { return (fromState == NORMAL || fromState == OVERVIEW) && (toState == NORMAL || toState == OVERVIEW) - && mPendingAnimation == null; + && mGoingBetweenStates; } @Override @@ -412,9 +413,8 @@ public abstract class AbstractStateChangeTouchController ? mToState : mFromState; // snap to top or bottom using the release velocity } else { - float successProgress = mToState == ALL_APPS - ? MIN_PROGRESS_TO_ALL_APPS : SUCCESS_TRANSITION_PROGRESS; - targetState = (interpolatedProgress > successProgress) ? mToState : mFromState; + targetState = + (interpolatedProgress > SUCCESS_TRANSITION_PROGRESS) ? mToState : mFromState; } final float endProgress; @@ -438,7 +438,8 @@ public abstract class AbstractStateChangeTouchController } else { // Let the state manager know that the animation didn't go to the target state, // but don't cancel ourselves (we already clean up when the animation completes). - mCurrentAnimation.dispatchOnCancelWithoutCancelRunnable(); + mCurrentAnimation.getTarget().removeListener(mClearStateOnCancelListener); + mCurrentAnimation.dispatchOnCancel(); endProgress = 0; if (progress <= 0) { @@ -522,13 +523,7 @@ public abstract class AbstractStateChangeTouchController mAtomicComponentsController = null; } clearState(); - boolean shouldGoToTargetState = true; - if (mPendingAnimation != null) { - boolean reachedTarget = mToState == targetState; - mPendingAnimation.finish(reachedTarget); - mPendingAnimation = null; - shouldGoToTargetState = !reachedTarget; - } + boolean shouldGoToTargetState = mGoingBetweenStates || (mToState != targetState); if (shouldGoToTargetState) { goToTargetState(targetState); } @@ -568,6 +563,7 @@ public abstract class AbstractStateChangeTouchController mAtomicAnim.cancel(); mAtomicAnim = null; } + mGoingBetweenStates = true; mScheduleResumeAtomicComponent = false; mDetector.finishedScrolling(); mDetector.setDetectableScrollConditions(0, false); diff --git a/src/com/android/launcher3/touch/ItemClickHandler.java b/src/com/android/launcher3/touch/ItemClickHandler.java index d56391da3d..9b9cb0a61b 100644 --- a/src/com/android/launcher3/touch/ItemClickHandler.java +++ b/src/com/android/launcher3/touch/ItemClickHandler.java @@ -96,6 +96,8 @@ public class ItemClickHandler { if (v instanceof PendingAppWidgetHostView) { onClickPendingWidget((PendingAppWidgetHostView) v, launcher); } + } else if (tag instanceof RemoteActionItemInfo) { + onClickRemoteAction(launcher, (RemoteActionItemInfo) tag); } } diff --git a/src/com/android/launcher3/views/HeroSearchResultView.java b/src/com/android/launcher3/views/HeroSearchResultView.java deleted file mode 100644 index a098df9f31..0000000000 --- a/src/com/android/launcher3/views/HeroSearchResultView.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (C) 2020 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.views; - -import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_ALL_APPS; -import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; -import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; - -import android.content.Context; -import android.content.pm.ShortcutInfo; -import android.graphics.Point; -import android.util.AttributeSet; -import android.util.Pair; -import android.view.View; -import android.view.ViewGroup; -import android.widget.LinearLayout; - -import com.android.launcher3.BubbleTextView; -import com.android.launcher3.DeviceProfile; -import com.android.launcher3.DragSource; -import com.android.launcher3.DropTarget; -import com.android.launcher3.Launcher; -import com.android.launcher3.LauncherAppState; -import com.android.launcher3.R; -import com.android.launcher3.allapps.AllAppsStore; -import com.android.launcher3.allapps.search.AllAppsSearchBarController.SearchTargetHandler; -import com.android.launcher3.allapps.search.SearchEventTracker; -import com.android.launcher3.dragndrop.DragOptions; -import com.android.launcher3.dragndrop.DraggableView; -import com.android.launcher3.graphics.DragPreviewProvider; -import com.android.launcher3.model.data.AppInfo; -import com.android.launcher3.model.data.ItemInfo; -import com.android.launcher3.model.data.ItemInfoWithIcon; -import com.android.launcher3.model.data.WorkspaceItemInfo; -import com.android.launcher3.shortcuts.ShortcutDragPreviewProvider; -import com.android.launcher3.touch.ItemLongClickListener; -import com.android.launcher3.util.ComponentKey; -import com.android.systemui.plugins.shared.SearchTarget; -import com.android.systemui.plugins.shared.SearchTargetEvent; - -import java.util.ArrayList; -import java.util.List; - -/** - * A view representing a high confidence app search result that includes shortcuts - * TODO (sfufa@) consolidate this with SearchResultIconRow - */ -public class HeroSearchResultView extends LinearLayout implements DragSource, SearchTargetHandler { - - public static final String TARGET_TYPE_HERO_APP = "hero_app"; - - public static final int MAX_SHORTCUTS_COUNT = 2; - - private SearchTarget mSearchTarget; - private BubbleTextView mBubbleTextView; - private View mIconView; - private BubbleTextView[] mDeepShortcutTextViews = new BubbleTextView[2]; - - - public HeroSearchResultView(Context context) { - super(context); - } - - public HeroSearchResultView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public HeroSearchResultView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - Launcher launcher = Launcher.getLauncher(getContext()); - DeviceProfile grid = launcher.getDeviceProfile(); - mIconView = findViewById(R.id.icon); - ViewGroup.LayoutParams iconParams = mIconView.getLayoutParams(); - iconParams.height = grid.allAppsIconSizePx; - iconParams.width = grid.allAppsIconSizePx; - - - mBubbleTextView = findViewById(R.id.bubble_text); - mBubbleTextView.setOnClickListener(view -> { - handleSelection(SearchTargetEvent.SELECT); - launcher.getItemOnClickListener().onClick(view); - }); - mBubbleTextView.setOnLongClickListener(new HeroItemDragHandler(getContext(), this)); - - - mDeepShortcutTextViews[0] = findViewById(R.id.shortcut_0); - mDeepShortcutTextViews[1] = findViewById(R.id.shortcut_1); - for (BubbleTextView bubbleTextView : mDeepShortcutTextViews) { - bubbleTextView.setLayoutParams( - new LinearLayout.LayoutParams(grid.allAppsIconSizePx, - grid.allAppsIconSizePx)); - bubbleTextView.setOnClickListener(view -> { - WorkspaceItemInfo itemInfo = (WorkspaceItemInfo) bubbleTextView.getTag(); - SearchTargetEvent event = new SearchTargetEvent.Builder(mSearchTarget, - SearchTargetEvent.CHILD_SELECT).setShortcutPosition(itemInfo.rank).build(); - SearchEventTracker.getInstance(getContext()).notifySearchTargetEvent(event); - launcher.getItemOnClickListener().onClick(view); - }); - } - } - - @Override - public void applySearchTarget(SearchTarget searchTarget) { - mSearchTarget = searchTarget; - AllAppsStore apps = Launcher.getLauncher(getContext()).getAppsView().getAppsStore(); - AppInfo appInfo = apps.getApp(new ComponentKey(searchTarget.getComponentName(), - searchTarget.getUserHandle())); - List infos = mSearchTarget.getShortcutInfos(); - - ArrayList> shortcuts = new ArrayList<>(); - for (int i = 0; infos != null && i < infos.size() && i < MAX_SHORTCUTS_COUNT; i++) { - ShortcutInfo shortcutInfo = infos.get(i); - ItemInfoWithIcon si = new WorkspaceItemInfo(shortcutInfo, getContext()); - si.rank = i; - shortcuts.add(new Pair<>(shortcutInfo, si)); - } - - mBubbleTextView.applyFromApplicationInfo(appInfo); - mIconView.setBackground(mBubbleTextView.getIcon()); - mIconView.setTag(appInfo); - LauncherAppState appState = LauncherAppState.getInstance(getContext()); - for (int i = 0; i < mDeepShortcutTextViews.length; i++) { - BubbleTextView shortcutView = mDeepShortcutTextViews[i]; - mDeepShortcutTextViews[i].setVisibility(shortcuts.size() > i ? VISIBLE : GONE); - if (i < shortcuts.size()) { - Pair p = shortcuts.get(i); - //apply ItemInfo and prepare view - shortcutView.applyFromWorkspaceItem((WorkspaceItemInfo) p.second); - MODEL_EXECUTOR.execute(() -> { - // load unbadged shortcut in background and update view when icon ready - appState.getIconCache().getUnbadgedShortcutIcon(p.second, p.first); - MAIN_EXECUTOR.post(() -> shortcutView.reapplyItemInfo(p.second)); - }); - } - } - SearchEventTracker.INSTANCE.get(getContext()).registerWeakHandler(searchTarget, this); - } - - @Override - public void onDropCompleted(View target, DropTarget.DragObject d, boolean success) { - mBubbleTextView.setVisibility(VISIBLE); - mBubbleTextView.setIconVisible(true); - } - - private void setWillDrawIcon(boolean willDraw) { - mIconView.setVisibility(willDraw ? View.VISIBLE : View.INVISIBLE); - } - - /** - * Drag and drop handler for popup items in Launcher activity - */ - public static class HeroItemDragHandler implements OnLongClickListener { - private final Launcher mLauncher; - private final HeroSearchResultView mContainer; - - HeroItemDragHandler(Context context, HeroSearchResultView container) { - mLauncher = Launcher.getLauncher(context); - mContainer = container; - } - - @Override - public boolean onLongClick(View v) { - if (!ItemLongClickListener.canStartDrag(mLauncher)) return false; - mContainer.setWillDrawIcon(false); - - DraggableView draggableView = DraggableView.ofType(DraggableView.DRAGGABLE_ICON); - WorkspaceItemInfo itemInfo = new WorkspaceItemInfo((AppInfo) v.getTag()); - itemInfo.container = CONTAINER_ALL_APPS; - DragPreviewProvider previewProvider = new ShortcutDragPreviewProvider( - mContainer.mIconView, new Point()); - mLauncher.getWorkspace().beginDragShared(mContainer.mBubbleTextView, - draggableView, mContainer, itemInfo, previewProvider, new DragOptions()); - - SearchTargetEvent event = new SearchTargetEvent.Builder(mContainer.mSearchTarget, - SearchTargetEvent.LONG_PRESS).build(); - SearchEventTracker.INSTANCE.get(mLauncher).notifySearchTargetEvent(event); - return false; - } - } - - @Override - public void handleSelection(int eventType) { - ItemInfo itemInfo = (ItemInfo) mBubbleTextView.getTag(); - if (itemInfo == null) return; - Launcher launcher = Launcher.getLauncher(getContext()); - launcher.startActivitySafely(this, itemInfo.getIntent(), itemInfo); - - SearchEventTracker.INSTANCE.get(getContext()).notifySearchTargetEvent( - new SearchTargetEvent.Builder(mSearchTarget, eventType).build()); - } -} diff --git a/src/com/android/launcher3/views/SearchResultIcon.java b/src/com/android/launcher3/views/SearchResultIcon.java index ea06d5b20f..51c741b342 100644 --- a/src/com/android/launcher3/views/SearchResultIcon.java +++ b/src/com/android/launcher3/views/SearchResultIcon.java @@ -15,21 +15,40 @@ */ package com.android.launcher3.views; +import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; +import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; +import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; + +import android.app.RemoteAction; +import android.content.ComponentName; import android.content.Context; +import android.content.pm.ShortcutInfo; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.UserHandle; import android.util.AttributeSet; import android.view.View; +import android.view.ViewGroup; import com.android.launcher3.BubbleTextView; import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; import com.android.launcher3.allapps.AllAppsStore; import com.android.launcher3.allapps.search.AllAppsSearchBarController; import com.android.launcher3.allapps.search.SearchEventTracker; +import com.android.launcher3.icons.BitmapInfo; +import com.android.launcher3.icons.LauncherIcons; import com.android.launcher3.model.data.AppInfo; +import com.android.launcher3.model.data.ItemInfoWithIcon; +import com.android.launcher3.model.data.RemoteActionItemInfo; +import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.touch.ItemLongClickListener; import com.android.launcher3.util.ComponentKey; import com.android.systemui.plugins.shared.SearchTarget; import com.android.systemui.plugins.shared.SearchTargetEvent; +import java.util.function.Consumer; + /** * A {@link BubbleTextView} representing a single cell result in AllApps */ @@ -39,10 +58,21 @@ public class SearchResultIcon extends BubbleTextView implements public static final String TARGET_TYPE_APP = "app"; + public static final String TARGET_TYPE_HERO_APP = "hero_app"; + public static final String TARGET_TYPE_SHORTCUT = "shortcut"; + public static final String TARGET_TYPE_REMOTE_ACTION = "remote_action"; + + public static final String REMOTE_ACTION_SHOULD_START = "should_start_for_result"; + public static final String REMOTE_ACTION_TOKEN = "action_token"; + + + private static final String[] LONG_PRESS_SUPPORTED_TYPES = + new String[]{TARGET_TYPE_APP, TARGET_TYPE_SHORTCUT, TARGET_TYPE_HERO_APP}; private final Launcher mLauncher; private SearchTarget mSearchTarget; + private Consumer mOnItemInfoChanged; public SearchResultIcon(Context context) { this(context, null, 0); @@ -64,26 +94,96 @@ public class SearchResultIcon extends BubbleTextView implements setOnFocusChangeListener(mLauncher.getFocusHandler()); setOnClickListener(this); setOnLongClickListener(this); - getLayoutParams().height = mLauncher.getDeviceProfile().allAppsCellHeightPx; + setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + mLauncher.getDeviceProfile().allAppsCellHeightPx)); + } + + /** + * Applies search target with a ItemInfoWithIcon consumer to be called after itemInfo is + * constructed + */ + public void applySearchTarget(SearchTarget searchTarget, Consumer cb) { + mOnItemInfoChanged = cb; + applySearchTarget(searchTarget); } @Override public void applySearchTarget(SearchTarget searchTarget) { mSearchTarget = searchTarget; - AllAppsStore appsStore = mLauncher.getAppsView().getAppsStore(); SearchEventTracker.getInstance(getContext()).registerWeakHandler(mSearchTarget, this); - if (searchTarget.getItemType().equals(TARGET_TYPE_APP)) { - AppInfo appInfo = appsStore.getApp(new ComponentKey(searchTarget.getComponentName(), - searchTarget.getUserHandle())); - applyFromApplicationInfo(appInfo); + switch (searchTarget.getItemType()) { + case TARGET_TYPE_APP: + case TARGET_TYPE_HERO_APP: + prepareUsingApp(searchTarget.getComponentName(), searchTarget.getUserHandle()); + break; + case TARGET_TYPE_SHORTCUT: + prepareUsingShortcutInfo(searchTarget.getShortcutInfos().get(0)); + break; + case TARGET_TYPE_REMOTE_ACTION: + prepareUsingRemoteAction(searchTarget.getRemoteAction(), + searchTarget.getExtras().getString(REMOTE_ACTION_TOKEN), + searchTarget.getExtras().getBoolean(REMOTE_ACTION_SHOULD_START), + searchTarget.getItemType().equals(TARGET_TYPE_REMOTE_ACTION)); + break; } } + private void prepareUsingApp(ComponentName componentName, UserHandle userHandle) { + AllAppsStore appsStore = mLauncher.getAppsView().getAppsStore(); + AppInfo appInfo = appsStore.getApp(new ComponentKey(componentName, userHandle)); + applyFromApplicationInfo(appInfo); + notifyItemInfoChanged(appInfo); + } + + + private void prepareUsingShortcutInfo(ShortcutInfo shortcutInfo) { + WorkspaceItemInfo workspaceItemInfo = new WorkspaceItemInfo(shortcutInfo, getContext()); + notifyItemInfoChanged(workspaceItemInfo); + LauncherAppState launcherAppState = LauncherAppState.getInstance(getContext()); + MODEL_EXECUTOR.execute(() -> { + launcherAppState.getIconCache().getShortcutIcon(workspaceItemInfo, shortcutInfo); + MAIN_EXECUTOR.post(() -> applyFromWorkspaceItem(workspaceItemInfo)); + }); + } + + private void prepareUsingRemoteAction(RemoteAction remoteAction, String token, boolean start, + boolean useIconToBadge) { + RemoteActionItemInfo itemInfo = new RemoteActionItemInfo(remoteAction, token, start); + notifyItemInfoChanged(itemInfo); + UI_HELPER_EXECUTOR.post(() -> { + // If the Drawable from the remote action is not AdaptiveBitmap, styling will not + // work. + try (LauncherIcons li = LauncherIcons.obtain(getContext())) { + Drawable d = itemInfo.getRemoteAction().getIcon().loadDrawable(getContext()); + BitmapInfo bitmap = li.createBadgedIconBitmap(d, itemInfo.user, + Build.VERSION.SDK_INT); + + if (useIconToBadge) { + BitmapInfo placeholder = li.createIconBitmap( + itemInfo.getRemoteAction().getTitle().toString().substring(0, 1), + bitmap.color); + itemInfo.bitmap = li.badgeBitmap(placeholder.icon, bitmap); + } else { + itemInfo.bitmap = bitmap; + } + } + MAIN_EXECUTOR.post(() -> applyFromRemoteActionInfo(itemInfo)); + }); + } + @Override public void handleSelection(int eventType) { mLauncher.getItemOnClickListener().onClick(this); - SearchEventTracker.INSTANCE.get(mLauncher).notifySearchTargetEvent( - new SearchTargetEvent.Builder(mSearchTarget, eventType).build()); + reportEvent(eventType); + } + + private void reportEvent(int eventType) { + SearchTargetEvent.Builder b = new SearchTargetEvent.Builder(mSearchTarget, eventType); + if (mSearchTarget.getItemType().equals(TARGET_TYPE_SHORTCUT)) { + b.setShortcutPosition(0); + } + SearchEventTracker.INSTANCE.get(mLauncher).notifySearchTargetEvent(b.build()); + } @Override @@ -93,8 +193,25 @@ public class SearchResultIcon extends BubbleTextView implements @Override public boolean onLongClick(View view) { - SearchEventTracker.INSTANCE.get(mLauncher).notifySearchTargetEvent( - new SearchTargetEvent.Builder(mSearchTarget, SearchTargetEvent.LONG_PRESS).build()); + if (!supportsLongPress(mSearchTarget.getItemType())) { + return false; + } + reportEvent(SearchTargetEvent.LONG_PRESS); return ItemLongClickListener.INSTANCE_ALL_APPS.onLongClick(view); + + } + + private boolean supportsLongPress(String type) { + for (String t : LONG_PRESS_SUPPORTED_TYPES) { + if (t.equals(type)) return true; + } + return false; + } + + private void notifyItemInfoChanged(ItemInfoWithIcon itemInfoWithIcon) { + if (mOnItemInfoChanged != null) { + mOnItemInfoChanged.accept(itemInfoWithIcon); + mOnItemInfoChanged = null; + } } } diff --git a/src/com/android/launcher3/views/SearchResultIconRow.java b/src/com/android/launcher3/views/SearchResultIconRow.java index bdbe890e8d..e3c7661166 100644 --- a/src/com/android/launcher3/views/SearchResultIconRow.java +++ b/src/com/android/launcher3/views/SearchResultIconRow.java @@ -17,188 +17,185 @@ package com.android.launcher3.views; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; -import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; -import android.app.RemoteAction; +import android.content.ComponentName; import android.content.Context; import android.content.pm.ShortcutInfo; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.drawable.Drawable; -import android.os.Build; +import android.os.UserHandle; +import android.text.TextUtils; import android.util.AttributeSet; -import android.widget.EditText; +import android.util.Pair; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.launcher3.BubbleTextView; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; import com.android.launcher3.allapps.search.AllAppsSearchBarController; import com.android.launcher3.allapps.search.SearchEventTracker; -import com.android.launcher3.icons.BitmapInfo; -import com.android.launcher3.icons.LauncherIcons; -import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.ItemInfoWithIcon; -import com.android.launcher3.model.data.RemoteActionItemInfo; +import com.android.launcher3.model.data.PackageItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; -import com.android.launcher3.touch.ItemClickHandler; -import com.android.launcher3.util.Themes; import com.android.systemui.plugins.shared.SearchTarget; import com.android.systemui.plugins.shared.SearchTargetEvent; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + /** - * A view representing a stand alone shortcut search result + * A full width representation of {@link SearchResultIcon} with a secondary label and inline + * shortcuts */ -public class SearchResultIconRow extends DoubleShadowBubbleTextView implements - AllAppsSearchBarController.SearchTargetHandler { +public class SearchResultIconRow extends LinearLayout implements + AllAppsSearchBarController.SearchTargetHandler, View.OnClickListener, + View.OnLongClickListener, Consumer { + public static final int MAX_SHORTCUTS_COUNT = 2; - public static final String TARGET_TYPE_REMOTE_ACTION = "remote_action"; - public static final String TARGET_TYPE_SUGGEST = "suggest"; - public static final String TARGET_TYPE_SHORTCUT = "shortcut"; - - - public static final String REMOTE_ACTION_SHOULD_START = "should_start_for_result"; - public static final String REMOTE_ACTION_TOKEN = "action_token"; - - private final boolean mMatchesInset; + private final Launcher mLauncher; + private final LauncherAppState mLauncherAppState; + private SearchResultIcon mResultIcon; + private TextView mTitleView; + private TextView mDescriptionView; + private BubbleTextView[] mShortcutViews = new BubbleTextView[2]; private SearchTarget mSearchTarget; + private PackageItemInfo mProviderInfo; - @Nullable private Drawable mCustomIcon; - public SearchResultIconRow(@NonNull Context context) { + public SearchResultIconRow(Context context) { this(context, null, 0); } - public SearchResultIconRow(@NonNull Context context, + public SearchResultIconRow(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } - public SearchResultIconRow(@NonNull Context context, @Nullable AttributeSet attrs, - int defStyleAttr) { + public SearchResultIconRow(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - TypedArray a = context.obtainStyledAttributes(attrs, - R.styleable.SearchResultIconRow, defStyleAttr, 0); - mMatchesInset = a.getBoolean(R.styleable.SearchResultIconRow_matchTextInsetWithQuery, - false); - - int customIconResId = a.getResourceId(R.styleable.SearchResultIconRow_customIcon, 0); - - if (customIconResId != 0) { - mCustomIcon = Launcher.getLauncher(context).getDrawable(customIconResId); - } - - a.recycle(); - } - - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - Launcher launcher = Launcher.getLauncher(getContext()); - if (mMatchesInset && launcher.getAppsView() != null && getParent() != null) { - EditText editText = launcher.getAppsView().getSearchUiManager().getEditText(); - if (editText != null) { - int counterOffset = getIconSize() + getCompoundDrawablePadding() / 2; - setPadding(editText.getLeft() - counterOffset, getPaddingTop(), - getPaddingRight(), getPaddingBottom()); - } - } + mLauncher = Launcher.getLauncher(getContext()); + mLauncherAppState = LauncherAppState.getInstance(getContext()); } @Override - protected void drawFocusHighlight(Canvas canvas) { - mHighlightPaint.setColor(mHighlightColor); - float r = Themes.getDialogCornerRadius(getContext()); - canvas.drawRoundRect(0, 0, getWidth(), getHeight(), r, r, mHighlightPaint); - } + protected void onFinishInflate() { + super.onFinishInflate(); + int iconSize = mLauncher.getDeviceProfile().allAppsIconSizePx; + mResultIcon = findViewById(R.id.icon); + mTitleView = findViewById(R.id.title); + mDescriptionView = findViewById(R.id.desc); + mShortcutViews[0] = findViewById(R.id.shortcut_0); + mShortcutViews[1] = findViewById(R.id.shortcut_1); + mResultIcon.getLayoutParams().height = iconSize; + mResultIcon.getLayoutParams().width = iconSize; + mResultIcon.setTextVisibility(false); + for (BubbleTextView bubbleTextView : mShortcutViews) { + ViewGroup.LayoutParams lp = bubbleTextView.getLayoutParams(); + lp.width = iconSize; + bubbleTextView.setOnClickListener(view -> { + WorkspaceItemInfo itemInfo = (WorkspaceItemInfo) bubbleTextView.getTag(); + SearchTargetEvent event = new SearchTargetEvent.Builder(mSearchTarget, + SearchTargetEvent.CHILD_SELECT).setShortcutPosition(itemInfo.rank).build(); + SearchEventTracker.getInstance(getContext()).notifySearchTargetEvent(event); + mLauncher.getItemOnClickListener().onClick(view); + }); + } + setOnClickListener(this); + setOnLongClickListener(this); + } @Override public void applySearchTarget(SearchTarget searchTarget) { mSearchTarget = searchTarget; - String type = searchTarget.getItemType(); - if (type.equals(TARGET_TYPE_REMOTE_ACTION) || type.equals(TARGET_TYPE_SUGGEST)) { - prepareUsingRemoteAction(searchTarget.getRemoteAction(), - searchTarget.getExtras().getString(REMOTE_ACTION_TOKEN), - searchTarget.getExtras().getBoolean(REMOTE_ACTION_SHOULD_START), - type.equals(TARGET_TYPE_REMOTE_ACTION)); + mResultIcon.applySearchTarget(searchTarget, this); + String itemType = searchTarget.getItemType(); + boolean showDesc = itemType.equals(SearchResultIcon.TARGET_TYPE_SHORTCUT); + mDescriptionView.setVisibility(showDesc ? VISIBLE : GONE); - } else if (type.equals(TARGET_TYPE_SHORTCUT)) { - prepareUsingShortcutInfo(searchTarget.getShortcutInfos().get(0)); + if (itemType.equals(SearchResultIcon.TARGET_TYPE_SHORTCUT)) { + ShortcutInfo shortcutInfo = searchTarget.getShortcutInfos().get(0); + setProviderDetails(new ComponentName(shortcutInfo.getPackage(), ""), + shortcutInfo.getUserHandle()); + } else if (itemType.equals(SearchResultIcon.TARGET_TYPE_HERO_APP)) { + showInlineShortcuts(mSearchTarget.getShortcutInfos()); + } else if (itemType.equals(SearchResultIcon.TARGET_TYPE_REMOTE_ACTION)) { + CharSequence desc = mSearchTarget.getRemoteAction().getContentDescription(); + if (!TextUtils.isEmpty(desc)) { + mDescriptionView.setVisibility(VISIBLE); + mDescriptionView.setText(desc); + } + } + if (!itemType.equals(SearchResultIcon.TARGET_TYPE_HERO_APP)) { + showInlineShortcuts(new ArrayList<>()); } - setOnClickListener(v -> handleSelection(SearchTargetEvent.SELECT)); - SearchEventTracker.INSTANCE.get(getContext()).registerWeakHandler(searchTarget, this); } - private void prepareUsingShortcutInfo(ShortcutInfo shortcutInfo) { - WorkspaceItemInfo workspaceItemInfo = new WorkspaceItemInfo(shortcutInfo, getContext()); - applyFromWorkspaceItem(workspaceItemInfo); - LauncherAppState launcherAppState = LauncherAppState.getInstance(getContext()); - if (!loadIconFromResource()) { - MODEL_EXECUTOR.execute(() -> { - launcherAppState.getIconCache().getShortcutIcon(workspaceItemInfo, shortcutInfo); - reapplyItemInfoAsync(workspaceItemInfo); + @Override + public void accept(ItemInfoWithIcon itemInfoWithIcon) { + mTitleView.setText(itemInfoWithIcon.title); + } + + private void showInlineShortcuts(List infos) { + if (infos == null) return; + ArrayList> shortcuts = new ArrayList<>(); + for (int i = 0; infos != null && i < infos.size() && i < MAX_SHORTCUTS_COUNT; i++) { + ShortcutInfo shortcutInfo = infos.get(i); + ItemInfoWithIcon si = new WorkspaceItemInfo(shortcutInfo, getContext()); + si.rank = i; + shortcuts.add(new Pair<>(shortcutInfo, si)); + } + + for (int i = 0; i < mShortcutViews.length; i++) { + BubbleTextView shortcutView = mShortcutViews[i]; + mShortcutViews[i].setVisibility(shortcuts.size() > i ? VISIBLE : GONE); + if (i < shortcuts.size()) { + Pair p = shortcuts.get(i); + //apply ItemInfo and prepare view + shortcutView.applyFromWorkspaceItem((WorkspaceItemInfo) p.second); + MODEL_EXECUTOR.execute(() -> { + // load unbadged shortcut in background and update view when icon ready + mLauncherAppState.getIconCache().getUnbadgedShortcutIcon(p.second, p.first); + MAIN_EXECUTOR.post(() -> shortcutView.reapplyItemInfo(p.second)); + }); + } + } + } + + + private void setProviderDetails(ComponentName componentName, UserHandle userHandle) { + PackageItemInfo packageItemInfo = new PackageItemInfo(componentName.getPackageName()); + if (mProviderInfo == packageItemInfo) return; + MODEL_EXECUTOR.post(() -> { + packageItemInfo.user = userHandle; + mLauncherAppState.getIconCache().getTitleAndIconForApp(packageItemInfo, true); + MAIN_EXECUTOR.post(() -> { + mDescriptionView.setText(packageItemInfo.title); + mProviderInfo = packageItemInfo; }); - } - } - - private void prepareUsingRemoteAction(RemoteAction remoteAction, String token, boolean start, - boolean useIconToBadge) { - RemoteActionItemInfo itemInfo = new RemoteActionItemInfo(remoteAction, token, start); - - applyFromRemoteActionInfo(itemInfo); - if (itemInfo.isEscapeHatch() || !loadIconFromResource()) { - UI_HELPER_EXECUTOR.post(() -> { - // If the Drawable from the remote action is not AdaptiveBitmap, styling will not - // work. - try (LauncherIcons li = LauncherIcons.obtain(getContext())) { - Drawable d = itemInfo.getRemoteAction().getIcon().loadDrawable(getContext()); - BitmapInfo bitmap = li.createBadgedIconBitmap(d, itemInfo.user, - Build.VERSION.SDK_INT); - - if (useIconToBadge) { - BitmapInfo placeholder = li.createIconBitmap( - itemInfo.getRemoteAction().getTitle().toString().substring(0, 1), - bitmap.color); - itemInfo.bitmap = li.badgeBitmap(placeholder.icon, bitmap); - } else { - itemInfo.bitmap = bitmap; - } - reapplyItemInfoAsync(itemInfo); - } - }); - } - - } - - private boolean loadIconFromResource() { - if (mCustomIcon == null) return false; - setIcon(mCustomIcon); - return true; - } - - void reapplyItemInfoAsync(ItemInfoWithIcon itemInfoWithIcon) { - MAIN_EXECUTOR.post(() -> { - reapplyItemInfo(itemInfoWithIcon); - mCustomIcon = getIcon(); }); } @Override public void handleSelection(int eventType) { - ItemInfo itemInfo = (ItemInfo) getTag(); - Launcher launcher = Launcher.getLauncher(getContext()); - if (itemInfo instanceof WorkspaceItemInfo) { - ItemClickHandler.onClickAppShortcut(this, (WorkspaceItemInfo) itemInfo, launcher); - } else { - ItemClickHandler.onClickRemoteAction(launcher, (RemoteActionItemInfo) itemInfo); - } - SearchEventTracker.INSTANCE.get(getContext()).notifySearchTargetEvent( - new SearchTargetEvent.Builder(mSearchTarget, eventType).build()); + mResultIcon.handleSelection(eventType); + } + + @Override + public void onClick(View view) { + mResultIcon.performClick(); + } + + @Override + public boolean onLongClick(View view) { + mResultIcon.performLongClick(); + return false; } } diff --git a/src/com/android/launcher3/views/SearchResultSettingsSlice.java b/src/com/android/launcher3/views/SearchResultSettingsSlice.java new file mode 100644 index 0000000000..29e6c1b8cf --- /dev/null +++ b/src/com/android/launcher3/views/SearchResultSettingsSlice.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2020 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.views; + +import android.content.Context; +import android.net.Uri; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.LiveData; +import androidx.slice.Slice; +import androidx.slice.SliceItem; +import androidx.slice.widget.EventInfo; +import androidx.slice.widget.SliceView; + +import com.android.launcher3.Launcher; +import com.android.launcher3.R; +import com.android.launcher3.allapps.search.AllAppsSearchBarController; +import com.android.launcher3.allapps.search.SearchEventTracker; +import com.android.systemui.plugins.shared.SearchTarget; +import com.android.systemui.plugins.shared.SearchTargetEvent; + +/** + * A slice view wrapper with settings app icon at start + */ +public class SearchResultSettingsSlice extends LinearLayout implements + AllAppsSearchBarController.SearchTargetHandler, SliceView.OnSliceActionListener { + + + public static final String TARGET_TYPE_SLICE = "settings_slice"; + + private static final String TAG = "SearchSliceController"; + private static final String URI_EXTRA_KEY = "slice_uri"; + + private SliceView mSliceView; + private View mIcon; + private LiveData mSliceLiveData; + private SearchTarget mSearchTarget; + private final Launcher mLauncher; + + public SearchResultSettingsSlice(Context context) { + this(context, null, 0); + } + + public SearchResultSettingsSlice(Context context, + @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public SearchResultSettingsSlice(Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + mLauncher = Launcher.getLauncher(getContext()); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mSliceView = findViewById(R.id.slice); + mIcon = findViewById(R.id.icon); + SearchSettingsRowView.applySettingsIcon(mLauncher, mIcon); + } + + @Override + public void applySearchTarget(SearchTarget searchTarget) { + reset(); + mSearchTarget = searchTarget; + try { + mSliceLiveData = mLauncher.getLiveSearchManager().getSliceForUri(getSliceUri()); + mSliceLiveData.observe(mLauncher, mSliceView); + } catch (Exception ex) { + Log.e(TAG, "unable to bind slice", ex); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mSliceView.setOnSliceActionListener(this); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + reset(); + } + + @Override + public void handleSelection(int eventType) { + SearchEventTracker.INSTANCE.get(mLauncher).notifySearchTargetEvent( + new SearchTargetEvent.Builder(mSearchTarget, + SearchTargetEvent.CHILD_SELECT).build()); + } + + private void reset() { + mSliceView.setOnSliceActionListener(null); + if (mSliceLiveData != null) { + mSliceLiveData.removeObservers(mLauncher); + } + } + + @Override + public void onSliceAction(@NonNull EventInfo eventInfo, @NonNull SliceItem sliceItem) { + handleSelection(SearchTargetEvent.CHILD_SELECT); + } + + private Uri getSliceUri() { + return mSearchTarget.getExtras().getParcelable(URI_EXTRA_KEY); + } + +} diff --git a/src/com/android/launcher3/views/SearchResultSuggestion.java b/src/com/android/launcher3/views/SearchResultSuggestion.java new file mode 100644 index 0000000000..c67b1cf7f3 --- /dev/null +++ b/src/com/android/launcher3/views/SearchResultSuggestion.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2020 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.views; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.ViewGroup; + +import com.android.launcher3.R; + +/** + * {@link SearchResultIconRow} with custom drawable resource + */ +public class SearchResultSuggestion extends SearchResultIcon { + + public static final String TARGET_TYPE_SUGGEST = "suggest"; + private final Drawable mCustomIcon; + + public SearchResultSuggestion(Context context) { + this(context, null, 0); + } + + public SearchResultSuggestion(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SearchResultSuggestion(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.SearchResultSuggestion, defStyle, 0); + mCustomIcon = a.getDrawable(R.styleable.SearchResultSuggestion_customIcon); + a.recycle(); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + ViewGroup.LayoutParams lp = getLayoutParams(); + lp.height = BaseDragLayer.LayoutParams.WRAP_CONTENT; + } + + @Override + protected void setIcon(Drawable icon) { + super.setIcon(mCustomIcon); + } +} diff --git a/src/com/android/launcher3/views/SearchResultWidget.java b/src/com/android/launcher3/views/SearchResultWidget.java new file mode 100644 index 0000000000..7d539553f0 --- /dev/null +++ b/src/com/android/launcher3/views/SearchResultWidget.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2020 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.views; + +import android.appwidget.AppWidgetHostView; +import android.appwidget.AppWidgetProviderInfo; +import android.content.Context; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.widget.RelativeLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.launcher3.AppWidgetResizeFrame; +import com.android.launcher3.CheckLongPressHelper; +import com.android.launcher3.DeviceProfile; +import com.android.launcher3.Launcher; +import com.android.launcher3.allapps.search.AllAppsSearchBarController; +import com.android.launcher3.allapps.search.SearchEventTracker; +import com.android.launcher3.allapps.search.SearchWidgetInfoContainer; +import com.android.launcher3.dragndrop.DraggableView; +import com.android.launcher3.touch.ItemLongClickListener; +import com.android.launcher3.widget.PendingAddWidgetInfo; +import com.android.systemui.plugins.shared.SearchTarget; +import com.android.systemui.plugins.shared.SearchTargetEvent; + +/** + * displays live version of a widget upon receiving {@link AppWidgetProviderInfo} from Search + * provider + */ +public class SearchResultWidget extends RelativeLayout implements + AllAppsSearchBarController.SearchTargetHandler, DraggableView, View.OnLongClickListener { + + private static final String TAG = "SearchResultWidget"; + + public static final String TARGET_TYPE_WIDGET_LIVE = "widget"; + + private final Rect mWidgetOffset = new Rect(); + + private final Launcher mLauncher; + private final CheckLongPressHelper mLongPressHelper; + private final GestureDetector mClickDetector; + private final AppWidgetHostView mHostView; + private final float mScaleToFit; + + private SearchTarget mSearchTarget; + private AppWidgetProviderInfo mProviderInfo; + + private SearchWidgetInfoContainer mInfoContainer; + + public SearchResultWidget(@NonNull Context context) { + this(context, null, 0); + } + + public SearchResultWidget(@NonNull Context context, + @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public SearchResultWidget(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + mLauncher = Launcher.getLauncher(context); + mHostView = new AppWidgetHostView(context); + DeviceProfile grid = mLauncher.getDeviceProfile(); + mScaleToFit = Math.min(grid.appWidgetScale.x, grid.appWidgetScale.y); + + // detect tap event on widget container for search target event reporting + mClickDetector = new GestureDetector(context, + new ClickListener(() -> handleSelection(SearchTargetEvent.CHILD_SELECT))); + + mLongPressHelper = new CheckLongPressHelper(this); + mLongPressHelper.setLongPressTimeoutFactor(1); + setOnLongClickListener(this); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + addView(mHostView); + } + + @Override + public void applySearchTarget(SearchTarget searchTarget) { + if (searchTarget.getExtras() == null + || searchTarget.getExtras().getParcelable("provider") == null) { + setVisibility(GONE); + return; + } + AppWidgetProviderInfo providerInfo = searchTarget.getExtras().getParcelable("provider"); + if (mProviderInfo != null && providerInfo.provider.equals(mProviderInfo.provider) + && providerInfo.getProfile().equals(mProviderInfo.getProfile())) { + return; + } + removeListener(); + + mSearchTarget = searchTarget; + mProviderInfo = providerInfo; + + mInfoContainer = mLauncher.getLiveSearchManager().getPlaceHolderWidget(providerInfo); + if (mInfoContainer == null) { + setVisibility(GONE); + return; + } + setVisibility(VISIBLE); + mInfoContainer.attachWidget(mHostView); + PendingAddWidgetInfo info = (PendingAddWidgetInfo) mHostView.getTag(); + int[] size = mLauncher.getWorkspace().estimateItemSize(info); + mHostView.getLayoutParams().width = size[0]; + mHostView.getLayoutParams().height = size[1]; + AppWidgetResizeFrame.updateWidgetSizeRanges(mHostView, mLauncher, info.spanX, + info.spanY); + mHostView.requestLayout(); + setTag(info); + } + + /** + * Stops hostView from getting updates on a widget provider + */ + public void removeListener() { + if (mInfoContainer != null) { + mInfoContainer.detachWidget(mHostView); + } + } + + @Override + public void handleSelection(int eventType) { + SearchEventTracker.INSTANCE.get(getContext()).notifySearchTargetEvent( + new SearchTargetEvent.Builder(mSearchTarget, eventType).build()); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + mLongPressHelper.onTouchEvent(ev); + mClickDetector.onTouchEvent(ev); + if (ev.getAction() == MotionEvent.ACTION_UP && !mLongPressHelper.hasPerformedLongPress()) { + handleSelection(SearchTargetEvent.CHILD_SELECT); + } + return super.onInterceptTouchEvent(ev); + } + + + @Override + public void cancelLongPress() { + super.cancelLongPress(); + mLongPressHelper.cancelLongPress(); + } + + @Override + public int getViewType() { + return DraggableView.DRAGGABLE_WIDGET; + } + + @Override + public void getSourceVisualDragBounds(Rect bounds) { + mHostView.getHitRect(mWidgetOffset); + int width = (int) (mHostView.getMeasuredWidth() * mScaleToFit); + int height = (int) (mHostView.getMeasuredHeight() * mScaleToFit); + bounds.set(mWidgetOffset.left, + mWidgetOffset.top, + width + mWidgetOffset.left, + height + mWidgetOffset.top); + } + + @Override + public boolean onLongClick(View view) { + ItemLongClickListener.INSTANCE_ALL_APPS.onLongClick(view); + handleSelection(SearchTargetEvent.LONG_PRESS); + return false; + } + + static class ClickListener extends GestureDetector.SimpleOnGestureListener { + private final Runnable mCb; + + ClickListener(Runnable cb) { + mCb = cb; + } + + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + mCb.run(); + return super.onSingleTapConfirmed(e); + } + } +} diff --git a/src/com/android/launcher3/views/SearchResultWidgetPreview.java b/src/com/android/launcher3/views/SearchResultWidgetPreview.java new file mode 100644 index 0000000000..c11c232307 --- /dev/null +++ b/src/com/android/launcher3/views/SearchResultWidgetPreview.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2020 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.views; + +import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; +import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; + +import android.appwidget.AppWidgetProviderInfo; +import android.content.Context; +import android.graphics.Point; +import android.util.AttributeSet; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.Toast; + +import androidx.annotation.Nullable; + +import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; +import com.android.launcher3.LauncherAppWidgetProviderInfo; +import com.android.launcher3.R; +import com.android.launcher3.allapps.search.AllAppsSearchBarController; +import com.android.launcher3.allapps.search.SearchEventTracker; +import com.android.launcher3.dragndrop.DragOptions; +import com.android.launcher3.model.WidgetItem; +import com.android.launcher3.touch.ItemLongClickListener; +import com.android.launcher3.widget.BaseWidgetSheet; +import com.android.launcher3.widget.PendingItemDragHelper; +import com.android.launcher3.widget.WidgetCell; +import com.android.launcher3.widget.WidgetImageView; +import com.android.systemui.plugins.shared.SearchTarget; +import com.android.systemui.plugins.shared.SearchTargetEvent; + +/** + * displays preview of a widget upon receiving {@link AppWidgetProviderInfo} from Search provider + */ +public class SearchResultWidgetPreview extends LinearLayout implements + AllAppsSearchBarController.SearchTargetHandler, View.OnLongClickListener, + View.OnClickListener { + + public static final String TARGET_TYPE_WIDGET_PREVIEW = "widget_preview"; + private final Launcher mLauncher; + private final LauncherAppState mAppState; + private WidgetCell mWidgetCell; + private Toast mWidgetToast; + + private SearchTarget mSearchTarget; + + + public SearchResultWidgetPreview(Context context) { + this(context, null, 0); + } + + public SearchResultWidgetPreview(Context context, + @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public SearchResultWidgetPreview(Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + mLauncher = Launcher.getLauncher(context); + mAppState = LauncherAppState.getInstance(context); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mWidgetCell = findViewById(R.id.widget_cell); + mWidgetCell.setOnLongClickListener(this); + mWidgetCell.setOnClickListener(this); + } + + @Override + public void applySearchTarget(SearchTarget searchTarget) { + if (searchTarget.getExtras() == null + || searchTarget.getExtras().getParcelable("provider") == null) { + setVisibility(GONE); + return; + } + mSearchTarget = searchTarget; + AppWidgetProviderInfo providerInfo = searchTarget.getExtras().getParcelable("provider"); + LauncherAppWidgetProviderInfo pInfo = LauncherAppWidgetProviderInfo.fromProviderInfo( + getContext(), providerInfo); + MODEL_EXECUTOR.post(() -> { + WidgetItem widgetItem = new WidgetItem(pInfo, mLauncher.getDeviceProfile().inv, + mAppState.getIconCache()); + MAIN_EXECUTOR.post(() -> { + mWidgetCell.applyFromCellItem(widgetItem, mAppState.getWidgetCache()); + mWidgetCell.ensurePreview(); + }); + }); + + } + + @Override + public boolean onLongClick(View view) { + view.cancelLongPress(); + if (!ItemLongClickListener.canStartDrag(mLauncher)) return false; + if (mWidgetCell.getTag() == null) return false; + + WidgetImageView imageView = mWidgetCell.getWidgetView(); + if (imageView.getBitmap() == null) { + return false; + } + + int[] loc = new int[2]; + mLauncher.getDragLayer().getLocationInDragLayer(imageView, loc); + + new PendingItemDragHelper(mWidgetCell).startDrag( + imageView.getBitmapBounds(), imageView.getBitmap().getWidth(), imageView.getWidth(), + new Point(loc[0], loc[1]), mLauncher.getAppsView(), new DragOptions()); + handleSelection(SearchTargetEvent.LONG_PRESS); + return true; + } + + @Override + public void onClick(View view) { + mWidgetToast = BaseWidgetSheet.showWidgetToast(getContext(), mWidgetToast); + handleSelection(SearchTargetEvent.SELECT); + } + + @Override + public void handleSelection(int eventType) { + SearchEventTracker.INSTANCE.get(getContext()).notifySearchTargetEvent( + new SearchTargetEvent.Builder(mSearchTarget, eventType).build()); + } +} diff --git a/src/com/android/launcher3/views/SearchSettingsRowView.java b/src/com/android/launcher3/views/SearchSettingsRowView.java index f0884f88aa..160ee6514b 100644 --- a/src/com/android/launcher3/views/SearchSettingsRowView.java +++ b/src/com/android/launcher3/views/SearchSettingsRowView.java @@ -15,8 +15,14 @@ */ package com.android.launcher3.views; +import static com.android.launcher3.FastBitmapDrawable.newIcon; +import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; +import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; + import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.os.Bundle; import android.text.TextUtils; import android.util.AttributeSet; @@ -27,38 +33,41 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.launcher3.FastBitmapDrawable; import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; import com.android.launcher3.R; import com.android.launcher3.allapps.search.AllAppsSearchBarController; import com.android.launcher3.allapps.search.SearchEventTracker; +import com.android.launcher3.model.data.PackageItemInfo; import com.android.systemui.plugins.shared.SearchTarget; import com.android.systemui.plugins.shared.SearchTargetEvent; import java.util.ArrayList; +import java.util.List; /** - * A row of tappable TextViews with a breadcrumb for settings search. + * A row of clickable TextViews with a breadcrumb for settings search. */ public class SearchSettingsRowView extends LinearLayout implements View.OnClickListener, AllAppsSearchBarController.SearchTargetHandler { public static final String TARGET_TYPE_SETTINGS_ROW = "settings_row"; - + private View mIconView; private TextView mTitleView; - private TextView mDescriptionView; private TextView mBreadcrumbsView; private Intent mIntent; private SearchTarget mSearchTarget; public SearchSettingsRowView(@NonNull Context context) { - super(context); + this(context, null, 0); } public SearchSettingsRowView(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs); + this(context, attrs, 0); } public SearchSettingsRowView(@NonNull Context context, @Nullable AttributeSet attrs, @@ -69,10 +78,11 @@ public class SearchSettingsRowView extends LinearLayout implements @Override protected void onFinishInflate() { super.onFinishInflate(); + mIconView = findViewById(R.id.icon); mTitleView = findViewById(R.id.title); - mDescriptionView = findViewById(R.id.description); mBreadcrumbsView = findViewById(R.id.breadcrumbs); setOnClickListener(this); + applySettingsIcon(Launcher.getLauncher(getContext()), mIconView); } @Override @@ -81,6 +91,7 @@ public class SearchSettingsRowView extends LinearLayout implements Bundle bundle = searchTarget.getExtras(); mIntent = bundle.getParcelable("intent"); showIfAvailable(mTitleView, bundle.getString("title")); + mIconView.setContentDescription(bundle.getString("title")); ArrayList breadcrumbs = bundle.getStringArrayList("breadcrumbs"); //TODO: implement RTL friendly breadcrumbs view showIfAvailable(mBreadcrumbsView, breadcrumbs != null @@ -113,4 +124,30 @@ public class SearchSettingsRowView extends LinearLayout implements SearchEventTracker.INSTANCE.get(getContext()).notifySearchTargetEvent( new SearchTargetEvent.Builder(mSearchTarget, eventType).build()); } + + /** + * Requests settings app icon from {@link com.android.launcher3.icons.IconCache} and applies + * to to view + */ + public static void applySettingsIcon(Launcher launcher, View view) { + LauncherAppState appState = LauncherAppState.getInstance(launcher); + MODEL_EXECUTOR.post(() -> { + PackageItemInfo packageItemInfo = new PackageItemInfo(getSettingsPackageName(launcher)); + appState.getIconCache().getTitleAndIconForApp(packageItemInfo, false); + MAIN_EXECUTOR.post(() -> { + FastBitmapDrawable iconDrawable = newIcon(appState.getContext(), packageItemInfo); + view.setBackground(iconDrawable); + }); + }); + } + + private static String getSettingsPackageName(Launcher launcher) { + Intent intent = new Intent(android.provider.Settings.ACTION_SETTINGS); + List resolveInfos = launcher.getPackageManager().queryIntentActivities(intent, + PackageManager.MATCH_DEFAULT_ONLY); + if (resolveInfos.size() == 0) { + return ""; + } + return resolveInfos.get(0).activityInfo.packageName; + } } diff --git a/src/com/android/launcher3/views/SearchSliceWrapper.java b/src/com/android/launcher3/views/SearchSliceWrapper.java deleted file mode 100644 index f8a7dc08d6..0000000000 --- a/src/com/android/launcher3/views/SearchSliceWrapper.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2020 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.views; - -import android.content.Context; -import android.net.Uri; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.lifecycle.LiveData; -import androidx.slice.Slice; -import androidx.slice.SliceItem; -import androidx.slice.widget.EventInfo; -import androidx.slice.widget.SliceLiveData; -import androidx.slice.widget.SliceView; - -import com.android.launcher3.Launcher; -import com.android.launcher3.allapps.search.SearchEventTracker; -import com.android.systemui.plugins.shared.SearchTarget; -import com.android.systemui.plugins.shared.SearchTargetEvent; - -/** - * A Wrapper class for {@link SliceView} search results - */ -public class SearchSliceWrapper implements SliceView.OnSliceActionListener { - - public static final String TARGET_TYPE_SLICE = "settings_slice"; - - private static final String TAG = "SearchSliceController"; - private static final String URI_EXTRA_KEY = "slice_uri"; - - - private final Launcher mLauncher; - private final SearchTarget mSearchTarget; - private final SliceView mSliceView; - private LiveData mSliceLiveData; - - public SearchSliceWrapper(Context context, SliceView sliceView, SearchTarget searchTarget) { - mLauncher = Launcher.getLauncher(context); - mSearchTarget = searchTarget; - mSliceView = sliceView; - sliceView.setOnSliceActionListener(this); - try { - mSliceLiveData = SliceLiveData.fromUri(mLauncher, getSliceUri()); - mSliceLiveData.observe((Launcher) mLauncher, sliceView); - } catch (Exception ex) { - Log.e(TAG, "unable to bind slice", ex); - } - } - - /** - * Unregisters event handlers and removes lifecycle observer - */ - public void destroy() { - mSliceView.setOnSliceActionListener(null); - mSliceLiveData.removeObservers(mLauncher); - } - - @Override - public void onSliceAction(@NonNull EventInfo info, @NonNull SliceItem item) { - SearchEventTracker.INSTANCE.get(mLauncher).notifySearchTargetEvent( - new SearchTargetEvent.Builder(mSearchTarget, - SearchTargetEvent.CHILD_SELECT).build()); - } - - private Uri getSliceUri() { - return mSearchTarget.getExtras().getParcelable(URI_EXTRA_KEY); - } -} diff --git a/src/com/android/launcher3/views/ThumbnailSearchResultView.java b/src/com/android/launcher3/views/ThumbnailSearchResultView.java index e929d7fbd8..f213f229a8 100644 --- a/src/com/android/launcher3/views/ThumbnailSearchResultView.java +++ b/src/com/android/launcher3/views/ThumbnailSearchResultView.java @@ -15,8 +15,9 @@ */ package com.android.launcher3.views; -import static com.android.launcher3.views.SearchResultIconRow.REMOTE_ACTION_SHOULD_START; -import static com.android.launcher3.views.SearchResultIconRow.REMOTE_ACTION_TOKEN; + +import static com.android.launcher3.views.SearchResultIcon.REMOTE_ACTION_SHOULD_START; +import static com.android.launcher3.views.SearchResultIcon.REMOTE_ACTION_TOKEN; import android.content.Context; import android.content.Intent; diff --git a/src/com/android/launcher3/widget/BaseWidgetSheet.java b/src/com/android/launcher3/widget/BaseWidgetSheet.java index 01af96cb33..a38e90dfd8 100644 --- a/src/com/android/launcher3/widget/BaseWidgetSheet.java +++ b/src/com/android/launcher3/widget/BaseWidgetSheet.java @@ -42,7 +42,7 @@ import com.android.launcher3.views.AbstractSlideInView; /** * Base class for various widgets popup */ -abstract class BaseWidgetSheet extends AbstractSlideInView +public abstract class BaseWidgetSheet extends AbstractSlideInView implements OnClickListener, OnLongClickListener, DragSource, PopupDataProvider.PopupDataChangeListener { @@ -74,16 +74,7 @@ abstract class BaseWidgetSheet extends AbstractSlideInView @Override public final void onClick(View v) { - // Let the user know that they have to long press to add a widget - if (mWidgetInstructionToast != null) { - mWidgetInstructionToast.cancel(); - } - - CharSequence msg = Utilities.wrapForTts( - getContext().getText(R.string.long_press_widget_to_add), - getContext().getString(R.string.long_accessible_way_to_add)); - mWidgetInstructionToast = Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT); - mWidgetInstructionToast.show(); + mWidgetInstructionToast = showWidgetToast(getContext(), mWidgetInstructionToast); } @Override @@ -146,4 +137,21 @@ abstract class BaseWidgetSheet extends AbstractSlideInView protected SystemUiController getSystemUiController() { return mLauncher.getSystemUiController(); } + + /** + * Show Widget tap toast prompting user to drag instead + */ + public static Toast showWidgetToast(Context context, Toast toast) { + // Let the user know that they have to long press to add a widget + if (toast != null) { + toast.cancel(); + } + + CharSequence msg = Utilities.wrapForTts( + context.getText(R.string.long_press_widget_to_add), + context.getString(R.string.long_accessible_way_to_add)); + toast = Toast.makeText(context, msg, Toast.LENGTH_SHORT); + toast.show(); + return toast; + } } diff --git a/tests/src/com/android/launcher3/ui/WorkTabTest.java b/tests/src/com/android/launcher3/ui/WorkTabTest.java index 8d594de926..df0770d931 100644 --- a/tests/src/com/android/launcher3/ui/WorkTabTest.java +++ b/tests/src/com/android/launcher3/ui/WorkTabTest.java @@ -65,12 +65,13 @@ public class WorkTabTest extends AbstractLauncherUiTest { String[] tokens = output.split("\\s+"); mProfileUserId = Integer.parseInt(tokens[tokens.length - 1]); - + Log.d(TestProtocol.WORK_PROFILE_REMOVED, "Created new user uid" + mProfileUserId); mDevice.executeShellCommand("am start-user " + mProfileUserId); } @After public void removeWorkProfile() throws Exception { + Log.d(TestProtocol.WORK_PROFILE_REMOVED, "(teardown) removing uid" + mProfileUserId); mDevice.executeShellCommand("pm remove-user " + mProfileUserId); }