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

Commit 8e791bb9 authored by Bartosz Chomiński's avatar Bartosz Chomiński Committed by Android (Google) Code Review
Browse files

Merge changes from topics "chominskib-swm-movetaskto", "chominskib-swm-tma-exception" into main

* changes:
  Handle freeform task move transitions in Shell
  Add API for moving windows
  Throw exception for invalid display ID in AM#isTaskMoveAllowedOnDisplay
parents 5a196c7f ecede62c
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -4849,6 +4849,7 @@ package android.app {
  public static class ActivityManager.AppTask {
    method public void finishAndRemoveTask();
    method public android.app.ActivityManager.RecentTaskInfo getTaskInfo();
    method @FlaggedApi("com.android.window.flags.enable_window_repositioning_api") @RequiresPermission(android.Manifest.permission.REPOSITION_SELF_WINDOWS) public void moveTaskTo(@NonNull android.app.TaskLocation, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.app.TaskLocation,java.lang.Exception>);
    method public void moveToFront();
    method public void setExcludeFromRecents(boolean);
    method public void startActivity(android.content.Context, android.content.Intent, android.os.Bundle);
@@ -7855,6 +7856,12 @@ package android.app {
    field @Nullable public android.content.ComponentName topActivity;
  }
  @FlaggedApi("com.android.window.flags.enable_window_repositioning_api") public class TaskLocation {
    ctor public TaskLocation(int, @NonNull android.graphics.Rect);
    method @NonNull public android.graphics.Rect getBounds();
    method public int getDisplayId();
  }
  public class TaskStackBuilder {
    method public android.app.TaskStackBuilder addNextIntent(android.content.Intent);
    method public android.app.TaskStackBuilder addNextIntentWithParentStack(android.content.Intent);
+57 −0
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE;

import android.Manifest;
import android.annotation.CallbackExecutor;
import android.annotation.ColorInt;
import android.annotation.DrawableRes;
import android.annotation.FlaggedApi;
@@ -70,6 +71,7 @@ import android.os.Handler;
import android.os.IBinder;
import android.os.IpcDataCache;
import android.os.LocaleList;
import android.os.OutcomeReceiver;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.PowerExemptionManager;
@@ -3197,6 +3199,8 @@ public class ActivityManager {
     *
     * @param displayId Target display ID
     * @return Whether the windowing mode active on display with given ID allows task repositioning
     *
     * @throws IllegalArgumentException if there is no display with given display ID
     */
    @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_WINDOW_REPOSITIONING_API)
    @SuppressLint("RequiresPermission")
@@ -6187,6 +6191,59 @@ public class ActivityManager {
            }
        }

        /**
         * Repositions the task to the specified {@link TaskLocation}.
         * <p>
         * If the {@link TaskLocation}'s bounds are invalid (i.e. too small or not fully inside the
         * target display), the request will be rejected.
         * <p>
         * When the repositioning request is approved or rejected, the {@code callback} will be
         * invoked. If the request is approved, the callback will receive a {@link TaskLocation}
         * containing the new display ID and bounds of the task. The final display ID and bounds may
         * be adjusted to the closest acceptable states from the requested ones at the system's
         * discretion.
         * <p>
         * If the request is rejected, the callback will receive an exception with a descriptive
         * message accessible via {@link Exception#getMessage()}. This exception will be one of
         * the following:
         * <ul>
         * <li>{@link SecurityException} if the requester doesn't hold the permission required to
         * use this method;</li>
         * <li>{@link IllegalStateException} if the task is not in a state that allows it to
         * change its {@link TaskLocation} programmatically at runtime of the request;</li>
         * <li>{@link SecurityException} if this task cannot be placed on the target display
         * requested. This can happen when the display is not trusted and not owned by the calling
         * app;</li>
         * <li>{@link IllegalArgumentException} if the {@link TaskLocation} provided does not
         * include a valid display ID or the bounds provided are not fully contained inside the
         * given display or the bounds provided are smaller than the minimum size defined in the <a
         * href="https://source.android.com/docs/compatibility/16/android-16-cdd#3814_multi-windows">
         * CDD</a> in either direction.</li>
         * </ul>
         * <p>
         * It is allowed to move a task to a display with which the call of
         * {@link ActivityManager#isTaskMoveAllowedOnDisplay()} returns {@code false}, but the task
         * won't be allowed to be programmatically moved again until users take actions to make the
         * same task movable again.
         * <p>
         * If the task stays on its original display, its z-order will not be affected. Otherwise,
         * if the task moves to a different display, it will become focused.
         *
         * @param location {@link TaskLocation} with the target display or {@link
         * Display#INVALID_DISPLAY} if the target display is the current display,
         * and the new desired bounds
         * @param executor an Executor used to invoke the callback
         * @param callback a callback to receive the result of the request
         */
        @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_WINDOW_REPOSITIONING_API)
        @RequiresPermission(Manifest.permission.REPOSITION_SELF_WINDOWS)
        public void moveTaskTo(
                @NonNull TaskLocation location,
                @NonNull @CallbackExecutor Executor executor,
                @NonNull OutcomeReceiver<TaskLocation, Exception> callback) {
            TaskMoveRequestHandler.moveTaskTo(location, executor, callback, mAppTaskImpl);
        }

        /**
         * Start an activity in this task.  Brings the task to the foreground.  If this task
         * is not currently active (that is, its id < 0), then a new activity for the given
+3 −0
Original line number Diff line number Diff line
@@ -19,7 +19,9 @@ package android.app;
import android.app.ActivityManager;
import android.app.IApplicationThread;
import android.content.Intent;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IRemoteCallback;

/** @hide */
interface IAppTask {
@@ -27,6 +29,7 @@ interface IAppTask {
    @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553)
    ActivityManager.RecentTaskInfo getTaskInfo();
    void moveToFront(in IApplicationThread appThread, in String callingPackage);
    void moveTaskTo(in int displayId, in Rect bounds, in IRemoteCallback outcomeCallback);
    int startActivity(IBinder whoThread, String callingPackage, String callingFeatureId,
            in Intent intent, String resolvedType, in Bundle options);
    void setExcludeFromRecents(boolean exclude);
+53 −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 android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.graphics.Rect;

/**
 * Represents location of an {@link ActivityManager.AppTask} that consists of the host display
 * identifier and rectangular bounds in the pixel-based coordinate system relative to host display.
 */
@FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_WINDOW_REPOSITIONING_API)
public class TaskLocation {
    private final int mDisplayId;
    private final Rect mBounds;

    /**
     * Creates a {@link TaskLocation} with the given display ID and bounds.
     */
    public TaskLocation(int displayId, @NonNull Rect bounds) {
        mDisplayId = displayId;
        mBounds = bounds;
    }

    /**
     * Gets ID of the display.
     */
    public int getDisplayId() {
        return mDisplayId;
    }

    /**
     * Gets the bounds in the display, in the local coordinates of the display in pixels.
     */
    @NonNull public Rect getBounds() {
        return mBounds;
    }
}
+193 −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.Manifest.permission.REPOSITION_SELF_WINDOWS;
import static android.view.Display.INVALID_DISPLAY;

import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.IRemoteCallback;
import android.os.OutcomeReceiver;
import android.os.RemoteException;

import java.util.Objects;
import java.util.concurrent.Executor;

/**
 * This class holds utility constants used for handling {@link ActivityManager.AppTask#moveTaskTo}
 * requests and is responsible for client-side handling of server's responses.
 * @hide
 */
public class TaskMoveRequestHandler {
    @IntDef(prefix = { "RESULT_" }, value = {
            RESULT_APPROVED,
            RESULT_FAILED_BAD_STATE,
            RESULT_FAILED_UNABLE_TO_PLACE_TASK,
            RESULT_FAILED_NONEXISTENT_DISPLAY,
            RESULT_FAILED_BAD_BOUNDS,
            RESULT_FAILED_IMMOVABLE_TASK,
            RESULT_FAILED_NO_PERMISSIONS
    })
    public @interface RequestResult {}

    /**
     * The request has been ultimately approved.
     */
    public static final int RESULT_APPROVED = 0;

    /**
     * The request has been rejected because, broadly speaking, the system's window hierarchy
     * was in an inappropriate state to handle the request.
     */
    public static final int RESULT_FAILED_BAD_STATE = 1;

    /**
     * The request has been rejected because, broadly speaking, the target display was not able
     * to host the moved task due to security reasons.
     */
    public static final int RESULT_FAILED_UNABLE_TO_PLACE_TASK = 2;

    /**
     * The request has been rejected because the {@code displayId} provided does not point to a
     * valid display.
     */
    public static final int RESULT_FAILED_NONEXISTENT_DISPLAY = 3;

    /**
     * The request has been rejected because the bounds provided were irrecoverably invalid.
     */
    public static final int RESULT_FAILED_BAD_BOUNDS = 4;

    /**
     * The request has been rejected because the target task is marked as not movable.
     */
    public static final int RESULT_FAILED_IMMOVABLE_TASK = 5;

    /**
     * The request has been rejected because the caller hasn't had the
     * {@link android.Manifest.REPOSITION_SELF_WINDOWS} permission.
     */
    public static final int RESULT_FAILED_NO_PERMISSIONS = 6;

    /**
     * The key used for specifying the final display ID of the task being moved in the
     * {@link android.os.Bundle} returned by the server.
     */
    public static final String REMOTE_CALLBACK_DISPLAY_ID_KEY = "display_id";

    /**
     * The key used for specifying the final bounds of the task being moved in the
     * {@link android.os.Bundle} returned by the server.
     */
    public static final String REMOTE_CALLBACK_BOUNDS_KEY = "bounds";

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

    @RequiresPermission(REPOSITION_SELF_WINDOWS)
    static void moveTaskTo(
            @NonNull TaskLocation location,
            @NonNull @CallbackExecutor Executor executor,
            @NonNull OutcomeReceiver<TaskLocation, Exception> callback,
            @NonNull IAppTask appTaskImpl) {
        preValidateTaskMoveRequest(location, executor, callback);
        try {
            IRemoteCallback remoteCallback = new IRemoteCallback.Stub() {
                    @Override public void sendResult(Bundle res) {
                        int displayId = INVALID_DISPLAY;
                        Rect bounds = null;
                        int result = res.getInt(REMOTE_CALLBACK_RESULT_KEY);
                        if (result == RESULT_APPROVED) {
                            displayId = res.getInt(REMOTE_CALLBACK_DISPLAY_ID_KEY);
                            bounds = res.getParcelable(REMOTE_CALLBACK_BOUNDS_KEY, Rect.class);
                        }
                        notifyTaskMoveRequestResult(executor, callback, displayId, bounds, result);
                    }
            };
            appTaskImpl.moveTaskTo(
                    location.getDisplayId(), location.getBounds(), remoteCallback);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * At the moment this is a {@code void} method because all the errors we catch here warrant
     * throwing an exception back to the caller instead of passing the error message through the
     * callback provided by the caller.
     */
    private static void preValidateTaskMoveRequest(
            @NonNull TaskLocation location,
            @NonNull @CallbackExecutor Executor executor,
            @NonNull OutcomeReceiver<TaskLocation, Exception> callback) {
        Objects.requireNonNull(location, "The location provided is null.");
        Objects.requireNonNull(executor, "The executor provided is null.");
        Objects.requireNonNull(callback, "The callback provided is null.");
    }

    private static void notifyTaskMoveRequestResult(
            @NonNull @CallbackExecutor Executor executor,
            @NonNull OutcomeReceiver<TaskLocation, Exception> callback,
            int displayId,
            Rect bounds,
            int result) {
        switch (result) {
            case RESULT_APPROVED:
                executor.execute(() -> callback.onResult(
                        new TaskLocation(displayId, bounds)));
                break;
            case RESULT_FAILED_BAD_STATE:
                executor.execute(() -> callback.onError(new IllegalStateException(
                        "The windowing mode currently present at target screen is not feasible for"
                        + "placing freeform windows.")));
                break;
            case RESULT_FAILED_UNABLE_TO_PLACE_TASK:
                executor.execute(() -> callback.onError(new SecurityException(
                        "The task cannot be placed on the target display.")));
                break;
            case RESULT_FAILED_NONEXISTENT_DISPLAY:
                executor.execute(() -> callback.onError(new IllegalArgumentException(
                        "The target display does not exist.")));
                break;
            case RESULT_FAILED_BAD_BOUNDS:
                executor.execute(() -> callback.onError(new IllegalArgumentException(
                        "The target bounds were impossible to recover to valid ones.")));
                break;
            case RESULT_FAILED_IMMOVABLE_TASK:
                executor.execute(() -> callback.onError(new IllegalStateException(
                        "The target task had been marked as not movable.")));
                break;
            case RESULT_FAILED_NO_PERMISSIONS:
                executor.execute(() -> callback.onError(new SecurityException(
                        "The caller does not hold the permission to reposition its windows.")));
                break;
            default:
                executor.execute(() -> callback.onError(new IllegalStateException(
                        "Unknown error.")));
        }
    }

    private TaskMoveRequestHandler() {}
}
Loading