blob: 6fcb1c1152d1df9ea29cf247d7c6df69f6a6e63c [file] [log] [blame]
/*
* Copyright (C) 2015 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.systemui.recents;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.view.View.MeasureSpec;
import static com.android.systemui.statusbar.phone.StatusBar.SYSTEM_DIALOG_REASON_RECENT_APPS;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.trust.TrustManager;
import android.content.ActivityNotFoundException;
import android.content.ComponentCallbacks2;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.SystemClock;
import android.util.ArraySet;
import android.util.Log;
import android.util.MutableBoolean;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.ViewConfiguration;
import android.view.WindowManager;
import android.widget.Toast;
import com.android.systemui.Dependency;
import com.android.systemui.OverviewProxyService;
import com.google.android.collect.Lists;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.policy.DockedDividerUtils;
import com.android.systemui.R;
import com.android.systemui.SystemUIApplication;
import com.android.systemui.recents.events.EventBus;
import com.android.systemui.recents.events.activity.DockedTopTaskEvent;
import com.android.systemui.recents.events.activity.EnterRecentsWindowLastAnimationFrameEvent;
import com.android.systemui.recents.events.activity.HideRecentsEvent;
import com.android.systemui.recents.events.activity.LaunchMostRecentTaskRequestEvent;
import com.android.systemui.recents.events.activity.LaunchNextTaskRequestEvent;
import com.android.systemui.recents.events.activity.RecentsActivityStartingEvent;
import com.android.systemui.recents.events.activity.ToggleRecentsEvent;
import com.android.systemui.recents.events.component.ActivityPinnedEvent;
import com.android.systemui.recents.events.component.ActivityUnpinnedEvent;
import com.android.systemui.recents.events.component.HidePipMenuEvent;
import com.android.systemui.recents.events.component.RecentsVisibilityChangedEvent;
import com.android.systemui.recents.events.component.ScreenPinningRequestEvent;
import com.android.systemui.recents.events.ui.DraggingInRecentsEndedEvent;
import com.android.systemui.recents.events.ui.DraggingInRecentsEvent;
import com.android.systemui.recents.events.ui.TaskSnapshotChangedEvent;
import com.android.systemui.recents.misc.DozeTrigger;
import com.android.systemui.recents.misc.ForegroundThread;
import com.android.systemui.recents.misc.SystemServicesProxy;
import com.android.systemui.recents.misc.SysUiTaskStackChangeListener;
import com.android.systemui.shared.recents.model.RecentsTaskLoadPlan;
import com.android.systemui.shared.recents.model.RecentsTaskLoader;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.Task.TaskKey;
import com.android.systemui.shared.recents.model.TaskStack;
import com.android.systemui.shared.recents.model.ThumbnailData;
import com.android.systemui.recents.views.TaskStackLayoutAlgorithm;
import com.android.systemui.recents.views.TaskStackLayoutAlgorithm.VisibilityReport;
import com.android.systemui.recents.views.TaskStackView;
import com.android.systemui.recents.views.TaskViewHeader;
import com.android.systemui.recents.views.TaskViewTransform;
import com.android.systemui.recents.views.grid.TaskGridLayoutAlgorithm;
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.stackdivider.DividerView;
import com.android.systemui.statusbar.phone.StatusBar;
import java.util.ArrayList;
import java.util.List;
/**
* An implementation of the Recents component for the current user. For secondary users, this can
* be called remotely from the system user.
*/
public class RecentsImpl implements ActivityOptions.OnAnimationFinishedListener {
private final static String TAG = "RecentsImpl";
// The minimum amount of time between each recents button press that we will handle
private final static int MIN_TOGGLE_DELAY_MS = 350;
// The duration within which the user releasing the alt tab (from when they pressed alt tab)
// that the fast alt-tab animation will run. If the user's alt-tab takes longer than this
// duration, then we will toggle recents after this duration.
private final static int FAST_ALT_TAB_DELAY_MS = 225;
private final static ArraySet<TaskKey> EMPTY_SET = new ArraySet<>();
public final static String RECENTS_PACKAGE = "com.android.systemui";
public final static String RECENTS_ACTIVITY = "com.android.systemui.recents.RecentsActivity";
/**
* An implementation of SysUiTaskStackChangeListener, that allows us to listen for changes to the system
* task stacks and update recents accordingly.
*/
class TaskStackListenerImpl extends SysUiTaskStackChangeListener {
@Override
public void onTaskStackChangedBackground() {
// Skip background preloading recents in SystemUI if the overview services is bound
if (Dependency.get(OverviewProxyService.class).isEnabled()) {
return;
}
// Check this is for the right user
if (!checkCurrentUserId(mContext, false /* debug */)) {
return;
}
// Preloads the next task
RecentsConfiguration config = Recents.getConfiguration();
if (config.svelteLevel == RecentsTaskLoader.SVELTE_NONE) {
Rect windowRect = getWindowRect(null /* windowRectOverride */);
if (windowRect.isEmpty()) {
return;
}
// Load the next task only if we aren't svelte
ActivityManager.RunningTaskInfo runningTaskInfo =
ActivityManagerWrapper.getInstance().getRunningTask();
RecentsTaskLoader loader = Recents.getTaskLoader();
RecentsTaskLoadPlan plan = new RecentsTaskLoadPlan(mContext);
loader.preloadTasks(plan, -1);
TaskStack stack = plan.getTaskStack();
RecentsActivityLaunchState launchState = new RecentsActivityLaunchState();
RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options();
synchronized (mBackgroundLayoutAlgorithm) {
// This callback is made when a new activity is launched and the old one is
// paused so ignore the current activity and try and preload the thumbnail for
// the previous one.
updateDummyStackViewLayout(mBackgroundLayoutAlgorithm, stack, windowRect);
// Launched from app is always the worst case (in terms of how many
// thumbnails/tasks visible)
launchState.launchedFromApp = true;
mBackgroundLayoutAlgorithm.update(plan.getTaskStack(), EMPTY_SET, launchState,
-1 /* lastScrollPPresent */);
VisibilityReport visibilityReport =
mBackgroundLayoutAlgorithm.computeStackVisibilityReport(
stack.getTasks());
launchOpts.runningTaskId = runningTaskInfo != null ? runningTaskInfo.id : -1;
launchOpts.numVisibleTasks = visibilityReport.numVisibleTasks;
launchOpts.numVisibleTaskThumbnails = visibilityReport.numVisibleThumbnails;
launchOpts.onlyLoadForCache = true;
launchOpts.onlyLoadPausedActivities = true;
launchOpts.loadThumbnails = true;
}
loader.loadTasks(plan, launchOpts);
}
}
@Override
public void onActivityPinned(String packageName, int userId, int taskId, int stackId) {
// Check this is for the right user
if (!checkCurrentUserId(mContext, false /* debug */)) {
return;
}
// This time needs to be fetched the same way the last active time is fetched in
// {@link TaskRecord#touchActiveTime}
Recents.getConfiguration().getLaunchState().launchedFromPipApp = true;
Recents.getConfiguration().getLaunchState().launchedWithNextPipApp = false;
EventBus.getDefault().send(new ActivityPinnedEvent(taskId));
consumeInstanceLoadPlan();
sLastPipTime = System.currentTimeMillis();
}
@Override
public void onActivityUnpinned() {
// Check this is for the right user
if (!checkCurrentUserId(mContext, false /* debug */)) {
return;
}
EventBus.getDefault().send(new ActivityUnpinnedEvent());
sLastPipTime = -1;
}
@Override
public void onTaskSnapshotChanged(int taskId, ThumbnailData snapshot) {
// Check this is for the right user
if (!checkCurrentUserId(mContext, false /* debug */)) {
return;
}
EventBus.getDefault().send(new TaskSnapshotChangedEvent(taskId, snapshot));
}
}
protected static RecentsTaskLoadPlan sInstanceLoadPlan;
// Stores the last pinned task time
protected static long sLastPipTime = -1;
// Stores whether we are waiting for a transition to/from recents to start. During this time,
// we disallow the user from manually toggling recents until the transition has started.
private static boolean mWaitingForTransitionStart = false;
// Stores whether or not the user toggled while we were waiting for a transition to/from
// recents. In this case, we defer the toggle state until then and apply it immediately after.
private static boolean mToggleFollowingTransitionStart = true;
private Runnable mResetToggleFlagListener = new Runnable() {
@Override
public void run() {
setWaitingForTransitionStart(false);
}
};
private TrustManager mTrustManager;
protected Context mContext;
protected Handler mHandler;
TaskStackListenerImpl mTaskStackListener;
boolean mDraggingInRecents;
boolean mLaunchedWhileDocking;
// Task launching
Rect mTmpBounds = new Rect();
TaskViewTransform mTmpTransform = new TaskViewTransform();
int mTaskBarHeight;
// Header (for transition)
TaskViewHeader mHeaderBar;
final Object mHeaderBarLock = new Object();
private TaskStackView mDummyStackView;
private TaskStackLayoutAlgorithm mBackgroundLayoutAlgorithm;
// Variables to keep track of if we need to start recents after binding
protected boolean mTriggeredFromAltTab;
protected long mLastToggleTime;
DozeTrigger mFastAltTabTrigger = new DozeTrigger(FAST_ALT_TAB_DELAY_MS, new Runnable() {
@Override
public void run() {
// When this fires, then the user has not released alt-tab for at least
// FAST_ALT_TAB_DELAY_MS milliseconds
showRecents(mTriggeredFromAltTab, false /* draggingInRecents */, true /* animate */,
DividerView.INVALID_RECENTS_GROW_TARGET);
}
});
private OverviewProxyService.OverviewProxyListener mOverviewProxyListener =
new OverviewProxyService.OverviewProxyListener() {
@Override
public void onConnectionChanged(boolean isConnected) {
if (!isConnected) {
// Clear everything when the connection to the overview service
Recents.getTaskLoader().onTrimMemory(ComponentCallbacks2.TRIM_MEMORY_COMPLETE);
}
}
};
// Used to reset the dummy stack view
private final TaskStack mEmptyTaskStack = new TaskStack();
public RecentsImpl(Context context) {
mContext = context;
mHandler = new Handler();
mBackgroundLayoutAlgorithm = new TaskStackLayoutAlgorithm(context, null);
// Initialize the static foreground thread
ForegroundThread.get();
// Register the task stack listener
mTaskStackListener = new TaskStackListenerImpl();
ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
// Initialize the static configuration resources
mDummyStackView = new TaskStackView(mContext);
reloadResources();
mTrustManager = (TrustManager) mContext.getSystemService(Context.TRUST_SERVICE);
}
public void onBootCompleted() {
// Skip preloading tasks if we are already bound to the service
if (Dependency.get(OverviewProxyService.class).isEnabled()) {
return;
}
// When we start, preload the data associated with the previous recent tasks.
// We can use a new plan since the caches will be the same.
RecentsTaskLoader loader = Recents.getTaskLoader();
RecentsTaskLoadPlan plan = new RecentsTaskLoadPlan(mContext);
loader.preloadTasks(plan, -1);
RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options();
launchOpts.numVisibleTasks = loader.getIconCacheSize();
launchOpts.numVisibleTaskThumbnails = loader.getThumbnailCacheSize();
launchOpts.onlyLoadForCache = true;
loader.loadTasks(plan, launchOpts);
}
public void onConfigurationChanged() {
reloadResources();
mDummyStackView.reloadOnConfigurationChange();
synchronized (mBackgroundLayoutAlgorithm) {
mBackgroundLayoutAlgorithm.reloadOnConfigurationChange(mContext);
}
}
/**
* This is only called from the system user's Recents. Secondary users will instead proxy their
* visibility change events through to the system user via
* {@link Recents#onBusEvent(RecentsVisibilityChangedEvent)}.
*/
public void onVisibilityChanged(Context context, boolean visible) {
Recents.getSystemServices().setRecentsVisibility(visible);
}
/**
* This is only called from the system user's Recents. Secondary users will instead proxy their
* visibility change events through to the system user via
* {@link Recents#onBusEvent(ScreenPinningRequestEvent)}.
*/
public void onStartScreenPinning(Context context, int taskId) {
final StatusBar statusBar = getStatusBar();
if (statusBar != null) {
statusBar.showScreenPinningRequest(taskId, false);
}
}
public void showRecents(boolean triggeredFromAltTab, boolean draggingInRecents,
boolean animate, int growTarget) {
final SystemServicesProxy ssp = Recents.getSystemServices();
final MutableBoolean isHomeStackVisible = new MutableBoolean(true);
final boolean isRecentsVisible = Recents.getSystemServices().isRecentsActivityVisible(
isHomeStackVisible);
final boolean fromHome = isHomeStackVisible.value;
final boolean launchedWhileDockingTask =
Recents.getSystemServices().getSplitScreenPrimaryStack() != null;
mTriggeredFromAltTab = triggeredFromAltTab;
mDraggingInRecents = draggingInRecents;
mLaunchedWhileDocking = launchedWhileDockingTask;
if (mFastAltTabTrigger.isAsleep()) {
// Fast alt-tab duration has elapsed, fall through to showing Recents and reset
mFastAltTabTrigger.stopDozing();
} else if (mFastAltTabTrigger.isDozing()) {
// Fast alt-tab duration has not elapsed. If this is triggered by a different
// showRecents() call, then ignore that call for now.
// TODO: We can not handle quick tabs that happen between the initial showRecents() call
// that started the activity and the activity starting up. The severity of this
// is inversely proportional to the FAST_ALT_TAB_DELAY_MS duration though.
if (!triggeredFromAltTab) {
return;
}
mFastAltTabTrigger.stopDozing();
} else if (triggeredFromAltTab) {
// The fast alt-tab detector is not yet running, so start the trigger and wait for the
// hideRecents() call, or for the fast alt-tab duration to elapse
mFastAltTabTrigger.startDozing();
return;
}
try {
// Check if the top task is in the home stack, and start the recents activity
final boolean forceVisible = launchedWhileDockingTask || draggingInRecents;
if (forceVisible || !isRecentsVisible) {
ActivityManager.RunningTaskInfo runningTask =
ActivityManagerWrapper.getInstance().getRunningTask();
startRecentsActivityAndDismissKeyguardIfNeeded(runningTask,
isHomeStackVisible.value || fromHome, animate, growTarget);
}
} catch (ActivityNotFoundException e) {
Log.e(TAG, "Failed to launch RecentsActivity", e);
}
}
public void hideRecents(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) {
if (triggeredFromAltTab && mFastAltTabTrigger.isDozing()) {
// The user has released alt-tab before the trigger has run, so just show the next
// task immediately
showNextTask();
// Cancel the fast alt-tab trigger
mFastAltTabTrigger.stopDozing();
return;
}
// Defer to the activity to handle hiding recents, if it handles it, then it must still
// be visible
EventBus.getDefault().post(new HideRecentsEvent(triggeredFromAltTab,
triggeredFromHomeKey));
}
public void toggleRecents(int growTarget) {
if (ActivityManagerWrapper.getInstance().isScreenPinningActive()) {
return;
}
// Skip this toggle if we are already waiting to trigger recents via alt-tab
if (mFastAltTabTrigger.isDozing()) {
return;
}
if (mWaitingForTransitionStart) {
mToggleFollowingTransitionStart = true;
return;
}
mDraggingInRecents = false;
mLaunchedWhileDocking = false;
mTriggeredFromAltTab = false;
try {
MutableBoolean isHomeStackVisible = new MutableBoolean(true);
long elapsedTime = SystemClock.elapsedRealtime() - mLastToggleTime;
SystemServicesProxy ssp = Recents.getSystemServices();
if (ssp.isRecentsActivityVisible(isHomeStackVisible)) {
RecentsConfiguration config = Recents.getConfiguration();
RecentsActivityLaunchState launchState = config.getLaunchState();
if (!launchState.launchedWithAltTab) {
if (Recents.getConfiguration().isGridEnabled) {
// Has the user tapped quickly?
boolean isQuickTap = elapsedTime < ViewConfiguration.getDoubleTapTimeout();
if (isQuickTap) {
EventBus.getDefault().post(new LaunchNextTaskRequestEvent());
} else {
EventBus.getDefault().post(new LaunchMostRecentTaskRequestEvent());
}
} else {
// Launch the next focused task
EventBus.getDefault().post(new LaunchNextTaskRequestEvent());
}
} else {
// If the user has toggled it too quickly, then just eat up the event here (it's
// better than showing a janky screenshot).
// NOTE: Ideally, the screenshot mechanism would take the window transform into
// account
if (elapsedTime < MIN_TOGGLE_DELAY_MS) {
return;
}
EventBus.getDefault().post(new ToggleRecentsEvent());
mLastToggleTime = SystemClock.elapsedRealtime();
}
return;
} else {
// If the user has toggled it too quickly, then just eat up the event here (it's
// better than showing a janky screenshot).
// NOTE: Ideally, the screenshot mechanism would take the window transform into
// account
if (elapsedTime < MIN_TOGGLE_DELAY_MS) {
return;
}
// Otherwise, start the recents activity
ActivityManager.RunningTaskInfo runningTask =
ActivityManagerWrapper.getInstance().getRunningTask();
startRecentsActivityAndDismissKeyguardIfNeeded(runningTask,
isHomeStackVisible.value, true /* animate */, growTarget);
// Only close the other system windows if we are actually showing recents
ActivityManagerWrapper.getInstance().closeSystemWindows(
SYSTEM_DIALOG_REASON_RECENT_APPS);
mLastToggleTime = SystemClock.elapsedRealtime();
}
} catch (ActivityNotFoundException e) {
Log.e(TAG, "Failed to launch RecentsActivity", e);
}
}
public void preloadRecents() {
if (ActivityManagerWrapper.getInstance().isScreenPinningActive()) {
return;
}
// Skip preloading recents when keyguard is showing
final StatusBar statusBar = getStatusBar();
if (statusBar != null && statusBar.isKeyguardShowing()) {
return;
}
// Preload only the raw task list into a new load plan (which will be consumed by the
// RecentsActivity) only if there is a task to animate to. Post this to ensure that we
// don't block the touch feedback on the nav bar button which triggers this.
mHandler.post(() -> {
SystemServicesProxy ssp = Recents.getSystemServices();
if (!ssp.isRecentsActivityVisible(null)) {
ActivityManager.RunningTaskInfo runningTask =
ActivityManagerWrapper.getInstance().getRunningTask();
if (runningTask == null) {
return;
}
RecentsTaskLoader loader = Recents.getTaskLoader();
sInstanceLoadPlan = new RecentsTaskLoadPlan(mContext);
loader.preloadTasks(sInstanceLoadPlan, runningTask.id);
TaskStack stack = sInstanceLoadPlan.getTaskStack();
if (stack.getTaskCount() > 0) {
// Only preload the icon (but not the thumbnail since it may not have been taken
// for the pausing activity)
preloadIcon(runningTask.id);
// At this point, we don't know anything about the stack state. So only
// calculate the dimensions of the thumbnail that we need for the transition
// into Recents, but do not draw it until we construct the activity options when
// we start Recents
updateHeaderBarLayout(stack, null /* window rect override*/);
}
}
});
}
public void cancelPreloadingRecents() {
// Do nothing
}
public void onDraggingInRecents(float distanceFromTop) {
EventBus.getDefault().sendOntoMainThread(new DraggingInRecentsEvent(distanceFromTop));
}
public void onDraggingInRecentsEnded(float velocity) {
EventBus.getDefault().sendOntoMainThread(new DraggingInRecentsEndedEvent(velocity));
}
public void onShowCurrentUserToast(int msgResId, int msgLength) {
Toast.makeText(mContext, msgResId, msgLength).show();
}
/**
* Transitions to the next recent task in the stack.
*/
public void showNextTask() {
SystemServicesProxy ssp = Recents.getSystemServices();
RecentsTaskLoader loader = Recents.getTaskLoader();
RecentsTaskLoadPlan plan = new RecentsTaskLoadPlan(mContext);
loader.preloadTasks(plan, -1);
TaskStack focusedStack = plan.getTaskStack();
// Return early if there are no tasks in the focused stack
if (focusedStack == null || focusedStack.getTaskCount() == 0) return;
// Return early if there is no running task
ActivityManager.RunningTaskInfo runningTask =
ActivityManagerWrapper.getInstance().getRunningTask();
if (runningTask == null) return;
// Find the task in the recents list
boolean isRunningTaskInHomeStack =
runningTask.configuration.windowConfiguration.getActivityType()
== ACTIVITY_TYPE_HOME;
ArrayList<Task> tasks = focusedStack.getTasks();
Task toTask = null;
ActivityOptions launchOpts = null;
int taskCount = tasks.size();
for (int i = taskCount - 1; i >= 1; i--) {
Task task = tasks.get(i);
if (isRunningTaskInHomeStack) {
toTask = tasks.get(i - 1);
launchOpts = ActivityOptions.makeCustomAnimation(mContext,
R.anim.recents_launch_next_affiliated_task_target,
R.anim.recents_fast_toggle_app_home_exit);
break;
} else if (task.key.id == runningTask.id) {
toTask = tasks.get(i - 1);
launchOpts = ActivityOptions.makeCustomAnimation(mContext,
R.anim.recents_launch_prev_affiliated_task_target,
R.anim.recents_launch_prev_affiliated_task_source);
break;
}
}
// Return early if there is no next task
if (toTask == null) {
ssp.startInPlaceAnimationOnFrontMostApplication(
ActivityOptions.makeCustomInPlaceAnimation(mContext,
R.anim.recents_launch_prev_affiliated_task_bounce));
return;
}
// Launch the task
ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(toTask.key, launchOpts,
null /* resultCallback */, null /* resultCallbackHandler */);
}
/**
* Transitions to the next affiliated task.
*/
public void showRelativeAffiliatedTask(boolean showNextTask) {
SystemServicesProxy ssp = Recents.getSystemServices();
RecentsTaskLoader loader = Recents.getTaskLoader();
RecentsTaskLoadPlan plan = new RecentsTaskLoadPlan(mContext);
loader.preloadTasks(plan, -1);
TaskStack focusedStack = plan.getTaskStack();
// Return early if there are no tasks in the focused stack
if (focusedStack == null || focusedStack.getTaskCount() == 0) return;
// Return early if there is no running task (can't determine affiliated tasks in this case)
ActivityManager.RunningTaskInfo runningTask =
ActivityManagerWrapper.getInstance().getRunningTask();
final int activityType = runningTask.configuration.windowConfiguration.getActivityType();
if (runningTask == null) return;
// Return early if the running task is in the home/recents stack (optimization)
if (activityType == ACTIVITY_TYPE_HOME || activityType == ACTIVITY_TYPE_RECENTS) return;
// Find the task in the recents list
ArrayList<Task> tasks = focusedStack.getTasks();
Task toTask = null;
ActivityOptions launchOpts = null;
int taskCount = tasks.size();
for (int i = 0; i < taskCount; i++) {
Task task = tasks.get(i);
if (task.key.id == runningTask.id) {
if (showNextTask) {
if ((i + 1) < taskCount) {
toTask = tasks.get(i + 1);
launchOpts = ActivityOptions.makeCustomAnimation(mContext,
R.anim.recents_launch_next_affiliated_task_target,
R.anim.recents_launch_next_affiliated_task_source);
}
} else {
if ((i - 1) >= 0) {
toTask = tasks.get(i - 1);
launchOpts = ActivityOptions.makeCustomAnimation(mContext,
R.anim.recents_launch_prev_affiliated_task_target,
R.anim.recents_launch_prev_affiliated_task_source);
}
}
break;
}
}
// Return early if there is no next task
if (toTask == null) {
if (showNextTask) {
ssp.startInPlaceAnimationOnFrontMostApplication(
ActivityOptions.makeCustomInPlaceAnimation(mContext,
R.anim.recents_launch_next_affiliated_task_bounce));
} else {
ssp.startInPlaceAnimationOnFrontMostApplication(
ActivityOptions.makeCustomInPlaceAnimation(mContext,
R.anim.recents_launch_prev_affiliated_task_bounce));
}
return;
}
// Keep track of actually launched affiliated tasks
MetricsLogger.count(mContext, "overview_affiliated_task_launch", 1);
// Launch the task
ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(toTask.key, launchOpts,
null /* resultListener */, null /* resultCallbackHandler */);
}
public void showNextAffiliatedTask() {
// Keep track of when the affiliated task is triggered
MetricsLogger.count(mContext, "overview_affiliated_task_next", 1);
showRelativeAffiliatedTask(true);
}
public void showPrevAffiliatedTask() {
// Keep track of when the affiliated task is triggered
MetricsLogger.count(mContext, "overview_affiliated_task_prev", 1);
showRelativeAffiliatedTask(false);
}
public void splitPrimaryTask(int taskId, int dragMode, int stackCreateMode,
Rect initialBounds) {
SystemServicesProxy ssp = Recents.getSystemServices();
// Make sure we inform DividerView before we actually start the activity so we can change
// the resize mode already.
if (ssp.setTaskWindowingModeSplitScreenPrimary(taskId, stackCreateMode, initialBounds)) {
EventBus.getDefault().send(new DockedTopTaskEvent(dragMode, initialBounds));
}
}
public void setWaitingForTransitionStart(boolean waitingForTransitionStart) {
if (mWaitingForTransitionStart == waitingForTransitionStart) {
return;
}
mWaitingForTransitionStart = waitingForTransitionStart;
if (!waitingForTransitionStart && mToggleFollowingTransitionStart) {
mHandler.post(() -> toggleRecents(DividerView.INVALID_RECENTS_GROW_TARGET));
}
mToggleFollowingTransitionStart = false;
}
/**
* Returns the preloaded load plan and invalidates it.
*/
public static RecentsTaskLoadPlan consumeInstanceLoadPlan() {
RecentsTaskLoadPlan plan = sInstanceLoadPlan;
sInstanceLoadPlan = null;
return plan;
}
/**
* @return the time at which a task last entered picture-in-picture.
*/
public static long getLastPipTime() {
return sLastPipTime;
}
/**
* Clears the time at which a task last entered picture-in-picture.
*/
public static void clearLastPipTime() {
sLastPipTime = -1;
}
/**
* Reloads all the resources for the current configuration.
*/
private void reloadResources() {
Resources res = mContext.getResources();
mTaskBarHeight = TaskStackLayoutAlgorithm.getDimensionForDevice(mContext,
R.dimen.recents_task_view_header_height,
R.dimen.recents_task_view_header_height,
R.dimen.recents_task_view_header_height,
R.dimen.recents_task_view_header_height_tablet_land,
R.dimen.recents_task_view_header_height,
R.dimen.recents_task_view_header_height_tablet_land,
R.dimen.recents_grid_task_view_header_height);
LayoutInflater inflater = LayoutInflater.from(mContext);
mHeaderBar = (TaskViewHeader) inflater.inflate(R.layout.recents_task_view_header,
null, false);
mHeaderBar.setLayoutDirection(res.getConfiguration().getLayoutDirection());
}
private void updateDummyStackViewLayout(TaskStackLayoutAlgorithm stackLayout,
TaskStack stack, Rect windowRect) {
SystemServicesProxy ssp = Recents.getSystemServices();
Rect displayRect = ssp.getDisplayRect();
Rect systemInsets = new Rect();
ssp.getStableInsets(systemInsets);
// When docked, the nav bar insets are consumed and the activity is measured without insets.
// However, the window bounds include the insets, so we need to subtract them here to make
// them identical.
if (ssp.hasDockedTask()) {
if (systemInsets.bottom < windowRect.height()) {
// Only apply inset if it isn't going to cause the rect height to go negative.
windowRect.bottom -= systemInsets.bottom;
}
systemInsets.bottom = 0;
}
calculateWindowStableInsets(systemInsets, windowRect, displayRect);
windowRect.offsetTo(0, 0);
// Rebind the header bar and draw it for the transition
stackLayout.setSystemInsets(systemInsets);
if (stack != null) {
stackLayout.getTaskStackBounds(displayRect, windowRect, systemInsets.top,
systemInsets.left, systemInsets.right, mTmpBounds);
stackLayout.reset();
stackLayout.initialize(displayRect, windowRect, mTmpBounds);
}
}
private Rect getWindowRect(Rect windowRectOverride) {
return windowRectOverride != null
? new Rect(windowRectOverride)
: Recents.getSystemServices().getWindowRect();
}
/**
* Prepares the header bar layout for the next transition, if the task view bounds has changed
* since the last call, it will attempt to re-measure and layout the header bar to the new size.
*
* @param stack the stack to initialize the stack layout with
* @param windowRectOverride the rectangle to use when calculating the stack state which can
* be different from the current window rect if recents is resizing
* while being launched
*/
private void updateHeaderBarLayout(TaskStack stack, Rect windowRectOverride) {
Rect windowRect = getWindowRect(windowRectOverride);
int taskViewWidth = 0;
boolean useGridLayout = mDummyStackView.useGridLayout();
updateDummyStackViewLayout(mDummyStackView.getStackAlgorithm(), stack, windowRect);
if (stack != null) {
TaskStackLayoutAlgorithm stackLayout = mDummyStackView.getStackAlgorithm();
mDummyStackView.getStack().removeAllTasks(false /* notifyStackChanges */);
mDummyStackView.setTasks(stack, false /* allowNotifyStackChanges */);
// Get the width of a task view so that we know how wide to draw the header bar.
if (useGridLayout) {
TaskGridLayoutAlgorithm gridLayout = mDummyStackView.getGridAlgorithm();
gridLayout.initialize(windowRect);
taskViewWidth = (int) gridLayout.getTransform(0 /* taskIndex */,
stack.getTaskCount(), new TaskViewTransform(),
stackLayout).rect.width();
} else {
Rect taskViewBounds = stackLayout.getUntransformedTaskViewBounds();
if (!taskViewBounds.isEmpty()) {
taskViewWidth = taskViewBounds.width();
}
}
}
if (stack != null && taskViewWidth > 0) {
synchronized (mHeaderBarLock) {
if (mHeaderBar.getMeasuredWidth() != taskViewWidth ||
mHeaderBar.getMeasuredHeight() != mTaskBarHeight) {
if (useGridLayout) {
mHeaderBar.setShouldDarkenBackgroundColor(true);
mHeaderBar.setNoUserInteractionState();
}
mHeaderBar.forceLayout();
mHeaderBar.measure(
MeasureSpec.makeMeasureSpec(taskViewWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(mTaskBarHeight, MeasureSpec.EXACTLY));
}
mHeaderBar.layout(0, 0, taskViewWidth, mTaskBarHeight);
}
}
}
/**
* Given the stable insets and the rect for our window, calculates the insets that affect our
* window.
*/
private void calculateWindowStableInsets(Rect inOutInsets, Rect windowRect, Rect displayRect) {
// Display rect without insets - available app space
Rect appRect = new Rect(displayRect);
appRect.inset(inOutInsets);
// Our window intersected with available app space
Rect windowRectWithInsets = new Rect(windowRect);
windowRectWithInsets.intersect(appRect);
inOutInsets.left = windowRectWithInsets.left - windowRect.left;
inOutInsets.top = windowRectWithInsets.top - windowRect.top;
inOutInsets.right = windowRect.right - windowRectWithInsets.right;
inOutInsets.bottom = windowRect.bottom - windowRectWithInsets.bottom;
}
/**
* Preloads the icon of a task.
*/
private void preloadIcon(int runningTaskId) {
// Ensure that we load the running task's icon
RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options();
launchOpts.runningTaskId = runningTaskId;
launchOpts.loadThumbnails = false;
launchOpts.onlyLoadForCache = true;
Recents.getTaskLoader().loadTasks(sInstanceLoadPlan, launchOpts);
}
/**
* Creates the activity options for a unknown state->recents transition.
*/
protected ActivityOptions getUnknownTransitionActivityOptions() {
return ActivityOptions.makeCustomAnimation(mContext,
R.anim.recents_from_unknown_enter,
R.anim.recents_from_unknown_exit,
mHandler, null);
}
/**
* Creates the activity options for a home->recents transition.
*/
protected ActivityOptions getHomeTransitionActivityOptions() {
return ActivityOptions.makeCustomAnimation(mContext,
R.anim.recents_from_launcher_enter,
R.anim.recents_from_launcher_exit,
mHandler, null);
}
/**
* Creates the activity options for an app->recents transition.
*/
private Pair<ActivityOptions, AppTransitionAnimationSpecsFuture>
getThumbnailTransitionActivityOptions(ActivityManager.RunningTaskInfo runningTask,
Rect windowOverrideRect) {
final boolean isLowRamDevice = Recents.getConfiguration().isLowRamDevice;
// Update the destination rect
Task toTask = new Task();
TaskViewTransform toTransform = getThumbnailTransitionTransform(mDummyStackView, toTask,
windowOverrideRect);
RectF toTaskRect = toTransform.rect;
AppTransitionAnimationSpecsFuture future = new AppTransitionAnimationSpecsFuture(mHandler) {
@Override
public List<AppTransitionAnimationSpecCompat> composeSpecs() {
Rect rect = new Rect();
toTaskRect.round(rect);
Bitmap thumbnail = drawThumbnailTransitionBitmap(toTask, toTransform);
return Lists.newArrayList(new AppTransitionAnimationSpecCompat(toTask.key.id,
thumbnail, rect));
}
};
// For low end ram devices, wait for transition flag is reset when Recents entrance
// animation is complete instead of when the transition animation starts
return new Pair<>(RecentsTransition.createAspectScaleAnimation(mContext, mHandler,
false /* scaleUp */, future, isLowRamDevice ? null : mResetToggleFlagListener),
future);
}
/**
* Returns the transition rect for the given task id.
*/
private TaskViewTransform getThumbnailTransitionTransform(TaskStackView stackView,
Task runningTaskOut, Rect windowOverrideRect) {
// Find the running task in the TaskStack
TaskStack stack = stackView.getStack();
Task launchTask = stack.getLaunchTarget();
if (launchTask != null) {
runningTaskOut.copyFrom(launchTask);
} else {
// If no task is specified or we can not find the task just use the front most one
launchTask = stack.getFrontMostTask();
runningTaskOut.copyFrom(launchTask);
}
// Get the transform for the running task
stackView.updateLayoutAlgorithm(true /* boundScroll */);
stackView.updateToInitialState();
stackView.getStackAlgorithm().getStackTransformScreenCoordinates(launchTask,
stackView.getScroller().getStackScroll(), mTmpTransform, null, windowOverrideRect);
return mTmpTransform;
}
/**
* Draws the header of a task used for the window animation into a bitmap.
*/
private Bitmap drawThumbnailTransitionBitmap(Task toTask,
TaskViewTransform toTransform) {
SystemServicesProxy ssp = Recents.getSystemServices();
int width = (int) toTransform.rect.width();
int height = (int) toTransform.rect.height();
if (toTransform != null && toTask.key != null && width > 0 && height > 0) {
synchronized (mHeaderBarLock) {
boolean disabledInSafeMode = !toTask.isSystemApp && ssp.isInSafeMode();
mHeaderBar.onTaskViewSizeChanged(width, height);
if (RecentsDebugFlags.Static.EnableTransitionThumbnailDebugMode) {
return RecentsTransition.drawViewIntoHardwareBitmap(width, mTaskBarHeight,
null, 1f, 0xFFff0000);
} else {
// Workaround for b/27815919, reset the callback so that we do not trigger an
// invalidate on the header bar as a result of updating the icon
Drawable icon = mHeaderBar.getIconView().getDrawable();
if (icon != null) {
icon.setCallback(null);
}
mHeaderBar.bindToTask(toTask, false /* touchExplorationEnabled */,
disabledInSafeMode);
mHeaderBar.onTaskDataLoaded();
mHeaderBar.setDimAlpha(toTransform.dimAlpha);
return RecentsTransition.drawViewIntoHardwareBitmap(width, mTaskBarHeight,
mHeaderBar, 1f, 0);
}
}
}
return null;
}
/**
* Shows the recents activity after dismissing the keyguard if visible
*/
protected void startRecentsActivityAndDismissKeyguardIfNeeded(
final ActivityManager.RunningTaskInfo runningTask, final boolean isHomeStackVisible,
final boolean animate, final int growTarget) {
// Preload only if device for current user is unlocked
final StatusBar statusBar = getStatusBar();
if (statusBar != null && statusBar.isKeyguardShowing()) {
statusBar.executeRunnableDismissingKeyguard(() -> {
// Flush trustmanager before checking device locked per user when preloading
mTrustManager.reportKeyguardShowingChanged();
mHandler.post(() -> startRecentsActivity(runningTask, isHomeStackVisible,
animate, growTarget));
}, null, true /* dismissShade */, false /* afterKeyguardGone */,
true /* deferred */);
} else {
startRecentsActivity(runningTask, isHomeStackVisible, animate, growTarget);
}
}
private void startRecentsActivity(ActivityManager.RunningTaskInfo runningTask,
boolean isHomeStackVisible, boolean animate, int growTarget) {
RecentsTaskLoader loader = Recents.getTaskLoader();
RecentsActivityLaunchState launchState = Recents.getConfiguration().getLaunchState();
int runningTaskId = !mLaunchedWhileDocking && (runningTask != null)
? runningTask.id
: -1;
// In the case where alt-tab is triggered, we never get a preloadRecents() call, so we
// should always preload the tasks now. If we are dragging in recents, reload them as
// the stacks might have changed.
if (mLaunchedWhileDocking || mTriggeredFromAltTab || sInstanceLoadPlan == null) {
// Create a new load plan if preloadRecents() was never triggered
sInstanceLoadPlan = new RecentsTaskLoadPlan(mContext);
}
if (mLaunchedWhileDocking || mTriggeredFromAltTab || !sInstanceLoadPlan.hasTasks()) {
loader.preloadTasks(sInstanceLoadPlan, runningTaskId);
}
TaskStack stack = sInstanceLoadPlan.getTaskStack();
boolean hasRecentTasks = stack.getTaskCount() > 0;
boolean useThumbnailTransition = (runningTask != null) && !isHomeStackVisible &&
hasRecentTasks;
// Update the launch state that we need in updateHeaderBarLayout()
launchState.launchedFromHome = !useThumbnailTransition && !mLaunchedWhileDocking;
launchState.launchedFromApp = useThumbnailTransition || mLaunchedWhileDocking;
launchState.launchedFromPipApp = false;
launchState.launchedWithNextPipApp =
stack.isNextLaunchTargetPip(RecentsImpl.getLastPipTime());
launchState.launchedViaDockGesture = mLaunchedWhileDocking;
launchState.launchedViaDragGesture = mDraggingInRecents;
launchState.launchedToTaskId = runningTaskId;
launchState.launchedWithAltTab = mTriggeredFromAltTab;
// Disable toggling of recents between starting the activity and it is visible and the app
// has started its transition into recents.
setWaitingForTransitionStart(useThumbnailTransition);
// Preload the icon (this will be a null-op if we have preloaded the icon already in
// preloadRecents())
preloadIcon(runningTaskId);
// Update the header bar if necessary
Rect windowOverrideRect = getWindowRectOverride(growTarget);
updateHeaderBarLayout(stack, windowOverrideRect);
// Prepare the dummy stack for the transition
TaskStackLayoutAlgorithm.VisibilityReport stackVr =
mDummyStackView.computeStackVisibilityReport();
// Update the remaining launch state
launchState.launchedNumVisibleTasks = stackVr.numVisibleTasks;
launchState.launchedNumVisibleThumbnails = stackVr.numVisibleThumbnails;
if (!animate) {
startRecentsActivity(ActivityOptions.makeCustomAnimation(mContext, -1, -1),
null /* future */);
return;
}
Pair<ActivityOptions, AppTransitionAnimationSpecsFuture> pair;
if (useThumbnailTransition) {
// Try starting with a thumbnail transition
pair = getThumbnailTransitionActivityOptions(runningTask, windowOverrideRect);
} else {
// If there is no thumbnail transition, but is launching from home into recents, then
// use a quick home transition
pair = new Pair<>(hasRecentTasks
? getHomeTransitionActivityOptions()
: getUnknownTransitionActivityOptions(), null);
}
startRecentsActivity(pair.first, pair.second);
mLastToggleTime = SystemClock.elapsedRealtime();
}
private Rect getWindowRectOverride(int growTarget) {
if (growTarget == DividerView.INVALID_RECENTS_GROW_TARGET) {
return SystemServicesProxy.getInstance(mContext).getWindowRect();
}
Rect result = new Rect();
Rect displayRect = Recents.getSystemServices().getDisplayRect();
DockedDividerUtils.calculateBoundsForPosition(growTarget, WindowManager.DOCKED_BOTTOM,
result, displayRect.width(), displayRect.height(),
Recents.getSystemServices().getDockedDividerSize(mContext));
return result;
}
private StatusBar getStatusBar() {
return ((SystemUIApplication) mContext).getComponent(StatusBar.class);
}
/**
* Starts the recents activity.
*/
private void startRecentsActivity(ActivityOptions opts,
final AppTransitionAnimationSpecsFuture future) {
Intent intent = new Intent();
intent.setClassName(RECENTS_PACKAGE, RECENTS_ACTIVITY);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
| Intent.FLAG_ACTIVITY_TASK_ON_HOME);
HidePipMenuEvent hideMenuEvent = new HidePipMenuEvent();
hideMenuEvent.addPostAnimationCallback(() -> {
Recents.getSystemServices().startActivityAsUserAsync(intent, opts);
EventBus.getDefault().send(new RecentsActivityStartingEvent());
if (future != null) {
future.composeSpecsSynchronous();
}
});
EventBus.getDefault().send(hideMenuEvent);
// Once we have launched the activity, reset the dummy stack view tasks so we don't hold
// onto references to the same tasks consumed by the activity
mDummyStackView.setTasks(mEmptyTaskStack, false /* notifyStackChanges */);
}
/**** OnAnimationFinishedListener Implementation ****/
@Override
public void onAnimationFinished() {
EventBus.getDefault().post(new EnterRecentsWindowLastAnimationFrameEvent());
}
}