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

Commit 1d47d8a4 authored by Kacper Kapłon's avatar Kacper Kapłon Committed by Android (Google) Code Review
Browse files

Merge changes I94cdc174,Ie3b5ec02 into main

* changes:
  Add #requestWindowingLayer() into IAppTask aidl
  Add AppTask#requestWindowingLayer api definition
parents fa2986b6 4a5d13dd
Loading
Loading
Loading
Loading
+56 −0
Original line number Diff line number Diff line
@@ -6312,6 +6312,38 @@ public class ActivityManager {
     * See {@link android.app.ActivityManager#getAppTasks()}
     */
    public static class AppTask {

        /**
         * The windowing layer is not specified. The system will use a
         * {@link #WINDOWING_LAYER_NORMAL_APP} layer.
         * @hide
         */
        public static final int WINDOWING_LAYER_UNDEFINED = 0;
        /**
         * The windowing layer for normal application windows.
         * @hide
         */
        public static final int WINDOWING_LAYER_NORMAL_APP = 1;
        /**
         * The windowing layer for pinned windows, these windows are typically displayed above
         * normal application windows.
         * @hide
         */
        public static final int WINDOWING_LAYER_PINNED = 2;

        /**
         * Defines the windowing layer for a task, which can affect its Z-ordering.
         * @hide
         */
        @IntDef(prefix = { "WINDOWING_LAYER_" }, value = {
                WINDOWING_LAYER_UNDEFINED,
                WINDOWING_LAYER_NORMAL_APP,
                WINDOWING_LAYER_PINNED,
        })
        @Retention(RetentionPolicy.SOURCE)
        public @interface WindowingLayer {
        }

        private IAppTask mAppTaskImpl;

        /** @hide */
@@ -6458,6 +6490,30 @@ public class ActivityManager {
                throw e.rethrowFromSystemServer();
            }
        }

        /**
         * Requests the windowing layer for this task. This can be used to affect the Z-ordering
         * of the activity's window relative to other windows.
         *
         * <p>
         * The task will be moved to the requested layer if possible.
         *
         * @param layer the {@link WindowingLayer} to move task to.
         * @param executor an Executor used to invoke the callback
         * @param callback a callback to receive the result of the request
         * @hide
         */
        // TODO(b/442807136): Complete javadoc, add all requirements and detals needed
        public void requestWindowingLayer(
                @WindowingLayer int layer,
                @NonNull @CallbackExecutor Executor executor,
                @NonNull OutcomeReceiver<Void, Exception> callback) {
            Objects.requireNonNull(executor, "executor cannot be null");
            Objects.requireNonNull(callback, "callback cannot be null");
            TaskWindowingLayerRequestHandler.requestWindowingLayer(
                    layer, executor, callback, mAppTaskImpl
            );
        }
    }

    /**
+1 −0
Original line number Diff line number Diff line
@@ -33,4 +33,5 @@ interface IAppTask {
    int startActivity(IBinder whoThread, String callingPackage, String callingFeatureId,
            in Intent intent, String resolvedType, in Bundle options);
    void setExcludeFromRecents(boolean exclude);
    void requestWindowingLayer(in int layer, in IRemoteCallback outcomeCallback);
}
+100 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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 com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;

import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.os.Bundle;
import android.os.IRemoteCallback;
import android.os.OutcomeReceiver;
import android.os.RemoteException;

import com.android.internal.annotations.VisibleForTesting;

import java.util.concurrent.Executor;

/**
 * Class to encapsulate handling {@link ActivityManager.AppTask#requestWindowingLayer} requests and
 * related utilities.
 * @hide
 */
public class TaskWindowingLayerRequestHandler {


    /**
     * The key used for specifying the final result of a windowing layer request in the
     * {@link android.os.Bundle} returned by the server.
     */
    public static final String REMOTE_CALLBACK_RESULT_KEY = "result";

    /**
     * The request had been approved.
     *
     * <p>
     * The task layer has been changed accordingly to the request.
     */
    public static final int RESULT_APPROVED = 0;

    /**
     * The request has been rejected because the windowing system was in an inappropriate state to
     * handle the request.
     */
    public static final int RESULT_FAILED_BAD_STATE = 1;

    /**
     * Requests the windowing layer via {@link IAppTask}.
     */
    @VisibleForTesting(visibility = PACKAGE)
    public static void requestWindowingLayer(
            @ActivityManager.AppTask.WindowingLayer int layer,
            @NonNull @CallbackExecutor Executor executor,
            @NonNull OutcomeReceiver<Void, Exception> callback,
            @NonNull IAppTask appTaskImpl) {
        try {
            appTaskImpl.requestWindowingLayer(layer, createRemoteCallback(executor, callback));
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    private static IRemoteCallback createRemoteCallback(
            @NonNull @CallbackExecutor Executor executor,
            @NonNull OutcomeReceiver<Void, Exception> callback
    ) {
        return new IRemoteCallback.Stub() {
            @Override
            public void sendResult(Bundle data) {
                final int result = data.getInt(REMOTE_CALLBACK_RESULT_KEY);
                switch (result) {
                    case RESULT_APPROVED:
                        executor.execute(() -> callback.onResult(null));
                        break;
                    case RESULT_FAILED_BAD_STATE:
                        executor.execute(() -> callback.onError(new IllegalStateException(
                                "The current system windowing state is not appropriate to fulfill"
                                        + " the request.")));
                        break;
                    default:
                        executor.execute(() -> callback.onError(new IllegalStateException(
                                "Unknown error, code=" + result)));
                        break;
                }
            }
        };
    }
}
+2 −0
Original line number Diff line number Diff line
# Files related to windowing
per-file Window*.java = file:/services/core/java/com/android/server/wm/OWNERS
per-file TaskWindowingLayerRequestHandlerTest.java = file:/services/core/java/com/android/server/wm/OWNERS

# Notification, DND, Status bar
per-file *Notification* = file:/packages/SystemUI/OWNERS
+159 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.ActivityManager.AppTask.WINDOWING_LAYER_NORMAL_APP;
import static android.app.ActivityManager.AppTask.WINDOWING_LAYER_PINNED;
import static android.app.TaskWindowingLayerRequestHandler.REMOTE_CALLBACK_RESULT_KEY;
import static android.app.TaskWindowingLayerRequestHandler.RESULT_APPROVED;
import static android.app.TaskWindowingLayerRequestHandler.RESULT_FAILED_BAD_STATE;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;

import android.os.Bundle;
import android.os.IRemoteCallback;
import android.os.OutcomeReceiver;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;

import java.util.concurrent.Executor;

/**
 * Test class for {@link TaskWindowingLayerRequestHandler}.
 *
 * Build/Install/Run:
 *  atest TaskWindowingLayerRequestHandlerTest
 */
@SmallTest
@Presubmit
@RunWith(AndroidJUnit4.class)
public class TaskWindowingLayerRequestHandlerTest {

    private static final Executor SAME_THREAD_EXECUTOR = Runnable::run;
    private IAppTask mAppTask;
    private OutcomeReceiver<Void, Exception> mCallback;
    private ArgumentCaptor<IRemoteCallback> mRemoteCallbackCaptor;
    private ArgumentCaptor<Exception> mExceptionCaptor;

    @Before
    public void setUp() {
        mAppTask = mock(IAppTask.class);
        mCallback = mock(OutcomeReceiver.class);
        mRemoteCallbackCaptor = ArgumentCaptor.forClass(IRemoteCallback.class);
        mExceptionCaptor = ArgumentCaptor.forClass(Exception.class);
    }

    @Test
    public void requestWindowingLayer_propagatesSuccessResult() throws RemoteException {
        final Bundle actualResult = resultWithCode(RESULT_APPROVED);

        TaskWindowingLayerRequestHandler.requestWindowingLayer(
                WINDOWING_LAYER_NORMAL_APP,
                SAME_THREAD_EXECUTOR,
                mCallback,
                mAppTask
        );
        verify(mAppTask).requestWindowingLayer(eq(WINDOWING_LAYER_NORMAL_APP),
                mRemoteCallbackCaptor.capture());
        mRemoteCallbackCaptor.getValue().sendResult(actualResult);

        verifyCallbackReceivedSuccessResultOnce();
    }

    @Test
    public void requestWindowingLayer_propagatesBadStateFailure() throws RemoteException {
        final Bundle actualResult = resultWithCode(RESULT_FAILED_BAD_STATE);

        TaskWindowingLayerRequestHandler.requestWindowingLayer(
                WINDOWING_LAYER_PINNED,
                SAME_THREAD_EXECUTOR,
                mCallback,
                mAppTask
        );
        verify(mAppTask).requestWindowingLayer(eq(WINDOWING_LAYER_PINNED),
                mRemoteCallbackCaptor.capture());
        mRemoteCallbackCaptor.getValue().sendResult(actualResult);

        verifyCallbackReceivedErrorOnce(IllegalStateException.class,
                "The current system windowing state is not appropriate to fulfill the request.");
    }

    @Test
    public void requestWindowingLayer_whenInvalidResult_propagatesError() throws RemoteException {
        final Bundle actualResult = resultWithCode(-1); // invalid code

        TaskWindowingLayerRequestHandler.requestWindowingLayer(
                WINDOWING_LAYER_PINNED,
                SAME_THREAD_EXECUTOR,
                mCallback,
                mAppTask
        );
        verify(mAppTask).requestWindowingLayer(eq(WINDOWING_LAYER_PINNED),
                mRemoteCallbackCaptor.capture());
        mRemoteCallbackCaptor.getValue().sendResult(actualResult);

        verifyCallbackReceivedErrorOnce(IllegalStateException.class, "Unknown error, code=-1");
    }

    @Test(expected = RuntimeException.class)
    public void requestWindowingLayer_whenRemoteException_isRethrown() throws RemoteException {
        doThrow(new RemoteException("foo")).when(mAppTask)
                .requestWindowingLayer(anyInt(), any());

        TaskWindowingLayerRequestHandler.requestWindowingLayer(
                WINDOWING_LAYER_PINNED,
                SAME_THREAD_EXECUTOR,
                mCallback,
                mAppTask
        );
    }

    private void verifyCallbackReceivedSuccessResultOnce() {
        verify(mCallback).onResult(any());
        verify(mCallback, never()).onError(any());
    }

    private <E extends Exception> void verifyCallbackReceivedErrorOnce(Class<E> expectedException,
            String expectedMessage) {
        verify(mCallback, never()).onResult(any());
        verify(mCallback).onError(mExceptionCaptor.capture());
        Exception captured = mExceptionCaptor.getValue();
        assertTrue(expectedException.isInstance(captured));
        assertEquals(captured.getMessage(), expectedMessage);
    }

    private static Bundle resultWithCode(int result) {
        Bundle resultData = new Bundle();
        resultData.putInt(REMOTE_CALLBACK_RESULT_KEY, result);
        return resultData;
    }
}
Loading