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.server.wm;
18 
19 import static android.view.Display.INVALID_DISPLAY;
20 import static android.view.Display.isSuspendedState;
21 import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
22 import static android.window.WindowProviderService.isWindowProviderService;
23 
24 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ADD_REMOVE;
25 import static com.android.internal.protolog.ProtoLogGroup.WM_ERROR;
26 
27 import android.annotation.NonNull;
28 import android.annotation.Nullable;
29 import android.app.IWindowToken;
30 import android.content.Context;
31 import android.content.res.Configuration;
32 import android.os.Bundle;
33 import android.os.IBinder;
34 import android.os.RemoteException;
35 import android.util.ArrayMap;
36 import android.view.View;
37 import android.view.WindowManager.LayoutParams.WindowType;
38 import android.window.WindowContext;
39 
40 import com.android.internal.annotations.VisibleForTesting;
41 import com.android.internal.protolog.common.ProtoLog;
42 
43 import java.util.Objects;
44 
45 /**
46  * A controller to register/unregister {@link WindowContainerListener} for {@link WindowContext}.
47  *
48  * <ul>
49  *   <li>When a {@link WindowContext} is created, it registers the listener via
50  *     {@link WindowManagerService#attachWindowContextToDisplayArea(IBinder, int, int, Bundle)}
51  *     automatically.</li>
52  *   <li>When the {@link WindowContext} adds the first window to the screen via
53  *     {@link android.view.WindowManager#addView(View, android.view.ViewGroup.LayoutParams)},
54  *     {@link WindowManagerService} then updates the {@link WindowContextListenerImpl} to listen
55  *     to corresponding {@link WindowToken} via this controller.</li>
56  *   <li>When the {@link WindowContext} is GCed, it unregisters the previously
57  *     registered listener via
58  *     {@link WindowManagerService#detachWindowContextFromWindowContainer(IBinder)}.
59  *     {@link WindowManagerService} is also responsible for removing the
60  *     {@link WindowContext} created {@link WindowToken}.</li>
61  * </ul>
62  * <p>Note that the listener may be removed earlier than the
63  * {@link #unregisterWindowContainerListener(IBinder)} if the listened {@link WindowContainer} was
64  * removed. An example is that the {@link DisplayArea} is removed when users unfold the
65  * foldable devices. Another example is that the associated external display is detached.</p>
66  */
67 class WindowContextListenerController {
68     @VisibleForTesting
69     final ArrayMap<IBinder, WindowContextListenerImpl> mListeners = new ArrayMap<>();
70 
71     /**
72      * @see #registerWindowContainerListener(IBinder, WindowContainer, int, int, Bundle, boolean)
73      */
registerWindowContainerListener(@onNull IBinder clientToken, @NonNull WindowContainer<?> container, int ownerUid, @WindowType int type, @Nullable Bundle options)74     void registerWindowContainerListener(@NonNull IBinder clientToken,
75             @NonNull WindowContainer<?> container, int ownerUid, @WindowType int type,
76             @Nullable Bundle options) {
77         registerWindowContainerListener(clientToken, container, ownerUid, type, options,
78                 true /* shouDispatchConfigWhenRegistering */);
79     }
80 
81     /**
82      * Registers the listener to a {@code container} which is associated with
83      * a {@code clientToken}, which is a {@link android.window.WindowContext} representation. If the
84      * listener associated with {@code clientToken} hasn't been initialized yet, create one
85      * {@link WindowContextListenerImpl}. Otherwise, the listener associated with
86      * {@code clientToken} switches to listen to the {@code container}.
87      *
88      * @param clientToken the token to associate with the listener
89      * @param container the {@link WindowContainer} which the listener is going to listen to.
90      * @param ownerUid the caller UID
91      * @param type the window type
92      * @param options a bundle used to pass window-related options.
93      * @param shouDispatchConfigWhenRegistering {@code true} to indicate the current
94      *                {@code container}'s config will dispatch to the client side when
95      *                registering the {@link WindowContextListenerImpl}
96      */
registerWindowContainerListener(@onNull IBinder clientToken, @NonNull WindowContainer<?> container, int ownerUid, @WindowType int type, @Nullable Bundle options, boolean shouDispatchConfigWhenRegistering)97     void registerWindowContainerListener(@NonNull IBinder clientToken,
98             @NonNull WindowContainer<?> container, int ownerUid, @WindowType int type,
99             @Nullable Bundle options, boolean shouDispatchConfigWhenRegistering) {
100         WindowContextListenerImpl listener = mListeners.get(clientToken);
101         if (listener == null) {
102             listener = new WindowContextListenerImpl(clientToken, container, ownerUid, type,
103                     options);
104             listener.register(shouDispatchConfigWhenRegistering);
105         } else {
106             listener.updateContainer(container);
107         }
108     }
109 
unregisterWindowContainerListener(IBinder clientToken)110     void unregisterWindowContainerListener(IBinder clientToken) {
111         final WindowContextListenerImpl listener = mListeners.get(clientToken);
112         // Listeners may be removed earlier. An example is the display where the listener is
113         // located is detached. In this case, all window containers on the display, as well as
114         // their listeners will be removed before their listeners are unregistered.
115         if (listener == null) {
116             return;
117         }
118         listener.unregister();
119         if (listener.mDeathRecipient != null) {
120             listener.mDeathRecipient.unlinkToDeath();
121         }
122     }
123 
dispatchPendingConfigurationIfNeeded(int displayId)124     void dispatchPendingConfigurationIfNeeded(int displayId) {
125         for (int i = mListeners.size() - 1; i >= 0; --i) {
126             final WindowContextListenerImpl listener = mListeners.valueAt(i);
127             if (listener.getWindowContainer().getDisplayContent().getDisplayId() == displayId
128                     && listener.mHasPendingConfiguration) {
129                 listener.reportConfigToWindowTokenClient();
130             }
131         }
132     }
133 
134     /**
135      * Verifies if the caller is allowed to do the operation to the listener specified by
136      * {@code clientToken}.
137      */
assertCallerCanModifyListener(IBinder clientToken, boolean callerCanManageAppTokens, int callingUid)138     boolean assertCallerCanModifyListener(IBinder clientToken, boolean callerCanManageAppTokens,
139             int callingUid) {
140         final WindowContextListenerImpl listener = mListeners.get(clientToken);
141         if (listener == null) {
142             ProtoLog.i(WM_DEBUG_ADD_REMOVE, "The listener does not exist.");
143             return false;
144         }
145         if (callerCanManageAppTokens) {
146             return true;
147         }
148         if (callingUid != listener.mOwnerUid) {
149             throw new UnsupportedOperationException("Uid mismatch. Caller uid is " + callingUid
150                     + ", while the listener's owner is from " + listener.mOwnerUid);
151         }
152         return true;
153     }
154 
hasListener(IBinder clientToken)155     boolean hasListener(IBinder clientToken) {
156         return mListeners.containsKey(clientToken);
157     }
158 
getWindowType(IBinder clientToken)159     @WindowType int getWindowType(IBinder clientToken) {
160         final WindowContextListenerImpl listener = mListeners.get(clientToken);
161         return listener != null ? listener.mType : INVALID_WINDOW_TYPE;
162     }
163 
getOptions(IBinder clientToken)164     @Nullable Bundle getOptions(IBinder clientToken) {
165         final WindowContextListenerImpl listener = mListeners.get(clientToken);
166         return listener != null ? listener.mOptions : null;
167     }
168 
getContainer(IBinder clientToken)169     @Nullable WindowContainer<?> getContainer(IBinder clientToken) {
170         final WindowContextListenerImpl listener = mListeners.get(clientToken);
171         return listener != null ? listener.mContainer : null;
172     }
173 
174     @Override
toString()175     public String toString() {
176         final StringBuilder builder = new StringBuilder("WindowContextListenerController{");
177         builder.append("mListeners=[");
178 
179         final int size = mListeners.values().size();
180         for (int i = 0; i < size; i++) {
181             builder.append(mListeners.valueAt(i));
182             if (i != size - 1) {
183                 builder.append(", ");
184             }
185         }
186         builder.append("]}");
187         return builder.toString();
188     }
189 
190     @VisibleForTesting
191     class WindowContextListenerImpl implements WindowContainerListener {
192         @NonNull private final IWindowToken mClientToken;
193         private final int mOwnerUid;
194         @NonNull private WindowContainer<?> mContainer;
195         /**
196          * The options from {@link Context#createWindowContext(int, Bundle)}.
197          * <p>It can be used for choosing the {@link DisplayArea} where the window context
198          * is located. </p>
199          */
200         @Nullable private final Bundle mOptions;
201         @WindowType private final int mType;
202 
203         private DeathRecipient mDeathRecipient;
204 
205         private int mLastReportedDisplay = INVALID_DISPLAY;
206         private Configuration mLastReportedConfig;
207 
208         private boolean mHasPendingConfiguration;
209 
WindowContextListenerImpl(IBinder clientToken, WindowContainer<?> container, int ownerUid, @WindowType int type, @Nullable Bundle options)210         private WindowContextListenerImpl(IBinder clientToken, WindowContainer<?> container,
211                 int ownerUid, @WindowType int type, @Nullable Bundle options) {
212             mClientToken = IWindowToken.Stub.asInterface(clientToken);
213             mContainer = Objects.requireNonNull(container);
214             mOwnerUid = ownerUid;
215             mType = type;
216             mOptions = options;
217 
218             final DeathRecipient deathRecipient = new DeathRecipient();
219             try {
220                 deathRecipient.linkToDeath();
221                 mDeathRecipient = deathRecipient;
222             } catch (RemoteException e) {
223                 ProtoLog.e(WM_ERROR, "Could not register window container listener token=%s, "
224                         + "container=%s", clientToken, mContainer);
225             }
226         }
227 
228         /** TEST ONLY: returns the {@link WindowContainer} of the listener */
229         @VisibleForTesting
getWindowContainer()230         WindowContainer<?> getWindowContainer() {
231             return mContainer;
232         }
233 
updateContainer(@onNull WindowContainer<?> newContainer)234         private void updateContainer(@NonNull WindowContainer<?> newContainer) {
235             Objects.requireNonNull(newContainer);
236 
237             if (mContainer.equals(newContainer)) {
238                 return;
239             }
240             mContainer.unregisterWindowContainerListener(this);
241             mContainer = newContainer;
242             clear();
243             register();
244         }
245 
register()246         private void register() {
247             register(true /* shouldDispatchConfig */);
248         }
249 
register(boolean shouldDispatchConfig)250         private void register(boolean shouldDispatchConfig) {
251             final IBinder token = mClientToken.asBinder();
252             if (mDeathRecipient == null) {
253                 throw new IllegalStateException("Invalid client token: " + token);
254             }
255             mListeners.putIfAbsent(token, this);
256             mContainer.registerWindowContainerListener(this, shouldDispatchConfig);
257         }
258 
unregister()259         private void unregister() {
260             mContainer.unregisterWindowContainerListener(this);
261             mListeners.remove(mClientToken.asBinder());
262         }
263 
clear()264         private void clear() {
265             mLastReportedConfig = null;
266             mLastReportedDisplay = INVALID_DISPLAY;
267         }
268 
269         @Override
onMergedOverrideConfigurationChanged(Configuration mergedOverrideConfig)270         public void onMergedOverrideConfigurationChanged(Configuration mergedOverrideConfig) {
271             reportConfigToWindowTokenClient();
272         }
273 
274         @Override
onDisplayChanged(DisplayContent dc)275         public void onDisplayChanged(DisplayContent dc) {
276             reportConfigToWindowTokenClient();
277         }
278 
reportConfigToWindowTokenClient()279         private void reportConfigToWindowTokenClient() {
280             if (mDeathRecipient == null) {
281                 throw new IllegalStateException("Invalid client token: " + mClientToken.asBinder());
282             }
283             final DisplayContent dc = mContainer.getDisplayContent();
284             if (!dc.isReady()) {
285                 // Do not report configuration when booting. The latest configuration will be sent
286                 // when WindowManagerService#displayReady().
287                 return;
288             }
289             // If the display of window context associated window container is suspended, don't
290             // report the configuration update. Note that we still dispatch the configuration update
291             // to WindowProviderService to make it compatible with Service#onConfigurationChanged.
292             // Service always receives #onConfigurationChanged callback regardless of display state.
293             if (!isWindowProviderService(mOptions) && isSuspendedState(dc.getDisplayInfo().state)) {
294                 mHasPendingConfiguration = true;
295                 return;
296             }
297             final Configuration config = mContainer.getConfiguration();
298             final int displayId = dc.getDisplayId();
299             if (mLastReportedConfig == null) {
300                 mLastReportedConfig = new Configuration();
301             }
302             if (config.equals(mLastReportedConfig) && displayId == mLastReportedDisplay) {
303                 // No changes since last reported time.
304                 return;
305             }
306 
307             mLastReportedConfig.setTo(config);
308             mLastReportedDisplay = displayId;
309 
310             try {
311                 mClientToken.onConfigurationChanged(config, displayId);
312             } catch (RemoteException e) {
313                 ProtoLog.w(WM_ERROR, "Could not report config changes to the window token client.");
314             }
315             mHasPendingConfiguration = false;
316         }
317 
318         @Override
onRemoved()319         public void onRemoved() {
320             if (mDeathRecipient == null) {
321                 throw new IllegalStateException("Invalid client token: " + mClientToken.asBinder());
322             }
323             final WindowToken windowToken = mContainer.asWindowToken();
324             if (windowToken != null && windowToken.isFromClient()) {
325                 // If the WindowContext created WindowToken is removed by
326                 // WMS#postWindowRemoveCleanupLocked, the WindowContext should switch back to
327                 // listen to previous associated DisplayArea.
328                 final DisplayContent dc = windowToken.mWmService.mRoot
329                         .getDisplayContent(mLastReportedDisplay);
330                 // If we cannot obtain the DisplayContent, the DisplayContent may also be removed.
331                 // We should proceed the removal process.
332                 if (dc != null) {
333                     final DisplayArea<?> da = dc.findAreaForToken(windowToken);
334                     updateContainer(da);
335                     return;
336                 }
337             }
338             mDeathRecipient.unlinkToDeath();
339             try {
340                 mClientToken.onWindowTokenRemoved();
341             } catch (RemoteException e) {
342                 ProtoLog.w(WM_ERROR, "Could not report token removal to the window token client.");
343             }
344             unregister();
345         }
346 
347         @Override
toString()348         public String toString() {
349             return "WindowContextListenerImpl{clientToken=" + mClientToken.asBinder() + ", "
350                     + "container=" + mContainer + "}";
351         }
352 
353         private class DeathRecipient implements IBinder.DeathRecipient {
354             @Override
binderDied()355             public void binderDied() {
356                 synchronized (mContainer.mWmService.mGlobalLock) {
357                     mDeathRecipient = null;
358                     unregister();
359                 }
360             }
361 
linkToDeath()362             void linkToDeath() throws RemoteException {
363                 mClientToken.asBinder().linkToDeath(this, 0);
364             }
365 
unlinkToDeath()366             void unlinkToDeath() {
367                 mClientToken.asBinder().unlinkToDeath(this, 0);
368             }
369         }
370     }
371 }
372