Merge "Implement screenshot smart actions with new action provider" into main
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt
index a1481f6..4cf18fb 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/LegacyScreenshotViewProxy.kt
@@ -34,9 +34,9 @@
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.res.R
-import com.android.systemui.screenshot.scroll.ScrollCaptureController
 import com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS
 import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER
+import com.android.systemui.screenshot.scroll.ScrollCaptureController
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
@@ -113,7 +113,7 @@
     override fun setChipIntents(imageData: ScreenshotController.SavedImageData) =
         view.setChipIntents(imageData)
 
-    override fun requestDismissal(event: ScreenshotEvent) {
+    override fun requestDismissal(event: ScreenshotEvent?) {
         if (DEBUG_DISMISS) {
             Log.d(TAG, "screenshot dismissal requested")
         }
@@ -124,7 +124,7 @@
             }
             return
         }
-        logger.log(event, 0, packageName)
+        event?.let { logger.log(event, 0, packageName) }
         view.animateDismissal()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
index bbf7ed5..4914409 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
@@ -165,6 +165,7 @@
                     mQuickShareData.quickShareAction, mScreenshotId, uri, mImageTime, image,
                     mParams.owner);
             mImageData.subject = getSubjectString(mImageTime);
+            mImageData.imageTime = mImageTime;
 
             mParams.mActionsReadyListener.onActionsReady(mImageData);
             if (DEBUG_CALLBACK) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt
index 5019a6f..ca0a539 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt
@@ -17,24 +17,37 @@
 package com.android.systemui.screenshot
 
 import android.app.ActivityOptions
+import android.app.BroadcastOptions
 import android.app.ExitTransitionCoordinator
+import android.app.PendingIntent
 import android.content.Context
 import android.content.Intent
+import android.os.Process
+import android.os.UserHandle
+import android.provider.DeviceConfig
 import android.util.Log
 import android.util.Pair
 import androidx.appcompat.content.res.AppCompatResources
 import com.android.app.tracing.coroutines.launch
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
+import com.android.internal.logging.UiEventLogger
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.log.DebugLogger.debugLog
 import com.android.systemui.res.R
 import com.android.systemui.screenshot.ActionIntentCreator.createEdit
 import com.android.systemui.screenshot.ActionIntentCreator.createShareWithSubject
 import com.android.systemui.screenshot.ScreenshotController.SavedImageData
+import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_EDIT_TAPPED
+import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED
+import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_SHARE_TAPPED
+import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED
 import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel
 import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
