Introduce Multiwindow Fullscreen API

An API to let app launched in default freeform windowing mode request
entering fullscreen windowing mode. This is useful to let apps, such as
video streaming apps, to provide a unified and immersive experience to
their users.

The request will only be approved if the freeform window is not
requested by the user, i.e., the app is launched in default freeform
windowing mode. If the user get the app from recent screen, or the app
is in other multi-window mode involves user interaction, such as
split-screen, the request will be rejected, and a corresponding callback
will be sent to the app.

The app can provide an optional consumer to receive callback from the
system to learn about the result of the validation.

When the WM core approves the request, a shell transition will be
initialted and the shell will have control of the final result and the
animation of the transition. When shell transition is not enabled, a
direct windowing mode change will be applied.

Bug: 222454650
Test: atest CtsWindowManagerDeviceTestCases:FreeformWindowingModeTests
Change-Id: I921e230ad08b0b2ce6df9e2ad6e01377cb459f3d
This commit is contained in:
Yunfan Chen
2022-11-30 17:14:57 +09:00
parent 2c67300e30
commit 55981b3cbd
9 changed files with 334 additions and 0 deletions

View File

@@ -4291,6 +4291,7 @@ package android.app {
method @Deprecated public final void removeDialog(int);
method public void reportFullyDrawn();
method public android.view.DragAndDropPermissions requestDragAndDropPermissions(android.view.DragEvent);
method public void requestFullscreenMode(@NonNull int, @Nullable android.os.OutcomeReceiver<java.lang.Void,java.lang.Throwable>);
method public final void requestPermissions(@NonNull String[], int);
method public final void requestShowKeyboardShortcuts();
method @Deprecated public boolean requestVisibleBehind(boolean);
@@ -4377,6 +4378,8 @@ package android.app {
field public static final int DEFAULT_KEYS_SEARCH_LOCAL = 3; // 0x3
field public static final int DEFAULT_KEYS_SHORTCUT = 2; // 0x2
field protected static final int[] FOCUSED_STATE_SET;
field public static final int FULLSCREEN_MODE_REQUEST_ENTER = 1; // 0x1
field public static final int FULLSCREEN_MODE_REQUEST_EXIT = 0; // 0x0
field public static final int RESULT_CANCELED = 0; // 0x0
field public static final int RESULT_FIRST_USER = 1; // 0x1
field public static final int RESULT_OK = -1; // 0xffffffff

View File

@@ -83,6 +83,7 @@ import android.os.GraphicsEnvironment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.OutcomeReceiver;
import android.os.Parcelable;
import android.os.PersistableBundle;
import android.os.Process;
@@ -987,6 +988,17 @@ public class Activity extends ContextThemeWrapper
/** @hide */
boolean mIsInPictureInPictureMode;
/** @hide */
@IntDef(prefix = { "FULLSCREEN_REQUEST_" }, value = {
FULLSCREEN_MODE_REQUEST_EXIT,
FULLSCREEN_MODE_REQUEST_ENTER
})
public @interface FullscreenModeRequest {}
public static final int FULLSCREEN_MODE_REQUEST_EXIT = 0;
public static final int FULLSCREEN_MODE_REQUEST_ENTER = 1;
private boolean mShouldDockBigOverlays;
private UiTranslationController mUiTranslationController;
@@ -3000,6 +3012,36 @@ public class Activity extends ContextThemeWrapper
return false;
}
/**
* Request to put the a freeform activity into fullscreen. This will only be allowed if the
* activity is on a freeform display, such as a desktop device. The requester has to be the
* top-most activity and the request should be a response to a user input. When getting
* fullscreen and receiving corresponding {@link #onConfigurationChanged(Configuration)} and
* {@link #onMultiWindowModeChanged(boolean, Configuration)}, the activity should relayout
* itself and the system bars' visibilities can be controlled as usual fullscreen apps.
*
* Calling it again with the exit request can restore the activity to the previous status.
* This will only happen when it got into fullscreen through this API.
*
* If an app wants to be in fullscreen always, it should claim as not being resizable
* by setting
* <a href="https://developer.android.com/guide/topics/large-screens/multi-window-support#resizeableActivity">
* {@code android:resizableActivity="false"}</a> instead of calling this API.
*
* @param request Can be {@link #FULLSCREEN_MODE_REQUEST_ENTER} or
* {@link #FULLSCREEN_MODE_REQUEST_EXIT} to indicate this request is to get
* fullscreen or get restored.
* @param approvalCallback Optional callback, use {@code null} when not necessary. When the
* request is approved or rejected, the callback will be triggered. This
* will happen before any configuration change. The callback will be
* dispatched on the main thread.
*/
public void requestFullscreenMode(@NonNull @FullscreenModeRequest int request,
@Nullable OutcomeReceiver<Void, Throwable> approvalCallback) {
FullscreenRequestHandler.requestFullscreenMode(
request, approvalCallback, mCurrentConfig, getActivityToken());
}
/**
* Specifies a preference to dock big overlays like the expanded picture-in-picture on TV
* (see {@link PictureInPictureParams.Builder#setExpandedAspectRatio}). Docking puts the

View File

@@ -24,6 +24,7 @@ import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.IBinder;
import android.os.IRemoteCallback;
import android.os.PersistableBundle;
import android.os.RemoteException;
import android.util.Singleton;
@@ -372,6 +373,14 @@ public class ActivityClient {
}
}
void requestMultiwindowFullscreen(IBinder token, int request, IRemoteCallback callback) {
try {
getActivityClientController().requestMultiwindowFullscreen(token, request, callback);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
void startLockTaskModeByToken(IBinder token) {
try {
getActivityClientController().startLockTaskModeByToken(token);

View File

@@ -61,6 +61,12 @@ public class ActivityTaskManager {
*/
public static final int INVALID_TASK_ID = -1;
/**
* Invalid windowing mode.
* @hide
*/
public static final int INVALID_WINDOWING_MODE = -1;
/**
* Input parameter to {@link IActivityTaskManager#resizeTask} which indicates
* that the resize doesn't need to preserve the window, and can be skipped if bounds

View File

@@ -0,0 +1,123 @@
/*
* 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 android.app;
import static android.app.Activity.FULLSCREEN_MODE_REQUEST_ENTER;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.IBinder;
import android.os.IRemoteCallback;
import android.os.OutcomeReceiver;
/**
* @hide
*/
public class FullscreenRequestHandler {
@IntDef(prefix = { "RESULT_" }, value = {
RESULT_APPROVED,
RESULT_FAILED_NOT_IN_FREEFORM,
RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY,
RESULT_FAILED_NOT_DEFAULT_FREEFORM,
RESULT_FAILED_NOT_TOP_FOCUSED
})
public @interface RequestResult {}
public static final int RESULT_APPROVED = 0;
public static final int RESULT_FAILED_NOT_IN_FREEFORM = 1;
public static final int RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY = 2;
public static final int RESULT_FAILED_NOT_DEFAULT_FREEFORM = 3;
public static final int RESULT_FAILED_NOT_TOP_FOCUSED = 4;
public static final String REMOTE_CALLBACK_RESULT_KEY = "result";
static void requestFullscreenMode(@NonNull @Activity.FullscreenModeRequest int request,
@Nullable OutcomeReceiver<Void, Throwable> approvalCallback, Configuration config,
IBinder token) {
int earlyCheck = earlyCheckRequestMatchesWindowingMode(
request, config.windowConfiguration.getWindowingMode());
if (earlyCheck != RESULT_APPROVED) {
if (approvalCallback != null) {
notifyFullscreenRequestResult(approvalCallback, earlyCheck);
}
return;
}
try {
if (approvalCallback != null) {
ActivityClient.getInstance().requestMultiwindowFullscreen(token, request,
new IRemoteCallback.Stub() {
@Override
public void sendResult(Bundle res) {
notifyFullscreenRequestResult(
approvalCallback, res.getInt(REMOTE_CALLBACK_RESULT_KEY));
}
});
} else {
ActivityClient.getInstance().requestMultiwindowFullscreen(token, request, null);
}
} catch (Throwable e) {
if (approvalCallback != null) {
approvalCallback.onError(e);
}
}
}
private static void notifyFullscreenRequestResult(
OutcomeReceiver<Void, Throwable> callback, int result) {
Throwable e = null;
switch (result) {
case RESULT_FAILED_NOT_IN_FREEFORM:
e = new IllegalStateException("The window is not a freeform window, the request "
+ "to get into fullscreen cannot be approved.");
break;
case RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY:
e = new IllegalStateException("The window is not in fullscreen by calling the "
+ "requestFullscreenMode API before, such that cannot be restored.");
break;
case RESULT_FAILED_NOT_DEFAULT_FREEFORM:
e = new IllegalStateException("The window is not launched in freeform by default.");
break;
case RESULT_FAILED_NOT_TOP_FOCUSED:
e = new IllegalStateException("The window is not the top focused window.");
break;
default:
callback.onResult(null);
break;
}
if (e != null) {
callback.onError(e);
}
}
private static int earlyCheckRequestMatchesWindowingMode(int request, int windowingMode) {
if (request == FULLSCREEN_MODE_REQUEST_ENTER) {
if (windowingMode != WINDOWING_MODE_FREEFORM) {
return RESULT_FAILED_NOT_IN_FREEFORM;
}
} else {
if (windowingMode != WINDOWING_MODE_FULLSCREEN) {
return RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY;
}
}
return RESULT_APPROVED;
}
}

View File

@@ -24,6 +24,7 @@ import android.content.ComponentName;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.IRemoteCallback;
import android.os.PersistableBundle;
import android.view.RemoteAnimationDefinition;
import android.window.SizeConfigurationBuckets;
@@ -102,6 +103,8 @@ interface IActivityClientController {
void setPictureInPictureParams(in IBinder token, in PictureInPictureParams params);
oneway void setShouldDockBigOverlays(in IBinder token, in boolean shouldDockBigOverlays);
void toggleFreeformWindowingMode(in IBinder token);
oneway void requestMultiwindowFullscreen(in IBinder token, in int request,
in IRemoteCallback callback);
oneway void startLockTaskModeByToken(in IBinder token);
oneway void stopLockTaskModeByToken(in IBinder token);

View File

@@ -619,6 +619,12 @@
"group": "WM_DEBUG_CONFIGURATION",
"at": "com\/android\/server\/wm\/ActivityStarter.java"
},
"-1484988952": {
"message": "Creating Pending Multiwindow Fullscreen Request: %s",
"level": "VERBOSE",
"group": "WM_DEBUG_WINDOW_TRANSITIONS",
"at": "com\/android\/server\/wm\/ActivityClientController.java"
},
"-1483435730": {
"message": "InsetsSource setWin %s for type %s",
"level": "DEBUG",

View File

@@ -17,7 +17,15 @@
package com.android.server.wm;
import static android.Manifest.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS;
import static android.app.Activity.FULLSCREEN_MODE_REQUEST_ENTER;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.app.ActivityTaskManager.INVALID_WINDOWING_MODE;
import static android.app.FullscreenRequestHandler.REMOTE_CALLBACK_RESULT_KEY;
import static android.app.FullscreenRequestHandler.RESULT_APPROVED;
import static android.app.FullscreenRequestHandler.RESULT_FAILED_NOT_DEFAULT_FREEFORM;
import static android.app.FullscreenRequestHandler.RESULT_FAILED_NOT_IN_FREEFORM;
import static android.app.FullscreenRequestHandler.RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY;
import static android.app.FullscreenRequestHandler.RESULT_FAILED_NOT_TOP_FOCUSED;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
@@ -27,6 +35,7 @@ import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
import static android.service.voice.VoiceInteractionSession.SHOW_SOURCE_APPLICATION;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
@@ -52,6 +61,7 @@ import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.app.FullscreenRequestHandler;
import android.app.IActivityClientController;
import android.app.ICompatCameraControlCallback;
import android.app.IRequestFinishCallback;
@@ -71,6 +81,7 @@ import android.content.res.Configuration;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.IRemoteCallback;
import android.os.Parcel;
import android.os.PersistableBundle;
import android.os.RemoteException;
@@ -85,6 +96,7 @@ import android.window.TransitionInfo;
import com.android.internal.app.AssistUtils;
import com.android.internal.policy.IKeyguardDismissCallback;
import com.android.internal.protolog.ProtoLogGroup;
import com.android.internal.protolog.common.ProtoLog;
import com.android.internal.util.FrameworkStatsLog;
import com.android.server.LocalServices;
@@ -1056,6 +1068,133 @@ class ActivityClientController extends IActivityClientController.Stub {
}
}
private @FullscreenRequestHandler.RequestResult int validateMultiwindowFullscreenRequestLocked(
Task topFocusedRootTask, int fullscreenRequest, ActivityRecord requesterActivity) {
// If the mode is not by default freeform, the freeform will be a user-driven event.
if (topFocusedRootTask.getParent().getWindowingMode() != WINDOWING_MODE_FREEFORM) {
return RESULT_FAILED_NOT_DEFAULT_FREEFORM;
}
// If this is not coming from the currently top-most activity, reject the request.
if (requesterActivity != topFocusedRootTask.getTopMostActivity()) {
return RESULT_FAILED_NOT_TOP_FOCUSED;
}
if (fullscreenRequest == FULLSCREEN_MODE_REQUEST_ENTER) {
if (topFocusedRootTask.getWindowingMode() != WINDOWING_MODE_FREEFORM) {
return RESULT_FAILED_NOT_IN_FREEFORM;
}
} else {
if (topFocusedRootTask.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
return RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY;
}
if (topFocusedRootTask.mMultiWindowRestoreWindowingMode == INVALID_WINDOWING_MODE) {
return RESULT_FAILED_NOT_IN_FULLSCREEN_WITH_HISTORY;
}
}
return RESULT_APPROVED;
}
@Override
public void requestMultiwindowFullscreen(IBinder callingActivity, int fullscreenRequest,
IRemoteCallback callback) {
final long ident = Binder.clearCallingIdentity();
try {
synchronized (mGlobalLock) {
requestMultiwindowFullscreenLocked(callingActivity, fullscreenRequest, callback);
}
} finally {
Binder.restoreCallingIdentity(ident);
}
}
private void requestMultiwindowFullscreenLocked(IBinder callingActivity, int fullscreenRequest,
IRemoteCallback callback) {
final ActivityRecord r = ActivityRecord.forTokenLocked(callingActivity);
if (r == null) {
return;
}
// If the shell transition is not enabled, just execute and done.
final TransitionController controller = r.mTransitionController;
if (!controller.isShellTransitionsEnabled()) {
final @FullscreenRequestHandler.RequestResult int validateResult;
final Task topFocusedRootTask;
topFocusedRootTask = mService.getTopDisplayFocusedRootTask();
validateResult = validateMultiwindowFullscreenRequestLocked(topFocusedRootTask,
fullscreenRequest, r);
reportMultiwindowFullscreenRequestValidatingResult(callback, validateResult);
if (validateResult == RESULT_APPROVED) {
executeMultiWindowFullscreenRequest(fullscreenRequest, topFocusedRootTask);
}
return;
}
// Initiate the transition.
final Transition transition = new Transition(TRANSIT_CHANGE, 0 /* flags */, controller,
mService.mWindowManager.mSyncEngine);
if (mService.mWindowManager.mSyncEngine.hasActiveSync()) {
ProtoLog.v(ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS,
"Creating Pending Multiwindow Fullscreen Request: %s", transition);
mService.mWindowManager.mSyncEngine.queueSyncSet(
() -> r.mTransitionController.moveToCollecting(transition),
() -> {
executeFullscreenRequestTransition(fullscreenRequest, callback, r,
transition, true /* queued */);
});
} else {
executeFullscreenRequestTransition(fullscreenRequest, callback, r, transition,
false /* queued */);
}
}
private void executeFullscreenRequestTransition(int fullscreenRequest, IRemoteCallback callback,
ActivityRecord r, Transition transition, boolean queued) {
final @FullscreenRequestHandler.RequestResult int validateResult;
final Task topFocusedRootTask;
topFocusedRootTask = mService.getTopDisplayFocusedRootTask();
validateResult = validateMultiwindowFullscreenRequestLocked(topFocusedRootTask,
fullscreenRequest, r);
reportMultiwindowFullscreenRequestValidatingResult(callback, validateResult);
if (validateResult != RESULT_APPROVED) {
if (queued) {
transition.abort();
}
return;
}
r.mTransitionController.requestStartTransition(transition, topFocusedRootTask,
null /* remoteTransition */, null /* displayChange */);
executeMultiWindowFullscreenRequest(fullscreenRequest, topFocusedRootTask);
transition.setReady(topFocusedRootTask, true);
}
private static void reportMultiwindowFullscreenRequestValidatingResult(IRemoteCallback callback,
@FullscreenRequestHandler.RequestResult int result) {
if (callback == null) {
return;
}
Bundle res = new Bundle();
res.putInt(REMOTE_CALLBACK_RESULT_KEY, result);
try {
callback.sendResult(res);
} catch (RemoteException e) {
Slog.w(TAG, "client throws an exception back to the server, ignore it");
}
}
private static void executeMultiWindowFullscreenRequest(int fullscreenRequest, Task requester) {
final int targetWindowingMode;
if (fullscreenRequest == FULLSCREEN_MODE_REQUEST_ENTER) {
requester.mMultiWindowRestoreWindowingMode =
requester.getRequestedOverrideWindowingMode();
targetWindowingMode = WINDOWING_MODE_FULLSCREEN;
} else {
targetWindowingMode = requester.mMultiWindowRestoreWindowingMode;
requester.mMultiWindowRestoreWindowingMode = INVALID_WINDOWING_MODE;
}
requester.setWindowingMode(targetWindowingMode);
if (targetWindowingMode == WINDOWING_MODE_FULLSCREEN) {
requester.setBounds(null);
}
}
@Override
public void startLockTaskModeByToken(IBinder token) {
synchronized (mGlobalLock) {

View File

@@ -18,6 +18,7 @@ package com.android.server.wm;
import static android.app.ActivityManager.isStartResultSuccessful;
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.app.ActivityTaskManager.INVALID_WINDOWING_MODE;
import static android.app.ActivityTaskManager.RESIZE_MODE_FORCED;
import static android.app.ActivityTaskManager.RESIZE_MODE_SYSTEM_SCREEN_ROTATION;
import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN;
@@ -443,6 +444,8 @@ class Task extends TaskFragment {
@Surface.Rotation
private int mRotation;
int mMultiWindowRestoreWindowingMode = INVALID_WINDOWING_MODE;
/**
* Last requested orientation reported to DisplayContent. This is different from {@link
* #mOrientation} in the sense that this takes activities' requested orientation into