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

Commit 4d2a38fa authored by Ajinkya Chalke's avatar Ajinkya Chalke
Browse files

Add screenshot permission, action, and API.

- Permisison restricts the API to NOTES_ROLE.
- Callers can use the SBM API to check if they should surface.
a UI element that allows user to launch flow which apps can do using
the intent action with startActivityForResult.

Test: CTS test
Bug: 251204849
Bug: 251205791
Change-Id: I9e27f6ed5a3cc62292a9e9839c0b71faa5fda190
parent fb7cf099
Loading
Loading
Loading
Loading
+9 −0
Original line number Diff line number Diff line
@@ -121,6 +121,7 @@ package android {
    field public static final String INTERACT_ACROSS_PROFILES = "android.permission.INTERACT_ACROSS_PROFILES";
    field public static final String INTERNET = "android.permission.INTERNET";
    field public static final String KILL_BACKGROUND_PROCESSES = "android.permission.KILL_BACKGROUND_PROCESSES";
    field public static final String LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE = "android.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE";
    field public static final String LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK = "android.permission.LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK";
    field public static final String LOADER_USAGE_STATS = "android.permission.LOADER_USAGE_STATS";
    field public static final String LOCATION_HARDWARE = "android.permission.LOCATION_HARDWARE";
@@ -7190,6 +7191,7 @@ package android.app {
  }
  public class StatusBarManager {
    method @RequiresPermission(android.Manifest.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE) public boolean canLaunchCaptureContentActivityForNote(@NonNull android.app.Activity);
    method public void requestAddTileService(@NonNull android.content.ComponentName, @NonNull CharSequence, @NonNull android.graphics.drawable.Icon, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
    field public static final int TILE_ADD_REQUEST_ERROR_APP_NOT_IN_FOREGROUND = 1004; // 0x3ec
    field public static final int TILE_ADD_REQUEST_ERROR_BAD_COMPONENT = 1002; // 0x3ea
@@ -10699,6 +10701,7 @@ package android.content {
    field public static final String ACTION_INSERT_OR_EDIT = "android.intent.action.INSERT_OR_EDIT";
    field public static final String ACTION_INSTALL_FAILURE = "android.intent.action.INSTALL_FAILURE";
    field @Deprecated public static final String ACTION_INSTALL_PACKAGE = "android.intent.action.INSTALL_PACKAGE";
    field @RequiresPermission(android.Manifest.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE) public static final String ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE = "android.intent.action.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE";
    field public static final String ACTION_LOCALE_CHANGED = "android.intent.action.LOCALE_CHANGED";
    field public static final String ACTION_LOCKED_BOOT_COMPLETED = "android.intent.action.LOCKED_BOOT_COMPLETED";
    field public static final String ACTION_MAIN = "android.intent.action.MAIN";
@@ -10792,6 +10795,11 @@ package android.content {
    field public static final String ACTION_VOICE_COMMAND = "android.intent.action.VOICE_COMMAND";
    field @Deprecated public static final String ACTION_WALLPAPER_CHANGED = "android.intent.action.WALLPAPER_CHANGED";
    field public static final String ACTION_WEB_SEARCH = "android.intent.action.WEB_SEARCH";
    field public static final int CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN = 4; // 0x4
    field public static final int CAPTURE_CONTENT_FOR_NOTE_FAILED = 1; // 0x1
    field public static final int CAPTURE_CONTENT_FOR_NOTE_SUCCESS = 0; // 0x0
    field public static final int CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED = 2; // 0x2
    field public static final int CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED = 3; // 0x3
    field public static final String CATEGORY_ACCESSIBILITY_SHORTCUT_TARGET = "android.intent.category.ACCESSIBILITY_SHORTCUT_TARGET";
    field public static final String CATEGORY_ALTERNATIVE = "android.intent.category.ALTERNATIVE";
    field public static final String CATEGORY_APP_BROWSER = "android.intent.category.APP_BROWSER";
@@ -10847,6 +10855,7 @@ package android.content {
    field public static final String EXTRA_AUTO_LAUNCH_SINGLE_CHOICE = "android.intent.extra.AUTO_LAUNCH_SINGLE_CHOICE";
    field public static final String EXTRA_BCC = "android.intent.extra.BCC";
    field public static final String EXTRA_BUG_REPORT = "android.intent.extra.BUG_REPORT";
    field public static final String EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE = "android.intent.extra.CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE";
    field public static final String EXTRA_CC = "android.intent.extra.CC";
    field @Deprecated public static final String EXTRA_CHANGED_COMPONENT_NAME = "android.intent.extra.changed_component_name";
    field public static final String EXTRA_CHANGED_COMPONENT_NAME_LIST = "android.intent.extra.changed_component_name_list";
+33 −0
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import android.compat.annotation.EnabledSince;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Icon;
import android.media.INearbyMediaDevicesProvider;
import android.media.INearbyMediaDevicesUpdateCallback;
@@ -47,6 +48,7 @@ import android.util.Pair;
import android.util.Slog;
import android.view.View;

import com.android.internal.statusbar.AppClipsServiceConnector;
import com.android.internal.statusbar.IAddTileResultCallback;
import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.statusbar.IUndoMediaTransferCallback;
@@ -1190,6 +1192,37 @@ public class StatusBarManager {
        return CompatChanges.isChangeEnabled(MEDIA_CONTROL_SESSION_ACTIONS, packageName, user);
    }

    /**
     * Checks whether the supplied activity can {@link Activity#startActivityForResult(Intent, int)}
     * a system activity that captures content on the screen to take a screenshot.
     *
     * <p>Note: The result should not be cached.
     *
     * <p>The system activity displays an editing tool that allows user to edit the screenshot, save
     * it on device, and return the edited screenshot as {@link android.net.Uri} to the calling
     * activity. User interaction is required to return the edited screenshot to the calling
     * activity.
     *
     * <p>When {@code true}, callers can use {@link Activity#startActivityForResult(Intent, int)}
     * to start start the content capture activity using
     * {@link Intent#ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE}.
     *
     * @param activity Calling activity
     * @return true if the activity supports launching the capture content activity for note.
     *
     * @see Intent#ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE
     * @see Manifest.permission#LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE
     * @see android.app.role.RoleManager#ROLE_NOTES
     */
    @RequiresPermission(Manifest.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE)
    public boolean canLaunchCaptureContentActivityForNote(@NonNull Activity activity) {
        Objects.requireNonNull(activity);
        IBinder activityToken = activity.getActivityToken();
        int taskId = ActivityClient.getInstance().getTaskForActivity(activityToken, false);
        return new AppClipsServiceConnector(mContext)
                .canLaunchCaptureContentActivityForNote(taskId);
    }

    /** @hide */
    public static String windowStateToString(int state) {
        if (state == WINDOW_STATE_HIDING) return "WINDOW_STATE_HIDING";
+82 −0
Original line number Diff line number Diff line
@@ -31,8 +31,10 @@ import android.annotation.SdkConstant.SdkConstantType;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.app.Activity;
import android.app.ActivityThread;
import android.app.AppGlobals;
import android.app.StatusBarManager;
import android.bluetooth.BluetoothDevice;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.pm.ActivityInfo;
@@ -5136,6 +5138,86 @@ public class Intent implements Parcelable, Cloneable {
     */
    public static final String EXTRA_USE_STYLUS_MODE = "android.intent.extra.USE_STYLUS_MODE";

    /**
     * Activity Action: Use with startActivityForResult to start a system activity that captures
     * content on the screen to take a screenshot and present it to the user for editing. The
     * edited screenshot is saved on device and returned to the calling activity as a {@link Uri}
     * through {@link #getData()}. User interaction is required to return the edited screenshot to
     * the calling activity.
     *
     * <p>This intent action requires the permission
     * {@link android.Manifest.permission#LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE}.
     *
     * <p>Callers should query
     * {@link StatusBarManager#canLaunchCaptureContentActivityForNote(Activity)} before showing a UI
     * element that allows users to trigger this flow.
     */
    @RequiresPermission(Manifest.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE)
    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
    public static final String ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE =
            "android.intent.action.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE";

    /**
     * An int extra used by activity started with
     * {@link #ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE} to indicate status of the response.
     * This extra is used along with result code set to {@link android.app.Activity#RESULT_OK}.
     *
     * <p>The value for this extra can be one of the following:
     * <ul>
     *     <li>{@link #CAPTURE_CONTENT_FOR_NOTE_SUCCESS}</li>
     *     <li>{@link #CAPTURE_CONTENT_FOR_NOTE_FAILED}</li>
     *     <li>{@link #CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED}</li>
     *     <li>{@link #CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED}</li>
     *     <li>{@link #CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN}</li>
     * </ul>
     */
    public static final String EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE =
            "android.intent.extra.CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE";

    /**
     * A response code used with {@link #EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE} to indicate
     * that the request was a success.
     *
     * <p>This code will only be returned after the user has interacted with the system screenshot
     * activity to consent to sharing the data with the note.
     *
     * <p>The captured screenshot is returned as a {@link Uri} through {@link #getData()}.
     */
    public static final int CAPTURE_CONTENT_FOR_NOTE_SUCCESS = 0;

    /**
     * A response code used with {@link #EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE} to indicate
     * that something went wrong.
     */
    public static final int CAPTURE_CONTENT_FOR_NOTE_FAILED = 1;

    /**
     * A response code used with {@link #EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE} to indicate
     * that user canceled the content capture flow.
     */
    public static final int CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED = 2;

    /**
     * A response code used with {@link #EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE} to indicate
     * that the intent action {@link #ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE} was started
     * by an activity that is running in a non-supported window mode.
     */
    public static final int CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED = 3;

    /**
     * A response code used with {@link #EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE} to indicate
     * that screenshot is blocked by IT admin.
     */
    public static final int CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN = 4;

    /** @hide */
    @IntDef(value = {
            CAPTURE_CONTENT_FOR_NOTE_SUCCESS, CAPTURE_CONTENT_FOR_NOTE_FAILED,
            CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED,
            CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN})
    @Retention(RetentionPolicy.SOURCE)
    public @interface CaptureContentForNoteStatusCodes {}

    /**
     * Broadcast Action: Sent to the integrity component when a package
     * needs to be verified. The data contains the package URI along with other relevant
+99 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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 com.android.internal.statusbar;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.util.Log;

import java.util.concurrent.CompletableFuture;

/**
 * A helper class to communicate with the App Clips service running in SystemUI.
 */
public class AppClipsServiceConnector {

    private static final String TAG = AppClipsServiceConnector.class.getSimpleName();

    private final Context mContext;
    private final Handler mHandler;

    public AppClipsServiceConnector(Context context) {
        mContext = context;
        HandlerThread handlerThread = new HandlerThread(TAG);
        handlerThread.start();
        mHandler = handlerThread.getThreadHandler();
    }

    /**
     * @return true if the task represented by {@code taskId} can launch App Clips screenshot flow,
     * false otherwise.
     */
    public boolean canLaunchCaptureContentActivityForNote(int taskId) {
        try {
            CompletableFuture<Boolean> future = new CompletableFuture<>();
            connectToServiceAndProcessRequest(taskId, future);
            return future.get();
        } catch (Exception e) {
            Log.d(TAG, "Exception from service\n" + e);
        }

        return false;
    }

    private void connectToServiceAndProcessRequest(int taskId, CompletableFuture<Boolean> future) {
        ServiceConnection serviceConnection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                try {
                    future.complete(IAppClipsService.Stub.asInterface(
                            service).canLaunchCaptureContentActivityForNote(taskId));
                } catch (Exception e) {
                    Log.d(TAG, "Exception from service\n" + e);
                }
                future.complete(false);
            }

            @Override
            public void onServiceDisconnected(ComponentName name) {
                if (!future.isDone()) {
                    future.complete(false);
                }
            }
        };

        final ComponentName serviceComponent = ComponentName.unflattenFromString(
                mContext.getResources().getString(
                        com.android.internal.R.string.config_screenshotAppClipsServiceComponent));
        final Intent serviceIntent = new Intent();
        serviceIntent.setComponent(serviceComponent);

        boolean bindService = mContext.bindServiceAsUser(serviceIntent, serviceConnection,
                Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE, mHandler,
                mContext.getUser());

        // Complete the future early if service not bound.
        if (!bindService) {
            future.complete(false);
        }
    }
}
+26 −0
Original line number Diff line number Diff line
/**
 * Copyright (C) 2023, 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 com.android.internal.statusbar;

/**
 * A service that runs in SystemUI and helps determine if App Clips flow is supported in the
 * current state of device. This service needs to run in SystemUI in order to communicate with the
 * instance of app bubbles.
 */
interface IAppClipsService {
    boolean canLaunchCaptureContentActivityForNote(in int taskId);
}
 No newline at end of file
Loading