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.server.wm;
18 
19 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.os.RemoteException;
24 import android.util.Slog;
25 import android.view.IDisplayChangeWindowCallback;
26 import android.window.DisplayAreaInfo;
27 import android.window.WindowContainerTransaction;
28 
29 import com.android.internal.annotations.VisibleForTesting;
30 import com.android.internal.protolog.common.ProtoLog;
31 
32 import java.util.ArrayList;
33 import java.util.List;
34 
35 /**
36  * A helper class, a wrapper around {@link android.view.IDisplayChangeWindowController} to perform
37  * a synchronous display change in other parts (e.g. in the Shell) and continue the process
38  * in the system server. It handles timeouts and multiple requests.
39  * We have an instance of this controller for each display.
40  */
41 public class RemoteDisplayChangeController {
42 
43     private static final String TAG = "RemoteDisplayChangeController";
44 
45     private static final int REMOTE_DISPLAY_CHANGE_TIMEOUT_MS = 800;
46 
47     private final WindowManagerService mService;
48     private final DisplayContent mDisplayContent;
49 
50     private final Runnable mTimeoutRunnable = this::onContinueTimedOut;
51 
52     // all remote changes that haven't finished yet.
53     private final List<ContinueRemoteDisplayChangeCallback> mCallbacks = new ArrayList<>();
54 
RemoteDisplayChangeController(@onNull DisplayContent displayContent)55     RemoteDisplayChangeController(@NonNull DisplayContent displayContent) {
56         mService = displayContent.mWmService;
57         mDisplayContent = displayContent;
58     }
59 
60     /**
61      * A Remote change is when we are waiting for some registered (remote)
62      * {@link IDisplayChangeWindowController} to calculate and return some hierarchy operations
63      *  to perform in sync with the display change.
64      */
isWaitingForRemoteDisplayChange()65     public boolean isWaitingForRemoteDisplayChange() {
66         return !mCallbacks.isEmpty();
67     }
68 
69     /**
70      * Starts remote display change
71      * @param fromRotation rotation before the change
72      * @param toRotation rotation after the change
73      * @param newDisplayAreaInfo display area info after change
74      * @param callback that will be called after completing remote display change
75      * @return true if the change successfully started, false otherwise
76      */
performRemoteDisplayChange( int fromRotation, int toRotation, @Nullable DisplayAreaInfo newDisplayAreaInfo, ContinueRemoteDisplayChangeCallback callback)77     public boolean performRemoteDisplayChange(
78             int fromRotation, int toRotation,
79             @Nullable DisplayAreaInfo newDisplayAreaInfo,
80             ContinueRemoteDisplayChangeCallback callback) {
81         if (mService.mDisplayChangeController == null) {
82             return false;
83         }
84         mCallbacks.add(callback);
85 
86         if (newDisplayAreaInfo != null) {
87             ProtoLog.v(WM_DEBUG_CONFIGURATION,
88                     "Starting remote display change: "
89                             + "from [rot = %d], "
90                             + "to [%dx%d, rot = %d]",
91                     fromRotation,
92                     newDisplayAreaInfo.configuration.windowConfiguration
93                             .getMaxBounds().width(),
94                     newDisplayAreaInfo.configuration.windowConfiguration
95                             .getMaxBounds().height(),
96                     toRotation);
97         }
98 
99         final IDisplayChangeWindowCallback remoteCallback = createCallback(callback);
100         try {
101             mService.mH.removeCallbacks(mTimeoutRunnable);
102             mService.mH.postDelayed(mTimeoutRunnable, REMOTE_DISPLAY_CHANGE_TIMEOUT_MS);
103             mService.mDisplayChangeController.onDisplayChange(mDisplayContent.mDisplayId,
104                     fromRotation, toRotation, newDisplayAreaInfo, remoteCallback);
105             return true;
106         } catch (RemoteException e) {
107             Slog.e(TAG, "Exception while dispatching remote display-change", e);
108             mCallbacks.remove(callback);
109             return false;
110         }
111     }
112 
onContinueTimedOut()113     private void onContinueTimedOut() {
114         Slog.e(TAG, "RemoteDisplayChange timed-out, UI might get messed-up after this.");
115         // timed-out, so run all continue callbacks and clear the list
116         synchronized (mService.mGlobalLock) {
117             for (int i = 0; i < mCallbacks.size(); ++i) {
118                 mCallbacks.get(i).onContinueRemoteDisplayChange(null /* transaction */);
119             }
120             mCallbacks.clear();
121             onCompleted();
122         }
123     }
124 
125     /** Called when all remote callbacks are done. */
onCompleted()126     private void onCompleted() {
127         // Because DisplayContent#sendNewConfiguration() will be skipped if there are pending remote
128         // changes, check again when all remote callbacks are done. E.g. callback X is done but
129         // there is a pending callback Y so its invocation is skipped, and when the callback Y is
130         // done, it doesn't call sendNewConfiguration().
131         if (mDisplayContent.mWaitingForConfig) {
132             mDisplayContent.sendNewConfiguration();
133         }
134     }
135 
136     @VisibleForTesting
continueDisplayChange(@onNull ContinueRemoteDisplayChangeCallback callback, @Nullable WindowContainerTransaction transaction)137     void continueDisplayChange(@NonNull ContinueRemoteDisplayChangeCallback callback,
138             @Nullable WindowContainerTransaction transaction) {
139         synchronized (mService.mGlobalLock) {
140             int idx = mCallbacks.indexOf(callback);
141             if (idx < 0) {
142                 // already called this callback or a more-recent one (eg. via timeout)
143                 return;
144             }
145             for (int i = 0; i < idx; ++i) {
146                 // Expect remote callbacks in order. If they don't come in order, then force
147                 // ordering by continuing everything up until this one with empty transactions.
148                 mCallbacks.get(i).onContinueRemoteDisplayChange(null /* transaction */);
149             }
150             // The "toIndex" is exclusive, so it needs +1 to clear the current calling callback.
151             mCallbacks.subList(0, idx + 1).clear();
152             final boolean completed = mCallbacks.isEmpty();
153             if (completed) {
154                 mService.mH.removeCallbacks(mTimeoutRunnable);
155             }
156             callback.onContinueRemoteDisplayChange(transaction);
157             if (completed) {
158                 onCompleted();
159             }
160         }
161     }
162 
createCallback( @onNull ContinueRemoteDisplayChangeCallback callback)163     private IDisplayChangeWindowCallback createCallback(
164             @NonNull ContinueRemoteDisplayChangeCallback callback) {
165         return new IDisplayChangeWindowCallback.Stub() {
166                     @Override
167                     public void continueDisplayChange(WindowContainerTransaction t) {
168                         synchronized (mService.mGlobalLock) {
169                             if (!mCallbacks.contains(callback)) {
170                                 // already ran this callback or a more-recent one.
171                                 return;
172                             }
173                             mService.mH.post(() -> RemoteDisplayChangeController.this
174                                     .continueDisplayChange(callback, t));
175                         }
176                     }
177                 };
178     }
179 
180     /**
181      * Callback interface to handle continuation of the remote display change
182      */
183     public interface ContinueRemoteDisplayChangeCallback {
184         /**
185          * This method is called when the remote display change has been applied
186          * @param transaction window changes collected by the remote display change
187          */
188         void onContinueRemoteDisplayChange(@Nullable WindowContainerTransaction transaction);
189     }
190 }
191