/* * Copyright (C) 2014 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 android.app.Activity; import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.ITaskStackListener; import android.appwidget.AppWidgetHost; import android.appwidget.AppWidgetProviderInfo; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; import android.os.Handler; import android.os.SystemClock; import android.os.UserHandle; import android.util.Pair; import android.view.LayoutInflater; import android.view.View; import com.android.systemui.R; import com.android.systemui.RecentsComponent; import com.android.systemui.recents.misc.Console; import com.android.systemui.recents.misc.SystemServicesProxy; import com.android.systemui.recents.model.RecentsTaskLoadPlan; import com.android.systemui.recents.model.RecentsTaskLoader; import com.android.systemui.recents.model.Task; import com.android.systemui.recents.model.TaskGrouping; import com.android.systemui.recents.model.TaskStack; import com.android.systemui.recents.views.TaskStackView; import com.android.systemui.recents.views.TaskStackViewLayoutAlgorithm; import com.android.systemui.recents.views.TaskViewHeader; import com.android.systemui.recents.views.TaskViewTransform; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; /** * Annotation for a method that is only called from the primary user's SystemUI process and will be * proxied to the current user. */ @interface ProxyFromPrimaryToCurrentUser {} /** * Annotation for a method that may be called from any user's SystemUI process and will be proxied * to the primary user. */ @interface ProxyFromAnyToPrimaryUser {} /** A proxy implementation for the recents component */ public class AlternateRecentsComponent implements ActivityOptions.OnAnimationStartedListener { final public static String EXTRA_TRIGGERED_FROM_ALT_TAB = "triggeredFromAltTab"; final public static String EXTRA_TRIGGERED_FROM_HOME_KEY = "triggeredFromHomeKey"; final public static String EXTRA_RECENTS_VISIBILITY = "recentsVisibility"; // Owner proxy events final public static String ACTION_PROXY_NOTIFY_RECENTS_VISIBLITY_TO_OWNER = "action_notify_recents_visibility_change"; final public static String ACTION_START_ENTER_ANIMATION = "action_start_enter_animation"; final public static String ACTION_TOGGLE_RECENTS_ACTIVITY = "action_toggle_recents_activity"; final public static String ACTION_HIDE_RECENTS_ACTIVITY = "action_hide_recents_activity"; final static int sMinToggleDelay = 350; final static String sToggleRecentsAction = "com.android.systemui.recents.SHOW_RECENTS"; public final static String sRecentsPackage = "com.android.systemui"; public final static String sRecentsActivity = "com.android.systemui.recents.RecentsActivity"; /** * An implementation of ITaskStackListener, that allows us to listen for changes to the system * task stacks and update recents accordingly. */ class TaskStackListenerImpl extends ITaskStackListener.Stub implements Runnable { Handler mHandler; public TaskStackListenerImpl(Handler handler) { mHandler = handler; } @Override public void onTaskStackChanged() { // Debounce any task stack changes mHandler.removeCallbacks(this); mHandler.post(this); } /** Preloads the next task */ public void run() { RecentsConfiguration config = RecentsConfiguration.getInstance(); if (config.svelteLevel == RecentsConfiguration.SVELTE_NONE) { RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); SystemServicesProxy ssp = loader.getSystemServicesProxy(); ActivityManager.RunningTaskInfo runningTaskInfo = ssp.getTopMostTask(); // Load the next task only if we aren't svelte RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext); loader.preloadTasks(plan, true /* isTopTaskHome */); RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options(); // 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. if (runningTaskInfo != null) { launchOpts.runningTaskId = runningTaskInfo.id; } launchOpts.numVisibleTasks = 2; launchOpts.numVisibleTaskThumbnails = 2; launchOpts.onlyLoadForCache = true; launchOpts.onlyLoadPausedActivities = true; loader.loadTasks(mContext, plan, launchOpts); } } } /** * A proxy for Recents events which happens strictly for the owner. */ class RecentsOwnerEventProxyReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { switch (intent.getAction()) { case ACTION_PROXY_NOTIFY_RECENTS_VISIBLITY_TO_OWNER: visibilityChanged(intent.getBooleanExtra(EXTRA_RECENTS_VISIBILITY, false)); break; } } } static RecentsComponent.Callbacks sRecentsComponentCallbacks; static RecentsTaskLoadPlan sInstanceLoadPlan; Context mContext; LayoutInflater mInflater; SystemServicesProxy mSystemServicesProxy; Handler mHandler; TaskStackListenerImpl mTaskStackListener; RecentsOwnerEventProxyReceiver mProxyBroadcastReceiver; boolean mBootCompleted; boolean mStartAnimationTriggered; boolean mCanReuseTaskStackViews = true; // Task launching RecentsConfiguration mConfig; Rect mWindowRect = new Rect(); Rect mTaskStackBounds = new Rect(); Rect mSystemInsets = new Rect(); TaskViewTransform mTmpTransform = new TaskViewTransform(); int mStatusBarHeight; int mNavBarHeight; int mNavBarWidth; // Header (for transition) TaskViewHeader mHeaderBar; TaskStackView mDummyStackView; // Variables to keep track of if we need to start recents after binding boolean mTriggeredFromAltTab; long mLastToggleTime; public AlternateRecentsComponent(Context context) { RecentsTaskLoader.initialize(context); mInflater = LayoutInflater.from(context); mContext = context; mSystemServicesProxy = new SystemServicesProxy(context); mHandler = new Handler(); mTaskStackBounds = new Rect(); // Register the task stack listener mTaskStackListener = new TaskStackListenerImpl(mHandler); mSystemServicesProxy.registerTaskStackListener(mTaskStackListener); // Only the owner has the callback to update the SysUI visibility flags, so all non-owner // instances of AlternateRecentsComponent needs to notify the owner when the visibility // changes. if (mSystemServicesProxy.isForegroundUserOwner()) { mProxyBroadcastReceiver = new RecentsOwnerEventProxyReceiver(); IntentFilter filter = new IntentFilter(); filter.addAction(AlternateRecentsComponent.ACTION_PROXY_NOTIFY_RECENTS_VISIBLITY_TO_OWNER); mContext.registerReceiverAsUser(mProxyBroadcastReceiver, UserHandle.CURRENT, filter, null, mHandler); } } /** Creates a new broadcast intent */ static Intent createLocalBroadcastIntent(Context context, String action) { Intent intent = new Intent(action); intent.setPackage(context.getPackageName()); intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT | Intent.FLAG_RECEIVER_FOREGROUND); return intent; } /** Initializes the Recents. */ @ProxyFromPrimaryToCurrentUser public void onStart() { // Initialize some static datastructures TaskStackViewLayoutAlgorithm.initializeCurve(); // Load the header bar layout reloadHeaderBarLayout(true); // 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 = RecentsTaskLoader.getInstance(); RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext); loader.preloadTasks(plan, true /* isTopTaskHome */); RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options(); launchOpts.numVisibleTasks = loader.getApplicationIconCacheSize(); launchOpts.numVisibleTaskThumbnails = loader.getThumbnailCacheSize(); launchOpts.onlyLoadForCache = true; loader.loadTasks(mContext, plan, launchOpts); } public void onBootCompleted() { mBootCompleted = true; } /** Shows the Recents. */ @ProxyFromPrimaryToCurrentUser public void onShowRecents(boolean triggeredFromAltTab) { if (mSystemServicesProxy.isForegroundUserOwner()) { showRecents(triggeredFromAltTab); } else { Intent intent = createLocalBroadcastIntent(mContext, RecentsUserEventProxyReceiver.ACTION_PROXY_SHOW_RECENTS_TO_USER); intent.putExtra(EXTRA_TRIGGERED_FROM_ALT_TAB, triggeredFromAltTab); mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); } } void showRecents(boolean triggeredFromAltTab) { mTriggeredFromAltTab = triggeredFromAltTab; try { startRecentsActivity(); } catch (ActivityNotFoundException e) { Console.logRawError("Failed to launch RecentAppsIntent", e); } } /** Hides the Recents. */ @ProxyFromPrimaryToCurrentUser public void onHideRecents(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) { if (mSystemServicesProxy.isForegroundUserOwner()) { hideRecents(triggeredFromAltTab, triggeredFromHomeKey); } else { Intent intent = createLocalBroadcastIntent(mContext, RecentsUserEventProxyReceiver.ACTION_PROXY_HIDE_RECENTS_TO_USER); intent.putExtra(EXTRA_TRIGGERED_FROM_ALT_TAB, triggeredFromAltTab); intent.putExtra(EXTRA_TRIGGERED_FROM_HOME_KEY, triggeredFromHomeKey); mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); } } void hideRecents(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) { if (mBootCompleted) { ActivityManager.RunningTaskInfo topTask = mSystemServicesProxy.getTopMostTask(); if (topTask != null && mSystemServicesProxy.isRecentsTopMost(topTask, null)) { // Notify recents to hide itself Intent intent = createLocalBroadcastIntent(mContext, ACTION_HIDE_RECENTS_ACTIVITY); intent.putExtra(EXTRA_TRIGGERED_FROM_ALT_TAB, triggeredFromAltTab); intent.putExtra(EXTRA_TRIGGERED_FROM_HOME_KEY, triggeredFromHomeKey); mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); } } } /** Toggles the Recents activity. */ @ProxyFromPrimaryToCurrentUser public void onToggleRecents() { if (mSystemServicesProxy.isForegroundUserOwner()) { toggleRecents(); } else { Intent intent = createLocalBroadcastIntent(mContext, RecentsUserEventProxyReceiver.ACTION_PROXY_TOGGLE_RECENTS_TO_USER); mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); } } void toggleRecents() { mTriggeredFromAltTab = false; try { toggleRecentsActivity(); } catch (ActivityNotFoundException e) { Console.logRawError("Failed to launch RecentAppsIntent", e); } } /** Preloads info for the Recents activity. */ @ProxyFromPrimaryToCurrentUser public void onPreloadRecents() { if (mSystemServicesProxy.isForegroundUserOwner()) { preloadRecents(); } else { Intent intent = createLocalBroadcastIntent(mContext, RecentsUserEventProxyReceiver.ACTION_PROXY_PRELOAD_RECENTS_TO_USER); mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); } } void preloadRecents() { // Preload only the raw task list into a new load plan (which will be consumed by the // RecentsActivity) RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); sInstanceLoadPlan = loader.createLoadPlan(mContext); sInstanceLoadPlan.preloadRawTasks(true); } public void onCancelPreloadingRecents() { // Do nothing } void showRelativeAffiliatedTask(boolean showNextTask) { RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); RecentsTaskLoadPlan plan = loader.createLoadPlan(mContext); loader.preloadTasks(plan, true /* isTopTaskHome */); TaskStack stack = plan.getTaskStack(); // Return early if there are no tasks if (stack.getTaskCount() == 0) return; ActivityManager.RunningTaskInfo runningTask = mSystemServicesProxy.getTopMostTask(); // Return early if there is no running task (can't determine affiliated tasks in this case) if (runningTask == null) return; // Return early if the running task is in the home stack (optimization) if (mSystemServicesProxy.isInHomeStack(runningTask.id)) return; // Find the task in the recents list ArrayList tasks = stack.getTasks(); Task toTask = null; ActivityOptions launchOpts = null; int taskCount = tasks.size(); int numAffiliatedTasks = 0; for (int i = 0; i < taskCount; i++) { Task task = tasks.get(i); if (task.key.id == runningTask.id) { TaskGrouping group = task.group; Task.TaskKey toTaskKey; if (showNextTask) { toTaskKey = group.getNextTaskInGroup(task); launchOpts = ActivityOptions.makeCustomAnimation(mContext, R.anim.recents_launch_next_affiliated_task_target, R.anim.recents_launch_next_affiliated_task_source); } else { toTaskKey = group.getPrevTaskInGroup(task); launchOpts = ActivityOptions.makeCustomAnimation(mContext, R.anim.recents_launch_prev_affiliated_task_target, R.anim.recents_launch_prev_affiliated_task_source); } if (toTaskKey != null) { toTask = stack.findTaskWithId(toTaskKey.id); } numAffiliatedTasks = group.getTaskCount(); break; } } // Return early if there is no next task if (toTask == null) { if (numAffiliatedTasks > 1) { if (showNextTask) { mSystemServicesProxy.startInPlaceAnimationOnFrontMostApplication( ActivityOptions.makeCustomInPlaceAnimation(mContext, R.anim.recents_launch_next_affiliated_task_bounce)); } else { mSystemServicesProxy.startInPlaceAnimationOnFrontMostApplication( ActivityOptions.makeCustomInPlaceAnimation(mContext, R.anim.recents_launch_prev_affiliated_task_bounce)); } } return; } // Launch the task if (toTask.isActive) { // Bring an active task to the foreground mSystemServicesProxy.moveTaskToFront(toTask.key.id, launchOpts); } else { mSystemServicesProxy.startActivityFromRecents(mContext, toTask.key.id, toTask.activityLabel, launchOpts); } } public void onShowNextAffiliatedTask() { showRelativeAffiliatedTask(true); } public void onShowPrevAffiliatedTask() { showRelativeAffiliatedTask(false); } /** Updates on configuration change. */ @ProxyFromPrimaryToCurrentUser public void onConfigurationChanged(Configuration newConfig) { if (mSystemServicesProxy.isForegroundUserOwner()) { configurationChanged(); } else { Intent intent = createLocalBroadcastIntent(mContext, RecentsUserEventProxyReceiver.ACTION_PROXY_CONFIG_CHANGE_TO_USER); mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); } } void configurationChanged() { // Don't reuse task stack views if the configuration changes mCanReuseTaskStackViews = false; // Reload the header bar layout reloadHeaderBarLayout(false); } /** Prepares the header bar layout. */ void reloadHeaderBarLayout(boolean reloadWidget) { Resources res = mContext.getResources(); mWindowRect = mSystemServicesProxy.getWindowRect(); mStatusBarHeight = res.getDimensionPixelSize(com.android.internal.R.dimen.status_bar_height); mNavBarHeight = res.getDimensionPixelSize(com.android.internal.R.dimen.navigation_bar_height); mNavBarWidth = res.getDimensionPixelSize(com.android.internal.R.dimen.navigation_bar_width); mConfig = RecentsConfiguration.reinitialize(mContext, mSystemServicesProxy); mConfig.updateOnConfigurationChange(); if (reloadWidget) { // Reload the widget id before we get the task stack bounds reloadSearchBarAppWidget(mContext, mSystemServicesProxy); } mConfig.getTaskStackBounds(mWindowRect.width(), mWindowRect.height(), mStatusBarHeight, (mConfig.hasTransposedNavBar ? mNavBarWidth : 0), mTaskStackBounds); if (mConfig.isLandscape && mConfig.hasTransposedNavBar) { mSystemInsets.set(0, mStatusBarHeight, mNavBarWidth, 0); } else { mSystemInsets.set(0, mStatusBarHeight, 0, mNavBarHeight); } // Inflate the header bar layout so that we can rebind and draw it for the transition TaskStack stack = new TaskStack(); mDummyStackView = new TaskStackView(mContext, stack); TaskStackViewLayoutAlgorithm algo = mDummyStackView.getStackAlgorithm(); Rect taskStackBounds = new Rect(mTaskStackBounds); taskStackBounds.bottom -= mSystemInsets.bottom; algo.computeRects(mWindowRect.width(), mWindowRect.height(), taskStackBounds); Rect taskViewSize = algo.getUntransformedTaskViewSize(); int taskBarHeight = res.getDimensionPixelSize(R.dimen.recents_task_bar_height); mHeaderBar = (TaskViewHeader) mInflater.inflate(R.layout.recents_task_view_header, null, false); mHeaderBar.measure( View.MeasureSpec.makeMeasureSpec(taskViewSize.width(), View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(taskBarHeight, View.MeasureSpec.EXACTLY)); mHeaderBar.layout(0, 0, taskViewSize.width(), taskBarHeight); } /** Prepares the search bar app widget */ void reloadSearchBarAppWidget(Context context, SystemServicesProxy ssp) { // Try and pre-emptively bind the search widget on startup to ensure that we // have the right thumbnail bounds to animate to. if (Constants.DebugFlags.App.EnableSearchLayout) { // If there is no id, then bind a new search app widget if (mConfig.searchBarAppWidgetId < 0) { AppWidgetHost host = new RecentsAppWidgetHost(context, Constants.Values.App.AppWidgetHostId); Pair widgetInfo = ssp.bindSearchAppWidget(host); if (widgetInfo != null) { // Save the app widget id into the settings mConfig.updateSearchBarAppWidgetId(context, widgetInfo.first); } } } } /** Toggles the recents activity */ void toggleRecentsActivity() { // 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 ((SystemClock.elapsedRealtime() - mLastToggleTime) < sMinToggleDelay) { return; } // If Recents is the front most activity, then we should just communicate with it directly // to launch the first task or dismiss itself ActivityManager.RunningTaskInfo topTask = mSystemServicesProxy.getTopMostTask(); AtomicBoolean isTopTaskHome = new AtomicBoolean(true); if (topTask != null && mSystemServicesProxy.isRecentsTopMost(topTask, isTopTaskHome)) { // Notify recents to toggle itself Intent intent = createLocalBroadcastIntent(mContext, ACTION_TOGGLE_RECENTS_ACTIVITY); mContext.sendBroadcastAsUser(intent, UserHandle.CURRENT); mLastToggleTime = SystemClock.elapsedRealtime(); return; } else { // Otherwise, start the recents activity startRecentsActivity(topTask, isTopTaskHome.get()); } } /** Starts the recents activity if it is not already running */ void startRecentsActivity() { // Check if the top task is in the home stack, and start the recents activity ActivityManager.RunningTaskInfo topTask = mSystemServicesProxy.getTopMostTask(); AtomicBoolean isTopTaskHome = new AtomicBoolean(true); if (topTask == null || !mSystemServicesProxy.isRecentsTopMost(topTask, isTopTaskHome)) { startRecentsActivity(topTask, isTopTaskHome.get()); } } /** * Creates the activity options for a unknown state->recents transition. */ ActivityOptions getUnknownTransitionActivityOptions() { mStartAnimationTriggered = false; return ActivityOptions.makeCustomAnimation(mContext, R.anim.recents_from_unknown_enter, R.anim.recents_from_unknown_exit, mHandler, this); } /** * Creates the activity options for a home->recents transition. */ ActivityOptions getHomeTransitionActivityOptions(boolean fromSearchHome) { mStartAnimationTriggered = false; if (fromSearchHome) { return ActivityOptions.makeCustomAnimation(mContext, R.anim.recents_from_search_launcher_enter, R.anim.recents_from_search_launcher_exit, mHandler, this); } return ActivityOptions.makeCustomAnimation(mContext, R.anim.recents_from_launcher_enter, R.anim.recents_from_launcher_exit, mHandler, this); } /** * Creates the activity options for an app->recents transition. */ ActivityOptions getThumbnailTransitionActivityOptions(ActivityManager.RunningTaskInfo topTask, TaskStack stack, TaskStackView stackView) { // Update the destination rect Task toTask = new Task(); TaskViewTransform toTransform = getThumbnailTransitionTransform(stack, stackView, topTask.id, toTask); if (toTransform != null && toTask.key != null) { Rect toTaskRect = toTransform.rect; int toHeaderWidth = (int) (mHeaderBar.getMeasuredWidth() * toTransform.scale); int toHeaderHeight = (int) (mHeaderBar.getMeasuredHeight() * toTransform.scale); Bitmap thumbnail = Bitmap.createBitmap(toHeaderWidth, toHeaderHeight, Bitmap.Config.ARGB_8888); if (Constants.DebugFlags.App.EnableTransitionThumbnailDebugMode) { thumbnail.eraseColor(0xFFff0000); } else { Canvas c = new Canvas(thumbnail); c.scale(toTransform.scale, toTransform.scale); mHeaderBar.rebindToTask(toTask); mHeaderBar.draw(c); c.setBitmap(null); } mStartAnimationTriggered = false; return ActivityOptions.makeThumbnailAspectScaleDownAnimation(mDummyStackView, thumbnail, toTaskRect.left, toTaskRect.top, toTaskRect.width(), toTaskRect.height(), mHandler, this); } // If both the screenshot and thumbnail fails, then just fall back to the default transition return getUnknownTransitionActivityOptions(); } /** Returns the transition rect for the given task id. */ TaskViewTransform getThumbnailTransitionTransform(TaskStack stack, TaskStackView stackView, int runningTaskId, Task runningTaskOut) { // Find the running task in the TaskStack Task task = null; ArrayList tasks = stack.getTasks(); if (runningTaskId != -1) { // Otherwise, try and find the task with the int taskCount = tasks.size(); for (int i = taskCount - 1; i >= 0; i--) { Task t = tasks.get(i); if (t.key.id == runningTaskId) { task = t; runningTaskOut.copyFrom(t); break; } } } if (task == null) { // If no task is specified or we can not find the task just use the front most one task = tasks.get(tasks.size() - 1); } // Get the transform for the running task stackView.getScroller().setStackScrollToInitialState(); mTmpTransform = stackView.getStackAlgorithm().getStackTransform(task, stackView.getScroller().getStackScroll(), mTmpTransform, null); return mTmpTransform; } /** Starts the recents activity */ void startRecentsActivity(ActivityManager.RunningTaskInfo topTask, boolean isTopTaskHome) { if (sInstanceLoadPlan == null) { // Create a new load plan if onPreloadRecents() was never triggered RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); sInstanceLoadPlan = loader.createLoadPlan(mContext); } RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); loader.preloadTasks(sInstanceLoadPlan, isTopTaskHome); TaskStack stack = sInstanceLoadPlan.getTaskStack(); // Prepare the dummy stack for the transition mDummyStackView.updateMinMaxScrollForStack(stack, mTriggeredFromAltTab, isTopTaskHome); TaskStackViewLayoutAlgorithm.VisibilityReport stackVr = mDummyStackView.computeStackVisibilityReport(); boolean hasRecentTasks = stack.getTaskCount() > 0; boolean useThumbnailTransition = (topTask != null) && !isTopTaskHome && hasRecentTasks; if (useThumbnailTransition) { // Ensure that we load the running task's icon RecentsTaskLoadPlan.Options launchOpts = new RecentsTaskLoadPlan.Options(); launchOpts.runningTaskId = topTask.id; launchOpts.loadThumbnails = false; launchOpts.onlyLoadForCache = true; loader.loadTasks(mContext, sInstanceLoadPlan, launchOpts); // Try starting with a thumbnail transition ActivityOptions opts = getThumbnailTransitionActivityOptions(topTask, stack, mDummyStackView); if (opts != null) { startAlternateRecentsActivity(topTask, opts, false /* fromHome */, false /* fromSearchHome */, true /* fromThumbnail */, stackVr); } else { // Fall through below to the non-thumbnail transition useThumbnailTransition = false; } } if (!useThumbnailTransition) { // If there is no thumbnail transition, but is launching from home into recents, then // use a quick home transition and do the animation from home if (hasRecentTasks) { // Get the home activity info String homeActivityPackage = mSystemServicesProxy.getHomeActivityPackageName(); // Get the search widget info AppWidgetProviderInfo searchWidget = null; String searchWidgetPackage = null; if (mConfig.hasSearchBarAppWidget()) { searchWidget = mSystemServicesProxy.getAppWidgetInfo( mConfig.searchBarAppWidgetId); } else { searchWidget = mSystemServicesProxy.resolveSearchAppWidget(); } if (searchWidget != null && searchWidget.provider != null) { searchWidgetPackage = searchWidget.provider.getPackageName(); } // Determine whether we are coming from a search owned home activity boolean fromSearchHome = false; if (homeActivityPackage != null && searchWidgetPackage != null && homeActivityPackage.equals(searchWidgetPackage)) { fromSearchHome = true; } ActivityOptions opts = getHomeTransitionActivityOptions(fromSearchHome); startAlternateRecentsActivity(topTask, opts, true /* fromHome */, fromSearchHome, false /* fromThumbnail */, stackVr); } else { // Otherwise we do the normal fade from an unknown source ActivityOptions opts = getUnknownTransitionActivityOptions(); startAlternateRecentsActivity(topTask, opts, true /* fromHome */, false /* fromSearchHome */, false /* fromThumbnail */, stackVr); } } mLastToggleTime = SystemClock.elapsedRealtime(); } /** Starts the recents activity */ void startAlternateRecentsActivity(ActivityManager.RunningTaskInfo topTask, ActivityOptions opts, boolean fromHome, boolean fromSearchHome, boolean fromThumbnail, TaskStackViewLayoutAlgorithm.VisibilityReport vr) { // Update the configuration based on the launch options mConfig.launchedFromHome = fromSearchHome || fromHome; mConfig.launchedFromSearchHome = fromSearchHome; mConfig.launchedFromAppWithThumbnail = fromThumbnail; mConfig.launchedToTaskId = (topTask != null) ? topTask.id : -1; mConfig.launchedWithAltTab = mTriggeredFromAltTab; mConfig.launchedReuseTaskStackViews = mCanReuseTaskStackViews; mConfig.launchedNumVisibleTasks = vr.numVisibleTasks; mConfig.launchedNumVisibleThumbnails = vr.numVisibleThumbnails; mConfig.launchedHasConfigurationChanged = false; Intent intent = new Intent(sToggleRecentsAction); intent.setClassName(sRecentsPackage, sRecentsActivity); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS | Intent.FLAG_ACTIVITY_TASK_ON_HOME); if (opts != null) { mContext.startActivityAsUser(intent, opts.toBundle(), UserHandle.CURRENT); } else { mContext.startActivityAsUser(intent, UserHandle.CURRENT); } mCanReuseTaskStackViews = true; } /** Sets the RecentsComponent callbacks. */ public void setRecentsComponentCallback(RecentsComponent.Callbacks cb) { sRecentsComponentCallbacks = cb; } /** Notifies the callbacks that the visibility of Recents has changed. */ @ProxyFromAnyToPrimaryUser public static void notifyVisibilityChanged(Context context, SystemServicesProxy ssp, boolean visible) { if (ssp.isForegroundUserOwner()) { visibilityChanged(visible); } else { Intent intent = createLocalBroadcastIntent(context, ACTION_PROXY_NOTIFY_RECENTS_VISIBLITY_TO_OWNER); intent.putExtra(EXTRA_RECENTS_VISIBILITY, visible); context.sendBroadcastAsUser(intent, UserHandle.OWNER); } } static void visibilityChanged(boolean visible) { if (sRecentsComponentCallbacks != null) { sRecentsComponentCallbacks.onVisibilityChanged(visible); } } /** * Returns the preloaded load plan and invalidates it. */ public static RecentsTaskLoadPlan consumeInstanceLoadPlan() { RecentsTaskLoadPlan plan = sInstanceLoadPlan; sInstanceLoadPlan = null; return plan; } /**** OnAnimationStartedListener Implementation ****/ @Override public void onAnimationStarted() { // Notify recents to start the enter animation if (!mStartAnimationTriggered) { // There can be a race condition between the start animation callback and // the start of the new activity (where we register the receiver that listens // to this broadcast, so we add our own receiver and if that gets called, then // we know the activity has not yet started and we can retry sending the broadcast. BroadcastReceiver fallbackReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (getResultCode() == Activity.RESULT_OK) { mStartAnimationTriggered = true; return; } // Schedule for the broadcast to be sent again after some time mHandler.postDelayed(new Runnable() { @Override public void run() { onAnimationStarted(); } }, 25); } }; // Send the broadcast to notify Recents that the animation has started Intent intent = createLocalBroadcastIntent(mContext, ACTION_START_ENTER_ANIMATION); mContext.sendOrderedBroadcastAsUser(intent, UserHandle.CURRENT, null, fallbackReceiver, null, Activity.RESULT_CANCELED, null, null); } } }