/* * Copyright (C) 2018 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; import static android.content.Intent.ACTION_PACKAGE_ADDED; import static android.content.Intent.ACTION_PACKAGE_CHANGED; import static android.content.Intent.ACTION_PACKAGE_REMOVED; import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; import static com.android.launcher3.anim.Interpolators.TOUCH_RESPONSE_INTERPOLATOR; import static com.android.quickstep.TouchConsumer.INTERACTION_NORMAL; import static com.android.systemui.shared.system.ActivityManagerWrapper .CLOSE_SYSTEM_WINDOWS_REASON_RECENTS; import static com.android.systemui.shared.system.PackageManagerWrapper .ACTION_PREFERRED_ACTIVITY_CHANGED; import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING; import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING; import android.animation.Animator; import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ResolveInfo; import android.graphics.Rect; import android.os.Build; import android.os.PatternMatcher; import android.os.SystemClock; import android.util.Log; import android.view.View; import android.view.ViewConfiguration; import com.android.launcher3.AbstractFloatingView; import com.android.launcher3.BaseDraggingActivity; import com.android.launcher3.InvariantDeviceProfile; import com.android.launcher3.MainThreadExecutor; import com.android.launcher3.anim.AnimationSuccessListener; import com.android.launcher3.logging.UserEventDispatcher; import com.android.launcher3.userevent.nano.LauncherLogProto.Action; import com.android.launcher3.userevent.nano.LauncherLogProto.ContainerType; import com.android.quickstep.ActivityControlHelper.ActivityInitListener; import com.android.quickstep.ActivityControlHelper.AnimationFactory; import com.android.quickstep.ActivityControlHelper.FallbackActivityControllerHelper; import com.android.quickstep.ActivityControlHelper.LauncherActivityControllerHelper; import com.android.quickstep.util.ClipAnimationHelper; import com.android.quickstep.util.TransformedRect; import com.android.quickstep.util.RemoteAnimationTargetSet; import com.android.quickstep.views.RecentsView; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.LatencyTrackerCompat; import com.android.systemui.shared.system.PackageManagerWrapper; import com.android.systemui.shared.system.RemoteAnimationTargetCompat; import com.android.systemui.shared.system.TransactionCompat; import java.util.ArrayList; /** * Helper class to handle various atomic commands for switching between Overview. */ @TargetApi(Build.VERSION_CODES.P) public class OverviewCommandHelper { private static final long RECENTS_LAUNCH_DURATION = 250; private static final String TAG = "OverviewCommandHelper"; private final Context mContext; private final ActivityManagerWrapper mAM; private final RecentsModel mRecentsModel; private final MainThreadExecutor mMainThreadExecutor; private final ComponentName mMyHomeComponent; private final BroadcastReceiver mUserPreferenceChangeReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { initOverviewTargets(); } }; private final BroadcastReceiver mOtherHomeAppUpdateReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { initOverviewTargets(); } }; private String mUpdateRegisteredPackage; public Intent overviewIntent; public ComponentName overviewComponent; private ActivityControlHelper mActivityControlHelper; private long mLastToggleTime; public OverviewCommandHelper(Context context) { mContext = context; mAM = ActivityManagerWrapper.getInstance(); mMainThreadExecutor = new MainThreadExecutor(); mRecentsModel = RecentsModel.getInstance(mContext); Intent myHomeIntent = new Intent(Intent.ACTION_MAIN) .addCategory(Intent.CATEGORY_HOME) .setPackage(mContext.getPackageName()); ResolveInfo info = context.getPackageManager().resolveActivity(myHomeIntent, 0); mMyHomeComponent = new ComponentName(context.getPackageName(), info.activityInfo.name); mContext.registerReceiver(mUserPreferenceChangeReceiver, new IntentFilter(ACTION_PREFERRED_ACTIVITY_CHANGED)); initOverviewTargets(); } private void initOverviewTargets() { ComponentName defaultHome = PackageManagerWrapper.getInstance() .getHomeActivities(new ArrayList<>()); final String overviewIntentCategory; if (defaultHome == null || mMyHomeComponent.equals(defaultHome)) { // User default home is same as out home app. Use Overview integrated in Launcher. overviewComponent = mMyHomeComponent; mActivityControlHelper = new LauncherActivityControllerHelper(); overviewIntentCategory = Intent.CATEGORY_HOME; if (mUpdateRegisteredPackage != null) { // Remove any update listener as we don't care about other packages. mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver); mUpdateRegisteredPackage = null; } } else { // The default home app is a different launcher. Use the fallback Overview instead. overviewComponent = new ComponentName(mContext, RecentsActivity.class); mActivityControlHelper = new FallbackActivityControllerHelper(defaultHome); overviewIntentCategory = Intent.CATEGORY_DEFAULT; // User's default home app can change as a result of package updates of this app (such // as uninstalling the app or removing the "Launcher" feature in an update). // Listen for package updates of this app (and remove any previously attached // package listener). if (!defaultHome.getPackageName().equals(mUpdateRegisteredPackage)) { if (mUpdateRegisteredPackage != null) { mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver); } mUpdateRegisteredPackage = defaultHome.getPackageName(); IntentFilter updateReceiver = new IntentFilter(ACTION_PACKAGE_ADDED); updateReceiver.addAction(ACTION_PACKAGE_CHANGED); updateReceiver.addAction(ACTION_PACKAGE_REMOVED); updateReceiver.addDataScheme("package"); updateReceiver.addDataSchemeSpecificPart(mUpdateRegisteredPackage, PatternMatcher.PATTERN_LITERAL); mContext.registerReceiver(mOtherHomeAppUpdateReceiver, updateReceiver); } } overviewIntent = new Intent(Intent.ACTION_MAIN) .addCategory(overviewIntentCategory) .setComponent(overviewComponent) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } public void onDestroy() { mContext.unregisterReceiver(mUserPreferenceChangeReceiver); if (mUpdateRegisteredPackage != null) { mContext.unregisterReceiver(mOtherHomeAppUpdateReceiver); mUpdateRegisteredPackage = null; } } public void onOverviewToggle() { // If currently screen pinning, do not enter overview if (mAM.isScreenPinningActive()) { return; } mAM.closeSystemWindows(CLOSE_SYSTEM_WINDOWS_REASON_RECENTS); mMainThreadExecutor.execute(new RecentsActivityCommand<>()); } public void onOverviewShown() { mMainThreadExecutor.execute(new ShowRecentsCommand()); } public void onTip(int actionType, int viewType) { mMainThreadExecutor.execute(new Runnable() { @Override public void run() { UserEventDispatcher.newInstance(mContext, new InvariantDeviceProfile(mContext).getDeviceProfile(mContext)) .logActionTip(actionType, viewType); } }); } public ActivityControlHelper getActivityControlHelper() { return mActivityControlHelper; } private class ShowRecentsCommand extends RecentsActivityCommand { @Override protected boolean handleCommand(long elapsedTime) { return mHelper.getVisibleRecentsView() != null; } } private class RecentsActivityCommand implements Runnable { protected final ActivityControlHelper mHelper; private final long mCreateTime; private final int mRunningTaskId; private ActivityInitListener mListener; private T mActivity; private RecentsView mRecentsView; private final long mToggleClickedTime = SystemClock.uptimeMillis(); private boolean mUserEventLogged; public RecentsActivityCommand() { mHelper = getActivityControlHelper(); mCreateTime = SystemClock.elapsedRealtime(); mRunningTaskId = mAM.getRunningTask().id; // Preload the plan mRecentsModel.loadTasks(mRunningTaskId, null); } @Override public void run() { long elapsedTime = mCreateTime - mLastToggleTime; mLastToggleTime = mCreateTime; if (!handleCommand(elapsedTime)) { // Start overview if (!mHelper.switchToRecentsIfVisible(true)) { mListener = mHelper.createActivityInitListener(this::onActivityReady); mListener.registerAndStartActivity(overviewIntent, this::createWindowAnimation, mContext, mMainThreadExecutor.getHandler(), RECENTS_LAUNCH_DURATION); } } } protected boolean handleCommand(long elapsedTime) { // TODO: We need to fix this case with PIP, when an activity first enters PIP, it shows // the menu activity which takes window focus, preventing the right condition from // being run below RecentsView recents = mHelper.getVisibleRecentsView(); if (recents != null) { // Launch the next task recents.showNextTask(); return true; } else if (elapsedTime < ViewConfiguration.getDoubleTapTimeout()) { // The user tried to launch back into overview too quickly, either after // launching an app, or before overview has actually shown, just ignore for now return true; } return false; } private boolean onActivityReady(T activity, Boolean wasVisible) { activity.getOverviewPanel().setCurrentTask(mRunningTaskId); AbstractFloatingView.closeAllOpenViews(activity, wasVisible); AnimationFactory factory = mHelper.prepareRecentsUI(activity, wasVisible, (controller) -> { controller.dispatchOnStart(); ValueAnimator anim = controller.getAnimationPlayer() .setDuration(RECENTS_LAUNCH_DURATION); anim.setInterpolator(FAST_OUT_SLOW_IN); anim.start(); }); factory.onRemoteAnimationReceived(null); if (wasVisible) { factory.createActivityController(RECENTS_LAUNCH_DURATION, INTERACTION_NORMAL); } mActivity = activity; mRecentsView = mActivity.getOverviewPanel(); mRecentsView.setRunningTaskIconScaledDown(true /* isScaledDown */, false /* animate */); if (!mUserEventLogged) { activity.getUserEventDispatcher().logActionCommand(Action.Command.RECENTS_BUTTON, mHelper.getContainerType(), ContainerType.TASKSWITCHER); mUserEventLogged = true; } return false; } private AnimatorSet createWindowAnimation(RemoteAnimationTargetCompat[] targetCompats) { if (LatencyTrackerCompat.isEnabled(mContext)) { LatencyTrackerCompat.logToggleRecents( (int) (SystemClock.uptimeMillis() - mToggleClickedTime)); } if (mListener != null) { mListener.unregister(); } AnimatorSet anim = new AnimatorSet(); anim.addListener(new AnimationSuccessListener() { @Override public void onAnimationSuccess(Animator animator) { if (mRecentsView != null) { mRecentsView.setRunningTaskIconScaledDown(false /* isScaledDown */, true /* animate */); } } }); if (mActivity == null) { Log.e(TAG, "Animation created, before activity"); anim.play(ValueAnimator.ofInt(0, 1).setDuration(100)); return anim; } RemoteAnimationTargetSet targetSet = new RemoteAnimationTargetSet(targetCompats, MODE_CLOSING); // Use the top closing app to determine the insets for the animation RemoteAnimationTargetCompat runningTaskTarget = targetSet.findTask(mRunningTaskId); if (runningTaskTarget == null) { Log.e(TAG, "No closing app"); anim.play(ValueAnimator.ofInt(0, 1).setDuration(100)); return anim; } final ClipAnimationHelper clipHelper = new ClipAnimationHelper(); // At this point, the activity is already started and laid-out. Get the home-bounds // relative to the screen using the rootView of the activity. int loc[] = new int[2]; View rootView = mActivity.getRootView(); rootView.getLocationOnScreen(loc); Rect homeBounds = new Rect(loc[0], loc[1], loc[0] + rootView.getWidth(), loc[1] + rootView.getHeight()); clipHelper.updateSource(homeBounds, runningTaskTarget); TransformedRect targetRect = new TransformedRect(); mHelper.getSwipeUpDestinationAndLength(mActivity.getDeviceProfile(), mActivity, INTERACTION_NORMAL, targetRect); clipHelper.updateTargetRect(targetRect); clipHelper.prepareAnimation(false /* isOpening */); ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1); valueAnimator.setDuration(RECENTS_LAUNCH_DURATION); valueAnimator.setInterpolator(TOUCH_RESPONSE_INTERPOLATOR); valueAnimator.addUpdateListener((v) -> clipHelper.applyTransform(targetSet, (float) v.getAnimatedValue())); if (targetSet.isAnimatingHome()) { // If we are animating home, fade in the opening targets RemoteAnimationTargetSet openingSet = new RemoteAnimationTargetSet(targetCompats, MODE_OPENING); TransactionCompat transaction = new TransactionCompat(); valueAnimator.addUpdateListener((v) -> { for (RemoteAnimationTargetCompat app : openingSet.apps) { transaction.setAlpha(app.leash, (float) v.getAnimatedValue()); } transaction.apply(); }); } anim.play(valueAnimator); return anim; } } }