1 /*
2  * Copyright (C) 2020 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.stackdivider;
18 
19 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
20 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
21 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
22 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
23 import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
24 import static android.view.Display.DEFAULT_DISPLAY;
25 
26 import android.app.ActivityManager.RunningTaskInfo;
27 import android.app.WindowConfiguration;
28 import android.graphics.Rect;
29 import android.os.RemoteException;
30 import android.util.Log;
31 import android.view.Display;
32 import android.view.SurfaceControl;
33 import android.view.SurfaceSession;
34 import android.window.TaskOrganizer;
35 
36 class SplitScreenTaskOrganizer extends TaskOrganizer {
37     private static final String TAG = "SplitScreenTaskOrg";
38     private static final boolean DEBUG = Divider.DEBUG;
39 
40     RunningTaskInfo mPrimary;
41     RunningTaskInfo mSecondary;
42     SurfaceControl mPrimarySurface;
43     SurfaceControl mSecondarySurface;
44     SurfaceControl mPrimaryDim;
45     SurfaceControl mSecondaryDim;
46     Rect mHomeBounds = new Rect();
47     final Divider mDivider;
48     private boolean mSplitScreenSupported = false;
49 
50     final SurfaceSession mSurfaceSession = new SurfaceSession();
51 
SplitScreenTaskOrganizer(Divider divider)52     SplitScreenTaskOrganizer(Divider divider) {
53         mDivider = divider;
54     }
55 
init()56     void init() throws RemoteException {
57         registerOrganizer(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
58         registerOrganizer(WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
59         synchronized (this) {
60             try {
61                 mPrimary = TaskOrganizer.createRootTask(Display.DEFAULT_DISPLAY,
62                         WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY);
63                 mSecondary = TaskOrganizer.createRootTask(Display.DEFAULT_DISPLAY,
64                         WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
65             } catch (Exception e) {
66                 // teardown to prevent callbacks
67                 unregisterOrganizer();
68                 throw e;
69             }
70         }
71     }
72 
isSplitScreenSupported()73     boolean isSplitScreenSupported() {
74         return mSplitScreenSupported;
75     }
76 
getTransaction()77     SurfaceControl.Transaction getTransaction() {
78         return mDivider.mTransactionPool.acquire();
79     }
80 
releaseTransaction(SurfaceControl.Transaction t)81     void releaseTransaction(SurfaceControl.Transaction t) {
82         mDivider.mTransactionPool.release(t);
83     }
84 
85     @Override
onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash)86     public void onTaskAppeared(RunningTaskInfo taskInfo, SurfaceControl leash) {
87         synchronized (this) {
88             if (mPrimary == null || mSecondary == null) {
89                 Log.w(TAG, "Received onTaskAppeared before creating root tasks " + taskInfo);
90                 return;
91             }
92 
93             if (taskInfo.token.equals(mPrimary.token)) {
94                 mPrimarySurface = leash;
95             } else if (taskInfo.token.equals(mSecondary.token)) {
96                 mSecondarySurface = leash;
97             }
98 
99             if (!mSplitScreenSupported && mPrimarySurface != null && mSecondarySurface != null) {
100                 mSplitScreenSupported = true;
101 
102                 // Initialize dim surfaces:
103                 mPrimaryDim = new SurfaceControl.Builder(mSurfaceSession)
104                         .setParent(mPrimarySurface).setColorLayer()
105                         .setName("Primary Divider Dim")
106                         .setCallsite("SplitScreenTaskOrganizer.onTaskAppeared")
107                         .build();
108                 mSecondaryDim = new SurfaceControl.Builder(mSurfaceSession)
109                         .setParent(mSecondarySurface).setColorLayer()
110                         .setName("Secondary Divider Dim")
111                         .setCallsite("SplitScreenTaskOrganizer.onTaskAppeared")
112                         .build();
113                 SurfaceControl.Transaction t = getTransaction();
114                 t.setLayer(mPrimaryDim, Integer.MAX_VALUE);
115                 t.setColor(mPrimaryDim, new float[]{0f, 0f, 0f});
116                 t.setLayer(mSecondaryDim, Integer.MAX_VALUE);
117                 t.setColor(mSecondaryDim, new float[]{0f, 0f, 0f});
118                 t.apply();
119                 releaseTransaction(t);
120             }
121         }
122     }
123 
124     @Override
onTaskVanished(RunningTaskInfo taskInfo)125     public void onTaskVanished(RunningTaskInfo taskInfo) {
126         synchronized (this) {
127             final boolean isPrimaryTask = mPrimary != null
128                     && taskInfo.token.equals(mPrimary.token);
129             final boolean isSecondaryTask = mSecondary != null
130                     && taskInfo.token.equals(mSecondary.token);
131 
132             if (mSplitScreenSupported && (isPrimaryTask || isSecondaryTask)) {
133                 mSplitScreenSupported = false;
134 
135                 SurfaceControl.Transaction t = getTransaction();
136                 t.remove(mPrimaryDim);
137                 t.remove(mSecondaryDim);
138                 t.remove(mPrimarySurface);
139                 t.remove(mSecondarySurface);
140                 t.apply();
141                 releaseTransaction(t);
142 
143                 mDivider.onTaskVanished();
144             }
145         }
146     }
147 
148     @Override
onTaskInfoChanged(RunningTaskInfo taskInfo)149     public void onTaskInfoChanged(RunningTaskInfo taskInfo) {
150         if (taskInfo.displayId != DEFAULT_DISPLAY) {
151             return;
152         }
153         mDivider.getHandler().post(() -> handleTaskInfoChanged(taskInfo));
154     }
155 
156     /**
157      * This is effectively a finite state machine which moves between the various split-screen
158      * presentations based on the contents of the split regions.
159      */
handleTaskInfoChanged(RunningTaskInfo info)160     private void handleTaskInfoChanged(RunningTaskInfo info) {
161         if (!mSplitScreenSupported) {
162             // This shouldn't happen; but apparently there is a chance that SysUI crashes without
163             // system server receiving binder-death (or maybe it receives binder-death too late?).
164             // In this situation, when sys-ui restarts, the split root-tasks will still exist so
165             // there is a small window of time during init() where WM might send messages here
166             // before init() fails. So, avoid a cycle of crashes by returning early.
167             Log.e(TAG, "Got handleTaskInfoChanged when not initialized: " + info);
168             return;
169         }
170         final boolean secondaryImpliedMinimize = mSecondary.topActivityType == ACTIVITY_TYPE_HOME
171                 || (mSecondary.topActivityType == ACTIVITY_TYPE_RECENTS
172                         && mDivider.isHomeStackResizable());
173         final boolean primaryWasEmpty = mPrimary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
174         final boolean secondaryWasEmpty = mSecondary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
175         if (info.token.asBinder() == mPrimary.token.asBinder()) {
176             mPrimary = info;
177         } else if (info.token.asBinder() == mSecondary.token.asBinder()) {
178             mSecondary = info;
179         }
180         final boolean primaryIsEmpty = mPrimary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
181         final boolean secondaryIsEmpty = mSecondary.topActivityType == ACTIVITY_TYPE_UNDEFINED;
182         final boolean secondaryImpliesMinimize = mSecondary.topActivityType == ACTIVITY_TYPE_HOME
183                 || (mSecondary.topActivityType == ACTIVITY_TYPE_RECENTS
184                         && mDivider.isHomeStackResizable());
185         if (DEBUG) {
186             Log.d(TAG, "onTaskInfoChanged " + mPrimary + "  " + mSecondary);
187         }
188         if (primaryIsEmpty == primaryWasEmpty && secondaryWasEmpty == secondaryIsEmpty
189                 && secondaryImpliedMinimize == secondaryImpliesMinimize) {
190             // No relevant changes
191             return;
192         }
193         if (primaryIsEmpty || secondaryIsEmpty) {
194             // At-least one of the splits is empty which means we are currently transitioning
195             // into or out-of split-screen mode.
196             if (DEBUG) {
197                 Log.d(TAG, " at-least one split empty " + mPrimary.topActivityType
198                         + "  " + mSecondary.topActivityType);
199             }
200             if (mDivider.isDividerVisible()) {
201                 // Was in split-mode, which means we are leaving split, so continue that.
202                 // This happens when the stack in the primary-split is dismissed.
203                 if (DEBUG) {
204                     Log.d(TAG, "    was in split, so this means leave it "
205                             + mPrimary.topActivityType + "  " + mSecondary.topActivityType);
206                 }
207                 mDivider.startDismissSplit();
208             } else if (!primaryIsEmpty && primaryWasEmpty && secondaryWasEmpty) {
209                 // Wasn't in split-mode (both were empty), but now that the primary split is
210                 // populated, we should fully enter split by moving everything else into secondary.
211                 // This just tells window-manager to reparent things, the UI will respond
212                 // when it gets new task info for the secondary split.
213                 if (DEBUG) {
214                     Log.d(TAG, "   was not in split, but primary is populated, so enter it");
215                 }
216                 mDivider.startEnterSplit();
217             }
218         } else if (secondaryImpliesMinimize) {
219             // Both splits are populated but the secondary split has a home/recents stack on top,
220             // so enter minimized mode.
221             mDivider.ensureMinimizedSplit();
222         } else {
223             // Both splits are populated by normal activities, so make sure we aren't minimized.
224             mDivider.ensureNormalSplit();
225         }
226     }
227 }
228