1 /*
2  * Copyright (C) 2022 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 static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
20 import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
21 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
22 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
23 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
24 
25 import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
26 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
27 import static com.android.internal.annotations.VisibleForTesting.Visibility.PROTECTED;
28 
29 import android.annotation.NonNull;
30 import android.annotation.Nullable;
31 import android.app.TaskInfo;
32 import android.content.Context;
33 import android.content.res.Configuration;
34 import android.graphics.PixelFormat;
35 import android.graphics.Rect;
36 import android.os.Binder;
37 import android.util.Log;
38 import android.view.IWindow;
39 import android.view.SurfaceControl;
40 import android.view.SurfaceControlViewHost;
41 import android.view.SurfaceSession;
42 import android.view.View;
43 import android.view.WindowManager;
44 import android.view.WindowlessWindowManager;
45 
46 import com.android.internal.annotations.VisibleForTesting;
47 import com.android.wm.shell.ShellTaskOrganizer;
48 import com.android.wm.shell.common.DisplayLayout;
49 import com.android.wm.shell.common.SyncTransactionQueue;
50 
51 /**
52  * A superclass for all Compat UI {@link WindowlessWindowManager}s that holds shared logic and
53  * exposes general API for {@link CompatUIController}.
54  *
55  * <p>Holds view hierarchy of a root surface and helps to inflate and manage layout.
56  */
57 public abstract class CompatUIWindowManagerAbstract extends WindowlessWindowManager {
58 
59     protected final int mTaskId;
60     protected Context mContext;
61 
62     private final SyncTransactionQueue mSyncQueue;
63     private final int mDisplayId;
64     private Configuration mTaskConfig;
65     private ShellTaskOrganizer.TaskListener mTaskListener;
66     private DisplayLayout mDisplayLayout;
67     private final Rect mStableBounds;
68 
69     @NonNull
70     private TaskInfo mTaskInfo;
71 
72     /**
73      * Utility class for adding and releasing a View hierarchy for this {@link
74      * WindowlessWindowManager} to {@code mLeash}.
75      */
76     @Nullable
77     protected SurfaceControlViewHost mViewHost;
78 
79     /**
80      * A surface leash to position the layout relative to the task, since we can't set position for
81      * the {@code mViewHost} directly.
82      */
83     @Nullable
84     protected SurfaceControl mLeash;
85 
CompatUIWindowManagerAbstract(Context context, TaskInfo taskInfo, SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout)86     protected CompatUIWindowManagerAbstract(Context context, TaskInfo taskInfo,
87             SyncTransactionQueue syncQueue, ShellTaskOrganizer.TaskListener taskListener,
88             DisplayLayout displayLayout) {
89         super(taskInfo.configuration, null /* rootSurface */, null /* hostInputToken */);
90         mTaskInfo = taskInfo;
91         mContext = context;
92         mSyncQueue = syncQueue;
93         mTaskConfig = taskInfo.configuration;
94         mDisplayId = mContext.getDisplayId();
95         mTaskId = taskInfo.taskId;
96         mTaskListener = taskListener;
97         mDisplayLayout = displayLayout;
98         mStableBounds = new Rect();
99         mDisplayLayout.getStableBounds(mStableBounds);
100     }
101 
102     /**
103      * @return {@code true} if the instance of the specific {@link CompatUIWindowManagerAbstract}
104      * for the current task id needs to be recreated loading the related resources. This happens
105      * if the user switches between Light/Dark mode, if the device is docked/undocked or if the
106      * user switches between multi-window mode to fullscreen where the
107      * {@link ShellTaskOrganizer.TaskListener} implementation is different.
108      */
needsToBeRecreated(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener)109     boolean needsToBeRecreated(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener) {
110         return hasUiModeChanged(mTaskInfo, taskInfo) || hasTaskListenerChanged(taskListener);
111     }
112 
113     /**
114      * Returns the z-order of this window which will be passed to the {@link SurfaceControl} once
115      * {@link #attachToParentSurface} is called.
116      *
117      * <p>See {@link SurfaceControl.Transaction#setLayer}.
118      */
getZOrder()119     protected abstract int getZOrder();
120 
121     /** Returns the layout of this window manager. */
getLayout()122     protected abstract @Nullable View getLayout();
123 
124     /**
125      * Inflates and inits the layout of this window manager on to the root surface if both {@code
126      * canShow} and {@link #eligibleToShowLayout} are true.
127      *
128      * <p>Doesn't do anything if layout is not eligible to be shown.
129      *
130      * @param canShow whether the layout is allowed to be shown by the parent controller.
131      * @return whether the layout is eligible to be shown.
132      */
133     @VisibleForTesting(visibility = PROTECTED)
createLayout(boolean canShow)134     public boolean createLayout(boolean canShow) {
135         if (!eligibleToShowLayout()) {
136             return false;
137         }
138         if (!canShow || getLayout() != null) {
139             // Wait until layout should be visible, or layout was already created.
140             return true;
141         }
142 
143         if (mViewHost != null) {
144             throw new IllegalStateException(
145                     "A UI has already been created with this window manager.");
146         }
147 
148         // Construction extracted into separate methods to allow injection for tests.
149         mViewHost = createSurfaceViewHost();
150         mViewHost.setView(createLayout(), getWindowLayoutParams());
151 
152         updateSurfacePosition();
153 
154         return true;
155     }
156 
157     /** Inflates and inits the layout of this window manager. */
createLayout()158     protected abstract View createLayout();
159 
removeLayout()160     protected abstract void removeLayout();
161 
162     /**
163      * Whether the layout is eligible to be shown according to the internal state of the subclass.
164      */
eligibleToShowLayout()165     protected abstract boolean eligibleToShowLayout();
166 
167     @Override
setConfiguration(Configuration configuration)168     public void setConfiguration(Configuration configuration) {
169         super.setConfiguration(configuration);
170         mContext = mContext.createConfigurationContext(configuration);
171     }
172 
173     @Override
getParentSurface(IWindow window, WindowManager.LayoutParams attrs)174     protected SurfaceControl getParentSurface(IWindow window, WindowManager.LayoutParams attrs) {
175         String className = getClass().getSimpleName();
176         final SurfaceControl.Builder builder = new SurfaceControl.Builder(new SurfaceSession())
177                 .setContainerLayer()
178                 .setName(className + "Leash")
179                 .setHidden(false)
180                 .setCallsite(className + "#attachToParentSurface");
181         attachToParentSurface(builder);
182         mLeash = builder.build();
183         initSurface(mLeash);
184         return mLeash;
185     }
186 
getTaskListener()187     protected ShellTaskOrganizer.TaskListener getTaskListener() {
188         return mTaskListener;
189     }
190 
191     /** Inits the z-order of the surface. */
initSurface(SurfaceControl leash)192     private void initSurface(SurfaceControl leash) {
193         final int z = getZOrder();
194         mSyncQueue.runInSync(t -> {
195             if (leash == null || !leash.isValid()) {
196                 Log.w(getTag(), "The leash has been released.");
197                 return;
198             }
199             t.setLayer(leash, z);
200         });
201     }
202 
203     /**
204      * Called when compat info changed.
205      *
206      * <p>The window manager is released if the layout is no longer eligible to be shown.
207      *
208      * @param canShow whether the layout is allowed to be shown by the parent controller.
209      * @return whether the layout is eligible to be shown.
210      */
211     @VisibleForTesting(visibility = PROTECTED)
updateCompatInfo(TaskInfo taskInfo, ShellTaskOrganizer.TaskListener taskListener, boolean canShow)212     public boolean updateCompatInfo(TaskInfo taskInfo,
213             ShellTaskOrganizer.TaskListener taskListener, boolean canShow) {
214         mTaskInfo = taskInfo;
215         final Configuration prevTaskConfig = mTaskConfig;
216         final ShellTaskOrganizer.TaskListener prevTaskListener = mTaskListener;
217         mTaskConfig = taskInfo.configuration;
218         mTaskListener = taskListener;
219 
220         // Update configuration.
221         setConfiguration(mTaskConfig);
222 
223         if (!eligibleToShowLayout()) {
224             release();
225             return false;
226         }
227 
228         View layout = getLayout();
229         if (layout == null || prevTaskListener != taskListener
230                 || mTaskConfig.uiMode != prevTaskConfig.uiMode) {
231             // Layout wasn't created yet or TaskListener changed, recreate the layout for new
232             // surface parent.
233             release();
234             return createLayout(canShow);
235         }
236 
237         boolean boundsUpdated = !mTaskConfig.windowConfiguration.getBounds().equals(
238                 prevTaskConfig.windowConfiguration.getBounds());
239         boolean layoutDirectionUpdated =
240                 mTaskConfig.getLayoutDirection() != prevTaskConfig.getLayoutDirection();
241         if (boundsUpdated || layoutDirectionUpdated) {
242             onParentBoundsChanged();
243         }
244 
245         if (layout != null && layoutDirectionUpdated) {
246             // Update layout for RTL.
247             layout.setLayoutDirection(mTaskConfig.getLayoutDirection());
248         }
249 
250         return true;
251     }
252 
253     /**
254      * Updates the visibility of the layout.
255      *
256      * @param canShow whether the layout is allowed to be shown by the parent controller.
257      */
258     @VisibleForTesting(visibility = PACKAGE)
updateVisibility(boolean canShow)259     public void updateVisibility(boolean canShow) {
260         View layout = getLayout();
261         if (layout == null) {
262             // Layout may not have been created because it was hidden previously.
263             createLayout(canShow);
264             return;
265         }
266 
267         final int newVisibility = canShow && eligibleToShowLayout() ? View.VISIBLE : View.GONE;
268         if (layout.getVisibility() != newVisibility) {
269             layout.setVisibility(newVisibility);
270         }
271     }
272 
273     /** Called when display layout changed. */
274     @VisibleForTesting(visibility = PACKAGE)
updateDisplayLayout(DisplayLayout displayLayout)275     public void updateDisplayLayout(DisplayLayout displayLayout) {
276         final Rect prevStableBounds = mStableBounds;
277         final Rect curStableBounds = new Rect();
278         displayLayout.getStableBounds(curStableBounds);
279         mDisplayLayout = displayLayout;
280         if (!prevStableBounds.equals(curStableBounds)) {
281             // mStableBounds should be updated before we call onParentBoundsChanged.
282             mStableBounds.set(curStableBounds);
283             onParentBoundsChanged();
284         }
285     }
286 
287     /** Called when the surface is ready to be placed under the task surface. */
288     @VisibleForTesting(visibility = PRIVATE)
attachToParentSurface(SurfaceControl.Builder b)289     void attachToParentSurface(SurfaceControl.Builder b) {
290         mTaskListener.attachChildSurfaceToTask(mTaskId, b);
291     }
292 
getDisplayId()293     public int getDisplayId() {
294         return mDisplayId;
295     }
296 
getTaskId()297     public int getTaskId() {
298         return mTaskId;
299     }
300 
301     /** Releases the surface control and tears down the view hierarchy. */
release()302     public void release() {
303         // Hiding before releasing to avoid flickering when transitioning to the Home screen.
304         View layout = getLayout();
305         if (layout != null) {
306             layout.setVisibility(View.GONE);
307         }
308         removeLayout();
309 
310         if (mViewHost != null) {
311             mViewHost.release();
312             mViewHost = null;
313         }
314 
315         if (mLeash != null) {
316             final SurfaceControl leash = mLeash;
317             mSyncQueue.runInSync(t -> t.remove(leash));
318             mLeash = null;
319         }
320     }
321 
322     /** Re-layouts the view host and updates the surface position. */
relayout()323     void relayout() {
324         relayout(getWindowLayoutParams());
325     }
326 
relayout(WindowManager.LayoutParams windowLayoutParams)327     protected void relayout(WindowManager.LayoutParams windowLayoutParams) {
328         if (mViewHost == null) {
329             return;
330         }
331         mViewHost.relayout(windowLayoutParams);
332         updateSurfacePosition();
333     }
334 
335     @NonNull
getLastTaskInfo()336     protected TaskInfo getLastTaskInfo() {
337         return mTaskInfo;
338     }
339 
340     /**
341      * Called following a change in the task bounds, display layout stable bounds, or the layout
342      * direction.
343      */
onParentBoundsChanged()344     protected void onParentBoundsChanged() {
345         updateSurfacePosition();
346     }
347 
348     /**
349      * Updates the position of the surface with respect to the parent bounds.
350      */
updateSurfacePosition()351     protected abstract void updateSurfacePosition();
352 
353     /**
354      * Updates the position of the surface with respect to the given {@code positionX} and {@code
355      * positionY}.
356      */
updateSurfacePosition(int positionX, int positionY)357     protected void updateSurfacePosition(int positionX, int positionY) {
358         if (mLeash == null) {
359             return;
360         }
361         mSyncQueue.runInSync(t -> {
362             if (mLeash == null || !mLeash.isValid()) {
363                 Log.w(getTag(), "The leash has been released.");
364                 return;
365             }
366             t.setPosition(mLeash, positionX, positionY);
367         });
368     }
369 
getLayoutDirection()370     protected int getLayoutDirection() {
371         return mContext.getResources().getConfiguration().getLayoutDirection();
372     }
373 
getTaskBounds()374     protected Rect getTaskBounds() {
375         return mTaskConfig.windowConfiguration.getBounds();
376     }
377 
378     /** Returns the intersection between the task bounds and the display layout stable bounds. */
getTaskStableBounds()379     protected Rect getTaskStableBounds() {
380         final Rect result = new Rect(mStableBounds);
381         result.intersect(getTaskBounds());
382         return result;
383     }
384 
385     /** Creates a {@link SurfaceControlViewHost} for this window manager. */
386     @VisibleForTesting(visibility = PRIVATE)
createSurfaceViewHost()387     public SurfaceControlViewHost createSurfaceViewHost() {
388         return new SurfaceControlViewHost(mContext, mContext.getDisplay(), this,
389                 getClass().getSimpleName());
390     }
391 
392     /** Gets the layout params. */
getWindowLayoutParams()393     protected WindowManager.LayoutParams getWindowLayoutParams() {
394         View layout = getLayout();
395         if (layout == null) {
396             return new WindowManager.LayoutParams();
397         }
398         // Measure how big the hint is since its size depends on the text size.
399         layout.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
400         return getWindowLayoutParams(layout.getMeasuredWidth(), layout.getMeasuredHeight());
401     }
402 
403     /** Gets the layout params given the width and height of the layout. */
getWindowLayoutParams(int width, int height)404     protected WindowManager.LayoutParams getWindowLayoutParams(int width, int height) {
405         final WindowManager.LayoutParams winParams = new WindowManager.LayoutParams(
406                 // Cannot be wrap_content as this determines the actual window size
407                 width, height,
408                 TYPE_APPLICATION_OVERLAY,
409                 getWindowManagerLayoutParamsFlags(),
410                 PixelFormat.TRANSLUCENT);
411         winParams.token = new Binder();
412         winParams.setTitle(getClass().getSimpleName() + mTaskId);
413         winParams.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION | PRIVATE_FLAG_TRUSTED_OVERLAY;
414         return winParams;
415     }
416 
417     /**
418      * @return Flags to use for the {@link WindowManager} layout
419      */
getWindowManagerLayoutParamsFlags()420     protected int getWindowManagerLayoutParamsFlags() {
421         return FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL;
422     }
423 
getTag()424     protected final String getTag() {
425         return getClass().getSimpleName();
426     }
427 
hasTaskListenerChanged(ShellTaskOrganizer.TaskListener newTaskListener)428     protected boolean hasTaskListenerChanged(ShellTaskOrganizer.TaskListener newTaskListener) {
429         return !mTaskListener.equals(newTaskListener);
430     }
431 
hasUiModeChanged(TaskInfo currentTaskInfo, TaskInfo newTaskInfo)432     protected static boolean hasUiModeChanged(TaskInfo currentTaskInfo, TaskInfo newTaskInfo) {
433         return currentTaskInfo.configuration.uiMode != newTaskInfo.configuration.uiMode;
434     }
435 }
436