blob: 67685b8737ce73529cb4651ebfad9724655e7a7d [file] [log] [blame]
/*
* 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.views;
import static android.app.ActivityManager.StackId.ASSISTANT_STACK_ID;
import static android.app.ActivityManager.StackId.DOCKED_STACK_ID;
import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
import android.annotation.Nullable;
import android.app.ActivityManager.StackId;
import android.app.ActivityOptions;
import android.app.ActivityOptions.OnAnimationStartedListener;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.GraphicBuffer;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Handler;
import android.os.IRemoteCallback;
import android.os.RemoteException;
import android.util.Log;
import android.view.AppTransitionAnimationSpec;
import android.view.DisplayListCanvas;
import android.view.IAppTransitionAnimationSpecsFuture;
import android.view.RenderNode;
import android.view.ThreadedRenderer;
import android.view.View;
import com.android.internal.annotations.GuardedBy;
import com.android.systemui.recents.Recents;
import com.android.systemui.recents.RecentsDebugFlags;
import com.android.systemui.recents.events.EventBus;
import com.android.systemui.recents.events.activity.CancelEnterRecentsWindowAnimationEvent;
import com.android.systemui.recents.events.activity.ExitRecentsWindowFirstAnimationFrameEvent;
import com.android.systemui.recents.events.activity.LaunchTaskFailedEvent;
import com.android.systemui.recents.events.activity.LaunchTaskStartedEvent;
import com.android.systemui.recents.events.activity.LaunchTaskSucceededEvent;
import com.android.systemui.recents.events.component.ScreenPinningRequestEvent;
import com.android.systemui.recents.events.component.SetWaitingForTransitionStartEvent;
import com.android.systemui.recents.misc.SystemServicesProxy;
import com.android.systemui.recents.model.Task;
import com.android.systemui.recents.model.TaskStack;
import com.android.systemui.statusbar.phone.StatusBar;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* A helper class to create transitions to/from Recents
*/
public class RecentsTransitionHelper {
private static final String TAG = "RecentsTransitionHelper";
private static final boolean DEBUG = false;
/**
* Special value for {@link #mAppTransitionAnimationSpecs}: Indicate that we are currently
* waiting for the specs to be retrieved.
*/
private static final List<AppTransitionAnimationSpec> SPECS_WAITING = new ArrayList<>();
@GuardedBy("this")
private List<AppTransitionAnimationSpec> mAppTransitionAnimationSpecs = SPECS_WAITING;
private Context mContext;
private Handler mHandler;
private TaskViewTransform mTmpTransform = new TaskViewTransform();
private class StartScreenPinningRunnableRunnable implements Runnable {
private int taskId = -1;
@Override
public void run() {
EventBus.getDefault().send(new ScreenPinningRequestEvent(mContext, taskId));
}
}
private StartScreenPinningRunnableRunnable mStartScreenPinningRunnable
= new StartScreenPinningRunnableRunnable();
public RecentsTransitionHelper(Context context) {
mContext = context;
mHandler = new Handler();
}
/**
* Launches the specified {@link Task}.
*/
public void launchTaskFromRecents(final TaskStack stack, @Nullable final Task task,
final TaskStackView stackView, final TaskView taskView,
final boolean screenPinningRequested, final int destinationStack) {
final ActivityOptions.OnAnimationStartedListener animStartedListener;
final AppTransitionAnimationSpecsFuture transitionFuture;
if (taskView != null) {
// Fetch window rect here already in order not to be blocked on lock contention in WM
// when the future calls it.
final Rect windowRect = Recents.getSystemServices().getWindowRect();
transitionFuture = getAppTransitionFuture(
() -> composeAnimationSpecs(task, stackView, destinationStack, windowRect));
animStartedListener = new OnAnimationStartedListener() {
private boolean mHandled;
@Override
public void onAnimationStarted() {
if (mHandled) {
return;
}
mHandled = true;
// If we are launching into another task, cancel the previous task's
// window transition
EventBus.getDefault().send(new CancelEnterRecentsWindowAnimationEvent(task));
EventBus.getDefault().send(new ExitRecentsWindowFirstAnimationFrameEvent());
stackView.cancelAllTaskViewAnimations();
if (screenPinningRequested) {
// Request screen pinning after the animation runs
mStartScreenPinningRunnable.taskId = task.key.id;
mHandler.postDelayed(mStartScreenPinningRunnable, 350);
}
// Reset the state where we are waiting for the transition to start
EventBus.getDefault().send(new SetWaitingForTransitionStartEvent(false));
}
};
} else {
// This is only the case if the task is not on screen (scrolled offscreen for example)
transitionFuture = null;
animStartedListener = new OnAnimationStartedListener() {
private boolean mHandled;
@Override
public void onAnimationStarted() {
if (mHandled) {
return;
}
mHandled = true;
// If we are launching into another task, cancel the previous task's
// window transition
EventBus.getDefault().send(new CancelEnterRecentsWindowAnimationEvent(task));
EventBus.getDefault().send(new ExitRecentsWindowFirstAnimationFrameEvent());
stackView.cancelAllTaskViewAnimations();
// Reset the state where we are waiting for the transition to start
EventBus.getDefault().send(new SetWaitingForTransitionStartEvent(false));
}
};
}
EventBus.getDefault().send(new SetWaitingForTransitionStartEvent(true));
final ActivityOptions opts = ActivityOptions.makeMultiThumbFutureAspectScaleAnimation(mContext,
mHandler, transitionFuture != null ? transitionFuture.future : null,
animStartedListener, true /* scaleUp */);
if (taskView == null) {
// If there is no task view, then we do not need to worry about animating out occluding
// task views, and we can launch immediately
startTaskActivity(stack, task, taskView, opts, transitionFuture, destinationStack);
} else {
LaunchTaskStartedEvent launchStartedEvent = new LaunchTaskStartedEvent(taskView,
screenPinningRequested);
if (task.group != null && !task.group.isFrontMostTask(task)) {
launchStartedEvent.addPostAnimationCallback(new Runnable() {
@Override
public void run() {
startTaskActivity(stack, task, taskView, opts, transitionFuture,
destinationStack);
}
});
EventBus.getDefault().send(launchStartedEvent);
} else {
EventBus.getDefault().send(launchStartedEvent);
startTaskActivity(stack, task, taskView, opts, transitionFuture, destinationStack);
}
}
Recents.getSystemServices().sendCloseSystemWindows(
StatusBar.SYSTEM_DIALOG_REASON_HOME_KEY);
}
public IRemoteCallback wrapStartedListener(final OnAnimationStartedListener listener) {
if (listener == null) {
return null;
}
return new IRemoteCallback.Stub() {
@Override
public void sendResult(Bundle data) throws RemoteException {
mHandler.post(new Runnable() {
@Override
public void run() {
listener.onAnimationStarted();
}
});
}
};
}
/**
* Starts the activity for the launch task.
*
* @param taskView this is the {@link TaskView} that we are launching from. This can be null if
* we are toggling recents and the launch-to task is now offscreen.
* @param destinationStack id of the stack to put the task into.
*/
private void startTaskActivity(TaskStack stack, Task task, @Nullable TaskView taskView,
ActivityOptions opts, AppTransitionAnimationSpecsFuture transitionFuture,
int destinationStack) {
SystemServicesProxy ssp = Recents.getSystemServices();
ssp.startActivityFromRecents(mContext, task.key, task.title, opts, destinationStack,
succeeded -> {
if (succeeded) {
// Keep track of the index of the task launch
int taskIndexFromFront = 0;
int taskIndex = stack.indexOfStackTask(task);
if (taskIndex > -1) {
taskIndexFromFront = stack.getTaskCount() - taskIndex - 1;
}
EventBus.getDefault().send(new LaunchTaskSucceededEvent(taskIndexFromFront));
} else {
// Dismiss the task if we fail to launch it
if (taskView != null) {
taskView.dismissTask();
}
// Keep track of failed launches
EventBus.getDefault().send(new LaunchTaskFailedEvent());
}
});
if (transitionFuture != null) {
mHandler.post(transitionFuture::precacheSpecs);
}
}
/**
* Creates a future which will later be queried for animation specs for this current transition.
*
* @param composer The implementation that composes the specs on the UI thread.
*/
public AppTransitionAnimationSpecsFuture getAppTransitionFuture(
final AnimationSpecComposer composer) {
synchronized (this) {
mAppTransitionAnimationSpecs = SPECS_WAITING;
}
IAppTransitionAnimationSpecsFuture future = new IAppTransitionAnimationSpecsFuture.Stub() {
@Override
public AppTransitionAnimationSpec[] get() throws RemoteException {
mHandler.post(() -> {
synchronized (RecentsTransitionHelper.this) {
mAppTransitionAnimationSpecs = composer.composeSpecs();
RecentsTransitionHelper.this.notifyAll();
}
});
synchronized (RecentsTransitionHelper.this) {
while (mAppTransitionAnimationSpecs == SPECS_WAITING) {
try {
RecentsTransitionHelper.this.wait();
} catch (InterruptedException e) {}
}
if (mAppTransitionAnimationSpecs == null) {
return null;
}
AppTransitionAnimationSpec[] specs
= new AppTransitionAnimationSpec[mAppTransitionAnimationSpecs.size()];
mAppTransitionAnimationSpecs.toArray(specs);
mAppTransitionAnimationSpecs = SPECS_WAITING;
return specs;
}
}
};
return new AppTransitionAnimationSpecsFuture(composer, future);
}
/**
* Composes the transition spec when docking a task, which includes a full task bitmap.
*/
public List<AppTransitionAnimationSpec> composeDockAnimationSpec(TaskView taskView,
Rect bounds) {
mTmpTransform.fillIn(taskView);
Task task = taskView.getTask();
GraphicBuffer buffer = RecentsTransitionHelper.composeTaskBitmap(taskView, mTmpTransform);
return Collections.singletonList(new AppTransitionAnimationSpec(task.key.id, buffer,
bounds));
}
/**
* Composes the animation specs for all the tasks in the target stack.
*/
private List<AppTransitionAnimationSpec> composeAnimationSpecs(final Task task,
final TaskStackView stackView, final int destinationStack, Rect windowRect) {
// Ensure we have a valid target stack id
final int targetStackId = destinationStack != INVALID_STACK_ID ?
destinationStack : task.key.stackId;
if (!StackId.useAnimationSpecForAppTransition(targetStackId)) {
return null;
}
// Calculate the offscreen task rect (for tasks that are not backed by views)
TaskView taskView = stackView.getChildViewForTask(task);
TaskStackLayoutAlgorithm stackLayout = stackView.getStackAlgorithm();
Rect offscreenTaskRect = new Rect();
stackLayout.getFrontOfStackTransform().rect.round(offscreenTaskRect);
// If this is a full screen stack, the transition will be towards the single, full screen
// task. We only need the transition spec for this task.
List<AppTransitionAnimationSpec> specs = new ArrayList<>();
// TODO: Sometimes targetStackId is not initialized after reboot, so we also have to
// check for INVALID_STACK_ID
if (targetStackId == FULLSCREEN_WORKSPACE_STACK_ID || targetStackId == DOCKED_STACK_ID
|| targetStackId == ASSISTANT_STACK_ID || targetStackId == INVALID_STACK_ID) {
if (taskView == null) {
specs.add(composeOffscreenAnimationSpec(task, offscreenTaskRect));
} else {
mTmpTransform.fillIn(taskView);
stackLayout.transformToScreenCoordinates(mTmpTransform, windowRect);
AppTransitionAnimationSpec spec = composeAnimationSpec(stackView, taskView,
mTmpTransform, true /* addHeaderBitmap */);
if (spec != null) {
specs.add(spec);
}
}
return specs;
}
// Otherwise, for freeform tasks, create a new animation spec for each task we have to
// launch
TaskStack stack = stackView.getStack();
ArrayList<Task> tasks = stack.getStackTasks();
int taskCount = tasks.size();
for (int i = taskCount - 1; i >= 0; i--) {
Task t = tasks.get(i);
if (t.isFreeformTask() || targetStackId == FREEFORM_WORKSPACE_STACK_ID) {
TaskView tv = stackView.getChildViewForTask(t);
if (tv == null) {
// TODO: Create a different animation task rect for this case (though it should
// never happen)
specs.add(composeOffscreenAnimationSpec(t, offscreenTaskRect));
} else {
mTmpTransform.fillIn(taskView);
stackLayout.transformToScreenCoordinates(mTmpTransform,
null /* windowOverrideRect */);
AppTransitionAnimationSpec spec = composeAnimationSpec(stackView, tv,
mTmpTransform, true /* addHeaderBitmap */);
if (spec != null) {
specs.add(spec);
}
}
}
}
return specs;
}
/**
* Composes a single animation spec for the given {@link Task}
*/
private static AppTransitionAnimationSpec composeOffscreenAnimationSpec(Task task,
Rect taskRect) {
return new AppTransitionAnimationSpec(task.key.id, null, taskRect);
}
public static GraphicBuffer composeTaskBitmap(TaskView taskView, TaskViewTransform transform) {
float scale = transform.scale;
int fromWidth = (int) (transform.rect.width() * scale);
int fromHeight = (int) (transform.rect.height() * scale);
if (fromWidth == 0 || fromHeight == 0) {
Log.e(TAG, "Could not compose thumbnail for task: " + taskView.getTask() +
" at transform: " + transform);
return drawViewIntoGraphicBuffer(1, 1, null, 1f, 0x00ffffff);
} else {
if (RecentsDebugFlags.Static.EnableTransitionThumbnailDebugMode) {
return drawViewIntoGraphicBuffer(fromWidth, fromHeight, null, 1f, 0xFFff0000);
} else {
return drawViewIntoGraphicBuffer(fromWidth, fromHeight, taskView, scale, 0);
}
}
}
private static GraphicBuffer composeHeaderBitmap(TaskView taskView,
TaskViewTransform transform) {
float scale = transform.scale;
int headerWidth = (int) (transform.rect.width());
int headerHeight = (int) (taskView.mHeaderView.getMeasuredHeight() * scale);
if (headerWidth == 0 || headerHeight == 0) {
return null;
}
if (RecentsDebugFlags.Static.EnableTransitionThumbnailDebugMode) {
return drawViewIntoGraphicBuffer(headerWidth, headerHeight, null, 1f, 0xFFff0000);
} else {
return drawViewIntoGraphicBuffer(headerWidth, headerHeight, taskView.mHeaderView,
scale, 0);
}
}
public static GraphicBuffer drawViewIntoGraphicBuffer(int bufferWidth, int bufferHeight,
View view, float scale, int eraseColor) {
RenderNode node = RenderNode.create("RecentsTransition", null);
node.setLeftTopRightBottom(0, 0, bufferWidth, bufferHeight);
node.setClipToBounds(false);
DisplayListCanvas c = node.start(bufferWidth, bufferHeight);
c.scale(scale, scale);
if (eraseColor != 0) {
c.drawColor(eraseColor);
}
if (view != null) {
view.draw(c);
}
node.end(c);
Bitmap hwBitmap = ThreadedRenderer.createHardwareBitmap(node, bufferWidth, bufferHeight);
return hwBitmap.createGraphicBufferHandle();
}
/**
* Composes a single animation spec for the given {@link TaskView}
*/
private static AppTransitionAnimationSpec composeAnimationSpec(TaskStackView stackView,
TaskView taskView, TaskViewTransform transform, boolean addHeaderBitmap) {
GraphicBuffer b = null;
if (addHeaderBitmap) {
b = composeHeaderBitmap(taskView, transform);
if (b == null) {
return null;
}
}
Rect taskRect = new Rect();
transform.rect.round(taskRect);
if (stackView.getStack().getStackFrontMostTask(false /* includeFreeformTasks */) !=
taskView.getTask()) {
taskRect.bottom = taskRect.top + stackView.getMeasuredHeight();
}
return new AppTransitionAnimationSpec(taskView.getTask().key.id, b, taskRect);
}
public interface AnimationSpecComposer {
List<AppTransitionAnimationSpec> composeSpecs();
}
/**
* Class to be returned from {@link #composeAnimationSpec} that gives access to both the future
* and the anonymous class used for composing.
*/
public class AppTransitionAnimationSpecsFuture {
private final AnimationSpecComposer composer;
private final IAppTransitionAnimationSpecsFuture future;
private AppTransitionAnimationSpecsFuture(AnimationSpecComposer composer,
IAppTransitionAnimationSpecsFuture future) {
this.composer = composer;
this.future = future;
}
public IAppTransitionAnimationSpecsFuture getFuture() {
return future;
}
/**
* Manually generates and caches the spec such that they are already available when the
* future needs.
*/
public void precacheSpecs() {
synchronized (RecentsTransitionHelper.this) {
mAppTransitionAnimationSpecs = composer.composeSpecs();
}
}
}
}