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