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