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