Home | History | Annotate | Download | only in views
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.systemui.recents.views;
     18 
     19 import static android.app.ActivityManager.StackId.ASSISTANT_STACK_ID;
     20 import static android.app.ActivityManager.StackId.DOCKED_STACK_ID;
     21 import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
     22 import static android.app.ActivityManager.StackId.FULLSCREEN_WORKSPACE_STACK_ID;
     23 import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
     24 
     25 import android.annotation.Nullable;
     26 import android.app.ActivityManager.StackId;
     27 import android.app.ActivityOptions;
     28 import android.app.ActivityOptions.OnAnimationStartedListener;
     29 import android.content.Context;
     30 import android.graphics.Bitmap;
     31 import android.graphics.Canvas;
     32 import android.graphics.Color;
     33 import android.graphics.GraphicBuffer;
     34 import android.graphics.Rect;
     35 import android.os.Bundle;
     36 import android.os.Handler;
     37 import android.os.IRemoteCallback;
     38 import android.os.RemoteException;
     39 import android.util.Log;
     40 import android.view.AppTransitionAnimationSpec;
     41 import android.view.DisplayListCanvas;
     42 import android.view.IAppTransitionAnimationSpecsFuture;
     43 import android.view.RenderNode;
     44 import android.view.ThreadedRenderer;
     45 import android.view.View;
     46 
     47 import com.android.internal.annotations.GuardedBy;
     48 import com.android.systemui.recents.Recents;
     49 import com.android.systemui.recents.RecentsDebugFlags;
     50 import com.android.systemui.recents.events.EventBus;
     51 import com.android.systemui.recents.events.activity.CancelEnterRecentsWindowAnimationEvent;
     52 import com.android.systemui.recents.events.activity.ExitRecentsWindowFirstAnimationFrameEvent;
     53 import com.android.systemui.recents.events.activity.LaunchTaskFailedEvent;
     54 import com.android.systemui.recents.events.activity.LaunchTaskStartedEvent;
     55 import com.android.systemui.recents.events.activity.LaunchTaskSucceededEvent;
     56 import com.android.systemui.recents.events.component.ScreenPinningRequestEvent;
     57 import com.android.systemui.recents.events.component.SetWaitingForTransitionStartEvent;
     58 import com.android.systemui.recents.misc.SystemServicesProxy;
     59 import com.android.systemui.recents.model.Task;
     60 import com.android.systemui.recents.model.TaskStack;
     61 import com.android.systemui.statusbar.phone.StatusBar;
     62 
     63 import java.util.ArrayList;
     64 import java.util.Collections;
     65 import java.util.List;
     66 
     67 /**
     68  * A helper class to create transitions to/from Recents
     69  */
     70 public class RecentsTransitionHelper {
     71 
     72     private static final String TAG = "RecentsTransitionHelper";
     73     private static final boolean DEBUG = false;
     74 
     75     /**
     76      * Special value for {@link #mAppTransitionAnimationSpecs}: Indicate that we are currently
     77      * waiting for the specs to be retrieved.
     78      */
     79     private static final List<AppTransitionAnimationSpec> SPECS_WAITING = new ArrayList<>();
     80 
     81     @GuardedBy("this")
     82     private List<AppTransitionAnimationSpec> mAppTransitionAnimationSpecs = SPECS_WAITING;
     83 
     84     private Context mContext;
     85     private Handler mHandler;
     86     private TaskViewTransform mTmpTransform = new TaskViewTransform();
     87 
     88     private class StartScreenPinningRunnableRunnable implements Runnable {
     89 
     90         private int taskId = -1;
     91 
     92         @Override
     93         public void run() {
     94             EventBus.getDefault().send(new ScreenPinningRequestEvent(mContext, taskId));
     95         }
     96     }
     97     private StartScreenPinningRunnableRunnable mStartScreenPinningRunnable
     98             = new StartScreenPinningRunnableRunnable();
     99 
    100     public RecentsTransitionHelper(Context context) {
    101         mContext = context;
    102         mHandler = new Handler();
    103     }
    104 
    105     /**
    106      * Launches the specified {@link Task}.
    107      */
    108     public void launchTaskFromRecents(final TaskStack stack, @Nullable final Task task,
    109             final TaskStackView stackView, final TaskView taskView,
    110             final boolean screenPinningRequested, final int destinationStack) {
    111 
    112         final ActivityOptions.OnAnimationStartedListener animStartedListener;
    113         final AppTransitionAnimationSpecsFuture transitionFuture;
    114         if (taskView != null) {
    115 
    116             // Fetch window rect here already in order not to be blocked on lock contention in WM
    117             // when the future calls it.
    118             final Rect windowRect = Recents.getSystemServices().getWindowRect();
    119             transitionFuture = getAppTransitionFuture(
    120                     () -> composeAnimationSpecs(task, stackView, destinationStack, windowRect));
    121             animStartedListener = new OnAnimationStartedListener() {
    122                 private boolean mHandled;
    123 
    124                 @Override
    125                 public void onAnimationStarted() {
    126                     if (mHandled) {
    127                         return;
    128                     }
    129                     mHandled = true;
    130 
    131                     // If we are launching into another task, cancel the previous task's
    132                     // window transition
    133                     EventBus.getDefault().send(new CancelEnterRecentsWindowAnimationEvent(task));
    134                     EventBus.getDefault().send(new ExitRecentsWindowFirstAnimationFrameEvent());
    135                     stackView.cancelAllTaskViewAnimations();
    136 
    137                     if (screenPinningRequested) {
    138                         // Request screen pinning after the animation runs
    139                         mStartScreenPinningRunnable.taskId = task.key.id;
    140                         mHandler.postDelayed(mStartScreenPinningRunnable, 350);
    141                     }
    142 
    143                     if (!Recents.getConfiguration().isLowRamDevice) {
    144                         // Reset the state where we are waiting for the transition to start
    145                         EventBus.getDefault().send(new SetWaitingForTransitionStartEvent(false));
    146                     }
    147                 }
    148             };
    149         } else {
    150             // This is only the case if the task is not on screen (scrolled offscreen for example)
    151             transitionFuture = null;
    152             animStartedListener = new OnAnimationStartedListener() {
    153                 private boolean mHandled;
    154 
    155                 @Override
    156                 public void onAnimationStarted() {
    157                     if (mHandled) {
    158                         return;
    159                     }
    160                     mHandled = true;
    161 
    162                     // If we are launching into another task, cancel the previous task's
    163                     // window transition
    164                     EventBus.getDefault().send(new CancelEnterRecentsWindowAnimationEvent(task));
    165                     EventBus.getDefault().send(new ExitRecentsWindowFirstAnimationFrameEvent());
    166                     stackView.cancelAllTaskViewAnimations();
    167 
    168                     if (!Recents.getConfiguration().isLowRamDevice) {
    169                         // Reset the state where we are waiting for the transition to start
    170                         EventBus.getDefault().send(new SetWaitingForTransitionStartEvent(false));
    171                     }
    172                 }
    173             };
    174         }
    175 
    176         EventBus.getDefault().send(new SetWaitingForTransitionStartEvent(true));
    177         final ActivityOptions opts = ActivityOptions.makeMultiThumbFutureAspectScaleAnimation(mContext,
    178                 mHandler, transitionFuture != null ? transitionFuture.future : null,
    179                 animStartedListener, true /* scaleUp */);
    180         if (taskView == null) {
    181             // If there is no task view, then we do not need to worry about animating out occluding
    182             // task views, and we can launch immediately
    183             startTaskActivity(stack, task, taskView, opts, transitionFuture, destinationStack);
    184         } else {
    185             LaunchTaskStartedEvent launchStartedEvent = new LaunchTaskStartedEvent(taskView,
    186                     screenPinningRequested);
    187             if (task.group != null && !task.group.isFrontMostTask(task)) {
    188                 launchStartedEvent.addPostAnimationCallback(new Runnable() {
    189                     @Override
    190                     public void run() {
    191                         startTaskActivity(stack, task, taskView, opts, transitionFuture,
    192                                 destinationStack);
    193                     }
    194                 });
    195                 EventBus.getDefault().send(launchStartedEvent);
    196             } else {
    197                 EventBus.getDefault().send(launchStartedEvent);
    198                 startTaskActivity(stack, task, taskView, opts, transitionFuture, destinationStack);
    199             }
    200         }
    201         Recents.getSystemServices().sendCloseSystemWindows(
    202                 StatusBar.SYSTEM_DIALOG_REASON_HOME_KEY);
    203     }
    204 
    205     public IRemoteCallback wrapStartedListener(final OnAnimationStartedListener listener) {
    206         if (listener == null) {
    207             return null;
    208         }
    209         return new IRemoteCallback.Stub() {
    210             @Override
    211             public void sendResult(Bundle data) throws RemoteException {
    212                 mHandler.post(new Runnable() {
    213                     @Override
    214                     public void run() {
    215                         listener.onAnimationStarted();
    216                     }
    217                 });
    218             }
    219         };
    220     }
    221 
    222     /**
    223      * Starts the activity for the launch task.
    224      *
    225      * @param taskView this is the {@link TaskView} that we are launching from. This can be null if
    226      *                 we are toggling recents and the launch-to task is now offscreen.
    227      * @param destinationStack id of the stack to put the task into.
    228      */
    229     private void startTaskActivity(TaskStack stack, Task task, @Nullable TaskView taskView,
    230             ActivityOptions opts, AppTransitionAnimationSpecsFuture transitionFuture,
    231             int destinationStack) {
    232         SystemServicesProxy ssp = Recents.getSystemServices();
    233         ssp.startActivityFromRecents(mContext, task.key, task.title, opts, destinationStack,
    234                 succeeded -> {
    235             if (succeeded) {
    236                 // Keep track of the index of the task launch
    237                 int taskIndexFromFront = 0;
    238                 int taskIndex = stack.indexOfStackTask(task);
    239                 if (taskIndex > -1) {
    240                     taskIndexFromFront = stack.getTaskCount() - taskIndex - 1;
    241                 }
    242                 EventBus.getDefault().send(new LaunchTaskSucceededEvent(taskIndexFromFront));
    243             } else {
    244                 // Dismiss the task if we fail to launch it
    245                 if (taskView != null) {
    246                     taskView.dismissTask();
    247                 }
    248 
    249                 // Keep track of failed launches
    250                 EventBus.getDefault().send(new LaunchTaskFailedEvent());
    251             }
    252         });
    253         if (transitionFuture != null) {
    254             mHandler.post(transitionFuture::precacheSpecs);
    255         }
    256     }
    257 
    258     /**
    259      * Creates a future which will later be queried for animation specs for this current transition.
    260      *
    261      * @param composer The implementation that composes the specs on the UI thread.
    262      */
    263     public AppTransitionAnimationSpecsFuture getAppTransitionFuture(
    264             final AnimationSpecComposer composer) {
    265         synchronized (this) {
    266             mAppTransitionAnimationSpecs = SPECS_WAITING;
    267         }
    268         IAppTransitionAnimationSpecsFuture future = new IAppTransitionAnimationSpecsFuture.Stub() {
    269             @Override
    270             public AppTransitionAnimationSpec[] get() throws RemoteException {
    271                 mHandler.post(() -> {
    272                     synchronized (RecentsTransitionHelper.this) {
    273                         mAppTransitionAnimationSpecs = composer.composeSpecs();
    274                         RecentsTransitionHelper.this.notifyAll();
    275                     }
    276                 });
    277                 synchronized (RecentsTransitionHelper.this) {
    278                     while (mAppTransitionAnimationSpecs == SPECS_WAITING) {
    279                         try {
    280                             RecentsTransitionHelper.this.wait();
    281                         } catch (InterruptedException e) {}
    282                     }
    283                     if (mAppTransitionAnimationSpecs == null) {
    284                         return null;
    285                     }
    286                     AppTransitionAnimationSpec[] specs
    287                             = new AppTransitionAnimationSpec[mAppTransitionAnimationSpecs.size()];
    288                     mAppTransitionAnimationSpecs.toArray(specs);
    289                     mAppTransitionAnimationSpecs = SPECS_WAITING;
    290                     return specs;
    291                 }
    292             }
    293         };
    294         return new AppTransitionAnimationSpecsFuture(composer, future);
    295     }
    296 
    297     /**
    298      * Composes the transition spec when docking a task, which includes a full task bitmap.
    299      */
    300     public List<AppTransitionAnimationSpec> composeDockAnimationSpec(TaskView taskView,
    301             Rect bounds) {
    302         mTmpTransform.fillIn(taskView);
    303         Task task = taskView.getTask();
    304         GraphicBuffer buffer = RecentsTransitionHelper.composeTaskBitmap(taskView, mTmpTransform);
    305         return Collections.singletonList(new AppTransitionAnimationSpec(task.key.id, buffer,
    306                 bounds));
    307     }
    308 
    309     /**
    310      * Composes the animation specs for all the tasks in the target stack.
    311      */
    312     private List<AppTransitionAnimationSpec> composeAnimationSpecs(final Task task,
    313             final TaskStackView stackView, final int destinationStack, Rect windowRect) {
    314         // Ensure we have a valid target stack id
    315         final int targetStackId = destinationStack != INVALID_STACK_ID ?
    316                 destinationStack : task.key.stackId;
    317         if (!StackId.useAnimationSpecForAppTransition(targetStackId)) {
    318             return null;
    319         }
    320 
    321         // Calculate the offscreen task rect (for tasks that are not backed by views)
    322         TaskView taskView = stackView.getChildViewForTask(task);
    323         TaskStackLayoutAlgorithm stackLayout = stackView.getStackAlgorithm();
    324         Rect offscreenTaskRect = new Rect();
    325         stackLayout.getFrontOfStackTransform().rect.round(offscreenTaskRect);
    326 
    327         // If this is a full screen stack, the transition will be towards the single, full screen
    328         // task. We only need the transition spec for this task.
    329         List<AppTransitionAnimationSpec> specs = new ArrayList<>();
    330 
    331         // TODO: Sometimes targetStackId is not initialized after reboot, so we also have to
    332         // check for INVALID_STACK_ID
    333         if (targetStackId == FULLSCREEN_WORKSPACE_STACK_ID || targetStackId == DOCKED_STACK_ID
    334                 || targetStackId == ASSISTANT_STACK_ID || targetStackId == INVALID_STACK_ID) {
    335             if (taskView == null) {
    336                 specs.add(composeOffscreenAnimationSpec(task, offscreenTaskRect));
    337             } else {
    338                 mTmpTransform.fillIn(taskView);
    339                 stackLayout.transformToScreenCoordinates(mTmpTransform, windowRect);
    340                 AppTransitionAnimationSpec spec = composeAnimationSpec(stackView, taskView,
    341                         mTmpTransform, true /* addHeaderBitmap */);
    342                 if (spec != null) {
    343                     specs.add(spec);
    344                 }
    345             }
    346             return specs;
    347         }
    348 
    349         // Otherwise, for freeform tasks, create a new animation spec for each task we have to
    350         // launch
    351         TaskStack stack = stackView.getStack();
    352         ArrayList<Task> tasks = stack.getStackTasks();
    353         int taskCount = tasks.size();
    354         for (int i = taskCount - 1; i >= 0; i--) {
    355             Task t = tasks.get(i);
    356             if (t.isFreeformTask() || targetStackId == FREEFORM_WORKSPACE_STACK_ID) {
    357                 TaskView tv = stackView.getChildViewForTask(t);
    358                 if (tv == null) {
    359                     // TODO: Create a different animation task rect for this case (though it should
    360                     //       never happen)
    361                     specs.add(composeOffscreenAnimationSpec(t, offscreenTaskRect));
    362                 } else {
    363                     mTmpTransform.fillIn(taskView);
    364                     stackLayout.transformToScreenCoordinates(mTmpTransform,
    365                             null /* windowOverrideRect */);
    366                     AppTransitionAnimationSpec spec = composeAnimationSpec(stackView, tv,
    367                             mTmpTransform, true /* addHeaderBitmap */);
    368                     if (spec != null) {
    369                         specs.add(spec);
    370                     }
    371                 }
    372             }
    373         }
    374 
    375         return specs;
    376     }
    377 
    378     /**
    379      * Composes a single animation spec for the given {@link Task}
    380      */
    381     private static AppTransitionAnimationSpec composeOffscreenAnimationSpec(Task task,
    382             Rect taskRect) {
    383         return new AppTransitionAnimationSpec(task.key.id, null, taskRect);
    384     }
    385 
    386     public static GraphicBuffer composeTaskBitmap(TaskView taskView, TaskViewTransform transform) {
    387         float scale = transform.scale;
    388         int fromWidth = (int) (transform.rect.width() * scale);
    389         int fromHeight = (int) (transform.rect.height() * scale);
    390         if (fromWidth == 0 || fromHeight == 0) {
    391             Log.e(TAG, "Could not compose thumbnail for task: " + taskView.getTask() +
    392                     " at transform: " + transform);
    393 
    394             return drawViewIntoGraphicBuffer(1, 1, null, 1f, 0x00ffffff);
    395         } else {
    396             if (RecentsDebugFlags.Static.EnableTransitionThumbnailDebugMode) {
    397                 return drawViewIntoGraphicBuffer(fromWidth, fromHeight, null, 1f, 0xFFff0000);
    398             } else {
    399                 return drawViewIntoGraphicBuffer(fromWidth, fromHeight, taskView, scale, 0);
    400             }
    401         }
    402     }
    403 
    404     private static GraphicBuffer composeHeaderBitmap(TaskView taskView,
    405             TaskViewTransform transform) {
    406         float scale = transform.scale;
    407         int headerWidth = (int) (transform.rect.width());
    408         int headerHeight = (int) (taskView.mHeaderView.getMeasuredHeight() * scale);
    409         if (headerWidth == 0 || headerHeight == 0) {
    410             return null;
    411         }
    412 
    413         if (RecentsDebugFlags.Static.EnableTransitionThumbnailDebugMode) {
    414             return drawViewIntoGraphicBuffer(headerWidth, headerHeight, null, 1f, 0xFFff0000);
    415         } else {
    416             return drawViewIntoGraphicBuffer(headerWidth, headerHeight, taskView.mHeaderView,
    417                     scale, 0);
    418         }
    419     }
    420 
    421     public static GraphicBuffer drawViewIntoGraphicBuffer(int bufferWidth, int bufferHeight,
    422             View view, float scale, int eraseColor) {
    423         RenderNode node = RenderNode.create("RecentsTransition", null);
    424         node.setLeftTopRightBottom(0, 0, bufferWidth, bufferHeight);
    425         node.setClipToBounds(false);
    426         DisplayListCanvas c = node.start(bufferWidth, bufferHeight);
    427         c.scale(scale, scale);
    428         if (eraseColor != 0) {
    429             c.drawColor(eraseColor);
    430         }
    431         if (view != null) {
    432             view.draw(c);
    433         }
    434         node.end(c);
    435         Bitmap hwBitmap = ThreadedRenderer.createHardwareBitmap(node, bufferWidth, bufferHeight);
    436         return hwBitmap.createGraphicBufferHandle();
    437     }
    438 
    439     /**
    440      * Composes a single animation spec for the given {@link TaskView}
    441      */
    442     private static AppTransitionAnimationSpec composeAnimationSpec(TaskStackView stackView,
    443             TaskView taskView, TaskViewTransform transform, boolean addHeaderBitmap) {
    444         GraphicBuffer b = null;
    445         if (addHeaderBitmap) {
    446             b = composeHeaderBitmap(taskView, transform);
    447             if (b == null) {
    448                 return null;
    449             }
    450         }
    451 
    452         Rect taskRect = new Rect();
    453         transform.rect.round(taskRect);
    454         // Disable in for low ram devices because each task does in Recents does not have fullscreen
    455         // height (stackView height) and when transitioning to fullscreen app, the code below would
    456         // force the task thumbnail to full stackView height immediately causing the transition
    457         // jarring.
    458         if (!Recents.getConfiguration().isLowRamDevice && taskView.getTask() !=
    459                 stackView.getStack().getStackFrontMostTask(false /* includeFreeformTasks */)) {
    460             taskRect.bottom = taskRect.top + stackView.getMeasuredHeight();
    461         }
    462         return new AppTransitionAnimationSpec(taskView.getTask().key.id, b, taskRect);
    463     }
    464 
    465     public interface AnimationSpecComposer {
    466         List<AppTransitionAnimationSpec> composeSpecs();
    467     }
    468 
    469     /**
    470      * Class to be returned from {@link #composeAnimationSpec} that gives access to both the future
    471      * and the anonymous class used for composing.
    472      */
    473     public class AppTransitionAnimationSpecsFuture {
    474 
    475         private final AnimationSpecComposer composer;
    476         private final IAppTransitionAnimationSpecsFuture future;
    477 
    478         private AppTransitionAnimationSpecsFuture(AnimationSpecComposer composer,
    479                 IAppTransitionAnimationSpecsFuture future) {
    480             this.composer = composer;
    481             this.future = future;
    482         }
    483 
    484         public IAppTransitionAnimationSpecsFuture getFuture() {
    485             return future;
    486         }
    487 
    488         /**
    489          * Manually generates and caches the spec such that they are already available when the
    490          * future needs.
    491          */
    492         public void precacheSpecs() {
    493             synchronized (RecentsTransitionHelper.this) {
    494                 mAppTransitionAnimationSpecs = composer.composeSpecs();
    495             }
    496         }
    497     }
    498 }
    499