Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 55981b3c authored by Yunfan Chen's avatar Yunfan Chen
Browse files

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
parent 2c67300e
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -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
+42 −0
Original line number Diff line number Diff line
@@ -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
+9 −0
Original line number Diff line number Diff line
@@ -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);
+6 −0
Original line number Diff line number Diff line
@@ -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
+123 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
Loading