diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index cd64a3880803d..cfe1baf6a1bbc 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -372,6 +372,10 @@ + + + > getSmartActionsFuture(Context context, + static CompletableFuture> 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> 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 getSmartActions( - CompletableFuture> smartActionsFuture, int timeoutMs) { + static List getSmartActions(String screenshotId, + CompletableFuture> smartActionsFuture, int timeoutMs, + ScreenshotNotificationSmartActionsProvider smartActionsProvider) { + long startTimeMs = SystemClock.uptimeMillis(); try { - long startTimeMs = SystemClock.uptimeMillis(); List 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); } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java index 083f9712d7050..e40f2d01b17e2 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java @@ -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 { 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 { 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 { 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 { return info.isManagedProfile(); } + private List buildSmartActions( + List actions, Context context) { + List 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 { Context context = mParams.context; Bitmap image = mParams.image; - boolean smartActionsEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, - SystemUiDeviceConfigFlags.ENABLE_SCREENSHOT_NOTIFICATION_SMART_ACTIONS, true); - CompletableFuture> - smartActionsFuture = GlobalScreenshot.getSmartActionsFuture( - context, image, mSmartActionsProvider, mHandler, smartActionsEnabled, - isManagedProfile(context)); - Resources r = context.getResources(); try { + CompletableFuture> 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 { 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 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 { return null; } + @VisibleForTesting + void populateNotificationActions(Context context, Resources r, Uri uri, + CompletableFuture> 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 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) { diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsProvider.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsProvider.java index fa23bf7d5bde1..b6f5447d2867c 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsProvider.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsProvider.java @@ -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> getActions(Bitmap bitmap, Context context, - Executor executor, Handler handler, ComponentName componentName, + public CompletableFuture> 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."); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java index 99850e7c922eb..3f32c66b2d037 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotNotificationSmartActionsTest.java @@ -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> smartActionsFuture = - GlobalScreenshot.getSmartActionsFuture(mContext, bitmap, - smartActionsProvider, mHandler, true, false); + GlobalScreenshot.getSmartActionsFuture("", bitmap, + smartActionsProvider, true, false); Assert.assertNotNull(smartActionsFuture); List 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 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> 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 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> smartActionsFuture = - GlobalScreenshot.getSmartActionsFuture(mContext, bitmap, + GlobalScreenshot.getSmartActionsFuture("", bitmap, actionsProvider, - mHandler, true, true); + true, true); Assert.assertNotNull(smartActionsFuture); List 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); + } }