/* * 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.ActivityOptions; import android.app.SearchManager; import android.appwidget.AppWidgetManager; import android.appwidget.AppWidgetProviderInfo; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.os.SystemClock; import android.os.UserHandle; import android.view.KeyEvent; import android.view.View; import android.view.ViewStub; import android.widget.Toast; import com.android.internal.logging.MetricsConstants; import com.android.internal.logging.MetricsLogger; import com.android.systemui.Prefs; import com.android.systemui.R; import com.android.systemui.recents.misc.Console; import com.android.systemui.recents.misc.DebugTrigger; import com.android.systemui.recents.misc.ReferenceCountedTrigger; 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.TaskStack; import com.android.systemui.recents.views.DebugOverlayView; import com.android.systemui.recents.views.RecentsView; import com.android.systemui.recents.views.SystemBarScrimViews; import com.android.systemui.recents.views.ViewAnimation; import java.lang.ref.WeakReference; import java.util.ArrayList; /** * The main Recents activity that is started from AlternateRecentsComponent. */ public class RecentsActivity extends Activity implements RecentsView.RecentsViewCallbacks, RecentsAppWidgetHost.RecentsAppWidgetHostCallbacks, DebugOverlayView.DebugOverlayViewCallbacks { RecentsConfiguration mConfig; long mLastTabKeyEventTime; // Top level views RecentsView mRecentsView; SystemBarScrimViews mScrimViews; ViewStub mEmptyViewStub; ViewStub mDebugOverlayStub; View mEmptyView; DebugOverlayView mDebugOverlay; // Resize task debug RecentsResizeTaskDialog mResizeTaskDebugDialog; // Search AppWidget AppWidgetProviderInfo mSearchWidgetInfo; RecentsAppWidgetHost mAppWidgetHost; RecentsAppWidgetHostView mSearchWidgetHostView; // Runnables to finish the Recents activity FinishRecentsRunnable mFinishLaunchHomeRunnable; // Runnable to be executed after we paused ourselves Runnable mAfterPauseRunnable; /** * A common Runnable to finish Recents either by calling finish() (with a custom animation) or * launching Home with some ActivityOptions. Generally we always launch home when we exit * Recents rather than just finishing the activity since we don't know what is behind Recents in * the task stack. The only case where we finish() directly is when we are cancelling the full * screen transition from the app. */ class FinishRecentsRunnable implements Runnable { Intent mLaunchIntent; ActivityOptions mLaunchOpts; /** * Creates a finish runnable that starts the specified intent, using the given * ActivityOptions. */ public FinishRecentsRunnable(Intent launchIntent, ActivityOptions opts) { mLaunchIntent = launchIntent; mLaunchOpts = opts; } @Override public void run() { // Finish Recents if (mLaunchIntent != null) { try { if (mLaunchOpts != null) { startActivityAsUser(mLaunchIntent, mLaunchOpts.toBundle(), UserHandle.CURRENT); } else { startActivityAsUser(mLaunchIntent, UserHandle.CURRENT); } } catch (Exception e) { Console.logError(RecentsActivity.this, getString(R.string.recents_launch_error_message, "Home")); } } else { finish(); overridePendingTransition(R.anim.recents_to_launcher_enter, R.anim.recents_to_launcher_exit); } } } /** * Broadcast receiver to handle messages from AlternateRecentsComponent. */ final BroadcastReceiver mServiceBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (action.equals(Recents.ACTION_HIDE_RECENTS_ACTIVITY)) { if (intent.getBooleanExtra(Recents.EXTRA_TRIGGERED_FROM_ALT_TAB, false)) { // If we are hiding from releasing Alt-Tab, dismiss Recents to the focused app dismissRecentsToFocusedTaskOrHome(false); } else if (intent.getBooleanExtra(Recents.EXTRA_TRIGGERED_FROM_HOME_KEY, false)) { // Otherwise, dismiss Recents to Home dismissRecentsToHomeRaw(true); } else { // Do nothing } } else if (action.equals(Recents.ACTION_TOGGLE_RECENTS_ACTIVITY)) { // If we are toggling Recents, then first unfilter any filtered stacks first dismissRecentsToFocusedTaskOrHome(true); } else if (action.equals(Recents.ACTION_START_ENTER_ANIMATION)) { // Trigger the enter animation onEnterAnimationTriggered(); // Notify the fallback receiver that we have successfully got the broadcast // See AlternateRecentsComponent.onAnimationStarted() setResultCode(Activity.RESULT_OK); } } }; /** * Broadcast receiver to handle messages from the system */ final BroadcastReceiver mSystemBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (action.equals(Intent.ACTION_SCREEN_OFF)) { // When the screen turns off, dismiss Recents to Home dismissRecentsToHome(false); } else if (action.equals(SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED)) { // When the search activity changes, update the search widget view SystemServicesProxy ssp = RecentsTaskLoader.getInstance().getSystemServicesProxy(); mSearchWidgetInfo = ssp.getOrBindSearchAppWidget(context, mAppWidgetHost); refreshSearchWidgetView(); } } }; /** * A custom debug trigger to listen for a debug key chord. */ final DebugTrigger mDebugTrigger = new DebugTrigger(new Runnable() { @Override public void run() { onDebugModeTriggered(); } }); /** Updates the set of recent tasks */ void updateRecentsTasks() { // If AlternateRecentsComponent has preloaded a load plan, then use that to prevent // reconstructing the task stack RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); RecentsTaskLoadPlan plan = Recents.consumeInstanceLoadPlan(); if (plan == null) { plan = loader.createLoadPlan(this); } // Start loading tasks according to the load plan if (!plan.hasTasks()) { loader.preloadTasks(plan, mConfig.launchedFromHome); } RecentsTaskLoadPlan.Options loadOpts = new RecentsTaskLoadPlan.Options(); loadOpts.runningTaskId = mConfig.launchedToTaskId; loadOpts.numVisibleTasks = mConfig.launchedNumVisibleTasks; loadOpts.numVisibleTaskThumbnails = mConfig.launchedNumVisibleThumbnails; loader.loadTasks(this, plan, loadOpts); ArrayList stacks = plan.getAllTaskStacks(); mConfig.launchedWithNoRecentTasks = !plan.hasTasks(); if (!mConfig.launchedWithNoRecentTasks) { mRecentsView.setTaskStacks(stacks); } // Create the home intent runnable Intent homeIntent = new Intent(Intent.ACTION_MAIN, null); homeIntent.addCategory(Intent.CATEGORY_HOME); homeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); mFinishLaunchHomeRunnable = new FinishRecentsRunnable(homeIntent, ActivityOptions.makeCustomAnimation(this, mConfig.launchedFromSearchHome ? R.anim.recents_to_search_launcher_enter : R.anim.recents_to_launcher_enter, mConfig.launchedFromSearchHome ? R.anim.recents_to_search_launcher_exit : R.anim.recents_to_launcher_exit)); // Mark the task that is the launch target int taskStackCount = stacks.size(); int launchTaskIndexInStack = 0; if (mConfig.launchedToTaskId != -1) { for (int i = 0; i < taskStackCount; i++) { TaskStack stack = stacks.get(i); ArrayList tasks = stack.getTasks(); int taskCount = tasks.size(); for (int j = 0; j < taskCount; j++) { Task t = tasks.get(j); if (t.key.id == mConfig.launchedToTaskId) { t.isLaunchTarget = true; launchTaskIndexInStack = tasks.size() - j - 1; break; } } } } // Update the top level view's visibilities if (mConfig.launchedWithNoRecentTasks) { if (mEmptyView == null) { mEmptyView = mEmptyViewStub.inflate(); } mEmptyView.setVisibility(View.VISIBLE); mRecentsView.setSearchBarVisibility(View.GONE); } else { if (mEmptyView != null) { mEmptyView.setVisibility(View.GONE); } if (mRecentsView.hasValidSearchBar()) { mRecentsView.setSearchBarVisibility(View.VISIBLE); } else { refreshSearchWidgetView(); } } // Animate the SystemUI scrims into view mScrimViews.prepareEnterRecentsAnimation(); // Keep track of whether we launched from the nav bar button or via alt-tab if (mConfig.launchedWithAltTab) { MetricsLogger.count(this, "overview_trigger_alttab", 1); } else { MetricsLogger.count(this, "overview_trigger_nav_btn", 1); } // Keep track of whether we launched from an app or from home if (mConfig.launchedFromAppWithThumbnail) { MetricsLogger.count(this, "overview_source_app", 1); // If from an app, track the stack index of the app in the stack (for affiliated tasks) MetricsLogger.histogram(this, "overview_source_app_index", launchTaskIndexInStack); } else { MetricsLogger.count(this, "overview_source_home", 1); } // Keep track of the total stack task count int taskCount = 0; for (int i = 0; i < stacks.size(); i++) { TaskStack stack = stacks.get(i); taskCount += stack.getTaskCount(); } MetricsLogger.histogram(this, "overview_task_count", taskCount); } /** Dismisses recents if we are already visible and the intent is to toggle the recents view */ boolean dismissRecentsToFocusedTaskOrHome(boolean checkFilteredStackState) { SystemServicesProxy ssp = RecentsTaskLoader.getInstance().getSystemServicesProxy(); if (ssp.isRecentsTopMost(ssp.getTopMostTask(), null)) { // If we currently have filtered stacks, then unfilter those first if (checkFilteredStackState && mRecentsView.unfilterFilteredStacks()) return true; // If we have a focused Task, launch that Task now if (mRecentsView.launchFocusedTask()) return true; // If we launched from Home, then return to Home if (mConfig.launchedFromHome) { dismissRecentsToHomeRaw(true); return true; } // Otherwise, try and return to the Task that Recents was launched from if (mRecentsView.launchPreviousTask()) return true; // If none of the other cases apply, then just go Home dismissRecentsToHomeRaw(true); return true; } return false; } /** Dismisses Recents directly to Home. */ void dismissRecentsToHomeRaw(boolean animated) { if (animated) { ReferenceCountedTrigger exitTrigger = new ReferenceCountedTrigger(this, null, mFinishLaunchHomeRunnable, null); mRecentsView.startExitToHomeAnimation( new ViewAnimation.TaskViewExitContext(exitTrigger)); } else { mFinishLaunchHomeRunnable.run(); } } /** Dismisses Recents directly to Home without transition animation. */ void dismissRecentsToHomeWithoutTransitionAnimation() { finish(); overridePendingTransition(0, 0); } /** Dismisses Recents directly to Home if we currently aren't transitioning. */ boolean dismissRecentsToHome(boolean animated) { SystemServicesProxy ssp = RecentsTaskLoader.getInstance().getSystemServicesProxy(); if (ssp.isRecentsTopMost(ssp.getTopMostTask(), null)) { // Return to Home dismissRecentsToHomeRaw(animated); return true; } return false; } /** Called with the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // For the non-primary user, ensure that the SystemServicesProxy and configuration is // initialized RecentsTaskLoader.initialize(this); SystemServicesProxy ssp = RecentsTaskLoader.getInstance().getSystemServicesProxy(); mConfig = RecentsConfiguration.reinitialize(this, ssp); // Initialize the widget host (the host id is static and does not change) mAppWidgetHost = new RecentsAppWidgetHost(this, Constants.Values.App.AppWidgetHostId); // Set the Recents layout setContentView(R.layout.recents); mRecentsView = (RecentsView) findViewById(R.id.recents_view); mRecentsView.setCallbacks(this); mRecentsView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); mEmptyViewStub = (ViewStub) findViewById(R.id.empty_view_stub); mDebugOverlayStub = (ViewStub) findViewById(R.id.debug_overlay_stub); mScrimViews = new SystemBarScrimViews(this, mConfig); inflateDebugOverlay(); // Bind the search app widget when we first start up mSearchWidgetInfo = ssp.getOrBindSearchAppWidget(this, mAppWidgetHost); // Register the broadcast receiver to handle messages when the screen is turned off IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_SCREEN_OFF); filter.addAction(SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED); registerReceiver(mSystemBroadcastReceiver, filter); } /** Inflates the debug overlay if debug mode is enabled. */ void inflateDebugOverlay() { if (!Constants.DebugFlags.App.EnableDebugMode) return; if (mConfig.debugModeEnabled && mDebugOverlay == null) { // Inflate the overlay and seek bars mDebugOverlay = (DebugOverlayView) mDebugOverlayStub.inflate(); mDebugOverlay.setCallbacks(this); mRecentsView.setDebugOverlay(mDebugOverlay); } } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); setIntent(intent); // Clear any debug rects if (mDebugOverlay != null) { mDebugOverlay.clear(); } } @Override protected void onStart() { super.onStart(); MetricsLogger.visible(this, MetricsLogger.OVERVIEW_ACTIVITY); RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); SystemServicesProxy ssp = loader.getSystemServicesProxy(); Recents.notifyVisibilityChanged(this, ssp, true); // Register the broadcast receiver to handle messages from our service IntentFilter filter = new IntentFilter(); filter.addAction(Recents.ACTION_HIDE_RECENTS_ACTIVITY); filter.addAction(Recents.ACTION_TOGGLE_RECENTS_ACTIVITY); filter.addAction(Recents.ACTION_START_ENTER_ANIMATION); registerReceiver(mServiceBroadcastReceiver, filter); // Register any broadcast receivers for the task loader loader.registerReceivers(this, mRecentsView); // Update the recent tasks updateRecentsTasks(); // If this is a new instance from a configuration change, then we have to manually trigger // the enter animation state, or if recents was relaunched by AM, without going through // the normal mechanisms boolean wasLaunchedByAm = !mConfig.launchedFromHome && !mConfig.launchedFromAppWithThumbnail; if (mConfig.launchedHasConfigurationChanged || wasLaunchedByAm) { onEnterAnimationTriggered(); } if (!mConfig.launchedHasConfigurationChanged) { mRecentsView.disableLayersForOneFrame(); } } @Override protected void onPause() { super.onPause(); if (mAfterPauseRunnable != null) { mRecentsView.post(mAfterPauseRunnable); mAfterPauseRunnable = null; } } @Override protected void onStop() { super.onStop(); MetricsLogger.hidden(this, MetricsLogger.OVERVIEW_ACTIVITY); RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); SystemServicesProxy ssp = loader.getSystemServicesProxy(); Recents.notifyVisibilityChanged(this, ssp, false); // Notify the views that we are no longer visible mRecentsView.onRecentsHidden(); // Unregister the RecentsService receiver unregisterReceiver(mServiceBroadcastReceiver); // Unregister any broadcast receivers for the task loader loader.unregisterReceivers(); // Workaround for b/22542869, if the RecentsActivity is started again, but without going // through SystemUI, we need to reset the config launch flags to ensure that we do not // wait on the system to send a signal that was never queued. mConfig.launchedFromHome = false; mConfig.launchedFromSearchHome = false; mConfig.launchedFromAppWithThumbnail = false; mConfig.launchedToTaskId = -1; mConfig.launchedWithAltTab = false; mConfig.launchedHasConfigurationChanged = false; } @Override protected void onDestroy() { super.onDestroy(); // Unregister the system broadcast receivers unregisterReceiver(mSystemBroadcastReceiver); // Stop listening for widget package changes if there was one bound mAppWidgetHost.stopListening(); } public void onEnterAnimationTriggered() { // Try and start the enter animation (or restart it on configuration changed) ReferenceCountedTrigger t = new ReferenceCountedTrigger(this, null, null, null); ViewAnimation.TaskViewEnterContext ctx = new ViewAnimation.TaskViewEnterContext(t); mRecentsView.startEnterRecentsAnimation(ctx); if (mSearchWidgetInfo != null) { final WeakReference cbRef = new WeakReference( RecentsActivity.this); ctx.postAnimationTrigger.addLastDecrementRunnable(new Runnable() { @Override public void run() { // Start listening for widget package changes if there is one bound RecentsAppWidgetHost.RecentsAppWidgetHostCallbacks cb = cbRef.get(); if (cb != null) { mAppWidgetHost.startListening(cb); } } }); } // Animate the SystemUI scrim views mScrimViews.startEnterRecentsAnimation(); } @Override public void onTrimMemory(int level) { RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); if (loader != null) { loader.onTrimMemory(level); } } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_TAB: { boolean hasRepKeyTimeElapsed = (SystemClock.elapsedRealtime() - mLastTabKeyEventTime) > mConfig.altTabKeyDelay; if (event.getRepeatCount() <= 0 || hasRepKeyTimeElapsed) { // Focus the next task in the stack final boolean backward = event.isShiftPressed(); mRecentsView.focusNextTask(!backward); mLastTabKeyEventTime = SystemClock.elapsedRealtime(); } return true; } case KeyEvent.KEYCODE_DPAD_UP: { mRecentsView.focusNextTask(true); return true; } case KeyEvent.KEYCODE_DPAD_DOWN: { mRecentsView.focusNextTask(false); return true; } case KeyEvent.KEYCODE_DEL: case KeyEvent.KEYCODE_FORWARD_DEL: { mRecentsView.dismissFocusedTask(); // Keep track of deletions by keyboard MetricsLogger.histogram(this, "overview_task_dismissed_source", Constants.Metrics.DismissSourceKeyboard); return true; } default: break; } // Pass through the debug trigger mDebugTrigger.onKeyEvent(keyCode); return super.onKeyDown(keyCode, event); } @Override public void onUserInteraction() { mRecentsView.onUserInteraction(); } @Override public void onBackPressed() { // Test mode where back does not do anything if (mConfig.debugModeEnabled) return; // Dismiss Recents to the focused Task or Home dismissRecentsToFocusedTaskOrHome(true); } /** Called when debug mode is triggered */ public void onDebugModeTriggered() { if (mConfig.developerOptionsEnabled) { if (Prefs.getBoolean(this, Prefs.Key.DEBUG_MODE_ENABLED, false /* boolean */)) { // Disable the debug mode Prefs.remove(this, Prefs.Key.DEBUG_MODE_ENABLED); mConfig.debugModeEnabled = false; inflateDebugOverlay(); if (mDebugOverlay != null) { mDebugOverlay.disable(); } } else { // Enable the debug mode Prefs.putBoolean(this, Prefs.Key.DEBUG_MODE_ENABLED, true); mConfig.debugModeEnabled = true; inflateDebugOverlay(); if (mDebugOverlay != null) { mDebugOverlay.enable(); } } Toast.makeText(this, "Debug mode (" + Constants.Values.App.DebugModeVersion + ") " + (mConfig.debugModeEnabled ? "Enabled" : "Disabled") + ", please restart Recents now", Toast.LENGTH_SHORT).show(); } } /**** RecentsResizeTaskDialog ****/ private RecentsResizeTaskDialog getResizeTaskDebugDialog() { if (mResizeTaskDebugDialog == null) { mResizeTaskDebugDialog = new RecentsResizeTaskDialog(getFragmentManager(), this); } return mResizeTaskDebugDialog; } @Override public void onTaskResize(Task t) { getResizeTaskDebugDialog().showResizeTaskDialog(t, mRecentsView); } /**** RecentsView.RecentsViewCallbacks Implementation ****/ @Override public void onExitToHomeAnimationTriggered() { // Animate the SystemUI scrim views out mScrimViews.startExitRecentsAnimation(); } @Override public void onTaskViewClicked() { } @Override public void onTaskLaunchFailed() { // Return to Home dismissRecentsToHomeRaw(true); } @Override public void onAllTaskViewsDismissed() { mFinishLaunchHomeRunnable.run(); } @Override public void onScreenPinningRequest() { RecentsTaskLoader loader = RecentsTaskLoader.getInstance(); SystemServicesProxy ssp = loader.getSystemServicesProxy(); Recents.startScreenPinning(this, ssp); MetricsLogger.count(this, "overview_screen_pinned", 1); } @Override public void runAfterPause(Runnable r) { mAfterPauseRunnable = r; } /**** RecentsAppWidgetHost.RecentsAppWidgetHostCallbacks Implementation ****/ @Override public void refreshSearchWidgetView() { if (mSearchWidgetInfo != null) { SystemServicesProxy ssp = RecentsTaskLoader.getInstance().getSystemServicesProxy(); int searchWidgetId = ssp.getSearchAppWidgetId(this); mSearchWidgetHostView = (RecentsAppWidgetHostView) mAppWidgetHost.createView( this, searchWidgetId, mSearchWidgetInfo); Bundle opts = new Bundle(); opts.putInt(AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, AppWidgetProviderInfo.WIDGET_CATEGORY_SEARCHBOX); mSearchWidgetHostView.updateAppWidgetOptions(opts); // Set the padding to 0 for this search widget mSearchWidgetHostView.setPadding(0, 0, 0, 0); mRecentsView.setSearchBar(mSearchWidgetHostView); } else { mRecentsView.setSearchBar(null); } } /**** DebugOverlayView.DebugOverlayViewCallbacks ****/ @Override public void onPrimarySeekBarChanged(float progress) { // Do nothing } @Override public void onSecondarySeekBarChanged(float progress) { // Do nothing } }