+import java.text.DateFormat
+import java.util.Date
 import kotlinx.coroutines.CoroutineScope
 
 /**
@@ -48,7 +61,9 @@
     interface Factory {
         fun create(
             request: ScreenshotData,
+            requestId: String,
             windowTransition: () -> Pair<ActivityOptions, ExitTransitionCoordinator>,
+            requestDismissal: () -> Unit,
         ): ScreenshotActionsProvider
     }
 }
@@ -59,9 +74,13 @@
     private val context: Context,
     private val viewModel: ScreenshotViewModel,
     private val actionExecutor: ActionIntentExecutor,
+    private val smartActionsProvider: SmartActionsProvider,
+    private val uiEventLogger: UiEventLogger,
     @Application private val applicationScope: CoroutineScope,
     @Assisted val request: ScreenshotData,
+    @Assisted val requestId: String,
     @Assisted val windowTransition: () -> Pair<ActivityOptions, ExitTransitionCoordinator>,
+    @Assisted val requestDismissal: () -> Unit,
 ) : ScreenshotActionsProvider {
     private var pendingAction: ((SavedImageData) -> Unit)? = null
     private var result: SavedImageData? = null
@@ -70,6 +89,7 @@
     init {
         viewModel.setPreviewAction {
             debugLog(LogConfig.DEBUG_ACTIONS) { "Preview tapped" }
+            uiEventLogger.log(SCREENSHOT_PREVIEW_TAPPED, 0, request.packageNameString)
             onDeferrableActionTapped { result ->
                 startSharedTransition(createEdit(result.uri, context), true)
             }
@@ -81,6 +101,7 @@
                 context.resources.getString(R.string.screenshot_edit_description),
             ) {
                 debugLog(LogConfig.DEBUG_ACTIONS) { "Edit tapped" }
+                uiEventLogger.log(SCREENSHOT_EDIT_TAPPED, 0, request.packageNameString)
                 onDeferrableActionTapped { result ->
                     startSharedTransition(createEdit(result.uri, context), true)
                 }
@@ -93,11 +114,46 @@
                 context.resources.getString(R.string.screenshot_share_description),
             ) {
                 debugLog(LogConfig.DEBUG_ACTIONS) { "Share tapped" }
+                uiEventLogger.log(SCREENSHOT_SHARE_TAPPED, 0, request.packageNameString)
                 onDeferrableActionTapped { result ->
                     startSharedTransition(createShareWithSubject(result.uri, result.subject), false)
                 }
             }
         )
+        if (smartActionsEnabled(request.userHandle ?: Process.myUserHandle())) {
+            smartActionsProvider.requestQuickShare(request, requestId) { quickShare ->
+                if (!quickShare.actionIntent.isImmutable) {
+                    viewModel.addAction(
+                        ActionButtonViewModel(
+                            quickShare.getIcon().loadDrawable(context),
+                            quickShare.title,
+                            quickShare.title
+                        ) {
+                            debugLog(LogConfig.DEBUG_ACTIONS) { "Quickshare tapped" }
+                            onDeferrableActionTapped { result ->
+                                uiEventLogger.log(
+                                    SCREENSHOT_SMART_ACTION_TAPPED,
+                                    0,
+                                    request.packageNameString
+                                )
+                                sendPendingIntent(
+                                    smartActionsProvider
+                                        .wrapIntent(
+                                            quickShare,
+                                            result.uri,
+                                            result.subject,
+                                            requestId
+                                        )
+                                        .actionIntent
+                                )
+                            }
+                        }
+                    )
+                } else {
+                    Log.w(TAG, "Received immutable quick share pending intent; ignoring")
+                }
+            }
+        }
     }
 
     override fun setCompletedScreenshot(result: SavedImageData) {
@@ -105,12 +161,30 @@
             Log.e(TAG, "Got a second completed screenshot for existing request!")
             return
         }
-        if (result.uri == null || result.owner == null || result.subject == null) {
+        if (result.uri == null || result.owner == null || result.imageTime == null) {
             Log.e(TAG, "Invalid result provided!")
             return
         }
+        if (result.subject == null) {
+            result.subject = getSubjectString(result.imageTime)
+        }
         this.result = result
         pendingAction?.invoke(result)
+        if (smartActionsEnabled(result.owner)) {
+            smartActionsProvider.requestSmartActions(request, requestId, result) { smartActions ->
+                viewModel.addActions(
+                    smartActions.map {
+                        ActionButtonViewModel(
+                            it.getIcon().loadDrawable(context),
+                            it.title,
+                            it.title
+                        ) {
+                            sendPendingIntent(it.actionIntent)
+                        }
+                    }
+                )
+            }
+        }
     }
 
     override fun isPendingSharedTransition(): Boolean {
@@ -134,15 +208,47 @@
         }
     }
 
+    private fun sendPendingIntent(pendingIntent: PendingIntent) {
+        try {
+            val options = BroadcastOptions.makeBasic()
+            options.setInteractive(true)
+            options.setPendingIntentBackgroundActivityStartMode(
+                ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
+            )
+            pendingIntent.send(options.toBundle())
+            requestDismissal.invoke()
+        } catch (e: PendingIntent.CanceledException) {
+            Log.e(TAG, "Intent cancelled", e)
+        }
+    }
+
+    private fun smartActionsEnabled(user: UserHandle): Boolean {
+        val savingToOtherUser = user != Process.myUserHandle()
+        return !savingToOtherUser &&
+            DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_SYSTEMUI,
+                SystemUiDeviceConfigFlags.ENABLE_SCREENSHOT_NOTIFICATION_SMART_ACTIONS,
+                true
+            )
+    }
+
+    private fun getSubjectString(imageTime: Long): String {
+        val subjectDate = DateFormat.getDateTimeInstance().format(Date(imageTime))
+        return String.format(SCREENSHOT_SHARE_SUBJECT_TEMPLATE, subjectDate)
+    }
+
     @AssistedFactory
     interface Factory : ScreenshotActionsProvider.Factory {
         override fun create(
             request: ScreenshotData,
+            requestId: String,
             windowTransition: () -> Pair<ActivityOptions, ExitTransitionCoordinator>,
+            requestDismissal: () -> Unit,
         ): DefaultScreenshotActionsProvider
     }
 
     companion object {
         private const val TAG = "ScreenshotActionsProvider"
+        private const val SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index b43137f..70d1129 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -96,7 +96,10 @@
 import dagger.assisted.AssistedFactory;
 import dagger.assisted.AssistedInject;
 
+import kotlin.Unit;
+
 import java.util.List;
+import java.util.UUID;
 import java.util.concurrent.CancellationException;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
@@ -167,6 +170,7 @@
         public Notification.Action quickShareAction;
         public UserHandle owner;
         public String subject;  // Title for sharing
+        public Long imageTime; // Time at which screenshot was saved
 
         /**
          * Used to reset the return data on error
@@ -176,6 +180,7 @@
             smartActions = null;
             quickShareAction = null;
             subject = null;
+            imageTime = null;
         }
     }
 
@@ -261,11 +266,9 @@
     private SaveImageInBackgroundTask mSaveInBgTask;
     private boolean mScreenshotTakenInPortrait;
     private boolean mBlockAttach;
-
-    private ScreenshotActionsProvider mActionsProvider;
-
     private Animator mScreenshotAnimation;
     private RequestCallback mCurrentRequestCallback;
+    private ScreenshotActionsProvider mActionsProvider;
     private String mPackageName = "";
     private final BroadcastReceiver mCopyBroadcastReceiver;
 
@@ -317,6 +320,7 @@
             @Assisted boolean showUIOnExternalDisplay
     ) {
         mScreenshotSmartActions = screenshotSmartActions;
+        mActionsProviderFactory = actionsProviderFactory;
         mNotificationsController = screenshotNotificationsControllerFactory.create(displayId);
         mScrollCaptureClient = scrollCaptureClient;
         mUiEventLogger = uiEventLogger;
@@ -347,7 +351,6 @@
         mAssistContentRequester = assistContentRequester;
 
         mViewProxy = viewProxyFactory.getProxy(mContext, mDisplayId);
-        mActionsProviderFactory = actionsProviderFactory;
 
         mScreenshotHandler.setOnTimeoutRunnable(() -> {
             if (DEBUG_UI) {
@@ -441,8 +444,19 @@
             return;
         }
 
-        saveScreenshotInWorkerThread(screenshot.getUserHandle(), finisher,
-                this::showUiOnActionsReady, this::showUiOnQuickShareActionReady);
+        if (screenshotShelfUi()) {
+            final UUID requestId = UUID.randomUUID();
+            final String screenshotId = String.format("Screenshot_%s", requestId);
+            mActionsProvider = mActionsProviderFactory.create(screenshot, screenshotId,
+                    this::createWindowTransition, () -> {
+                        mViewProxy.requestDismissal(null);
+                        return Unit.INSTANCE;
+                    });
+            saveScreenshotInBackground(screenshot, requestId, finisher);
+        } else {
+            saveScreenshotInWorkerThread(screenshot.getUserHandle(), finisher,
+                    this::showUiOnActionsReady, this::showUiOnQuickShareActionReady);
+        }
 
         // The window is focusable by default
         setWindowFocusable(true);
@@ -477,7 +491,9 @@
         // ignore system bar insets for the purpose of window layout
         mWindow.getDecorView().setOnApplyWindowInsetsListener(
                 (v, insets) -> WindowInsets.CONSUMED);
-        mScreenshotHandler.cancelTimeout(); // restarted after animation
+        if (!screenshotShelfUi()) {
+            mScreenshotHandler.cancelTimeout(); // restarted after animation
+        }
     }
 
     private boolean shouldShowUi() {
@@ -497,11 +513,6 @@
 
         mViewProxy.reset();
 
-        if (screenshotShelfUi()) {
-            mActionsProvider =
-                    mActionsProviderFactory.create(screenshot, this::createWindowTransition);
-        }
-
         if (mViewProxy.isAttachedToWindow()) {
             // if we didn't already dismiss for another reason
             if (!mViewProxy.isDismissing()) {
@@ -921,6 +932,39 @@
         mScreenshotHandler.cancelTimeout();
     }
 
+    private void saveScreenshotInBackground(
+            ScreenshotData screenshot, UUID requestId, Consumer<Uri> finisher) {
+        ListenableFuture<ImageExporter.Result> future = mImageExporter.export(mBgExecutor,
+                requestId, screenshot.getBitmap(), screenshot.getUserHandle(), mDisplayId);
+        future.addListener(() -> {
+            try {
+                ImageExporter.Result result = future.get();
+                Log.d(TAG, "Saved screenshot: " + result);
+                logScreenshotResultStatus(result.uri, screenshot.getUserHandle());
+                mScreenshotHandler.resetTimeout();
+                if (result.uri != null) {
+                    final SavedImageData savedImageData = new SavedImageData();
+                    savedImageData.uri = result.uri;
+                    savedImageData.owner = screenshot.getUserHandle();
+                    savedImageData.imageTime = result.timestamp;
+                    mActionsProvider.setCompletedScreenshot(savedImageData);
+                    mViewProxy.setChipIntents(savedImageData);
+                }
+                if (DEBUG_CALLBACK) {
+                    Log.d(TAG, "finished background processing, Calling (Consumer<Uri>) "
+                            + "finisher.accept(\"" + result.uri + "\"");
+                }
+                finisher.accept(result.uri);
+            } catch (Exception e) {
+                Log.d(TAG, "Failed to store screenshot", e);
+                if (DEBUG_CALLBACK) {
+                    Log.d(TAG, "Calling (Consumer<Uri>) finisher.accept(null)");
+                }
+                finisher.accept(null);
+            }
+        }, mMainExecutor);
+    }
+
     /**
      * Creates a new worker thread and saves the screenshot to the media store.
      */
