blob: 4d3b95b720736950654a00cec49454d477279893 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.wm;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.RemoteException;
import android.os.Trace;
import android.util.Slog;
import android.view.IDisplayChangeWindowCallback;
import android.window.DisplayAreaInfo;
import android.window.WindowContainerTransaction;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
import java.util.ArrayList;
import java.util.List;
/**
* A helper class, a wrapper around {@link android.view.IDisplayChangeWindowController} to perform
* a synchronous display change in other parts (e.g. in the Shell) and continue the process
* in the system server. It handles timeouts and multiple requests.
* We have an instance of this controller for each display.
*/
public class RemoteDisplayChangeController {
private static final String TAG = "RemoteDisplayChangeController";
private static final String REMOTE_DISPLAY_CHANGE_TRACE_TAG = "RemoteDisplayChange";
private static final int REMOTE_DISPLAY_CHANGE_TIMEOUT_MS = 800;
private final WindowManagerService mService;
private final DisplayContent mDisplayContent;
private final Runnable mTimeoutRunnable = this::onContinueTimedOut;
// all remote changes that haven't finished yet.
private final List<ContinueRemoteDisplayChangeCallback> mCallbacks = new ArrayList<>();
RemoteDisplayChangeController(@NonNull DisplayContent displayContent) {
mService = displayContent.mWmService;
mDisplayContent = displayContent;
}
/**
* A Remote change is when we are waiting for some registered (remote)
* {@link IDisplayChangeWindowController} to calculate and return some hierarchy operations
* to perform in sync with the display change.
*/
public boolean isWaitingForRemoteDisplayChange() {
return !mCallbacks.isEmpty();
}
/**
* Starts remote display change
* @param fromRotation rotation before the change
* @param toRotation rotation after the change
* @param newDisplayAreaInfo display area info after change
* @param callback that will be called after completing remote display change
* @return true if the change successfully started, false otherwise
*/
public boolean performRemoteDisplayChange(
int fromRotation, int toRotation,
@Nullable DisplayAreaInfo newDisplayAreaInfo,
ContinueRemoteDisplayChangeCallback callback) {
if (mService.mDisplayChangeController == null) {
return false;
}
mCallbacks.add(callback);
if (Trace.isTagEnabled(Trace.TRACE_TAG_WINDOW_MANAGER)) {
Trace.beginAsyncSection(REMOTE_DISPLAY_CHANGE_TRACE_TAG, callback.hashCode());
}
if (newDisplayAreaInfo != null) {
ProtoLog.v(WM_DEBUG_CONFIGURATION,
"Starting remote display change: "
+ "from [rot = %d], "
+ "to [%dx%d, rot = %d]",
fromRotation,
newDisplayAreaInfo.configuration.windowConfiguration
.getMaxBounds().width(),
newDisplayAreaInfo.configuration.windowConfiguration
.getMaxBounds().height(),
toRotation);
}
final IDisplayChangeWindowCallback remoteCallback = createCallback(callback);
try {
mService.mH.removeCallbacks(mTimeoutRunnable);
mService.mH.postDelayed(mTimeoutRunnable, REMOTE_DISPLAY_CHANGE_TIMEOUT_MS);
mService.mDisplayChangeController.onDisplayChange(mDisplayContent.mDisplayId,
fromRotation, toRotation, newDisplayAreaInfo, remoteCallback);
return true;
} catch (RemoteException e) {
Slog.e(TAG, "Exception while dispatching remote display-change", e);
mCallbacks.remove(callback);
return false;
}
}
private void onContinueTimedOut() {
Slog.e(TAG, "RemoteDisplayChange timed-out, UI might get messed-up after this.");
// timed-out, so run all continue callbacks and clear the list
synchronized (mService.mGlobalLock) {
for (int i = 0; i < mCallbacks.size(); ++i) {
final ContinueRemoteDisplayChangeCallback callback = mCallbacks.get(i);
if (i == mCallbacks.size() - 1) {
// Clear all callbacks before calling the last one, so that if the callback
// itself calls {@link #isWaitingForRemoteDisplayChange()}, it will get
// {@code false}. After all, there is nothing pending after this one.
mCallbacks.clear();
}
callback.onContinueRemoteDisplayChange(null /* transaction */);
if (Trace.isTagEnabled(Trace.TRACE_TAG_WINDOW_MANAGER)) {
Trace.endAsyncSection(REMOTE_DISPLAY_CHANGE_TRACE_TAG, callback.hashCode());
}
}
onCompleted();
}
}
/** Called when all remote callbacks are done. */
private void onCompleted() {
// Because DisplayContent#sendNewConfiguration() will be skipped if there are pending remote
// changes, check again when all remote callbacks are done. E.g. callback X is done but
// there is a pending callback Y so its invocation is skipped, and when the callback Y is
// done, it doesn't call sendNewConfiguration().
if (mDisplayContent.mWaitingForConfig) {
mDisplayContent.sendNewConfiguration();
}
}
@VisibleForTesting
void continueDisplayChange(@NonNull ContinueRemoteDisplayChangeCallback callback,
@Nullable WindowContainerTransaction transaction) {
synchronized (mService.mGlobalLock) {
int idx = mCallbacks.indexOf(callback);
if (idx < 0) {
// already called this callback or a more-recent one (eg. via timeout)
return;
}
for (int i = 0; i < idx; ++i) {
// Expect remote callbacks in order. If they don't come in order, then force
// ordering by continuing everything up until this one with empty transactions.
ContinueRemoteDisplayChangeCallback currentCallback = mCallbacks.get(i);
currentCallback.onContinueRemoteDisplayChange(null /* transaction */);
if (Trace.isTagEnabled(Trace.TRACE_TAG_WINDOW_MANAGER)) {
Trace.endAsyncSection(REMOTE_DISPLAY_CHANGE_TRACE_TAG,
currentCallback.hashCode());
}
}
// The "toIndex" is exclusive, so it needs +1 to clear the current calling callback.
mCallbacks.subList(0, idx + 1).clear();
final boolean completed = mCallbacks.isEmpty();
if (completed) {
mService.mH.removeCallbacks(mTimeoutRunnable);
}
callback.onContinueRemoteDisplayChange(transaction);
if (completed) {
onCompleted();
}
if (Trace.isTagEnabled(Trace.TRACE_TAG_WINDOW_MANAGER)) {
Trace.endAsyncSection(REMOTE_DISPLAY_CHANGE_TRACE_TAG, callback.hashCode());
}
}
}
private IDisplayChangeWindowCallback createCallback(
@NonNull ContinueRemoteDisplayChangeCallback callback) {
return new IDisplayChangeWindowCallback.Stub() {
@Override
public void continueDisplayChange(WindowContainerTransaction t) {
synchronized (mService.mGlobalLock) {
if (!mCallbacks.contains(callback)) {
// already ran this callback or a more-recent one.
return;
}
mService.mH.post(() -> RemoteDisplayChangeController.this
.continueDisplayChange(callback, t));
}
}
};
}
/**
* Callback interface to handle continuation of the remote display change
*/
public interface ContinueRemoteDisplayChangeCallback {
/**
* This method is called when the remote display change has been applied
* @param transaction window changes collected by the remote display change
*/
void onContinueRemoteDisplayChange(@Nullable WindowContainerTransaction transaction);
}
}