diff --git a/core/api/current.txt b/core/api/current.txt index 9a2c75fe490a2..d8d9a539b5cc7 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -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); 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 diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 30ff05284f3a2..a16d4ba7a3860 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -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 + * + * {@code android:resizableActivity="false"} 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 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 diff --git a/core/java/android/app/ActivityClient.java b/core/java/android/app/ActivityClient.java index 324b8e7a784f5..ce99119487103 100644 --- a/core/java/android/app/ActivityClient.java +++ b/core/java/android/app/ActivityClient.java @@ -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); diff --git a/core/java/android/app/ActivityTaskManager.java b/core/java/android/app/ActivityTaskManager.java index 7cfca979ed0f7..be8f48df4aa62 100644 --- a/core/java/android/app/ActivityTaskManager.java +++ b/core/java/android/app/ActivityTaskManager.java @@ -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 diff --git a/core/java/android/app/FullscreenRequestHandler.java b/core/java/android/app/FullscreenRequestHandler.java new file mode 100644 index 0000000000000..52f461daf233f --- /dev/null +++ b/core/java/android/app/FullscreenRequestHandler.java @@ -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 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 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; + } +} diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl index 8b655b9bf315e..286b84cef28a5 100644 --- a/core/java/android/app/IActivityClientController.aidl +++ b/core/java/android/app/IActivityClientController.aidl @@ -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); diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 33467404e38fe..529f82114b83b 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -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", diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index af430f99cf588..b70c8b0d58f48 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -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) { diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 6d4a526fd9543..8295b799c3f66 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -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