@@ -958,11 +1002,6 @@
         logSuccessOnActionsReady(imageData);
         mScreenshotHandler.resetTimeout();
 
-        if (screenshotShelfUi()) {
-            mActionsProvider.setCompletedScreenshot(imageData);
-            return;
-        }
-
         if (imageData.uri != null) {
             if (DEBUG_UI) {
                 Log.d(TAG, "Showing UI actions");
@@ -1014,20 +1053,27 @@
     /**
      * Logs success/failure of the screenshot saving task, and shows an error if it failed.
      */
-    private void logSuccessOnActionsReady(ScreenshotController.SavedImageData imageData) {
-        if (imageData.uri == null) {
+    private void logScreenshotResultStatus(Uri uri, UserHandle owner) {
+        if (uri == null) {
             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0, mPackageName);
             mNotificationsController.notifyScreenshotError(
                     R.string.screenshot_failed_to_save_text);
         } else {
             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, mPackageName);
-            if (mUserManager.isManagedProfile(imageData.owner.getIdentifier())) {
+            if (mUserManager.isManagedProfile(owner.getIdentifier())) {
                 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED_TO_WORK_PROFILE, 0,
                         mPackageName);
             }
         }
     }
 
+    /**
+     * Logs success/failure of the screenshot saving task, and shows an error if it failed.
+     */
+    private void logSuccessOnActionsReady(ScreenshotController.SavedImageData imageData) {
+        logScreenshotResultStatus(imageData.uri, imageData.owner);
+    }
+
     private boolean isUserSetupComplete(UserHandle owner) {
         return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0)
                 .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
index defddc3..6b9332b 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
@@ -98,7 +98,7 @@
 
     override fun setChipIntents(imageData: SavedImageData) {}
 
-    override fun requestDismissal(event: ScreenshotEvent) {
+    override fun requestDismissal(event: ScreenshotEvent?) {
         debugLog(DEBUG_DISMISS) { "screenshot dismissal requested: $event" }
 
         // If we're already animating out, don't restart the animation
@@ -106,7 +106,7 @@
             debugLog(DEBUG_DISMISS) { "Already dismissing, ignoring duplicate command $event" }
             return
         }
-        logger.log(event, 0, packageName)
+        event?.let { logger.log(it, 0, packageName) }
         val animator = animationController.getExitAnimation()
         animator.addListener(
             object : AnimatorListenerAdapter() {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt
index 6be32a9..a4069d1 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotViewProxy.kt
@@ -46,7 +46,7 @@
     fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator
     fun addQuickShareChip(quickShareAction: Notification.Action)
     fun setChipIntents(imageData: ScreenshotController.SavedImageData)
-    fun requestDismissal(event: ScreenshotEvent)
+    fun requestDismissal(event: ScreenshotEvent?)
 
     fun showScrollChip(packageName: String, onClick: Runnable)
     fun hideScrollChip()
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsProvider.kt
new file mode 100644
index 0000000..2eaff86
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/SmartActionsProvider.kt
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2024 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.systemui.screenshot
+
+import android.app.Notification
+import android.app.PendingIntent
+import android.content.ClipData
+import android.content.ClipDescription
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.net.Uri
+import android.os.Bundle
+import android.os.Process
+import android.os.SystemClock
+import android.os.UserHandle
+import android.provider.DeviceConfig
+import android.util.Log
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
+import com.android.systemui.log.DebugLogger.debugLog
+import com.android.systemui.screenshot.LogConfig.DEBUG_ACTIONS
+import com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType.QUICK_SHARE_ACTION
+import com.android.systemui.screenshot.ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType.REGULAR_SMART_ACTIONS
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
+import javax.inject.Inject
+import kotlin.random.Random
+
+/**
+ * Handle requesting smart/quickshare actions from the provider and executing an action when the
+ * action futures complete.
+ */
+class SmartActionsProvider
+@Inject
+constructor(
+    private val context: Context,
+    private val smartActions: ScreenshotNotificationSmartActionsProvider,
+) {
+    /**
+     * Requests quick share action for a given screenshot.
+     *
+     * @param data the ScreenshotData request
+     * @param id the request id for the screenshot
+     * @param onAction callback to run when quick share action is returned
+     */
+    fun requestQuickShare(
+        data: ScreenshotData,
+        id: String,
+        onAction: (Notification.Action) -> Unit
+    ) {
+        val bitmap = data.bitmap ?: return
+        val user = data.userHandle ?: return
+        val component = data.topComponent ?: ComponentName("", "")
+        requestQuickShareAction(id, bitmap, component, user) { quickShareAction ->
+            onAction(quickShareAction)
+        }
+    }
+
+    /**
+     * Requests smart actions for a given screenshot.
+     *
+     * @param data the ScreenshotData request
+     * @param id the request id for the screenshot
+     * @param result the data for the saved image
+     * @param onActions callback to run when actions are returned
+     */
+    fun requestSmartActions(
+        data: ScreenshotData,
+        id: String,
+        result: ScreenshotController.SavedImageData,
+        onActions: (List<Notification.Action>) -> Unit
+    ) {
+        val bitmap = data.bitmap ?: return
+        val user = data.userHandle ?: return
+        val uri = result.uri ?: return
+        val component = data.topComponent ?: ComponentName("", "")
+        requestSmartActions(id, bitmap, component, user, uri, REGULAR_SMART_ACTIONS) { actions ->
+            onActions(actions)
+        }
+    }
+
+    /**
+     * Wraps the given quick share action in a broadcast intent.
+     *
+     * @param quickShare the quick share action to wrap
+     * @param uri the URI of the saved screenshot
+     * @param subject the subject/title for the screenshot
+     * @param id the request ID of the screenshot
+     * @return the wrapped action
+     */
+    fun wrapIntent(
+        quickShare: Notification.Action,
+        uri: Uri,
+        subject: String,
+        id: String
+    ): Notification.Action {
+        val wrappedIntent: Intent =
+            Intent(context, SmartActionsReceiver::class.java)
+                .putExtra(ScreenshotController.EXTRA_ACTION_INTENT, quickShare.actionIntent)
+                .putExtra(
+                    ScreenshotController.EXTRA_ACTION_INTENT_FILLIN,
+                    createFillInIntent(uri, subject)
+                )
+                .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
+        val extras: Bundle = quickShare.extras
+        val actionType =
+            extras.getString(
+                ScreenshotNotificationSmartActionsProvider.ACTION_TYPE,
+                ScreenshotNotificationSmartActionsProvider.DEFAULT_ACTION_TYPE
+            )
+        // We only query for quick share actions when smart actions are enabled, so we can assert
+        // that it's true here.
+        wrappedIntent
+            .putExtra(ScreenshotController.EXTRA_ACTION_TYPE, actionType)
+            .putExtra(ScreenshotController.EXTRA_ID, id)
+            .putExtra(ScreenshotController.EXTRA_SMART_ACTIONS_ENABLED, true)
+        val broadcastIntent =
+            PendingIntent.getBroadcast(
+                context,
+                Random.nextInt(),
+                wrappedIntent,
+                PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
+            )
+        return Notification.Action.Builder(quickShare.getIcon(), quickShare.title, broadcastIntent)
+            .setContextual(true)
+            .addExtras(extras)
+            .build()
+    }
+
+    private fun createFillInIntent(uri: Uri, subject: String): Intent {
+        val fillIn = Intent()
+        fillIn.setType("image/png")
+        fillIn.putExtra(Intent.EXTRA_STREAM, uri)
+        fillIn.putExtra(Intent.EXTRA_SUBJECT, subject)
+        // Include URI in ClipData also, so that grantPermission picks it up.
+        // We don't use setData here because some apps interpret this as "to:".
+        val clipData =
+            ClipData(ClipDescription("content", arrayOf("image/png")), ClipData.Item(uri))
+        fillIn.clipData = clipData
+        fillIn.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+        return fillIn
+    }
+
+    private fun requestQuickShareAction(
+        id: String,
+        image: Bitmap,
+        component: ComponentName,
+        user: UserHandle,
+        timeoutMs: Long = 500,
+        onAction: (Notification.Action) -> Unit
+    ) {
+        requestSmartActions(id, image, component, user, null, QUICK_SHARE_ACTION, timeoutMs) {
+            it.firstOrNull()?.let { action -> onAction(action) }
+        }
+    }
+
+    private fun requestSmartActions(
+        id: String,
+        image: Bitmap,
+        component: ComponentName,
+        user: UserHandle,
+        uri: Uri?,
+        actionType: ScreenshotNotificationSmartActionsProvider.ScreenshotSmartActionType,
+        timeoutMs: Long = 500,
+        onActions: (List<Notification.Action>) -> Unit
+    ) {
+        val enabled = isSmartActionsEnabled(user)
+        debugLog(DEBUG_ACTIONS) {
+            ("getSmartActionsFuture id=$id, uri=$uri, provider=$smartActions, " +
+                "actionType=$actionType, smartActionsEnabled=$enabled, userHandle=$user")
+        }
+        if (!enabled) {
+            debugLog(DEBUG_ACTIONS) { "Screenshot Intelligence not enabled, returning empty list" }
+            onActions(listOf())
+            return
+        }
+        if (image.config != Bitmap.Config.HARDWARE) {
+            debugLog(DEBUG_ACTIONS) {
+                "Bitmap expected: Hardware, Bitmap found: ${image.config}. Returning empty list."
+            }
+            onActions(listOf())
+            return
+        }
+        var smartActionsFuture: CompletableFuture<List<Notification.Action>>
+        val startTimeMs = SystemClock.uptimeMillis()
+        try {
+            smartActionsFuture =
+                smartActions.getActions(id, uri, image, component, actionType, user)
+        } catch (e: Throwable) {
+            val waitTimeMs = SystemClock.uptimeMillis() - startTimeMs
+            debugLog(DEBUG_ACTIONS, error = e) {
+                "Failed to get future for screenshot notification smart actions."
+            }
+            notifyScreenshotOp(
+                id,
+                ScreenshotNotificationSmartActionsProvider.ScreenshotOp.REQUEST_SMART_ACTIONS,
+                ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus.ERROR,
+                waitTimeMs
+            )
+            onActions(listOf())
+            return
+        }
+        try {
+            val actions = smartActionsFuture.get(timeoutMs, TimeUnit.MILLISECONDS)
+            val waitTimeMs = SystemClock.uptimeMillis() - startTimeMs
+            debugLog(DEBUG_ACTIONS) {
+                ("Got ${actions.size} smart actions. Wait time: $waitTimeMs ms, " +
+                    "actionType=$actionType")
+            }
+            notifyScreenshotOp(
+                id,
+                ScreenshotNotificationSmartActionsProvider.ScreenshotOp.WAIT_FOR_SMART_ACTIONS,
+                ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus.SUCCESS,
+                waitTimeMs
+            )
+            onActions(actions)
+        } catch (e: Throwable) {
+            val waitTimeMs = SystemClock.uptimeMillis() - startTimeMs
+            debugLog(DEBUG_ACTIONS, error = e) {
+                "Error getting smart actions. Wait time: $waitTimeMs ms, actionType=$actionType"
+            }
+            val status =
+                if (e is TimeoutException) {
+                    ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus.TIMEOUT
+                } else {
+                    ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus.ERROR
+                }
+            notifyScreenshotOp(
+                id,
+                ScreenshotNotificationSmartActionsProvider.ScreenshotOp.WAIT_FOR_SMART_ACTIONS,
+                status,
+                waitTimeMs
+            )
+            onActions(listOf())
+        }
+    }
+
+    private fun notifyScreenshotOp(
+        screenshotId: String,
+        op: ScreenshotNotificationSmartActionsProvider.ScreenshotOp,
+        status: ScreenshotNotificationSmartActionsProvider.ScreenshotOpStatus,
+        durationMs: Long
+    ) {
+        debugLog(DEBUG_ACTIONS) {
+            "$smartActions notifyOp: $op id=$screenshotId, status=$status, durationMs=$durationMs"
+        }
+        try {
+            smartActions.notifyOp(screenshotId, op, status, durationMs)
+        } catch (e: Throwable) {
+            Log.e(TAG, "Error in notifyScreenshotOp: ", e)
+        }
+    }
+    private fun isSmartActionsEnabled(user: UserHandle): Boolean {
+        // Smart actions don't yet work for cross-user saves.
+        val savingToOtherUser = user !== Process.myUserHandle()
+        val actionsEnabled =
+            DeviceConfig.getBoolean(
+                DeviceConfig.NAMESPACE_SYSTEMUI,
+                SystemUiDeviceConfigFlags.ENABLE_SCREENSHOT_NOTIFICATION_SMART_ACTIONS,
+                true
+            )
+        return !savingToOtherUser && actionsEnabled
+    }
+
+    companion object {
+        private const val TAG = "SmartActionsProvider"
+        private const val SCREENSHOT_SHARE_SUBJECT_TEMPLATE = "Screenshot (%s)"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt
index ea05884..b191a1a 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.screenshot.ui.binder
 
+import android.util.Log
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt
index f49c6e2..4a5cf57 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/DefaultScreenshotActionsProviderTest.kt
@@ -18,13 +18,18 @@
 
 import android.app.ActivityOptions
 import android.app.ExitTransitionCoordinator
+import android.app.Notification
+import android.app.PendingIntent
 import android.content.Intent
 import android.net.Uri
 import android.os.UserHandle
 import android.testing.AndroidTestingRunner
 import android.view.accessibility.AccessibilityManager
 import androidx.test.filters.SmallTest
+import com.android.internal.logging.UiEventLogger
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.clipboardoverlay.EditTextActivity
+import com.android.systemui.res.R
 import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.capture
@@ -41,7 +46,11 @@
 import org.junit.Before
 import org.junit.runner.RunWith
 import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.kotlin.any
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
 import org.mockito.kotlin.verifyBlocking
+import org.mockito.kotlin.whenever
 
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
@@ -52,15 +61,19 @@
 
     private val actionIntentExecutor = mock<ActionIntentExecutor>()
     private val accessibilityManager = mock<AccessibilityManager>()
+    private val uiEventLogger = mock<UiEventLogger>()
+    private val smartActionsProvider = mock<SmartActionsProvider>()
     private val transition = mock<android.util.Pair<ActivityOptions, ExitTransitionCoordinator>>()
+    private val requestDismissal = mock<() -> Unit>()
 
     private val request = ScreenshotData.forTesting()
     private val invalidResult = ScreenshotController.SavedImageData()
     private val validResult =
         ScreenshotController.SavedImageData().apply {
             uri = Uri.EMPTY
-            owner = UserHandle.CURRENT
+            owner = UserHandle.OWNER
             subject = "Test"
+            imageTime = 0
         }
 
     private lateinit var viewModel: ScreenshotViewModel
@@ -69,20 +82,13 @@
     @Before
     fun setUp() {
         viewModel = ScreenshotViewModel(accessibilityManager)
-        actionsProvider =
-            DefaultScreenshotActionsProvider(
-                context,
-                viewModel,
-                actionIntentExecutor,
-                testScope,
-                request
-            ) {
-                transition
-            }
+        request.userHandle = UserHandle.OWNER
     }
 
     @Test
     fun previewActionAccessed_beforeScreenshotCompleted_doesNothing() {
+        actionsProvider = createActionsProvider()
+
         assertNotNull(viewModel.previewAction.value)
         viewModel.previewAction.value!!.invoke()
         verifyNoMoreInteractions(actionIntentExecutor)
@@ -90,6 +96,8 @@
 
     @Test
     fun actionButtonsAccessed_beforeScreenshotCompleted_doesNothing() {
+        actionsProvider = createActionsProvider()
+
         assertThat(viewModel.actions.value.size).isEqualTo(2)
         val firstAction = viewModel.actions.value[0]
         assertThat(firstAction.onClicked).isNotNull()
@@ -102,6 +110,8 @@
 
     @Test
     fun actionAccessed_withInvalidResult_doesNothing() {
+        actionsProvider = createActionsProvider()
+
         actionsProvider.setCompletedScreenshot(invalidResult)
         viewModel.previewAction.value!!.invoke()
         viewModel.actions.value[1].onClicked!!.invoke()
@@ -112,10 +122,13 @@
     @Test
     @Ignore("b/332526567")
     fun actionAccessed_withResult_launchesIntent() = runTest {
+        actionsProvider = createActionsProvider()
+
         actionsProvider.setCompletedScreenshot(validResult)
         viewModel.actions.value[0].onClicked!!.invoke()
         scheduler.advanceUntilIdle()
 
+        verify(uiEventLogger).log(eq(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED), eq(0), eq(""))
         val intentCaptor = argumentCaptor<Intent>()
         verifyBlocking(actionIntentExecutor) {
             launchIntent(capture(intentCaptor), eq(transition), eq(UserHandle.CURRENT), eq(true))
@@ -126,16 +139,65 @@
     @Test
     @Ignore("b/332526567")
     fun actionAccessed_whilePending_launchesMostRecentAction() = runTest {
+        actionsProvider = createActionsProvider()
+
         viewModel.actions.value[0].onClicked!!.invoke()
         viewModel.previewAction.value!!.invoke()
         viewModel.actions.value[1].onClicked!!.invoke()
         actionsProvider.setCompletedScreenshot(validResult)
         scheduler.advanceUntilIdle()
 
+        verify(uiEventLogger).log(eq(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED), eq(0), eq(""))
         val intentCaptor = argumentCaptor<Intent>()
         verifyBlocking(actionIntentExecutor) {
             launchIntent(capture(intentCaptor), eq(transition), eq(UserHandle.CURRENT), eq(false))
         }
         assertThat(intentCaptor.value.action).isEqualTo(Intent.ACTION_CHOOSER)
     }
+
+    @Test
+    fun quickShareTapped_wrapsAndSendsIntent() = runTest {
+        val quickShare =
+            Notification.Action(
+                R.drawable.ic_screenshot_edit,
+                "TestQuickShare",
+                PendingIntent.getActivity(
+                    context,
+                    0,
+                    Intent(context, EditTextActivity::class.java),
+                    PendingIntent.FLAG_MUTABLE
+                )
+            )
+        whenever(smartActionsProvider.requestQuickShare(any(), any(), any())).then {
+            (it.getArgument(2) as ((Notification.Action) -> Unit)).invoke(quickShare)
+        }
+        whenever(smartActionsProvider.wrapIntent(any(), any(), any(), any())).thenAnswer {
+            it.getArgument(0)
+        }
+        actionsProvider = createActionsProvider()
+
+        viewModel.actions.value[2].onClicked?.invoke()
+        verify(uiEventLogger, never())
+            .log(eq(ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED), any(), any())
+        verify(smartActionsProvider, never()).wrapIntent(any(), any(), any(), any())
+        actionsProvider.setCompletedScreenshot(validResult)
+        verify(smartActionsProvider)
+            .wrapIntent(eq(quickShare), eq(validResult.uri), eq(validResult.subject), eq("testid"))
+        verify(uiEventLogger).log(eq(ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED), eq(0), eq(""))
+    }
+
+    private fun createActionsProvider(): ScreenshotActionsProvider {
+        return DefaultScreenshotActionsProvider(
+            context,
+            viewModel,
+            actionIntentExecutor,
+            smartActionsProvider,
+            uiEventLogger,
+            testScope,
+            request,
+            "testid",
+            { transition },
+            requestDismissal,
+        )
+    }
 }