diff --git a/core/api/current.txt b/core/api/current.txt index 205be59c0f955..bd8bfc0bb21f9 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -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); 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"; diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java index 1c1a55817b0ae..9e310110d9357 100644 --- a/core/java/android/app/StatusBarManager.java +++ b/core/java/android/app/StatusBarManager.java @@ -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. + * + *

Note: The result should not be cached. + * + *

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. + * + *

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"; diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 30984b2a8ddad..d7ab6d7af77a5 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -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. + * + *

This intent action requires the permission + * {@link android.Manifest.permission#LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE}. + * + *

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}. + * + *

The value for this extra can be one of the following: + *

+ */ + 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. + * + *

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. + * + *

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 diff --git a/core/java/com/android/internal/statusbar/AppClipsServiceConnector.java b/core/java/com/android/internal/statusbar/AppClipsServiceConnector.java new file mode 100644 index 0000000000000..287b85f718ff2 --- /dev/null +++ b/core/java/com/android/internal/statusbar/AppClipsServiceConnector.java @@ -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 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 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); + } + } +} diff --git a/core/java/com/android/internal/statusbar/IAppClipsService.aidl b/core/java/com/android/internal/statusbar/IAppClipsService.aidl new file mode 100644 index 0000000000000..013d0d32e7a2a --- /dev/null +++ b/core/java/com/android/internal/statusbar/IAppClipsService.aidl @@ -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); +} \ No newline at end of file diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 49f4d35a0520d..e3ce84078ec41 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -2579,6 +2579,14 @@ + + + diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index afcc2fc095bdc..44973d2b03c5d 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -3220,6 +3220,12 @@ com.android.systemui/com.android.systemui.Somnambulator + + com.android.systemui/com.android.systemui.screenshot.appclips.AppClipsService + diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index b29dfb7e25c50..de02a45361872 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -370,6 +370,7 @@ +