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