blob: 71c21489554b0943b1c8dc069c699feaff2f2a88 [file] [log] [blame]
/*
* Copyright (C) 2016 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.grid;
import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
import android.app.Activity;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.ScaleAnimation;
import android.view.animation.TranslateAnimation;
import android.widget.FrameLayout;
import android.widget.TextView;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.systemui.R;
import com.android.systemui.recents.Recents;
import com.android.systemui.recents.RecentsActivity;
import com.android.systemui.recents.RecentsActivityLaunchState;
import com.android.systemui.recents.RecentsConfiguration;
import com.android.systemui.recents.RecentsImpl;
import com.android.systemui.recents.events.EventBus;
import com.android.systemui.recents.events.activity.ConfigurationChangedEvent;
import com.android.systemui.recents.events.activity.HideRecentsEvent;
import com.android.systemui.recents.events.activity.LaunchNextTaskRequestEvent;
import com.android.systemui.recents.events.activity.LaunchTaskEvent;
import com.android.systemui.recents.events.activity.ToggleRecentsEvent;
import com.android.systemui.recents.events.component.RecentsVisibilityChangedEvent;
import com.android.systemui.recents.events.ui.AllTaskViewsDismissedEvent;
import com.android.systemui.recents.events.ui.DeleteTaskDataEvent;
import com.android.systemui.recents.events.ui.DismissAllTaskViewsEvent;
import com.android.systemui.recents.events.ui.DismissTaskViewEvent;
import com.android.systemui.recents.events.ui.TaskViewDismissedEvent;
import com.android.systemui.recents.misc.SystemServicesProxy;
import com.android.systemui.recents.misc.Utilities;
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.TaskView;
import java.util.ArrayList;
import java.util.Arrays;
/**
* The main grid recents activity started by the RecentsImpl.
*/
public class RecentsGridActivity extends Activity {
public final static int MAX_VISIBLE_TASKS = 9;
private final static String TAG = "RecentsGridActivity";
private final static int TITLE_BAR_HEIGHT_DP = 64;
private ArrayList<Integer> mMargins = new ArrayList<>();
private TaskStack mTaskStack;
private ArrayList<Task> mTasks = new ArrayList<>();
private ArrayList<TaskView> mTaskViews = new ArrayList<>();
private ArrayList<Rect> mTaskViewRects;
private FrameLayout mRecentsView;
private TextView mEmptyView;
private View mClearAllButton;
private int mLastDisplayOrientation = Configuration.ORIENTATION_UNDEFINED;
private int mLastDisplayDensity;
private Rect mDisplayRect = new Rect();
private LayoutInflater mInflater;
private boolean mTouchExplorationEnabled;
private Point mScreenSize;
private int mTitleBarHeightPx;
private int mStatusBarHeightPx;
private int mNavigationBarHeightPx;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.recents_grid);
SystemServicesProxy ssp = Recents.getSystemServices();
Resources res = getResources();
Integer[] margins = {
res.getDimensionPixelSize(R.dimen.recents_grid_margin_left),
res.getDimensionPixelSize(R.dimen.recents_grid_margin_top),
res.getDimensionPixelSize(R.dimen.recents_grid_margin_right),
res.getDimensionPixelSize(R.dimen.recents_grid_margin_bottom),
};
mMargins.addAll(Arrays.asList(margins));
mInflater = LayoutInflater.from(this);
Configuration appConfiguration = Utilities.getAppConfiguration(this);
mDisplayRect = ssp.getDisplayRect();
mLastDisplayOrientation = appConfiguration.orientation;
mLastDisplayDensity = appConfiguration.densityDpi;
mTouchExplorationEnabled = ssp.isTouchExplorationEnabled();
mScreenSize = new Point();
getWindowManager().getDefaultDisplay().getRealSize(mScreenSize);
DisplayMetrics metrics = res.getDisplayMetrics();
mTitleBarHeightPx = (int) (TITLE_BAR_HEIGHT_DP *
((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT));
mStatusBarHeightPx = res.getDimensionPixelSize(R.dimen.status_bar_height);
mNavigationBarHeightPx = res.getDimensionPixelSize(R.dimen.navigation_bar_height);
mRecentsView = (FrameLayout) findViewById(R.id.recents_view);
mRecentsView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
getWindow().getAttributes().privateFlags |=
WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY;
mEmptyView = (TextView) mInflater.inflate(R.layout.recents_empty, mRecentsView, false);
mClearAllButton = findViewById(R.id.button);
FrameLayout.LayoutParams emptyViewLayoutParams = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT);
emptyViewLayoutParams.gravity = Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL;
mEmptyView.setLayoutParams(emptyViewLayoutParams);
mRecentsView.addView(mEmptyView);
mClearAllButton.setVisibility(View.VISIBLE);
FrameLayout.LayoutParams lp =
(FrameLayout.LayoutParams) mClearAllButton.getLayoutParams();
lp.gravity = Gravity.END;
mClearAllButton.setOnClickListener(v -> {
EventBus.getDefault().send(new DismissAllTaskViewsEvent());
});
mRecentsView.setOnClickListener(v -> {
EventBus.getDefault().send(new HideRecentsEvent(
false /* triggeredFromAltTab */, false /* triggeredFromHomeKey */));
});
EventBus.getDefault().register(this, RecentsActivity.EVENT_BUS_PRIORITY);
}
private TaskView createView() {
return (TaskView) mInflater.inflate(R.layout.recents_task_view, mRecentsView, false);
}
private void removeTaskViews() {
for (View taskView : mTaskViews) {
ViewGroup parent = (ViewGroup) taskView.getParent();
if (parent != null) {
parent.removeView(taskView);
}
}
}
private void clearTaskViews() {
removeTaskViews();
mTaskViews.clear();
}
private TaskView getChildViewForTask(Task task) {
for (TaskView tv : mTaskViews) {
if (tv.getTask() == task) {
return tv;
}
}
return null;
}
/**
* Starts animations for each task view to either enlarge it to the size of the screen (when
* launching a task), or (if {@code reverse} is true, to reduce it from the size of the screen
* back to its place in the recents layout (when opening recents).
* @param animationListener An animation listener for executing code before or after the
* animations run.
* @param reverse Whether the blow-up animations should be run in reverse.
*/
private void startBlowUpAnimations(Animation.AnimationListener animationListener,
boolean reverse) {
if (mTaskViews.size() == 0) {
return;
}
int screenWidth = mLastDisplayOrientation == Configuration.ORIENTATION_LANDSCAPE
? mScreenSize.x : mScreenSize.y;
int screenHeight = mLastDisplayOrientation == Configuration.ORIENTATION_LANDSCAPE
? mScreenSize.y : mScreenSize.x;
screenHeight -= mStatusBarHeightPx + mNavigationBarHeightPx;
for (int i = 0; i < mTaskViews.size(); i++) {
View tv = mTaskViews.get(i);
AnimationSet animations = new AnimationSet(true /* shareInterpolator */);
animations.setInterpolator(new DecelerateInterpolator());
if (i == 0 && animationListener != null) {
animations.setAnimationListener(animationListener);
}
animations.setFillBefore(reverse);
animations.setFillAfter(!reverse);
Rect initialRect = mTaskViewRects.get(mTaskViewRects.size() - 1 - i);
int xDelta = - initialRect.left;
int yDelta = - initialRect.top - mTitleBarHeightPx + mStatusBarHeightPx;
TranslateAnimation translate = new TranslateAnimation(
reverse ? xDelta : 0, reverse ? 0 : xDelta,
reverse ? yDelta : 0, reverse ? 0 : yDelta);
translate.setDuration(250);
animations.addAnimation(translate);
float xScale = (float) screenWidth / (float) initialRect.width();
float yScale = (float) screenHeight /
((float) initialRect.height() - mTitleBarHeightPx);
ScaleAnimation scale = new ScaleAnimation(
reverse ? xScale : 1, reverse ? 1 : xScale,
reverse ? yScale : 1, reverse ? 1 : yScale,
Animation.ABSOLUTE, 0, Animation.ABSOLUTE, mStatusBarHeightPx);
scale.setDuration(300);
animations.addAnimation(scale);
tv.startAnimation(animations);
}
}
private void updateControlVisibility() {
boolean empty = (mTasks.size() == 0);
mClearAllButton.setVisibility(empty ? View.INVISIBLE : View.VISIBLE);
mEmptyView.setVisibility(empty ? View.VISIBLE : View.INVISIBLE);
if (empty) {
mEmptyView.bringToFront();
}
}
private void updateModel() {
RecentsTaskLoader loader = Recents.getTaskLoader();
RecentsTaskLoadPlan plan = RecentsImpl.consumeInstanceLoadPlan();
if (plan == null) {
plan = loader.createLoadPlan(this);
}
RecentsConfiguration config = Recents.getConfiguration();
RecentsActivityLaunchState launchState = config.getLaunchState();
if (!plan.hasTasks()) {
loader.preloadTasks(plan, -1, !launchState.launchedFromHome);
}
mTaskStack = plan.getTaskStack();
RecentsTaskLoadPlan.Options loadOpts = new RecentsTaskLoadPlan.Options();
loadOpts.runningTaskId = launchState.launchedToTaskId;
loadOpts.numVisibleTasks = MAX_VISIBLE_TASKS;
loadOpts.numVisibleTaskThumbnails = MAX_VISIBLE_TASKS;
loader.loadTasks(this, plan, loadOpts);
mTasks = mTaskStack.getStackTasks();
}
private void updateViews() {
int screenWidth = mLastDisplayOrientation == Configuration.ORIENTATION_LANDSCAPE
? mScreenSize.x : mScreenSize.y;
int screenHeight = mLastDisplayOrientation == Configuration.ORIENTATION_LANDSCAPE
? mScreenSize.y : mScreenSize.x;
int paddingPixels = getResources().getDimensionPixelSize(
R.dimen.recents_grid_inter_task_padding);
mTaskViewRects = TaskGridLayoutAlgorithm.getRectsForTaskCount(
mTasks.size(), screenWidth, screenHeight, getAppRectRatio(), paddingPixels,
mMargins, mTitleBarHeightPx);
boolean recycleViews = (mTaskViews.size() == mTasks.size());
if (!recycleViews) {
clearTaskViews();
}
for (int i = 0; i < mTasks.size(); i++) {
Task task = mTasks.get(i);
// We keep the same ordering in the model as other Recents flavors (older tasks are
// first in the stack) so that the logic can be similar, but we reverse the order
// when placing views on the screen so that most recent tasks are displayed first.
Rect rect = mTaskViewRects.get(mTaskViewRects.size() - 1 - i);
TaskView taskView;
if (recycleViews) {
taskView = mTaskViews.get(i);
} else {
taskView = createView();
}
taskView.onTaskBound(task, mTouchExplorationEnabled, mLastDisplayOrientation,
mDisplayRect);
Recents.getTaskLoader().loadTaskData(task);
taskView.setTouchEnabled(true);
// Show dismiss button right away.
taskView.startNoUserInteractionAnimation();
taskView.setLayoutParams(new FrameLayout.LayoutParams(rect.width(), rect.height()));
taskView.setTranslationX(rect.left);
taskView.setTranslationY(rect.top);
if (!recycleViews) {
mRecentsView.addView(taskView);
mTaskViews.add(taskView);
}
}
updateControlVisibility();
}
private float getAppRectRatio() {
if (mLastDisplayOrientation == Configuration.ORIENTATION_LANDSCAPE) {
return (float) mScreenSize.x /
(float) (mScreenSize.y - mStatusBarHeightPx - mNavigationBarHeightPx);
} else {
return (float) mScreenSize.y /
(float) (mScreenSize.x - mStatusBarHeightPx - mNavigationBarHeightPx);
}
}
@Override
protected void onStart() {
super.onStart();
EventBus.getDefault().send(new RecentsVisibilityChangedEvent(this, true));
updateModel();
updateViews();
if (mTaskViews.size() > 0) {
mTaskViews.get(mTaskViews.size() - 1).bringToFront();
}
startBlowUpAnimations(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) { }
@Override
public void onAnimationEnd(Animation animation) {
updateViews();
}
@Override
public void onAnimationRepeat(Animation animation) { }
}, true /* reverse */);
}
@Override
protected void onStop() {
super.onStop();
EventBus.getDefault().send(new RecentsVisibilityChangedEvent(this, false));
}
@Override
protected void onDestroy() {
super.onDestroy();
EventBus.getDefault().unregister(this);
}
@Override
public void onBackPressed() {
// Back behaves like the recents button so just trigger a toggle event.
EventBus.getDefault().send(new ToggleRecentsEvent());
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Notify of the config change.
Configuration newDeviceConfiguration = Utilities.getAppConfiguration(this);
mDisplayRect = Recents.getSystemServices().getDisplayRect();
int numStackTasks = mTaskStack.getStackTaskCount();
EventBus.getDefault().send(new ConfigurationChangedEvent(false /* fromMultiWindow */,
mLastDisplayOrientation != newDeviceConfiguration.orientation,
mLastDisplayDensity != newDeviceConfiguration.densityDpi, numStackTasks > 0));
mLastDisplayOrientation = newDeviceConfiguration.orientation;
mLastDisplayDensity = newDeviceConfiguration.densityDpi;
updateViews();
}
void dismissRecentsToHome() {
Intent startMain = new Intent(Intent.ACTION_MAIN);
startMain.addCategory(Intent.CATEGORY_HOME);
startActivity(startMain);
}
/** Launches the task that recents was launched from if possible. */
boolean launchPreviousTask() {
if (mRecentsView != null) {
Task task = mTaskStack.getLaunchTarget();
if (task != null) {
TaskView taskView = getChildViewForTask(task);
EventBus.getDefault().send(new LaunchTaskEvent(taskView, task, null,
INVALID_STACK_ID, false));
return true;
}
}
return false;
}
/** Dismisses recents back to the launch target task. */
boolean dismissRecentsToLaunchTargetTaskOrHome() {
SystemServicesProxy ssp = Recents.getSystemServices();
if (ssp.isRecentsActivityVisible()) {
// If we can launch the task that Recents was launched from, do that, otherwise go home.
if (launchPreviousTask()) return true;
dismissRecentsToHome();
}
return false;
}
/**** EventBus events ****/
public final void onBusEvent(HideRecentsEvent event) {
if (event.triggeredFromAltTab) {
dismissRecentsToLaunchTargetTaskOrHome();
} else if (event.triggeredFromHomeKey) {
dismissRecentsToHome();
} else {
// Fall through tap on the background view but not on any of the tasks.
dismissRecentsToHome();
}
}
public final void onBusEvent(ToggleRecentsEvent event) {
dismissRecentsToLaunchTargetTaskOrHome();
}
public final void onBusEvent(DismissTaskViewEvent event) {
int taskIndex = mTaskViews.indexOf(event.taskView);
if (taskIndex != -1) {
mTasks.remove(taskIndex);
((ViewGroup) event.taskView.getParent()).removeView(event.taskView);
mTaskViews.remove(taskIndex);
EventBus.getDefault().send(
new TaskViewDismissedEvent(event.taskView.getTask(), event.taskView, null));
}
}
public final void onBusEvent(TaskViewDismissedEvent event) {
mRecentsView.announceForAccessibility(this.getString(
R.string.accessibility_recents_item_dismissed, event.task.title));
updateControlVisibility();
EventBus.getDefault().send(new DeleteTaskDataEvent(event.task));
MetricsLogger.action(this, MetricsEvent.OVERVIEW_DISMISS,
event.task.key.getComponent().toString());
}
public final void onBusEvent(DeleteTaskDataEvent event) {
// Remove any stored data from the loader.
RecentsTaskLoader loader = Recents.getTaskLoader();
loader.deleteTaskData(event.task, false);
// Remove the task from activity manager.
SystemServicesProxy ssp = Recents.getSystemServices();
ssp.removeTask(event.task.key.id);
}
public final void onBusEvent(final DismissAllTaskViewsEvent event) {
// Keep track of the tasks which will have their data removed.
ArrayList<Task> tasks = new ArrayList<>(mTaskStack.getStackTasks());
mRecentsView.announceForAccessibility(this.getString(
R.string.accessibility_recents_all_items_dismissed));
mTaskStack.removeAllTasks();
for (int i = tasks.size() - 1; i >= 0; i--) {
EventBus.getDefault().send(new DeleteTaskDataEvent(tasks.get(i)));
}
mTasks = new ArrayList<>();
updateModel();
updateViews();
MetricsLogger.action(this, MetricsEvent.OVERVIEW_DISMISS_ALL);
}
public final void onBusEvent(AllTaskViewsDismissedEvent event) {
SystemServicesProxy ssp = Recents.getSystemServices();
if (!ssp.hasDockedTask()) {
dismissRecentsToHome();
}
}
public final void onBusEvent(LaunchNextTaskRequestEvent event) {
if (mTaskStack.getTaskCount() > 0) {
Task launchTask = mTaskStack.getNextLaunchTarget();
TaskView launchTaskView = getChildViewForTask(launchTask);
if (launchTaskView != null) {
EventBus.getDefault().send(new LaunchTaskEvent(launchTaskView,
launchTask, null, INVALID_STACK_ID, false /* screenPinningRequested */));
MetricsLogger.action(this, MetricsEvent.OVERVIEW_LAUNCH_PREVIOUS_TASK,
launchTask.key.getComponent().toString());
return;
}
}
// We couldn't find a matching task view, or there are no tasks. Just hide recents back
// to home.
EventBus.getDefault().send(new HideRecentsEvent(false, true));
}
public final void onBusEvent(LaunchTaskEvent event) {
event.taskView.bringToFront();
startActivity(event.task.key.baseIntent);
// Eventually we should start blow-up animations here, but we need to make sure it's done
// in parallel with starting the activity so that we don't introduce unneeded latency.
}
}