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