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
This commit is contained in:
Ajinkya Chalke
2023-01-16 12:54:18 +00:00
parent fb7cf0994a
commit 4d2a38fa13
8 changed files with 264 additions and 0 deletions

View File

@@ -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";

View File

@@ -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";

View File

@@ -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

View File

@@ -0,0 +1,99 @@
/*
* 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);
}
}
}

View File

@@ -0,0 +1,26 @@
/**
* 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);
}

View File

@@ -2579,6 +2579,14 @@
<!-- ==================================================== -->
<eat-comment />
<!-- Allows an application to capture screen content to perform a screenshot using the intent
action {@link android.content.Intent#ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE}.
<p>Protection level: internal|role
<p>Intended for use by ROLE_NOTES only.
-->
<permission android:name="android.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE"
android:protectionLevel="internal|role" />
<!-- Allows an application to get notified when a screen capture of its windows is attempted.
<p>Protection level: normal
-->

View File

@@ -3220,6 +3220,12 @@
<string name="config_somnambulatorComponent" translatable="false"
>com.android.systemui/com.android.systemui.Somnambulator</string>
<!-- The component name of the screenshot App Clips service that communicates with SystemUI to
evaluate certain aspects of App Clips flow such as whether a calling activity can launch
capture content for note activity. -->
<string name="config_screenshotAppClipsServiceComponent" translatable="false"
>com.android.systemui/com.android.systemui.screenshot.appclips.AppClipsService</string>
<!-- The component name of a special dock app that merely launches a dream.
We don't want to launch this app when docked because it causes an unnecessary
activity transition. We just want to start the dream.. -->

View File

@@ -370,6 +370,7 @@
<java-symbol type="string" name="config_controlsPackage" />
<java-symbol type="string" name="config_screenRecorderComponent" />
<java-symbol type="string" name="config_somnambulatorComponent" />
<java-symbol type="string" name="config_screenshotAppClipsServiceComponent" />
<java-symbol type="string" name="config_screenshotServiceComponent" />
<java-symbol type="string" name="config_screenshotErrorReceiverComponent" />
<java-symbol type="string" name="config_slicePermissionComponent" />