Passing feedback for screenshots in SysUI

Test: Took a screenshot and verified that AiAi gets notified for
share/edit/delete/smart action clicked and exceptions thrown.
Ran tests-
'atest ScreenshotNotificationSmartActionsTest'
'atest ScreenshotNotificationSmartActionsGoogleTest'
Bug: 142669323
Change-Id: Ief6400549b30cf1c0c8a374aa443cf6347f84875
This commit is contained in:
Satakshi
2019-11-07 17:54:24 -08:00
parent 930aad0d43
commit aaf695366e
6 changed files with 403 additions and 138 deletions

View File

@@ -372,6 +372,10 @@
<receiver android:name=".screenshot.GlobalScreenshot$DeleteScreenshotReceiver"
android:exported="false" />
<!-- Callback for invoking a smart action from the screenshot notification. -->
<receiver android:name=".screenshot.GlobalScreenshot$SmartActionsReceiver"
android:exported="false"/>
<!-- started from UsbDeviceSettingsManager -->
<activity android:name=".usb.UsbConfirmActivity"
android:exported="true"

View File

@@ -45,6 +45,8 @@ import com.android.systemui.statusbar.phone.NotificationIconAreaController;
import com.android.systemui.statusbar.phone.StatusBar;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import java.util.concurrent.Executor;
import dagger.Module;
import dagger.Provides;
@@ -124,7 +126,9 @@ public class SystemUIFactory {
* This method is overridden in vendor specific implementation of Sys UI.
*/
public ScreenshotNotificationSmartActionsProvider
createScreenshotNotificationSmartActionsProvider() {
createScreenshotNotificationSmartActionsProvider(Context context,
Executor executor,
Handler uiHandler) {
return new ScreenshotNotificationSmartActionsProvider();
}

View File

@@ -75,6 +75,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.systemui.R;
import com.android.systemui.SystemUI;
import com.android.systemui.SystemUIFactory;
import com.android.systemui.dagger.qualifiers.MainResources;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.statusbar.phone.StatusBar;
@@ -125,8 +126,17 @@ public class GlobalScreenshot {
}
}
static final String SCREENSHOT_URI_ID = "android:screenshot_uri_id";
// These strings are used for communicating the action invoked to
// ScreenshotNotificationSmartActionsProvider.
static final String EXTRA_ACTION_TYPE = "android:screenshot_action_type";
static final String EXTRA_ID = "android:screenshot_id";
static final String ACTION_TYPE_DELETE = "Delete";
static final String ACTION_TYPE_SHARE = "Share";
static final String ACTION_TYPE_EDIT = "Edit";
static final String EXTRA_SMART_ACTIONS_ENABLED = "android:smart_actions_enabled";
static final String EXTRA_ACTION_INTENT = "android:screenshot_action_intent";
static final String SCREENSHOT_URI_ID = "android:screenshot_uri_id";
static final String EXTRA_CANCEL_NOTIFICATION = "android:screenshot_cancel_notification";
static final String EXTRA_DISALLOW_ENTER_PIP = "android:screenshot_disallow_enter_pip";
@@ -688,9 +698,9 @@ public class GlobalScreenshot {
}
@VisibleForTesting
static CompletableFuture<List<Notification.Action>> getSmartActionsFuture(Context context,
static CompletableFuture<List<Notification.Action>> getSmartActionsFuture(String screenshotId,
Bitmap image, ScreenshotNotificationSmartActionsProvider smartActionsProvider,
Handler handler, boolean smartActionsEnabled, boolean isManagedProfile) {
boolean smartActionsEnabled, boolean isManagedProfile) {
if (!smartActionsEnabled) {
Slog.i(TAG, "Screenshot Intelligence not enabled, returning empty list.");
return CompletableFuture.completedFuture(Collections.emptyList());
@@ -704,6 +714,7 @@ public class GlobalScreenshot {
Slog.d(TAG, "Screenshot from a managed profile: " + isManagedProfile);
CompletableFuture<List<Notification.Action>> smartActionsFuture;
long startTimeMs = SystemClock.uptimeMillis();
try {
ActivityManager.RunningTaskInfo runningTask =
ActivityManagerWrapper.getInstance().getRunningTask();
@@ -711,34 +722,74 @@ public class GlobalScreenshot {
(runningTask != null && runningTask.topActivity != null)
? runningTask.topActivity
: new ComponentName("", "");
smartActionsFuture = smartActionsProvider.getActions(image, context,
THREAD_POOL_EXECUTOR,
handler,
smartActionsFuture = smartActionsProvider.getActions(screenshotId, image,
componentName,
isManagedProfile);
} catch (Throwable e) {
long waitTimeMs = SystemClock.uptimeMillis() - startTimeMs;
smartActionsFuture = CompletableFuture.completedFuture(Collections.emptyList());
Slog.e(TAG, "Failed to get future for screenshot notification smart actions.", e);
notifyScreenshotOp(screenshotId, smartActionsProvider,
ScreenshotNotificationSmartActionsProvider.ScreenshotOp.REQUEST_SMART_ACTIONS,
ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus.ERROR,
waitTimeMs);
}
return smartActionsFuture;
}
@VisibleForTesting
static List<Notification.Action> getSmartActions(
CompletableFuture<List<Notification.Action>> smartActionsFuture, int timeoutMs) {
static List<Notification.Action> getSmartActions(String screenshotId,
CompletableFuture<List<Notification.Action>> smartActionsFuture, int timeoutMs,
ScreenshotNotificationSmartActionsProvider smartActionsProvider) {
long startTimeMs = SystemClock.uptimeMillis();
try {
long startTimeMs = SystemClock.uptimeMillis();
List<Notification.Action> actions = smartActionsFuture.get(timeoutMs,
TimeUnit.MILLISECONDS);
long waitTimeMs = SystemClock.uptimeMillis() - startTimeMs;
Slog.d(TAG, String.format("Wait time for smart actions: %d ms",
SystemClock.uptimeMillis() - startTimeMs));
waitTimeMs));
notifyScreenshotOp(screenshotId, smartActionsProvider,
ScreenshotNotificationSmartActionsProvider.ScreenshotOp.WAIT_FOR_SMART_ACTIONS,
ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus.SUCCESS,
waitTimeMs);
return actions;
} catch (Throwable e) {
Slog.e(TAG, "Failed to obtain screenshot notification smart actions.", e);
long waitTimeMs = SystemClock.uptimeMillis() - startTimeMs;
Slog.d(TAG, "Failed to obtain screenshot notification smart actions.", e);
ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus status =
(e instanceof TimeoutException)
? ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus.TIMEOUT
: ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus.ERROR;
notifyScreenshotOp(screenshotId, smartActionsProvider,
ScreenshotNotificationSmartActionsProvider.ScreenshotOp.WAIT_FOR_SMART_ACTIONS,
status, waitTimeMs);
return Collections.emptyList();
}
}
static void notifyScreenshotOp(String screenshotId,
ScreenshotNotificationSmartActionsProvider smartActionsProvider,
ScreenshotNotificationSmartActionsProvider.ScreenshotOp op,
ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus status, long durationMs) {
try {
smartActionsProvider.notifyOp(screenshotId, op, status, durationMs);
} catch (Throwable e) {
Slog.e(TAG, "Error in notifyScreenshotOp: ", e);
}
}
static void notifyScreenshotAction(Context context, String screenshotId, String action,
boolean isSmartAction) {
try {
ScreenshotNotificationSmartActionsProvider provider =
SystemUIFactory.getInstance().createScreenshotNotificationSmartActionsProvider(
context, THREAD_POOL_EXECUTOR, new Handler());
provider.notifyAction(screenshotId, action, isSmartAction);
} catch (Throwable e) {
Slog.e(TAG, "Error in notifyScreenshotAction: ", e);
}
}
/**
* Receiver to proxy the share or edit intent, used to clean up the notification and send
* appropriate signals to the system (ie. to dismiss the keyguard if necessary).
@@ -782,6 +833,13 @@ public class GlobalScreenshot {
} else {
startActivityRunnable.run();
}
if (intent.getBooleanExtra(EXTRA_SMART_ACTIONS_ENABLED, false)) {
String actionType = Intent.ACTION_EDIT.equals(intent.getAction()) ? ACTION_TYPE_EDIT
: ACTION_TYPE_SHARE;
notifyScreenshotAction(context, intent.getStringExtra(EXTRA_ID),
actionType, false);
}
}
}
@@ -812,6 +870,29 @@ public class GlobalScreenshot {
// And delete the image from the media store
final Uri uri = Uri.parse(intent.getStringExtra(SCREENSHOT_URI_ID));
new DeleteImageInBackgroundTask(context).execute(uri);
if (intent.getBooleanExtra(EXTRA_SMART_ACTIONS_ENABLED, false)) {
notifyScreenshotAction(context, intent.getStringExtra(EXTRA_ID),
ACTION_TYPE_DELETE,
false);
}
}
}
/**
* Executes the smart action tapped by the user in the notification.
*/
public static class SmartActionsReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
PendingIntent actionIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT);
ActivityOptions opts = ActivityOptions.makeBasic();
context.startActivityAsUser(actionIntent.getIntent(), opts.toBundle(),
UserHandle.CURRENT);
Slog.d(TAG, "Screenshot notification smart action is invoked.");
notifyScreenshotAction(context, intent.getStringExtra(EXTRA_ID),
intent.getStringExtra(EXTRA_ACTION_TYPE),
true);
}
}

View File

@@ -38,6 +38,7 @@ import android.media.ExifInterface;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.ParcelFileDescriptor;
@@ -50,6 +51,7 @@ import android.provider.MediaStore;
import android.text.TextUtils;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.messages.nano.SystemMessageProto;
import com.android.systemui.R;
@@ -69,9 +71,12 @@ import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
/**
@@ -81,6 +86,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
private static final String TAG = "SaveImageInBackgroundTask";
private static final String SCREENSHOT_FILE_NAME_TEMPLATE = "Screenshot_%s.png";
private static final String SCREENSHOT_ID_TEMPLATE = "Screenshot_%s";
private static final String SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)";
private final GlobalScreenshot.SaveImageInBackgroundData mParams;
@@ -91,8 +97,10 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
private final Notification.BigPictureStyle mNotificationStyle;
private final int mImageWidth;
private final int mImageHeight;
private final Handler mHandler;
private final ScreenshotNotificationSmartActionsProvider mSmartActionsProvider;
private final String mScreenshotId;
private final boolean mSmartActionsEnabled;
private final Random mRandom = new Random();
SaveImageInBackgroundTask(Context context, GlobalScreenshot.SaveImageInBackgroundData data,
NotificationManager nManager) {
@@ -103,11 +111,20 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
mImageTime = System.currentTimeMillis();
String imageDate = new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(mImageTime));
mImageFileName = String.format(SCREENSHOT_FILE_NAME_TEMPLATE, imageDate);
mScreenshotId = String.format(SCREENSHOT_ID_TEMPLATE, UUID.randomUUID());
// Initialize screenshot notification smart actions provider.
mHandler = new Handler();
mSmartActionsProvider =
SystemUIFactory.getInstance().createScreenshotNotificationSmartActionsProvider();
mSmartActionsEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
SystemUiDeviceConfigFlags.ENABLE_SCREENSHOT_NOTIFICATION_SMART_ACTIONS, false);
if (mSmartActionsEnabled) {
mSmartActionsProvider =
SystemUIFactory.getInstance()
.createScreenshotNotificationSmartActionsProvider(
context, THREAD_POOL_EXECUTOR, new Handler());
} else {
// If smart actions is not enabled use empty implementation.
mSmartActionsProvider = new ScreenshotNotificationSmartActionsProvider();
}
// Create the large notification icon
mImageWidth = data.image.getWidth();
@@ -201,6 +218,38 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
return info.isManagedProfile();
}
private List<Notification.Action> buildSmartActions(
List<Notification.Action> actions, Context context) {
List<Notification.Action> broadcastActions = new ArrayList<>();
for (Notification.Action action : actions) {
// Proxy smart actions through {@link GlobalScreenshot.SmartActionsReceiver}
// for logging smart actions.
Bundle extras = action.getExtras();
String actionType = extras.getString(
ScreenshotNotificationSmartActionsProvider.ACTION_TYPE,
ScreenshotNotificationSmartActionsProvider.DEFAULT_ACTION_TYPE);
Intent intent = new Intent(context,
GlobalScreenshot.SmartActionsReceiver.class).putExtra(
GlobalScreenshot.EXTRA_ACTION_INTENT, action.actionIntent);
addIntentExtras(mScreenshotId, intent, actionType, mSmartActionsEnabled);
PendingIntent broadcastIntent = PendingIntent.getBroadcast(context,
mRandom.nextInt(),
intent,
PendingIntent.FLAG_CANCEL_CURRENT);
broadcastActions.add(new Notification.Action.Builder(action.getIcon(), action.title,
broadcastIntent).setContextual(true).addExtras(extras).build());
}
return broadcastActions;
}
private static void addIntentExtras(String screenshotId, Intent intent, String actionType,
boolean smartActionsEnabled) {
intent
.putExtra(GlobalScreenshot.EXTRA_ACTION_TYPE, actionType)
.putExtra(GlobalScreenshot.EXTRA_ID, screenshotId)
.putExtra(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED, smartActionsEnabled);
}
/**
* Generates a new hardware bitmap with specified values, copying the content from the
* passed in bitmap.
@@ -227,16 +276,13 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
Context context = mParams.context;
Bitmap image = mParams.image;
boolean smartActionsEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
SystemUiDeviceConfigFlags.ENABLE_SCREENSHOT_NOTIFICATION_SMART_ACTIONS, true);
CompletableFuture<List<Notification.Action>>
smartActionsFuture = GlobalScreenshot.getSmartActionsFuture(
context, image, mSmartActionsProvider, mHandler, smartActionsEnabled,
isManagedProfile(context));
Resources r = context.getResources();
try {
CompletableFuture<List<Notification.Action>> smartActionsFuture =
GlobalScreenshot.getSmartActionsFuture(mScreenshotId, image,
mSmartActionsProvider, mSmartActionsEnabled, isManagedProfile(context));
// Save the screenshot to the MediaStore
final MediaStore.PendingParams params = new MediaStore.PendingParams(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mImageFileName, "image/png");
@@ -289,104 +335,11 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
IoUtils.closeQuietly(session);
}
// Note: Both the share and edit actions are proxied through ActionProxyReceiver in
// order to do some common work like dismissing the keyguard and sending
// closeSystemWindows
// Create a share intent, this will always go through the chooser activity first
// which should not trigger auto-enter PiP
String subjectDate = DateFormat.getDateTimeInstance().format(new Date(mImageTime));
String subject = String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate);
Intent sharingIntent = new Intent(Intent.ACTION_SEND);
sharingIntent.setType("image/png");
sharingIntent.putExtra(Intent.EXTRA_STREAM, uri);
// Include URI in ClipData also, so that grantPermission picks it up.
// We don't use setData here because some apps interpret this as "to:".
ClipData clipdata = new ClipData(new ClipDescription("content",
new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}),
new ClipData.Item(uri));
sharingIntent.setClipData(clipdata);
sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
sharingIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// Make sure pending intents for the system user are still unique across users
// by setting the (otherwise unused) request code to the current user id.
int requestCode = context.getUserId();
PendingIntent chooserAction = PendingIntent.getBroadcast(context, requestCode,
new Intent(context, GlobalScreenshot.TargetChosenReceiver.class),
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
Intent sharingChooserIntent = Intent.createChooser(sharingIntent, null,
chooserAction.getIntentSender())
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// Create a share action for the notification
PendingIntent shareAction = PendingIntent.getBroadcastAsUser(context, requestCode,
new Intent(context, GlobalScreenshot.ActionProxyReceiver.class)
.putExtra(GlobalScreenshot.EXTRA_ACTION_INTENT, sharingChooserIntent)
.putExtra(GlobalScreenshot.EXTRA_DISALLOW_ENTER_PIP, true)
.setAction(Intent.ACTION_SEND),
PendingIntent.FLAG_CANCEL_CURRENT, UserHandle.SYSTEM);
Notification.Action.Builder shareActionBuilder = new Notification.Action.Builder(
R.drawable.ic_screenshot_share,
r.getString(com.android.internal.R.string.share), shareAction);
mNotificationBuilder.addAction(shareActionBuilder.build());
// Create an edit intent, if a specific package is provided as the editor, then
// launch that directly
String editorPackage = context.getString(R.string.config_screenshotEditor);
Intent editIntent = new Intent(Intent.ACTION_EDIT);
if (!TextUtils.isEmpty(editorPackage)) {
editIntent.setComponent(ComponentName.unflattenFromString(editorPackage));
}
editIntent.setType("image/png");
editIntent.setData(uri);
editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
editIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
// Create a edit action
PendingIntent editAction = PendingIntent.getBroadcastAsUser(context, requestCode,
new Intent(context, GlobalScreenshot.ActionProxyReceiver.class)
.putExtra(GlobalScreenshot.EXTRA_ACTION_INTENT, editIntent)
.putExtra(GlobalScreenshot.EXTRA_CANCEL_NOTIFICATION,
editIntent.getComponent() != null)
.setAction(Intent.ACTION_EDIT),
PendingIntent.FLAG_CANCEL_CURRENT, UserHandle.SYSTEM);
Notification.Action.Builder editActionBuilder = new Notification.Action.Builder(
R.drawable.ic_screenshot_edit,
r.getString(com.android.internal.R.string.screenshot_edit), editAction);
mNotificationBuilder.addAction(editActionBuilder.build());
if (editAction != null && mParams.onEditReady != null) {
mParams.onEditReady.apply(editAction);
}
// Create a delete action for the notification
PendingIntent deleteAction = PendingIntent.getBroadcast(context, requestCode,
new Intent(context, GlobalScreenshot.DeleteScreenshotReceiver.class)
.putExtra(GlobalScreenshot.SCREENSHOT_URI_ID, uri.toString()),
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
Notification.Action.Builder deleteActionBuilder = new Notification.Action.Builder(
R.drawable.ic_screenshot_delete,
r.getString(com.android.internal.R.string.delete), deleteAction);
mNotificationBuilder.addAction(deleteActionBuilder.build());
populateNotificationActions(context, r, uri, smartActionsFuture, mNotificationBuilder);
mParams.imageUri = uri;
mParams.image = null;
mParams.errorMsgResId = 0;
if (smartActionsEnabled) {
int timeoutMs = DeviceConfig.getInt(DeviceConfig.NAMESPACE_SYSTEMUI,
SystemUiDeviceConfigFlags
.SCREENSHOT_NOTIFICATION_SMART_ACTIONS_TIMEOUT_MS,
1000);
List<Notification.Action> smartActions = GlobalScreenshot.getSmartActions(
smartActionsFuture,
timeoutMs);
for (Notification.Action action : smartActions) {
mNotificationBuilder.addAction(action);
}
}
} catch (Exception e) {
// IOException/UnsupportedOperationException may be thrown if external storage is
// not mounted
@@ -403,6 +356,115 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
return null;
}
@VisibleForTesting
void populateNotificationActions(Context context, Resources r, Uri uri,
CompletableFuture<List<Notification.Action>> smartActionsFuture,
Notification.Builder notificationBuilder) {
// Note: Both the share and edit actions are proxied through ActionProxyReceiver in
// order to do some common work like dismissing the keyguard and sending
// closeSystemWindows
// Create a share intent, this will always go through the chooser activity first
// which should not trigger auto-enter PiP
String subjectDate = DateFormat.getDateTimeInstance().format(new Date(mImageTime));
String subject = String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate);
Intent sharingIntent = new Intent(Intent.ACTION_SEND);
sharingIntent.setType("image/png");
sharingIntent.putExtra(Intent.EXTRA_STREAM, uri);
// Include URI in ClipData also, so that grantPermission picks it up.
// We don't use setData here because some apps interpret this as "to:".
ClipData clipdata = new ClipData(new ClipDescription("content",
new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}),
new ClipData.Item(uri));
sharingIntent.setClipData(clipdata);
sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
sharingIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// Make sure pending intents for the system user are still unique across users
// by setting the (otherwise unused) request code to the current user id.
int requestCode = context.getUserId();
PendingIntent chooserAction = PendingIntent.getBroadcast(context, requestCode,
new Intent(context, GlobalScreenshot.TargetChosenReceiver.class),
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
Intent sharingChooserIntent = Intent.createChooser(sharingIntent, null,
chooserAction.getIntentSender())
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// Create a share action for the notification
PendingIntent shareAction = PendingIntent.getBroadcastAsUser(context, requestCode,
new Intent(context, GlobalScreenshot.ActionProxyReceiver.class)
.putExtra(GlobalScreenshot.EXTRA_ACTION_INTENT, sharingChooserIntent)
.putExtra(GlobalScreenshot.EXTRA_DISALLOW_ENTER_PIP, true)
.putExtra(GlobalScreenshot.EXTRA_ID, mScreenshotId)
.putExtra(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED,
mSmartActionsEnabled)
.setAction(Intent.ACTION_SEND),
PendingIntent.FLAG_CANCEL_CURRENT, UserHandle.SYSTEM);
Notification.Action.Builder shareActionBuilder = new Notification.Action.Builder(
R.drawable.ic_screenshot_share,
r.getString(com.android.internal.R.string.share), shareAction);
notificationBuilder.addAction(shareActionBuilder.build());
// Create an edit intent, if a specific package is provided as the editor, then
// launch that directly
String editorPackage = context.getString(R.string.config_screenshotEditor);
Intent editIntent = new Intent(Intent.ACTION_EDIT);
if (!TextUtils.isEmpty(editorPackage)) {
editIntent.setComponent(ComponentName.unflattenFromString(editorPackage));
}
editIntent.setType("image/png");
editIntent.setData(uri);
editIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
editIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
// Create a edit action
PendingIntent editAction = PendingIntent.getBroadcastAsUser(context, requestCode,
new Intent(context, GlobalScreenshot.ActionProxyReceiver.class)
.putExtra(GlobalScreenshot.EXTRA_ACTION_INTENT, editIntent)
.putExtra(GlobalScreenshot.EXTRA_CANCEL_NOTIFICATION,
editIntent.getComponent() != null)
.putExtra(GlobalScreenshot.EXTRA_ID, mScreenshotId)
.putExtra(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED,
mSmartActionsEnabled)
.setAction(Intent.ACTION_EDIT),
PendingIntent.FLAG_CANCEL_CURRENT, UserHandle.SYSTEM);
Notification.Action.Builder editActionBuilder = new Notification.Action.Builder(
R.drawable.ic_screenshot_edit,
r.getString(com.android.internal.R.string.screenshot_edit), editAction);
notificationBuilder.addAction(editActionBuilder.build());
if (editAction != null && mParams.onEditReady != null) {
mParams.onEditReady.apply(editAction);
}
// Create a delete action for the notification
PendingIntent deleteAction = PendingIntent.getBroadcast(context, requestCode,
new Intent(context, GlobalScreenshot.DeleteScreenshotReceiver.class)
.putExtra(GlobalScreenshot.SCREENSHOT_URI_ID, uri.toString())
.putExtra(GlobalScreenshot.EXTRA_ID, mScreenshotId)
.putExtra(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED,
mSmartActionsEnabled),
PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
Notification.Action.Builder deleteActionBuilder = new Notification.Action.Builder(
R.drawable.ic_screenshot_delete,
r.getString(com.android.internal.R.string.delete), deleteAction);
notificationBuilder.addAction(deleteActionBuilder.build());
if (mSmartActionsEnabled) {
int timeoutMs = DeviceConfig.getInt(DeviceConfig.NAMESPACE_SYSTEMUI,
SystemUiDeviceConfigFlags
.SCREENSHOT_NOTIFICATION_SMART_ACTIONS_TIMEOUT_MS,
1000);
List<Notification.Action> smartActions = buildSmartActions(
GlobalScreenshot.getSmartActions(mScreenshotId, smartActionsFuture,
timeoutMs, mSmartActionsProvider), context);
for (Notification.Action action : smartActions) {
notificationBuilder.addAction(action);
}
}
}
@Override
protected void onPostExecute(Void params) {
if (mParams.errorMsgResId != 0) {

View File

@@ -18,41 +18,84 @@ package com.android.systemui.screenshot;
import android.app.Notification;
import android.content.ComponentName;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Handler;
import android.util.Log;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
/**
* This class can be overridden by a vendor-specific sys UI implementation,
* in order to provide smart actions in the screenshot notification.
*/
public class ScreenshotNotificationSmartActionsProvider {
/* Key provided in the notification action to get the type of smart action. */
public static final String ACTION_TYPE = "action_type";
public static final String DEFAULT_ACTION_TYPE = "Smart Action";
/* Define phases of screenshot execution. */
protected enum ScreenshotOp {
OP_UNKNOWN,
RETRIEVE_SMART_ACTIONS,
REQUEST_SMART_ACTIONS,
WAIT_FOR_SMART_ACTIONS
}
/* Enum to report success or failure for screenshot execution phases. */
protected enum ScreenshotOpStatus {
OP_STATUS_UNKNOWN,
SUCCESS,
ERROR,
TIMEOUT
}
private static final String TAG = "ScreenshotActions";
/**
* Default implementation that returns an empty list.
* This method is overridden in vendor-specific Sys UI implementation.
*
* @param screenshotId A generated random unique id for the screenshot.
* @param bitmap The bitmap of the screenshot. The bitmap config must be {@link
* HARDWARE}.
* @param context The current app {@link Context}.
* @param executor A {@link Executor} that can be used to execute tasks in parallel.
* @param handler A {@link Handler} to possibly run UI-thread code.
* @param componentName Contains package and activity class names where the screenshot was
* taken. This is used as an additional signal to generate and rank more
* relevant actions.
* @param isManagedProfile The screenshot was taken for a work profile app.
*/
public CompletableFuture<List<Notification.Action>> getActions(Bitmap bitmap, Context context,
Executor executor, Handler handler, ComponentName componentName,
public CompletableFuture<List<Notification.Action>> getActions(
String screenshotId,
Bitmap bitmap,
ComponentName componentName,
boolean isManagedProfile) {
Log.d(TAG, "Returning empty smart action list.");
return CompletableFuture.completedFuture(Collections.emptyList());
}
/**
* Notify exceptions and latency encountered during generating smart actions.
* This method is overridden in vendor-specific Sys UI implementation.
*
* @param screenshotId Unique id of the screenshot.
* @param op screenshot execution phase defined in {@link ScreenshotOp}
* @param status {@link ScreenshotOpStatus} to report success or failure.
* @param durationMs latency experienced in different phases of screenshots.
*/
public void notifyOp(String screenshotId, ScreenshotOp op, ScreenshotOpStatus status,
long durationMs) {
Log.d(TAG, "Return without notify.");
}
/**
* Notify screenshot notification action invoked.
* This method is overridden in vendor-specific Sys UI implementation.
*
* @param screenshotId Unique id of the screenshot.
* @param action type of notification action invoked.
* @param isSmartAction whether action invoked was a smart action.
*/
public void notifyAction(String screenshotId, String action, boolean isSmartAction) {
Log.d(TAG, "Return without notify.");
}
}

View File

@@ -16,8 +16,12 @@
package com.android.systemui.screenshot;
import static android.content.Context.NOTIFICATION_SERVICE;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
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.times;
@@ -25,14 +29,20 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.Notification;
import android.app.NotificationManager;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.testing.AndroidTestingRunner;
import androidx.test.filters.SmallTest;
import com.android.systemui.SystemUIFactory;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.util.NotificationChannels;
import org.junit.Assert;
import org.junit.Before;
@@ -70,12 +80,12 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
when(bitmap.getConfig()).thenReturn(Bitmap.Config.HARDWARE);
ScreenshotNotificationSmartActionsProvider smartActionsProvider = mock(
ScreenshotNotificationSmartActionsProvider.class);
when(smartActionsProvider.getActions(any(), any(), any(), any(), any(),
when(smartActionsProvider.getActions(any(), any(), any(),
eq(false))).thenThrow(
RuntimeException.class);
CompletableFuture<List<Notification.Action>> smartActionsFuture =
GlobalScreenshot.getSmartActionsFuture(mContext, bitmap,
smartActionsProvider, mHandler, true, false);
GlobalScreenshot.getSmartActionsFuture("", bitmap,
smartActionsProvider, true, false);
Assert.assertNotNull(smartActionsFuture);
List<Notification.Action> smartActions = smartActionsFuture.get(5, TimeUnit.MILLISECONDS);
Assert.assertEquals(Collections.emptyList(), smartActions);
@@ -92,10 +102,19 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
when(smartActionsFuture.get(timeoutMs, TimeUnit.MILLISECONDS)).thenThrow(
RuntimeException.class);
List<Notification.Action> actions = GlobalScreenshot.getSmartActions(
smartActionsFuture, timeoutMs);
"", smartActionsFuture, timeoutMs, mSmartActionsProvider);
Assert.assertEquals(Collections.emptyList(), actions);
}
// Tests any exception thrown in notifying feedback does not affect regular screenshot flow.
@Test
public void testExceptionHandlingInNotifyingFeedback()
throws Exception {
doThrow(RuntimeException.class).when(mSmartActionsProvider).notifyOp(any(), any(), any(),
anyLong());
GlobalScreenshot.notifyScreenshotOp(null, mSmartActionsProvider, null, null, -1);
}
// Tests for a non-hardware bitmap, ScreenshotNotificationSmartActionsProvider is never invoked
// and a completed future is returned.
@Test
@@ -104,9 +123,9 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
Bitmap bitmap = mock(Bitmap.class);
when(bitmap.getConfig()).thenReturn(Bitmap.Config.RGB_565);
CompletableFuture<List<Notification.Action>> smartActionsFuture =
GlobalScreenshot.getSmartActionsFuture(mContext, bitmap,
mSmartActionsProvider, mHandler, true, true);
verify(mSmartActionsProvider, never()).getActions(any(), any(), any(), any(), any(),
GlobalScreenshot.getSmartActionsFuture("", bitmap,
mSmartActionsProvider, true, true);
verify(mSmartActionsProvider, never()).getActions(any(), any(), any(),
eq(false));
Assert.assertNotNull(smartActionsFuture);
List<Notification.Action> smartActions = smartActionsFuture.get(5, TimeUnit.MILLISECONDS);
@@ -118,10 +137,10 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
public void testScreenshotNotificationSmartActionsProviderInvokedOnce() {
Bitmap bitmap = mock(Bitmap.class);
when(bitmap.getConfig()).thenReturn(Bitmap.Config.HARDWARE);
GlobalScreenshot.getSmartActionsFuture(mContext, bitmap, mSmartActionsProvider,
mHandler, true, true);
GlobalScreenshot.getSmartActionsFuture("", bitmap, mSmartActionsProvider,
true, true);
verify(mSmartActionsProvider, times(1))
.getActions(any(), any(), any(), any(), any(), eq(true));
.getActions(any(), any(), any(), eq(true));
}
// Tests for a hardware bitmap, a completed future is returned.
@@ -131,13 +150,65 @@ public class ScreenshotNotificationSmartActionsTest extends SysuiTestCase {
Bitmap bitmap = mock(Bitmap.class);
when(bitmap.getConfig()).thenReturn(Bitmap.Config.HARDWARE);
ScreenshotNotificationSmartActionsProvider actionsProvider =
SystemUIFactory.getInstance().createScreenshotNotificationSmartActionsProvider();
SystemUIFactory.getInstance().createScreenshotNotificationSmartActionsProvider(
mContext, null, mHandler);
CompletableFuture<List<Notification.Action>> smartActionsFuture =
GlobalScreenshot.getSmartActionsFuture(mContext, bitmap,
GlobalScreenshot.getSmartActionsFuture("", bitmap,
actionsProvider,
mHandler, true, true);
true, true);
Assert.assertNotNull(smartActionsFuture);
List<Notification.Action> smartActions = smartActionsFuture.get(5, TimeUnit.MILLISECONDS);
Assert.assertEquals(smartActions.size(), 0);
}
// Tests for notification action extras.
@Test
public void testNotificationActionExtras() {
if (Looper.myLooper() == null) {
Looper.prepare();
}
NotificationManager notificationManager =
(NotificationManager) mContext.getSystemService(NOTIFICATION_SERVICE);
GlobalScreenshot.SaveImageInBackgroundData
data = new GlobalScreenshot.SaveImageInBackgroundData();
data.context = mContext;
data.image = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
data.iconSize = 10;
data.finisher = null;
data.onEditReady = null;
data.previewWidth = 10;
data.previewheight = 10;
SaveImageInBackgroundTask task = new SaveImageInBackgroundTask(mContext, data,
notificationManager);
Notification.Builder notificationBuilder = new Notification.Builder(mContext,
NotificationChannels.SCREENSHOTS_HEADSUP);
task.populateNotificationActions(mContext, mContext.getResources(),
Uri.parse("Screenshot_123.png"),
CompletableFuture.completedFuture(Collections.emptyList()), notificationBuilder);
Notification notification = notificationBuilder.build();
Assert.assertEquals(notification.actions.length, 3);
boolean isShareFound = false;
boolean isEditFound = false;
boolean isDeleteFound = false;
for (Notification.Action action : notification.actions) {
Intent intent = action.actionIntent.getIntent();
Assert.assertNotNull(intent);
Bundle bundle = intent.getExtras();
Assert.assertTrue(bundle.containsKey(GlobalScreenshot.EXTRA_ID));
Assert.assertTrue(bundle.containsKey(GlobalScreenshot.EXTRA_SMART_ACTIONS_ENABLED));
if (action.title.equals(GlobalScreenshot.ACTION_TYPE_DELETE)) {
isDeleteFound = intent.getAction() == null;
} else if (action.title.equals(GlobalScreenshot.ACTION_TYPE_EDIT)) {
isEditFound = Intent.ACTION_EDIT.equals(intent.getAction());
} else if (action.title.equals(GlobalScreenshot.ACTION_TYPE_SHARE)) {
isShareFound = Intent.ACTION_SEND.equals(intent.getAction());
}
}
Assert.assertTrue(isEditFound);
Assert.assertTrue(isDeleteFound);
Assert.assertTrue(isShareFound);
}
}