diff --git a/quickstep/src/com/android/quickstep/ActivityControlHelper.java b/quickstep/src/com/android/quickstep/ActivityControlHelper.java index b43352a046..9e2e5acf5f 100644 --- a/quickstep/src/com/android/quickstep/ActivityControlHelper.java +++ b/quickstep/src/com/android/quickstep/ActivityControlHelper.java @@ -28,11 +28,14 @@ import android.content.Intent; import android.graphics.Rect; import android.os.Handler; import android.os.Looper; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; import android.view.View; import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.DeviceProfile; import com.android.launcher3.Launcher; +import com.android.launcher3.LauncherAppState; import com.android.launcher3.LauncherInitListener; import com.android.launcher3.LauncherState; import com.android.launcher3.allapps.AllAppsTransitionController; @@ -79,6 +82,13 @@ public interface ActivityControlHelper { void startRecents(Context context, Intent intent, AssistDataReceiver assistDataReceiver, RecentsAnimationListener remoteAnimationListener); + @UiThread + @Nullable + RecentsView getVisibleRecentsView(); + + @UiThread + boolean switchToRecentsIfVisible(); + class LauncherActivityControllerHelper implements ActivityControlHelper { @Override @@ -198,6 +208,36 @@ public interface ActivityControlHelper { ActivityManagerWrapper.getInstance().startRecentsActivity( intent, assistDataReceiver, remoteAnimationListener, null, null); } + + @Nullable + @UiThread + private Launcher getVisibleLaucher() { + LauncherAppState app = LauncherAppState.getInstanceNoCreate(); + if (app == null) { + return null; + } + Launcher launcher = (Launcher) app.getModel().getCallback(); + return (launcher != null) && launcher.isStarted() && launcher.hasWindowFocus() ? + launcher : null; + } + + @Nullable + @Override + public RecentsView getVisibleRecentsView() { + Launcher launcher = getVisibleLaucher(); + return launcher != null && launcher.isInState(OVERVIEW) + ? launcher.getOverviewPanel() : null; + } + + @Override + public boolean switchToRecentsIfVisible() { + Launcher launcher = getVisibleLaucher(); + if (launcher != null) { + launcher.getStateManager().goToState(OVERVIEW); + return true; + } + return false; + } } class FallbackActivityControllerHelper implements ActivityControlHelper { @@ -292,6 +332,21 @@ public interface ActivityControlHelper { new FallbackActivityOptions(remoteAnimationListener), 10000, 10000)); context.startActivity(intent, options.toBundle()); } + + @Nullable + @Override + public RecentsView getVisibleRecentsView() { + RecentsActivity activity = RecentsActivityTracker.getCurrentActivity(); + if (activity != null && activity.hasWindowFocus()) { + return activity.getOverviewPanel(); + } + return null; + } + + @Override + public boolean switchToRecentsIfVisible() { + return false; + } } interface LayoutListener { diff --git a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java index 38c25a341b..964fcc0cd4 100644 --- a/quickstep/src/com/android/quickstep/OverviewCommandHelper.java +++ b/quickstep/src/com/android/quickstep/OverviewCommandHelper.java @@ -17,25 +17,54 @@ package com.android.quickstep; import static com.android.launcher3.LauncherState.OVERVIEW; +import android.animation.Animator; +import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.app.ActivityManager.RecentTaskInfo; +import android.app.ActivityManager.RunningTaskInfo; +import android.app.ActivityOptions; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ResolveInfo; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; import android.os.Build; +import android.os.Bundle; import android.os.SystemClock; import android.os.UserHandle; +import android.support.annotation.UiThread; +import android.support.annotation.WorkerThread; +import android.util.SparseArray; import android.view.ViewConfiguration; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherAppState; +import com.android.launcher3.MainThreadExecutor; +import com.android.launcher3.Utilities; +import com.android.launcher3.anim.AnimationSuccessListener; +import com.android.launcher3.anim.Interpolators; import com.android.launcher3.states.InternalStateHandler; +import com.android.launcher3.util.TraceHelper; import com.android.quickstep.ActivityControlHelper.FallbackActivityControllerHelper; import com.android.quickstep.ActivityControlHelper.LauncherActivityControllerHelper; import com.android.quickstep.views.RecentsView; +import com.android.quickstep.views.TaskView; +import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecCompat; +import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture; +import com.android.systemui.shared.recents.view.RecentsTransition; import com.android.systemui.shared.system.ActivityManagerWrapper; +import com.android.systemui.shared.system.AssistDataReceiver; +import com.android.systemui.shared.system.BackgroundExecutor; +import com.android.systemui.shared.system.RecentsAnimationControllerCompat; +import com.android.systemui.shared.system.RecentsAnimationListener; +import com.android.systemui.shared.system.RemoteAnimationTargetCompat; + +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; /** * Helper class to handle various atomic commands for switching between Overview. @@ -43,19 +72,39 @@ import com.android.systemui.shared.system.ActivityManagerWrapper; @TargetApi(Build.VERSION_CODES.P) public class OverviewCommandHelper extends InternalStateHandler { + private static final int RID_RESET_SWIPE_HANDLER = 0; + private static final int RID_CANCEL_CONTROLLER = 1; + private static final int RID_CANCEL_ZOOM_OUT_ANIMATION = 2; + + private static final long RECENTS_LAUNCH_DURATION = 150; + + private static final String TAG = "OverviewCommandHelper"; private static final boolean DEBUG_START_FALLBACK_ACTIVITY = false; private final Context mContext; private final ActivityManagerWrapper mAM; + private final RecentsModel mRecentsModel; + private final MainThreadExecutor mMainThreadExecutor; public final Intent homeIntent; public final ComponentName launcher; + private final SparseArray mCurrentCommandFinishRunnables = new SparseArray<>(); + // Monotonically increasing command ids. + private int mCurrentCommandId = 0; + private long mLastToggleTime; + private WindowTransformSwipeHandler mWindowTransformSwipeHandler; + + private final Point mWindowSize = new Point(); + private final Rect mTaskTargetRect = new Rect(); + private final RectF mTempTaskTargetRect = new RectF(); public OverviewCommandHelper(Context context) { mContext = context; mAM = ActivityManagerWrapper.getInstance(); + mMainThreadExecutor = new MainThreadExecutor(); + mRecentsModel = RecentsModel.getInstance(mContext); homeIntent = new Intent(Intent.ACTION_MAIN) .addCategory(Intent.CATEGORY_HOME) @@ -81,26 +130,152 @@ public class OverviewCommandHelper extends InternalStateHandler { initWhenReady(); } + @UiThread + private void addFinishCommand(int requestId, int id, Runnable action) { + if (requestId < mCurrentCommandId) { + action.run(); + } else { + mCurrentCommandFinishRunnables.put(id, action); + } + } + + @UiThread + private void clearFinishCommand(int requestId, int id) { + if (requestId == mCurrentCommandId) { + mCurrentCommandFinishRunnables.remove(id); + } + } + + @UiThread + private void initSwipeHandler(ActivityControlHelper helper, long time, + Consumer onAnimationInitCallback) { + final int commandId = mCurrentCommandId; + RunningTaskInfo taskInfo = ActivityManagerWrapper.getInstance().getRunningTask(); + final WindowTransformSwipeHandler handler = + new WindowTransformSwipeHandler(taskInfo, mContext, time, helper); + + // Preload the plan + mRecentsModel.loadTasks(taskInfo.id, null); + mWindowTransformSwipeHandler = handler; + + mTempTaskTargetRect.setEmpty(); + handler.setGestureEndCallback(() -> { + if (mWindowTransformSwipeHandler == handler) { + mWindowTransformSwipeHandler = null; + mTempTaskTargetRect.setEmpty(); + } + clearFinishCommand(commandId, RID_RESET_SWIPE_HANDLER); + clearFinishCommand(commandId, RID_CANCEL_CONTROLLER); + }); + handler.initWhenReady(); + addFinishCommand(commandId, RID_RESET_SWIPE_HANDLER, handler::reset); + + TraceHelper.beginSection(TAG); + Runnable startActivity = () -> helper.startRecents(mContext, homeIntent, + new AssistDataReceiver() { + @Override + public void onHandleAssistData(Bundle bundle) { + mRecentsModel.preloadAssistData(taskInfo.id, bundle); + } + }, + new RecentsAnimationListener() { + public void onAnimationStart( + RecentsAnimationControllerCompat controller, + RemoteAnimationTargetCompat[] apps, Rect homeContentInsets, + Rect minimizedHomeBounds) { + if (mWindowTransformSwipeHandler == handler) { + TraceHelper.partitionSection(TAG, "Received"); + handler.onRecentsAnimationStart(controller, apps, homeContentInsets, + minimizedHomeBounds); + mTempTaskTargetRect.set(handler.getTargetRect(mWindowSize)); + + mMainThreadExecutor.execute(() -> { + addFinishCommand(commandId, + RID_CANCEL_CONTROLLER, () -> controller.finish(true)); + if (commandId == mCurrentCommandId) { + onAnimationInitCallback.accept(handler); + } + }); + } else { + TraceHelper.endSection(TAG, "Finishing no handler"); + controller.finish(false /* toHome */); + } + } + + public void onAnimationCanceled() { + TraceHelper.endSection(TAG, "Cancelled: " + handler); + if (mWindowTransformSwipeHandler == handler) { + handler.onRecentsAnimationCanceled(); + } + } + }); + + // We should almost always get touch-town on background thread. This is an edge case + // when the background Choreographer has not yet initialized. + BackgroundExecutor.get().submit(startActivity); + } + + @UiThread + private void startZoomOutAnim(final WindowTransformSwipeHandler handler) { + final int commandId = mCurrentCommandId; + ValueAnimator anim = ValueAnimator.ofInt(0, -handler.getTransitionLength()); + anim.addUpdateListener((a) -> handler.updateDisplacement((Integer) a.getAnimatedValue())); + anim.addListener(new AnimationSuccessListener() { + @Override + public void onAnimationSuccess(Animator animator) { + handler.onGestureEnded(0); + clearFinishCommand(commandId, RID_CANCEL_ZOOM_OUT_ANIMATION); + } + }); + handler.onGestureStarted(); + anim.setDuration(RECENTS_LAUNCH_DURATION); + anim.setInterpolator(Interpolators.AGGRESSIVE_EASE); + anim.start(); + addFinishCommand(commandId, RID_CANCEL_ZOOM_OUT_ANIMATION, anim::cancel); + } + public void onOverviewToggle() { - getLauncher().runOnUiThread(() -> { - if (isUsingFallbackActivity()) { - mContext.startActivity(new Intent(mContext, RecentsActivity.class) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent - .FLAG_ACTIVITY_CLEAR_TASK)); - return; - } + long time = SystemClock.elapsedRealtime(); + mMainThreadExecutor.execute(() -> { + long elapsedTime = time - mLastToggleTime; + mLastToggleTime = time; - long elapsedTime = SystemClock.elapsedRealtime() - mLastToggleTime; - mLastToggleTime = SystemClock.elapsedRealtime(); - - if (isOverviewAlmostVisible()) { - boolean isQuickTap = elapsedTime < ViewConfiguration.getDoubleTapTimeout(); - startNonLauncherTask(isQuickTap ? 2 : 1); - } else { - openRecents(); - } + mCurrentCommandId++; + mTempTaskTargetRect.round(mTaskTargetRect); + boolean isQuickTap = elapsedTime < ViewConfiguration.getDoubleTapTimeout(); + int runnableCount = mCurrentCommandFinishRunnables.size(); + if (runnableCount > 0) { + for (int i = 0; i < runnableCount; i++) { + mCurrentCommandFinishRunnables.valueAt(i).run(); } - ); + mCurrentCommandFinishRunnables.clear(); + isQuickTap = true; + } + + ActivityControlHelper helper = getActivityControlHelper(); + RecentsView recents = helper.getVisibleRecentsView(); + if (recents != null) { + int childCount = recents.getChildCount(); + if (childCount != 0) { + ((TaskView) recents.getChildAt(childCount >= 2 ? 1 : 0)).launchTask(true); + } + + // There are not enough tasks. Skip + return; + } + + if (isQuickTap) { + // Focus last task. Start is on background thread so that all ActivityManager calls + // are serialized + BackgroundExecutor.get().submit(this::startLastTask); + return; + } + if (helper.switchToRecentsIfVisible()) { + return; + } + + initSwipeHandler(helper, time, this::startZoomOutAnim); + }); } public void onOverviewShown() { @@ -123,13 +298,35 @@ public class OverviewCommandHelper extends InternalStateHandler { ); } - private void startNonLauncherTask(int backStackCount) { - for (RecentTaskInfo rti : mAM.getRecentTasks(backStackCount, UserHandle.myUserId())) { - backStackCount--; - if (backStackCount == 0) { - mAM.startActivityFromRecents(rti.id, null); - break; + @WorkerThread + private void startLastTask() { + // TODO: This should go through recents model. + List tasks = mAM.getRecentTasks(2, UserHandle.myUserId()); + if (tasks.size() > 1) { + RecentTaskInfo rti = tasks.get(1); + + final ActivityOptions options; + if (!mTaskTargetRect.isEmpty()) { + final Rect targetRect = new Rect(mTaskTargetRect); + targetRect.offset(Utilities.isRtl(mContext.getResources()) + ? - mTaskTargetRect.width() : mTaskTargetRect.width(), 0); + final AppTransitionAnimationSpecCompat specCompat = + new AppTransitionAnimationSpecCompat(rti.id, null, targetRect); + AppTransitionAnimationSpecsFuture specFuture = + new AppTransitionAnimationSpecsFuture(mMainThreadExecutor.getHandler()) { + + @Override + public List composeSpecs() { + return Collections.singletonList(specCompat); + } + }; + options = RecentsTransition.createAspectScaleAnimation(mContext, + mMainThreadExecutor.getHandler(), true /* scaleUp */, + specFuture, () -> {}); + } else { + options = ActivityOptions.makeBasic(); } + mAM.startActivityFromRecents(rti.id, options); } } diff --git a/quickstep/src/com/android/quickstep/RecentsActivity.java b/quickstep/src/com/android/quickstep/RecentsActivity.java index 12e1a2bd8e..e5792056aa 100644 --- a/quickstep/src/com/android/quickstep/RecentsActivity.java +++ b/quickstep/src/com/android/quickstep/RecentsActivity.java @@ -93,4 +93,10 @@ public class RecentsActivity extends BaseDraggingActivity { super.onTrimMemory(level); UiFactory.onTrimMemory(this, level); } + + @Override + protected void onDestroy() { + super.onDestroy(); + RecentsActivityTracker.onRecentsActivityDestroy(this); + } } diff --git a/quickstep/src/com/android/quickstep/RecentsActivityTracker.java b/quickstep/src/com/android/quickstep/RecentsActivityTracker.java index 6a82dc0d00..5bd606eb4a 100644 --- a/quickstep/src/com/android/quickstep/RecentsActivityTracker.java +++ b/quickstep/src/com/android/quickstep/RecentsActivityTracker.java @@ -31,6 +31,7 @@ public class RecentsActivityTracker implements ActivityInitListener { private static final Object LOCK = new Object(); private static WeakReference sTracker = new WeakReference<>(null); + private static WeakReference sCurrentActivity = new WeakReference<>(null); private final BiPredicate mOnInitListener; @@ -60,6 +61,21 @@ public class RecentsActivityTracker implements ActivityInitListener { if (tracker != null && tracker.mOnInitListener.test(activity, false)) { sTracker.clear(); } + sCurrentActivity = new WeakReference<>(activity); + } + } + + public static void onRecentsActivityDestroy(RecentsActivity activity) { + synchronized (LOCK) { + if (sCurrentActivity.get() == activity) { + sCurrentActivity.clear(); + } + } + } + + public static RecentsActivity getCurrentActivity() { + synchronized (LOCK) { + return sCurrentActivity.get(); } } } diff --git a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java index 2d2a483b59..f0bb3676c3 100644 --- a/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java +++ b/quickstep/src/com/android/quickstep/WindowTransformSwipeHandler.java @@ -301,6 +301,15 @@ public class WindowTransformSwipeHandler { mSourceRect.set(scaledTargetRect); } + public int getTransitionLength() { + return mTransitionDragLength; + } + + public RectF getTargetRect(Point outWindowSize) { + outWindowSize.set(mDp.widthPx, mDp.heightPx); + return mInitialTargetRect; + } + private long getFadeInDuration() { if (mCurrentShift.getCurrentAnimation() != null) { ObjectAnimator anim = mCurrentShift.getCurrentAnimation();