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.compatui; 18 19 import android.annotation.Nullable; 20 import android.content.Context; 21 import android.content.res.Configuration; 22 import android.hardware.display.DisplayManager; 23 import android.util.ArraySet; 24 import android.util.Log; 25 import android.util.SparseArray; 26 import android.view.Display; 27 import android.view.InsetsSourceControl; 28 import android.view.InsetsState; 29 30 import com.android.internal.annotations.VisibleForTesting; 31 import com.android.wm.shell.ShellTaskOrganizer; 32 import com.android.wm.shell.common.DisplayController; 33 import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener; 34 import com.android.wm.shell.common.DisplayImeController; 35 import com.android.wm.shell.common.DisplayInsetsController; 36 import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener; 37 import com.android.wm.shell.common.DisplayLayout; 38 import com.android.wm.shell.common.ShellExecutor; 39 import com.android.wm.shell.common.SyncTransactionQueue; 40 import com.android.wm.shell.common.annotations.ExternalThread; 41 42 import java.lang.ref.WeakReference; 43 import java.util.ArrayList; 44 import java.util.List; 45 import java.util.Set; 46 import java.util.function.Consumer; 47 import java.util.function.Predicate; 48 49 /** 50 * Controls to show/update restart-activity buttons on Tasks based on whether the foreground 51 * activities are in compatibility mode. 52 */ 53 public class CompatUIController implements OnDisplaysChangedListener, 54 DisplayImeController.ImePositionProcessor { 55 56 /** Callback for size compat UI interaction. */ 57 public interface CompatUICallback { 58 /** Called when the size compat restart button appears. */ onSizeCompatRestartButtonAppeared(int taskId)59 void onSizeCompatRestartButtonAppeared(int taskId); 60 /** Called when the size compat restart button is clicked. */ onSizeCompatRestartButtonClicked(int taskId)61 void onSizeCompatRestartButtonClicked(int taskId); 62 } 63 64 private static final String TAG = "CompatUIController"; 65 66 /** Whether the IME is shown on display id. */ 67 private final Set<Integer> mDisplaysWithIme = new ArraySet<>(1); 68 69 /** {@link PerDisplayOnInsetsChangedListener} by display id. */ 70 private final SparseArray<PerDisplayOnInsetsChangedListener> mOnInsetsChangedListeners = 71 new SparseArray<>(0); 72 73 /** The showing UIs by task id. */ 74 private final SparseArray<CompatUIWindowManager> mActiveLayouts = new SparseArray<>(0); 75 76 /** Avoid creating display context frequently for non-default display. */ 77 private final SparseArray<WeakReference<Context>> mDisplayContextCache = new SparseArray<>(0); 78 79 private final Context mContext; 80 private final DisplayController mDisplayController; 81 private final DisplayInsetsController mDisplayInsetsController; 82 private final DisplayImeController mImeController; 83 private final SyncTransactionQueue mSyncQueue; 84 private final ShellExecutor mMainExecutor; 85 private final CompatUIImpl mImpl = new CompatUIImpl(); 86 87 private CompatUICallback mCallback; 88 89 /** Only show once automatically in the process life. */ 90 private boolean mHasShownHint; 91 /** Indicates if the keyguard is currently occluded, in which case compat UIs shouldn't 92 * be shown. */ 93 private boolean mKeyguardOccluded; 94 CompatUIController(Context context, DisplayController displayController, DisplayInsetsController displayInsetsController, DisplayImeController imeController, SyncTransactionQueue syncQueue, ShellExecutor mainExecutor)95 public CompatUIController(Context context, 96 DisplayController displayController, 97 DisplayInsetsController displayInsetsController, 98 DisplayImeController imeController, 99 SyncTransactionQueue syncQueue, 100 ShellExecutor mainExecutor) { 101 mContext = context; 102 mDisplayController = displayController; 103 mDisplayInsetsController = displayInsetsController; 104 mImeController = imeController; 105 mSyncQueue = syncQueue; 106 mMainExecutor = mainExecutor; 107 mDisplayController.addDisplayWindowListener(this); 108 mImeController.addPositionProcessor(this); 109 } 110 111 /** Returns implementation of {@link CompatUI}. */ asCompatUI()112 public CompatUI asCompatUI() { 113 return mImpl; 114 } 115 116 /** Sets the callback for UI interactions. */ setCompatUICallback(CompatUICallback callback)117 public void setCompatUICallback(CompatUICallback callback) { 118 mCallback = callback; 119 } 120 121 /** 122 * Called when the Task info changed. Creates and updates the compat UI if there is an 123 * activity in size compat, or removes the UI if there is no size compat activity. 124 * 125 * @param displayId display the task and activity are in. 126 * @param taskId task the activity is in. 127 * @param taskConfig task config to place the compat UI with. 128 * @param taskListener listener to handle the Task Surface placement. 129 */ onCompatInfoChanged(int displayId, int taskId, @Nullable Configuration taskConfig, @Nullable ShellTaskOrganizer.TaskListener taskListener)130 public void onCompatInfoChanged(int displayId, int taskId, 131 @Nullable Configuration taskConfig, 132 @Nullable ShellTaskOrganizer.TaskListener taskListener) { 133 if (taskConfig == null || taskListener == null) { 134 // Null token means the current foreground activity is not in compatibility mode. 135 removeLayout(taskId); 136 } else if (mActiveLayouts.contains(taskId)) { 137 // UI already exists, update the UI layout. 138 updateLayout(taskId, taskConfig, taskListener); 139 } else { 140 // Create a new compat UI. 141 createLayout(displayId, taskId, taskConfig, taskListener); 142 } 143 } 144 145 @Override onDisplayAdded(int displayId)146 public void onDisplayAdded(int displayId) { 147 addOnInsetsChangedListener(displayId); 148 } 149 150 @Override onDisplayRemoved(int displayId)151 public void onDisplayRemoved(int displayId) { 152 mDisplayContextCache.remove(displayId); 153 removeOnInsetsChangedListener(displayId); 154 155 // Remove all compat UIs on the removed display. 156 final List<Integer> toRemoveTaskIds = new ArrayList<>(); 157 forAllLayoutsOnDisplay(displayId, layout -> toRemoveTaskIds.add(layout.getTaskId())); 158 for (int i = toRemoveTaskIds.size() - 1; i >= 0; i--) { 159 removeLayout(toRemoveTaskIds.get(i)); 160 } 161 } 162 addOnInsetsChangedListener(int displayId)163 private void addOnInsetsChangedListener(int displayId) { 164 PerDisplayOnInsetsChangedListener listener = new PerDisplayOnInsetsChangedListener( 165 displayId); 166 listener.register(); 167 mOnInsetsChangedListeners.put(displayId, listener); 168 } 169 removeOnInsetsChangedListener(int displayId)170 private void removeOnInsetsChangedListener(int displayId) { 171 PerDisplayOnInsetsChangedListener listener = mOnInsetsChangedListeners.get(displayId); 172 if (listener == null) { 173 return; 174 } 175 listener.unregister(); 176 mOnInsetsChangedListeners.remove(displayId); 177 } 178 179 180 @Override onDisplayConfigurationChanged(int displayId, Configuration newConfig)181 public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { 182 updateDisplayLayout(displayId); 183 } 184 updateDisplayLayout(int displayId)185 private void updateDisplayLayout(int displayId) { 186 final DisplayLayout displayLayout = mDisplayController.getDisplayLayout(displayId); 187 forAllLayoutsOnDisplay(displayId, layout -> layout.updateDisplayLayout(displayLayout)); 188 } 189 190 @Override onImeVisibilityChanged(int displayId, boolean isShowing)191 public void onImeVisibilityChanged(int displayId, boolean isShowing) { 192 if (isShowing) { 193 mDisplaysWithIme.add(displayId); 194 } else { 195 mDisplaysWithIme.remove(displayId); 196 } 197 198 // Hide the compat UIs when input method is showing. 199 forAllLayoutsOnDisplay(displayId, 200 layout -> layout.updateVisibility(showOnDisplay(displayId))); 201 } 202 203 @VisibleForTesting onKeyguardOccludedChanged(boolean occluded)204 void onKeyguardOccludedChanged(boolean occluded) { 205 mKeyguardOccluded = occluded; 206 // Hide the compat UIs when keyguard is occluded. 207 forAllLayouts(layout -> layout.updateVisibility(showOnDisplay(layout.getDisplayId()))); 208 } 209 showOnDisplay(int displayId)210 private boolean showOnDisplay(int displayId) { 211 return !mKeyguardOccluded && !isImeShowingOnDisplay(displayId); 212 } 213 isImeShowingOnDisplay(int displayId)214 private boolean isImeShowingOnDisplay(int displayId) { 215 return mDisplaysWithIme.contains(displayId); 216 } 217 createLayout(int displayId, int taskId, Configuration taskConfig, ShellTaskOrganizer.TaskListener taskListener)218 private void createLayout(int displayId, int taskId, Configuration taskConfig, 219 ShellTaskOrganizer.TaskListener taskListener) { 220 final Context context = getOrCreateDisplayContext(displayId); 221 if (context == null) { 222 Log.e(TAG, "Cannot get context for display " + displayId); 223 return; 224 } 225 226 final CompatUIWindowManager compatUIWindowManager = 227 createLayout(context, displayId, taskId, taskConfig, taskListener); 228 mActiveLayouts.put(taskId, compatUIWindowManager); 229 compatUIWindowManager.createLayout(showOnDisplay(displayId)); 230 } 231 232 @VisibleForTesting createLayout(Context context, int displayId, int taskId, Configuration taskConfig, ShellTaskOrganizer.TaskListener taskListener)233 CompatUIWindowManager createLayout(Context context, int displayId, int taskId, 234 Configuration taskConfig, ShellTaskOrganizer.TaskListener taskListener) { 235 final CompatUIWindowManager compatUIWindowManager = new CompatUIWindowManager(context, 236 taskConfig, mSyncQueue, mCallback, taskId, taskListener, 237 mDisplayController.getDisplayLayout(displayId), mHasShownHint); 238 // Only show hint for the first time. 239 mHasShownHint = true; 240 return compatUIWindowManager; 241 } 242 updateLayout(int taskId, Configuration taskConfig, ShellTaskOrganizer.TaskListener taskListener)243 private void updateLayout(int taskId, Configuration taskConfig, 244 ShellTaskOrganizer.TaskListener taskListener) { 245 final CompatUIWindowManager layout = mActiveLayouts.get(taskId); 246 if (layout == null) { 247 return; 248 } 249 layout.updateCompatInfo(taskConfig, taskListener, showOnDisplay(layout.getDisplayId())); 250 } 251 removeLayout(int taskId)252 private void removeLayout(int taskId) { 253 final CompatUIWindowManager layout = mActiveLayouts.get(taskId); 254 if (layout != null) { 255 layout.release(); 256 mActiveLayouts.remove(taskId); 257 } 258 } 259 getOrCreateDisplayContext(int displayId)260 private Context getOrCreateDisplayContext(int displayId) { 261 if (displayId == Display.DEFAULT_DISPLAY) { 262 return mContext; 263 } 264 Context context = null; 265 final WeakReference<Context> ref = mDisplayContextCache.get(displayId); 266 if (ref != null) { 267 context = ref.get(); 268 } 269 if (context == null) { 270 Display display = mContext.getSystemService(DisplayManager.class).getDisplay(displayId); 271 if (display != null) { 272 context = mContext.createDisplayContext(display); 273 mDisplayContextCache.put(displayId, new WeakReference<>(context)); 274 } 275 } 276 return context; 277 } 278 forAllLayoutsOnDisplay(int displayId, Consumer<CompatUIWindowManager> callback)279 private void forAllLayoutsOnDisplay(int displayId, Consumer<CompatUIWindowManager> callback) { 280 forAllLayouts(layout -> layout.getDisplayId() == displayId, callback); 281 } 282 forAllLayouts(Consumer<CompatUIWindowManager> callback)283 private void forAllLayouts(Consumer<CompatUIWindowManager> callback) { 284 forAllLayouts(layout -> true, callback); 285 } 286 forAllLayouts(Predicate<CompatUIWindowManager> condition, Consumer<CompatUIWindowManager> callback)287 private void forAllLayouts(Predicate<CompatUIWindowManager> condition, 288 Consumer<CompatUIWindowManager> callback) { 289 for (int i = 0; i < mActiveLayouts.size(); i++) { 290 final int taskId = mActiveLayouts.keyAt(i); 291 final CompatUIWindowManager layout = mActiveLayouts.get(taskId); 292 if (layout != null && condition.test(layout)) { 293 callback.accept(layout); 294 } 295 } 296 } 297 298 /** 299 * The interface for calls from outside the Shell, within the host process. 300 */ 301 @ExternalThread 302 private class CompatUIImpl implements CompatUI { 303 @Override onKeyguardOccludedChanged(boolean occluded)304 public void onKeyguardOccludedChanged(boolean occluded) { 305 mMainExecutor.execute(() -> { 306 CompatUIController.this.onKeyguardOccludedChanged(occluded); 307 }); 308 } 309 } 310 311 /** An implementation of {@link OnInsetsChangedListener} for a given display id. */ 312 private class PerDisplayOnInsetsChangedListener implements OnInsetsChangedListener { 313 final int mDisplayId; 314 final InsetsState mInsetsState = new InsetsState(); 315 PerDisplayOnInsetsChangedListener(int displayId)316 PerDisplayOnInsetsChangedListener(int displayId) { 317 mDisplayId = displayId; 318 } 319 register()320 void register() { 321 mDisplayInsetsController.addInsetsChangedListener(mDisplayId, this); 322 } 323 unregister()324 void unregister() { 325 mDisplayInsetsController.removeInsetsChangedListener(mDisplayId, this); 326 } 327 328 @Override insetsChanged(InsetsState insetsState)329 public void insetsChanged(InsetsState insetsState) { 330 if (mInsetsState.equals(insetsState)) { 331 return; 332 } 333 mInsetsState.set(insetsState); 334 updateDisplayLayout(mDisplayId); 335 } 336 337 @Override insetsControlChanged(InsetsState insetsState, InsetsSourceControl[] activeControls)338 public void insetsControlChanged(InsetsState insetsState, 339 InsetsSourceControl[] activeControls) { 340 insetsChanged(insetsState); 341 } 342 } 343 } 344