1 /*
2  * Copyright (C) 2021 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.wm.shell.taskview;
18 
19 import static android.view.WindowManager.TRANSIT_CHANGE;
20 import static android.view.WindowManager.TRANSIT_CLOSE;
21 import static android.view.WindowManager.TRANSIT_OPEN;
22 import static android.view.WindowManager.TRANSIT_TO_BACK;
23 import static android.view.WindowManager.TRANSIT_TO_FRONT;
24 
25 import android.annotation.NonNull;
26 import android.annotation.Nullable;
27 import android.app.ActivityManager;
28 import android.graphics.Rect;
29 import android.os.IBinder;
30 import android.util.ArrayMap;
31 import android.util.Slog;
32 import android.view.SurfaceControl;
33 import android.view.WindowManager;
34 import android.window.TransitionInfo;
35 import android.window.TransitionRequestInfo;
36 import android.window.WindowContainerTransaction;
37 
38 import androidx.annotation.VisibleForTesting;
39 
40 import com.android.wm.shell.transition.Transitions;
41 import com.android.wm.shell.util.TransitionUtil;
42 
43 import java.util.ArrayList;
44 import java.util.Objects;
45 
46 /**
47  * Handles Shell Transitions that involve TaskView tasks.
48  */
49 public class TaskViewTransitions implements Transitions.TransitionHandler {
50     static final String TAG = "TaskViewTransitions";
51 
52     private final ArrayMap<TaskViewTaskController, TaskViewRequestedState> mTaskViews =
53             new ArrayMap<>();
54     private final ArrayList<PendingTransition> mPending = new ArrayList<>();
55     private final Transitions mTransitions;
56     private final boolean[] mRegistered = new boolean[]{ false };
57 
58     /**
59      * TaskView makes heavy use of startTransition. Only one shell-initiated transition can be
60      * in-flight (collecting) at a time (because otherwise, the operations could get merged into
61      * a single transition). So, keep a queue here until we add a queue in server-side.
62      */
63     @VisibleForTesting
64     static class PendingTransition {
65         final @WindowManager.TransitionType int mType;
66         final WindowContainerTransaction mWct;
67         final @NonNull TaskViewTaskController mTaskView;
68         IBinder mClaimed;
69 
70         /**
71          * This is needed because arbitrary activity launches can still "intrude" into any
72          * transition since `startActivity` is a synchronous call. Once that is solved, we can
73          * remove this.
74          */
75         final IBinder mLaunchCookie;
76 
PendingTransition(@indowManager.TransitionType int type, @Nullable WindowContainerTransaction wct, @NonNull TaskViewTaskController taskView, @Nullable IBinder launchCookie)77         PendingTransition(@WindowManager.TransitionType int type,
78                 @Nullable WindowContainerTransaction wct,
79                 @NonNull TaskViewTaskController taskView,
80                 @Nullable IBinder launchCookie) {
81             mType = type;
82             mWct = wct;
83             mTaskView = taskView;
84             mLaunchCookie = launchCookie;
85         }
86     }
87 
88     /**
89      * Visibility and bounds state that has been requested for a {@link TaskViewTaskController}.
90      */
91     private static class TaskViewRequestedState {
92         boolean mVisible;
93         Rect mBounds = new Rect();
94     }
95 
TaskViewTransitions(Transitions transitions)96     public TaskViewTransitions(Transitions transitions) {
97         mTransitions = transitions;
98         // Defer registration until the first TaskView because we want this to be the "first" in
99         // priority when handling requests.
100         // TODO(210041388): register here once we have an explicit ordering mechanism.
101     }
102 
addTaskView(TaskViewTaskController tv)103     void addTaskView(TaskViewTaskController tv) {
104         synchronized (mRegistered) {
105             if (!mRegistered[0]) {
106                 mRegistered[0] = true;
107                 mTransitions.addHandler(this);
108             }
109         }
110         mTaskViews.put(tv, new TaskViewRequestedState());
111     }
112 
removeTaskView(TaskViewTaskController tv)113     void removeTaskView(TaskViewTaskController tv) {
114         mTaskViews.remove(tv);
115         // Note: Don't unregister handler since this is a singleton with lifetime bound to Shell
116     }
117 
isEnabled()118     boolean isEnabled() {
119         return mTransitions.isRegistered();
120     }
121 
122     /**
123      * Looks through the pending transitions for a closing transaction that matches the provided
124      * `taskView`.
125      * @param taskView the pending transition should be for this.
126      */
findPendingCloseTransition(TaskViewTaskController taskView)127     private PendingTransition findPendingCloseTransition(TaskViewTaskController taskView) {
128         for (int i = mPending.size() - 1; i >= 0; --i) {
129             if (mPending.get(i).mTaskView != taskView) continue;
130             if (TransitionUtil.isClosingType(mPending.get(i).mType)) {
131                 return mPending.get(i);
132             }
133         }
134         return null;
135     }
136 
137     /**
138      * Looks through the pending transitions for a opening transaction that matches the provided
139      * `taskView`.
140      * @param taskView the pending transition should be for this.
141      */
142     @VisibleForTesting
findPendingOpeningTransition(TaskViewTaskController taskView)143     PendingTransition findPendingOpeningTransition(TaskViewTaskController taskView) {
144         for (int i = mPending.size() - 1; i >= 0; --i) {
145             if (mPending.get(i).mTaskView != taskView) continue;
146             if (TransitionUtil.isOpeningType(mPending.get(i).mType)) {
147                 return mPending.get(i);
148             }
149         }
150         return null;
151     }
152 
153     /**
154      * Looks through the pending transitions for one matching `taskView`.
155      * @param taskView the pending transition should be for this.
156      * @param type the type of transition it's looking for
157      */
findPending(TaskViewTaskController taskView, int type)158     PendingTransition findPending(TaskViewTaskController taskView, int type) {
159         for (int i = mPending.size() - 1; i >= 0; --i) {
160             if (mPending.get(i).mTaskView != taskView) continue;
161             if (mPending.get(i).mType == type) {
162                 return mPending.get(i);
163             }
164         }
165         return null;
166     }
167 
168     /**
169      * Returns all the pending transitions for a given `taskView`.
170      * @param taskView the pending transition should be for this.
171      */
findAllPending(TaskViewTaskController taskView)172     ArrayList<PendingTransition> findAllPending(TaskViewTaskController taskView) {
173         ArrayList<PendingTransition> list = new ArrayList<>();
174         for (int i = mPending.size() - 1; i >= 0; --i) {
175             if (mPending.get(i).mTaskView != taskView) continue;
176             list.add(mPending.get(i));
177         }
178         return list;
179     }
180 
findPending(IBinder claimed)181     private PendingTransition findPending(IBinder claimed) {
182         for (int i = 0; i < mPending.size(); ++i) {
183             if (mPending.get(i).mClaimed != claimed) continue;
184             return mPending.get(i);
185         }
186         return null;
187     }
188 
189     /** @return whether there are pending transitions on TaskViews. */
hasPending()190     public boolean hasPending() {
191         return !mPending.isEmpty();
192     }
193 
194     @Override
handleRequest(@onNull IBinder transition, @Nullable TransitionRequestInfo request)195     public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
196             @Nullable TransitionRequestInfo request) {
197         final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask();
198         if (triggerTask == null) {
199             return null;
200         }
201         final TaskViewTaskController taskView = findTaskView(triggerTask);
202         if (taskView == null) return null;
203         // Opening types should all be initiated by shell
204         if (!TransitionUtil.isClosingType(request.getType())) return null;
205         PendingTransition pending = new PendingTransition(request.getType(), null,
206                 taskView, null /* cookie */);
207         pending.mClaimed = transition;
208         mPending.add(pending);
209         return new WindowContainerTransaction();
210     }
211 
findTaskView(ActivityManager.RunningTaskInfo taskInfo)212     private TaskViewTaskController findTaskView(ActivityManager.RunningTaskInfo taskInfo) {
213         for (int i = 0; i < mTaskViews.size(); ++i) {
214             if (mTaskViews.keyAt(i).getTaskInfo() == null) continue;
215             if (taskInfo.token.equals(mTaskViews.keyAt(i).getTaskInfo().token)) {
216                 return mTaskViews.keyAt(i);
217             }
218         }
219         return null;
220     }
221 
startTaskView(@onNull WindowContainerTransaction wct, @NonNull TaskViewTaskController taskView, @NonNull IBinder launchCookie)222     void startTaskView(@NonNull WindowContainerTransaction wct,
223             @NonNull TaskViewTaskController taskView, @NonNull IBinder launchCookie) {
224         updateVisibilityState(taskView, true /* visible */);
225         mPending.add(new PendingTransition(TRANSIT_OPEN, wct, taskView, launchCookie));
226         startNextTransition();
227     }
228 
closeTaskView(@onNull WindowContainerTransaction wct, @NonNull TaskViewTaskController taskView)229     void closeTaskView(@NonNull WindowContainerTransaction wct,
230             @NonNull TaskViewTaskController taskView) {
231         updateVisibilityState(taskView, false /* visible */);
232         mPending.add(new PendingTransition(TRANSIT_CLOSE, wct, taskView, null /* cookie */));
233         startNextTransition();
234     }
235 
setTaskViewVisible(TaskViewTaskController taskView, boolean visible)236     void setTaskViewVisible(TaskViewTaskController taskView, boolean visible) {
237         if (mTaskViews.get(taskView) == null) return;
238         if (mTaskViews.get(taskView).mVisible == visible) return;
239         if (taskView.getTaskInfo() == null) {
240             // Nothing to update, task is not yet available
241             return;
242         }
243         mTaskViews.get(taskView).mVisible = visible;
244         final WindowContainerTransaction wct = new WindowContainerTransaction();
245         wct.setHidden(taskView.getTaskInfo().token, !visible /* hidden */);
246         wct.setBounds(taskView.getTaskInfo().token, mTaskViews.get(taskView).mBounds);
247         PendingTransition pending = new PendingTransition(
248                 visible ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK, wct, taskView, null /* cookie */);
249         mPending.add(pending);
250         startNextTransition();
251         // visibility is reported in transition.
252     }
253 
updateBoundsState(TaskViewTaskController taskView, Rect boundsOnScreen)254     void updateBoundsState(TaskViewTaskController taskView, Rect boundsOnScreen) {
255         TaskViewRequestedState state = mTaskViews.get(taskView);
256         if (state == null) return;
257         state.mBounds.set(boundsOnScreen);
258     }
259 
updateVisibilityState(TaskViewTaskController taskView, boolean visible)260     void updateVisibilityState(TaskViewTaskController taskView, boolean visible) {
261         TaskViewRequestedState state = mTaskViews.get(taskView);
262         if (state == null) return;
263         state.mVisible = visible;
264     }
265 
setTaskBounds(TaskViewTaskController taskView, Rect boundsOnScreen)266     void setTaskBounds(TaskViewTaskController taskView, Rect boundsOnScreen) {
267         TaskViewRequestedState state = mTaskViews.get(taskView);
268         if (state == null || Objects.equals(boundsOnScreen, state.mBounds)) {
269             return;
270         }
271         state.mBounds.set(boundsOnScreen);
272         if (!state.mVisible) {
273             // Task view isn't visible, the bounds will next visibility update.
274             return;
275         }
276         PendingTransition pendingOpen = findPendingOpeningTransition(taskView);
277         if (pendingOpen != null) {
278             // There is already an opening transition in-flight, the window bounds will be
279             // set in prepareOpenAnimation (via the window crop) if needed.
280             return;
281         }
282         WindowContainerTransaction wct = new WindowContainerTransaction();
283         wct.setBounds(taskView.getTaskInfo().token, boundsOnScreen);
284         mPending.add(new PendingTransition(TRANSIT_CHANGE, wct, taskView, null /* cookie */));
285         startNextTransition();
286     }
287 
startNextTransition()288     private void startNextTransition() {
289         if (mPending.isEmpty()) return;
290         final PendingTransition pending = mPending.get(0);
291         if (pending.mClaimed != null) {
292             // Wait for this to start animating.
293             return;
294         }
295         pending.mClaimed = mTransitions.startTransition(pending.mType, pending.mWct, this);
296     }
297 
298     @Override
onTransitionConsumed(@onNull IBinder transition, boolean aborted, @NonNull SurfaceControl.Transaction finishTransaction)299     public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
300             @NonNull SurfaceControl.Transaction finishTransaction) {
301         if (!aborted) return;
302         final PendingTransition pending = findPending(transition);
303         if (pending == null) return;
304         mPending.remove(pending);
305         startNextTransition();
306     }
307 
308     @Override
startAnimation(@onNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback)309     public boolean startAnimation(@NonNull IBinder transition,
310             @NonNull TransitionInfo info,
311             @NonNull SurfaceControl.Transaction startTransaction,
312             @NonNull SurfaceControl.Transaction finishTransaction,
313             @NonNull Transitions.TransitionFinishCallback finishCallback) {
314         PendingTransition pending = findPending(transition);
315         if (pending != null) {
316             mPending.remove(pending);
317         }
318         if (mTaskViews.isEmpty()) {
319             if (pending != null) {
320                 Slog.e(TAG, "Pending taskview transition but no task-views");
321             }
322             return false;
323         }
324         boolean stillNeedsMatchingLaunch = pending != null && pending.mLaunchCookie != null;
325         int changesHandled = 0;
326         WindowContainerTransaction wct = null;
327         for (int i = 0; i < info.getChanges().size(); ++i) {
328             final TransitionInfo.Change chg = info.getChanges().get(i);
329             if (chg.getTaskInfo() == null) continue;
330             if (TransitionUtil.isClosingType(chg.getMode())) {
331                 final boolean isHide = chg.getMode() == TRANSIT_TO_BACK;
332                 TaskViewTaskController tv = findTaskView(chg.getTaskInfo());
333                 if (tv == null && !isHide) {
334                     // TaskView can be null when closing
335                     changesHandled++;
336                     continue;
337                 }
338                 if (tv == null) {
339                     if (pending != null) {
340                         Slog.w(TAG, "Found a non-TaskView task in a TaskView Transition. This "
341                                 + "shouldn't happen, so there may be a visual artifact: "
342                                 + chg.getTaskInfo().taskId);
343                     }
344                     continue;
345                 }
346                 if (isHide) {
347                     tv.prepareHideAnimation(finishTransaction);
348                 } else {
349                     tv.prepareCloseAnimation();
350                 }
351                 changesHandled++;
352             } else if (TransitionUtil.isOpeningType(chg.getMode())) {
353                 final boolean taskIsNew = chg.getMode() == TRANSIT_OPEN;
354                 final TaskViewTaskController tv;
355                 if (taskIsNew) {
356                     if (pending == null
357                             || !chg.getTaskInfo().containsLaunchCookie(pending.mLaunchCookie)) {
358                         Slog.e(TAG, "Found a launching TaskView in the wrong transition. All "
359                                 + "TaskView launches should be initiated by shell and in their "
360                                 + "own transition: " + chg.getTaskInfo().taskId);
361                         continue;
362                     }
363                     stillNeedsMatchingLaunch = false;
364                     tv = pending.mTaskView;
365                 } else {
366                     tv = findTaskView(chg.getTaskInfo());
367                     if (tv == null) {
368                         if (pending != null) {
369                             Slog.w(TAG, "Found a non-TaskView task in a TaskView Transition. This "
370                                     + "shouldn't happen, so there may be a visual artifact: "
371                                     + chg.getTaskInfo().taskId);
372                         }
373                         continue;
374                     }
375                 }
376                 if (wct == null) wct = new WindowContainerTransaction();
377                 tv.prepareOpenAnimation(taskIsNew, startTransaction, finishTransaction,
378                         chg.getTaskInfo(), chg.getLeash(), wct);
379                 changesHandled++;
380             } else if (chg.getMode() == TRANSIT_CHANGE) {
381                 TaskViewTaskController tv = findTaskView(chg.getTaskInfo());
382                 if (tv == null) {
383                     if (pending != null) {
384                         Slog.w(TAG, "Found a non-TaskView task in a TaskView Transition. This "
385                                 + "shouldn't happen, so there may be a visual artifact: "
386                                 + chg.getTaskInfo().taskId);
387                     }
388                     continue;
389                 }
390                 startTransaction.reparent(chg.getLeash(), tv.getSurfaceControl());
391                 finishTransaction.reparent(chg.getLeash(), tv.getSurfaceControl())
392                     .setPosition(chg.getLeash(), 0, 0);
393                 changesHandled++;
394             }
395         }
396         if (stillNeedsMatchingLaunch) {
397             Slog.w(TAG, "Expected a TaskView launch in this transition but didn't get one, "
398                     + "cleaning up the task view");
399             // Didn't find a task so the task must have never launched
400             pending.mTaskView.setTaskNotFound();
401         } else if (wct == null && pending == null && changesHandled != info.getChanges().size()) {
402             // Just some house-keeping, let another handler animate.
403             return false;
404         }
405         // No animation, just show it immediately.
406         startTransaction.apply();
407         finishCallback.onTransitionFinished(wct);
408         startNextTransition();
409         return true;
410     }
411 }
412