Merge changes from topic "screenshot-permission"

* changes:
  Implement the new screenshot activities.
  WM changes to support new screenshot functionality.
  Add flag for the new App Clips screenshot feature.
  Add screenshot permission, action, and API.
diff --git a/core/api/current.txt b/core/api/current.txt
index dfc2602..09faa05 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -121,6 +121,7 @@
     field public static final String INTERACT_ACROSS_PROFILES = "android.permission.INTERACT_ACROSS_PROFILES";
     field public static final String INTERNET = "android.permission.INTERNET";
     field public static final String KILL_BACKGROUND_PROCESSES = "android.permission.KILL_BACKGROUND_PROCESSES";
+    field public static final String LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE = "android.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE";
     field public static final String LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK = "android.permission.LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK";
     field public static final String LOADER_USAGE_STATS = "android.permission.LOADER_USAGE_STATS";
     field public static final String LOCATION_HARDWARE = "android.permission.LOCATION_HARDWARE";
@@ -7197,6 +7198,7 @@
   }
 
   public class StatusBarManager {
+    method @RequiresPermission(android.Manifest.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE) public boolean canLaunchCaptureContentActivityForNote(@NonNull android.app.Activity);
     method public void requestAddTileService(@NonNull android.content.ComponentName, @NonNull CharSequence, @NonNull android.graphics.drawable.Icon, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
     field public static final int TILE_ADD_REQUEST_ERROR_APP_NOT_IN_FOREGROUND = 1004; // 0x3ec
     field public static final int TILE_ADD_REQUEST_ERROR_BAD_COMPONENT = 1002; // 0x3ea
@@ -10711,6 +10713,7 @@
     field public static final String ACTION_INSERT_OR_EDIT = "android.intent.action.INSERT_OR_EDIT";
     field public static final String ACTION_INSTALL_FAILURE = "android.intent.action.INSTALL_FAILURE";
     field @Deprecated public static final String ACTION_INSTALL_PACKAGE = "android.intent.action.INSTALL_PACKAGE";
+    field @RequiresPermission(android.Manifest.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE) public static final String ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE = "android.intent.action.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE";
     field public static final String ACTION_LOCALE_CHANGED = "android.intent.action.LOCALE_CHANGED";
     field public static final String ACTION_LOCKED_BOOT_COMPLETED = "android.intent.action.LOCKED_BOOT_COMPLETED";
     field public static final String ACTION_MAIN = "android.intent.action.MAIN";
@@ -10804,6 +10807,11 @@
     field public static final String ACTION_VOICE_COMMAND = "android.intent.action.VOICE_COMMAND";
     field @Deprecated public static final String ACTION_WALLPAPER_CHANGED = "android.intent.action.WALLPAPER_CHANGED";
     field public static final String ACTION_WEB_SEARCH = "android.intent.action.WEB_SEARCH";
+    field public static final int CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN = 4; // 0x4
+    field public static final int CAPTURE_CONTENT_FOR_NOTE_FAILED = 1; // 0x1
+    field public static final int CAPTURE_CONTENT_FOR_NOTE_SUCCESS = 0; // 0x0
+    field public static final int CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED = 2; // 0x2
+    field public static final int CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED = 3; // 0x3
     field public static final String CATEGORY_ACCESSIBILITY_SHORTCUT_TARGET = "android.intent.category.ACCESSIBILITY_SHORTCUT_TARGET";
     field public static final String CATEGORY_ALTERNATIVE = "android.intent.category.ALTERNATIVE";
     field public static final String CATEGORY_APP_BROWSER = "android.intent.category.APP_BROWSER";
@@ -10859,6 +10867,7 @@
     field public static final String EXTRA_AUTO_LAUNCH_SINGLE_CHOICE = "android.intent.extra.AUTO_LAUNCH_SINGLE_CHOICE";
     field public static final String EXTRA_BCC = "android.intent.extra.BCC";
     field public static final String EXTRA_BUG_REPORT = "android.intent.extra.BUG_REPORT";
+    field public static final String EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE = "android.intent.extra.CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE";
     field public static final String EXTRA_CC = "android.intent.extra.CC";
     field @Deprecated public static final String EXTRA_CHANGED_COMPONENT_NAME = "android.intent.extra.changed_component_name";
     field public static final String EXTRA_CHANGED_COMPONENT_NAME_LIST = "android.intent.extra.changed_component_name_list";
diff --git a/core/java/android/app/StatusBarManager.java b/core/java/android/app/StatusBarManager.java
index 1c1a558..9e31011 100644
--- a/core/java/android/app/StatusBarManager.java
+++ b/core/java/android/app/StatusBarManager.java
@@ -30,6 +30,7 @@
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
 import android.graphics.drawable.Icon;
 import android.media.INearbyMediaDevicesProvider;
 import android.media.INearbyMediaDevicesUpdateCallback;
@@ -47,6 +48,7 @@
 import android.util.Slog;
 import android.view.View;
 
+import com.android.internal.statusbar.AppClipsServiceConnector;
 import com.android.internal.statusbar.IAddTileResultCallback;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.internal.statusbar.IUndoMediaTransferCallback;
@@ -1190,6 +1192,37 @@
         return CompatChanges.isChangeEnabled(MEDIA_CONTROL_SESSION_ACTIONS, packageName, user);
     }
 
+    /**
+     * Checks whether the supplied activity can {@link Activity#startActivityForResult(Intent, int)}
+     * a system activity that captures content on the screen to take a screenshot.
+     *
+     * <p>Note: The result should not be cached.
+     *
+     * <p>The system activity displays an editing tool that allows user to edit the screenshot, save
+     * it on device, and return the edited screenshot as {@link android.net.Uri} to the calling
+     * activity. User interaction is required to return the edited screenshot to the calling
+     * activity.
+     *
+     * <p>When {@code true}, callers can use {@link Activity#startActivityForResult(Intent, int)}
+     * to start start the content capture activity using
+     * {@link Intent#ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE}.
+     *
+     * @param activity Calling activity
+     * @return true if the activity supports launching the capture content activity for note.
+     *
+     * @see Intent#ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE
+     * @see Manifest.permission#LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE
+     * @see android.app.role.RoleManager#ROLE_NOTES
+     */
+    @RequiresPermission(Manifest.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE)
+    public boolean canLaunchCaptureContentActivityForNote(@NonNull Activity activity) {
+        Objects.requireNonNull(activity);
+        IBinder activityToken = activity.getActivityToken();
+        int taskId = ActivityClient.getInstance().getTaskForActivity(activityToken, false);
+        return new AppClipsServiceConnector(mContext)
+                .canLaunchCaptureContentActivityForNote(taskId);
+    }
+
     /** @hide */
     public static String windowStateToString(int state) {
         if (state == WINDOW_STATE_HIDING) return "WINDOW_STATE_HIDING";
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 30984b2..d7ab6d7 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -31,8 +31,10 @@
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
+import android.app.Activity;
 import android.app.ActivityThread;
 import android.app.AppGlobals;
+import android.app.StatusBarManager;
 import android.bluetooth.BluetoothDevice;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.pm.ActivityInfo;
@@ -5137,6 +5139,86 @@
     public static final String EXTRA_USE_STYLUS_MODE = "android.intent.extra.USE_STYLUS_MODE";
 
     /**
+     * Activity Action: Use with startActivityForResult to start a system activity that captures
+     * content on the screen to take a screenshot and present it to the user for editing. The
+     * edited screenshot is saved on device and returned to the calling activity as a {@link Uri}
+     * through {@link #getData()}. User interaction is required to return the edited screenshot to
+     * the calling activity.
+     *
+     * <p>This intent action requires the permission
+     * {@link android.Manifest.permission#LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE}.
+     *
+     * <p>Callers should query
+     * {@link StatusBarManager#canLaunchCaptureContentActivityForNote(Activity)} before showing a UI
+     * element that allows users to trigger this flow.
+     */
+    @RequiresPermission(Manifest.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE)
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE =
+            "android.intent.action.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE";
+
+    /**
+     * An int extra used by activity started with
+     * {@link #ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE} to indicate status of the response.
+     * This extra is used along with result code set to {@link android.app.Activity#RESULT_OK}.
+     *
+     * <p>The value for this extra can be one of the following:
+     * <ul>
+     *     <li>{@link #CAPTURE_CONTENT_FOR_NOTE_SUCCESS}</li>
+     *     <li>{@link #CAPTURE_CONTENT_FOR_NOTE_FAILED}</li>
+     *     <li>{@link #CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED}</li>
+     *     <li>{@link #CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED}</li>
+     *     <li>{@link #CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN}</li>
+     * </ul>
+     */
+    public static final String EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE =
+            "android.intent.extra.CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE";
+
+    /**
+     * A response code used with {@link #EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE} to indicate
+     * that the request was a success.
+     *
+     * <p>This code will only be returned after the user has interacted with the system screenshot
+     * activity to consent to sharing the data with the note.
+     *
+     * <p>The captured screenshot is returned as a {@link Uri} through {@link #getData()}.
+     */
+    public static final int CAPTURE_CONTENT_FOR_NOTE_SUCCESS = 0;
+
+    /**
+     * A response code used with {@link #EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE} to indicate
+     * that something went wrong.
+     */
+    public static final int CAPTURE_CONTENT_FOR_NOTE_FAILED = 1;
+
+    /**
+     * A response code used with {@link #EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE} to indicate
+     * that user canceled the content capture flow.
+     */
+    public static final int CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED = 2;
+
+    /**
+     * A response code used with {@link #EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE} to indicate
+     * that the intent action {@link #ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE} was started
+     * by an activity that is running in a non-supported window mode.
+     */
+    public static final int CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED = 3;
+
+    /**
+     * A response code used with {@link #EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE} to indicate
+     * that screenshot is blocked by IT admin.
+     */
+    public static final int CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN = 4;
+
+    /** @hide */
+    @IntDef(value = {
+            CAPTURE_CONTENT_FOR_NOTE_SUCCESS, CAPTURE_CONTENT_FOR_NOTE_FAILED,
+            CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED,
+            CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface CaptureContentForNoteStatusCodes {}
+
+    /**
      * Broadcast Action: Sent to the integrity component when a package
      * needs to be verified. The data contains the package URI along with other relevant
      * information.
diff --git a/core/java/com/android/internal/statusbar/AppClipsServiceConnector.java b/core/java/com/android/internal/statusbar/AppClipsServiceConnector.java
new file mode 100644
index 0000000..287b85f
--- /dev/null
+++ b/core/java/com/android/internal/statusbar/AppClipsServiceConnector.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.statusbar;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.util.Log;
+
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * A helper class to communicate with the App Clips service running in SystemUI.
+ */
+public class AppClipsServiceConnector {
+
+    private static final String TAG = AppClipsServiceConnector.class.getSimpleName();
+
+    private final Context mContext;
+    private final Handler mHandler;
+
+    public AppClipsServiceConnector(Context context) {
+        mContext = context;
+        HandlerThread handlerThread = new HandlerThread(TAG);
+        handlerThread.start();
+        mHandler = handlerThread.getThreadHandler();
+    }
+
+    /**
+     * @return true if the task represented by {@code taskId} can launch App Clips screenshot flow,
+     * false otherwise.
+     */
+    public boolean canLaunchCaptureContentActivityForNote(int taskId) {
+        try {
+            CompletableFuture<Boolean> future = new CompletableFuture<>();
+            connectToServiceAndProcessRequest(taskId, future);
+            return future.get();
+        } catch (Exception e) {
+            Log.d(TAG, "Exception from service\n" + e);
+        }
+
+        return false;
+    }
+
+    private void connectToServiceAndProcessRequest(int taskId, CompletableFuture<Boolean> future) {
+        ServiceConnection serviceConnection = new ServiceConnection() {
+            @Override
+            public void onServiceConnected(ComponentName name, IBinder service) {
+                try {
+                    future.complete(IAppClipsService.Stub.asInterface(
+                            service).canLaunchCaptureContentActivityForNote(taskId));
+                } catch (Exception e) {
+                    Log.d(TAG, "Exception from service\n" + e);
+                }
+                future.complete(false);
+            }
+
+            @Override
+            public void onServiceDisconnected(ComponentName name) {
+                if (!future.isDone()) {
+                    future.complete(false);
+                }
+            }
+        };
+
+        final ComponentName serviceComponent = ComponentName.unflattenFromString(
+                mContext.getResources().getString(
+                        com.android.internal.R.string.config_screenshotAppClipsServiceComponent));
+        final Intent serviceIntent = new Intent();
+        serviceIntent.setComponent(serviceComponent);
+
+        boolean bindService = mContext.bindServiceAsUser(serviceIntent, serviceConnection,
+                Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE, mHandler,
+                mContext.getUser());
+
+        // Complete the future early if service not bound.
+        if (!bindService) {
+            future.complete(false);
+        }
+    }
+}
diff --git a/core/java/com/android/internal/statusbar/IAppClipsService.aidl b/core/java/com/android/internal/statusbar/IAppClipsService.aidl
new file mode 100644
index 0000000..013d0d3
--- /dev/null
+++ b/core/java/com/android/internal/statusbar/IAppClipsService.aidl
@@ -0,0 +1,26 @@
+/**
+ * Copyright (C) 2023, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.statusbar;
+
+/**
+ * A service that runs in SystemUI and helps determine if App Clips flow is supported in the
+ * current state of device. This service needs to run in SystemUI in order to communicate with the
+ * instance of app bubbles.
+ */
+interface IAppClipsService {
+    boolean canLaunchCaptureContentActivityForNote(in int taskId);
+}
\ No newline at end of file
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 99ac708..6aee3cd 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2579,6 +2579,14 @@
     <!-- ==================================================== -->
     <eat-comment />
 
+    <!-- Allows an application to capture screen content to perform a screenshot using the intent
+         action {@link android.content.Intent#ACTION_LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE}.
+         <p>Protection level: internal|role
+         <p>Intended for use by ROLE_NOTES only.
+    -->
+    <permission android:name="android.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE"
+        android:protectionLevel="internal|role" />
+
     <!-- Allows an application to get notified when a screen capture of its windows is attempted.
          <p>Protection level: normal
     -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 1e3074c..0c13484 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -3233,6 +3233,12 @@
     <string name="config_somnambulatorComponent" translatable="false"
             >com.android.systemui/com.android.systemui.Somnambulator</string>
 
+    <!-- The component name of the screenshot App Clips service that communicates with SystemUI to
+         evaluate certain aspects of App Clips flow such as whether a calling activity can launch
+         capture content for note activity. -->
+    <string name="config_screenshotAppClipsServiceComponent" translatable="false"
+            >com.android.systemui/com.android.systemui.screenshot.appclips.AppClipsService</string>
+
     <!-- The component name of a special dock app that merely launches a dream.
          We don't want to launch this app when docked because it causes an unnecessary
          activity transition.  We just want to start the dream.. -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 11aac16..18084d8 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -370,6 +370,7 @@
   <java-symbol type="string" name="config_controlsPackage" />
   <java-symbol type="string" name="config_screenRecorderComponent" />
   <java-symbol type="string" name="config_somnambulatorComponent" />
+  <java-symbol type="string" name="config_screenshotAppClipsServiceComponent" />
   <java-symbol type="string" name="config_screenshotServiceComponent" />
   <java-symbol type="string" name="config_screenshotErrorReceiverComponent" />
   <java-symbol type="string" name="config_slicePermissionComponent" />
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 360bfe7..e36dfc3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -68,10 +68,14 @@
 import android.util.Log;
 import android.util.Pair;
 import android.util.SparseArray;
+import android.view.IWindowManager;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.WindowInsets;
 import android.view.WindowManager;
+import android.window.ScreenCapture;
+import android.window.ScreenCapture.ScreenCaptureListener;
+import android.window.ScreenCapture.ScreenshotSync;
 
 import androidx.annotation.MainThread;
 import androidx.annotation.Nullable;
@@ -143,6 +147,7 @@
     private final SyncTransactionQueue mSyncQueue;
     private final ShellController mShellController;
     private final ShellCommandHandler mShellCommandHandler;
+    private final IWindowManager mWmService;
 
     // Used to post to main UI thread
     private final ShellExecutor mMainExecutor;
@@ -237,7 +242,8 @@
             @ShellMainThread Handler mainHandler,
             @ShellBackgroundThread ShellExecutor bgExecutor,
             TaskViewTransitions taskViewTransitions,
-            SyncTransactionQueue syncQueue) {
+            SyncTransactionQueue syncQueue,
+            IWindowManager wmService) {
         mContext = context;
         mShellCommandHandler = shellCommandHandler;
         mShellController = shellController;
@@ -269,6 +275,7 @@
         mOneHandedOptional = oneHandedOptional;
         mDragAndDropController = dragAndDropController;
         mSyncQueue = syncQueue;
+        mWmService = wmService;
         shellInit.addInitCallback(this::onInit, this);
     }
 
@@ -1037,6 +1044,21 @@
     }
 
     /**
+     * Performs a screenshot that may exclude the bubble layer, if one is present. The screenshot
+     * can be access via the supplied {@link ScreenshotSync#get()} asynchronously.
+     *
+     * TODO(b/267324693): Implement the exclude layer functionality in screenshot.
+     */
+    public void getScreenshotExcludingBubble(int displayId,
+            Pair<ScreenCaptureListener, ScreenshotSync> screenCaptureListener) {
+        try {
+            mWmService.captureDisplay(displayId, null, screenCaptureListener.first);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to capture screenshot");
+        }
+    }
+
+    /**
      * Fills the overflow bubbles by loading them from disk.
      */
     void loadOverflowBubblesFromDisk() {
@@ -1750,6 +1772,25 @@
         }
 
         @Override
+        public boolean isAppBubbleTaskId(int taskId) {
+            Bubble appBubble = mBubbleData.getBubbleInStackWithKey(KEY_APP_BUBBLE);
+            return appBubble != null && appBubble.getTaskId() == taskId;
+        }
+
+        @Override
+        @Nullable
+        public ScreenshotSync getScreenshotExcludingBubble(int displayId) {
+            Pair<ScreenCaptureListener, ScreenshotSync> screenCaptureListener =
+                    ScreenCapture.createSyncCaptureListener();
+
+            mMainExecutor.execute(
+                    () -> BubbleController.this.getScreenshotExcludingBubble(displayId,
+                            screenCaptureListener));
+
+            return screenCaptureListener.second;
+        }
+
+        @Override
         public boolean handleDismissalInterception(BubbleEntry entry,
                 @Nullable List<BubbleEntry> children, IntConsumer removeCallback,
                 Executor callbackExecutor) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
index df43257..1753cda 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
@@ -16,6 +16,8 @@
 
 package com.android.wm.shell.bubbles;
 
+import static android.window.ScreenCapture.ScreenshotSync;
+
 import static java.lang.annotation.ElementType.FIELD;
 import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
 import static java.lang.annotation.ElementType.PARAMETER;
@@ -24,11 +26,13 @@
 import android.app.NotificationChannel;
 import android.content.Intent;
 import android.content.pm.UserInfo;
+import android.hardware.HardwareBuffer;
 import android.os.UserHandle;
 import android.service.notification.NotificationListenerService;
 import android.service.notification.NotificationListenerService.RankingMap;
 import android.util.Pair;
 import android.util.SparseArray;
+import android.window.ScreenCapture.ScreenshotHardwareBuffer;
 
 import androidx.annotation.IntDef;
 import androidx.annotation.Nullable;
@@ -132,6 +136,18 @@
      */
     void showOrHideAppBubble(Intent intent);
 
+    /** @return true if the specified {@code taskId} corresponds to app bubble's taskId. */
+    boolean isAppBubbleTaskId(int taskId);
+
+    /**
+     * @return a {@link ScreenshotSync} after performing a screenshot that may exclude the bubble
+     * layer, if one is present. The underlying {@link ScreenshotHardwareBuffer} can be access via
+     * {@link ScreenshotSync#get()} asynchronously and care should be taken to
+     * {@link HardwareBuffer#close()} the associated
+     * {@link ScreenshotHardwareBuffer#getHardwareBuffer()} when no longer required.
+     */
+    ScreenshotSync getScreenshotExcludingBubble(int displayId);
+
     /**
      * @return a bubble that matches the provided shortcutId, if one exists.
      */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 4879d86..d83f1eb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -21,6 +21,7 @@
 import android.os.Handler;
 import android.os.UserManager;
 import android.view.Choreographer;
+import android.view.IWindowManager;
 import android.view.WindowManager;
 
 import com.android.internal.jank.InteractionJankMonitor;
@@ -170,14 +171,15 @@
             @ShellMainThread Handler mainHandler,
             @ShellBackgroundThread ShellExecutor bgExecutor,
             TaskViewTransitions taskViewTransitions,
-            SyncTransactionQueue syncQueue) {
+            SyncTransactionQueue syncQueue,
+            IWindowManager wmService) {
         return new BubbleController(context, shellInit, shellCommandHandler, shellController, data,
                 null /* synchronizer */, floatingContentCoordinator,
                 new BubbleDataRepository(context, launcherApps, mainExecutor),
                 statusBarService, windowManager, windowManagerShellWrapper, userManager,
                 launcherApps, logger, taskStackListener, organizer, positioner, displayController,
                 oneHandedOptional, dragAndDropController, mainExecutor, mainHandler, bgExecutor,
-                taskViewTransitions, syncQueue);
+                taskViewTransitions, syncQueue, wmService);
     }
 
     //
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 8e98d2d..64e1bc2 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -418,6 +418,34 @@
                  android:permission="com.android.systemui.permission.SELF"
                  android:exported="false" />
 
+        <activity android:name=".screenshot.AppClipsTrampolineActivity"
+            android:theme="@style/AppClipsTrampolineActivity"
+            android:label="@string/screenshot_preview_description"
+            android:permission="android.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE"
+            android:exported="true">
+            <intent-filter android:priority="1">
+                <action android:name="android.intent.action.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
+        <activity android:name=".screenshot.AppClipsActivity"
+            android:theme="@style/AppClipsActivity"
+            android:process=":appclips.screenshot"
+            android:label="@string/screenshot_preview_description"
+            android:permission="com.android.systemui.permission.SELF"
+            android:excludeFromRecents="true"
+            android:exported="false"
+            android:noHistory="true" />
+
+        <service android:name=".screenshot.appclips.AppClipsScreenshotHelperService"
+            android:permission="com.android.systemui.permission.SELF"
+            android:exported="false" />
+
+        <service android:name=".screenshot.appclips.AppClipsService"
+            android:permission="android.permission.LAUNCH_CAPTURE_CONTENT_ACTIVITY_FOR_NOTE"
+            android:exported="true" />
+
         <service android:name=".screenrecord.RecordingService"
                  android:foregroundServiceType="systemExempted"/>
 
diff --git a/packages/SystemUI/res/layout/app_clips_screenshot.xml b/packages/SystemUI/res/layout/app_clips_screenshot.xml
new file mode 100644
index 0000000..5155b77
--- /dev/null
+++ b/packages/SystemUI/res/layout/app_clips_screenshot.xml
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:background="@null"
+    android:id="@+id/root"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <Button
+        android:id="@+id/save"
+        style="@android:style/Widget.DeviceDefault.Button.Colored"
+        android:layout_width="wrap_content"
+        android:layout_height="48dp"
+        android:text="@string/app_clips_save_add_to_note"
+        android:layout_marginStart="8dp"
+        android:background="@drawable/overlay_button_background"
+        android:textColor="?android:textColorSecondary"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toTopOf="@id/preview" />
+
+    <Button
+        android:id="@+id/cancel"
+        style="@android:style/Widget.DeviceDefault.Button.Colored"
+        android:layout_width="wrap_content"
+        android:layout_height="48dp"
+        android:text="@android:string/cancel"
+        android:layout_marginStart="6dp"
+        android:background="@drawable/overlay_button_background"
+        android:textColor="?android:textColorSecondary"
+        app:layout_constraintStart_toEndOf="@id/save"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toTopOf="@id/preview" />
+
+    <ImageView
+        android:id="@+id/preview"
+        android:layout_width="0px"
+        android:layout_height="0px"
+        android:paddingHorizontal="48dp"
+        android:paddingTop="8dp"
+        android:paddingBottom="42dp"
+        android:contentDescription="@string/screenshot_preview_description"
+        app:layout_constrainedHeight="true"
+        app:layout_constrainedWidth="true"
+        app:layout_constraintTop_toBottomOf="@id/save"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        tools:background="?android:colorBackground"
+        tools:minHeight="100dp"
+        tools:minWidth="100dp" />
+
+    <com.android.systemui.screenshot.CropView
+        android:id="@+id/crop_view"
+        android:layout_width="0px"
+        android:layout_height="0px"
+        android:paddingTop="8dp"
+        android:paddingBottom="42dp"
+        app:layout_constrainedHeight="true"
+        app:layout_constrainedWidth="true"
+        app:layout_constraintTop_toTopOf="@id/preview"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:handleThickness="@dimen/screenshot_crop_handle_thickness"
+        app:handleColor="?android:attr/colorAccent"
+        app:scrimColor="?android:colorBackgroundFloating"
+        app:scrimAlpha="128"
+        app:containerBackgroundColor="?android:colorBackgroundFloating"
+        tools:background="?android:colorBackground"
+        tools:minHeight="100dp"
+        tools:minWidth="100dp" />
+
+    <com.android.systemui.screenshot.MagnifierView
+        android:id="@+id/magnifier"
+        android:visibility="invisible"
+        android:layout_width="200dp"
+        android:layout_height="200dp"
+        android:elevation="2dp"
+        app:layout_constraintTop_toTopOf="@id/preview"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:handleThickness="@dimen/screenshot_crop_handle_thickness"
+        app:handleColor="?android:attr/colorAccent"
+        app:scrimColor="?android:colorBackgroundFloating"
+        app:scrimAlpha="128"
+        app:borderThickness="4dp"
+        app:borderColor="#fff" />
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 034f145..050b1e1 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -445,6 +445,13 @@
          screenshot has been saved to work profile. If blank, a default icon will be shown. -->
     <string name="config_sceenshotWorkProfileFilesApp" translatable="false"></string>
 
+    <!-- The component name of the screenshot editing activity that provides the App Clips flow.
+         The App Clips flow includes taking a screenshot, showing user screenshot cropping activity
+         and finally letting user send the screenshot to the calling notes app. This activity
+         should not send the screenshot to the calling activity without user consent. -->
+    <string name="config_screenshotAppClipsActivityComponent" translatable="false"
+            >com.android.systemui/com.android.systemui.screenshot.AppClipsActivity</string>
+
     <!-- Remote copy default activity.  Must handle REMOTE_COPY_ACTION intents.
      This name is in the ComponentName flattened format (package/class)  -->
     <string name="config_remoteCopyPackage" translatable="false"></string>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 943844f..dd9794f 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -247,6 +247,8 @@
     <string name="screenshot_detected_template"><xliff:g id="appName" example="Google Chrome">%1$s</xliff:g> detected this screenshot.</string>
     <!-- A notice shown to the user to indicate that multiple apps have detected the screenshot that the user has just taken. [CHAR LIMIT=75] -->
     <string name="screenshot_detected_multiple_template"><xliff:g id="appName" example="Google Chrome">%1$s</xliff:g> and other open apps detected this screenshot.</string>
+    <!-- Add to note button used in App Clips flow to return the saved screenshot image to notes app. [CHAR LIMIT=NONE] -->
+    <string name="app_clips_save_add_to_note">Add to note</string>
 
     <!-- Notification title displayed for screen recording [CHAR LIMIT=50]-->
     <string name="screenrecord_name">Screen Recorder</string>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index dd87e91..4998d68 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -758,6 +758,18 @@
     </style>
 
     <!-- Screenshots -->
+    <style name="AppClipsTrampolineActivity">
+        <item name="android:windowIsTranslucent">true</item>
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowIsFloating">true</item>
+        <item name="android:backgroundDimEnabled">true</item>
+    </style>
+
+    <style name="AppClipsActivity" parent="LongScreenshotActivity">
+        <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:windowIsTranslucent">true</item>
+    </style>
+
     <style name="LongScreenshotActivity" parent="@android:style/Theme.DeviceDefault.DayNight">
         <item name="android:windowNoTitle">true</item>
         <item name="android:windowLightStatusBar">true</item>
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
index 4eb444e..a5beb4e 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java
@@ -23,6 +23,8 @@
 import com.android.systemui.keyguard.WorkLockActivity;
 import com.android.systemui.people.PeopleSpaceActivity;
 import com.android.systemui.people.widget.LaunchConversationActivity;
+import com.android.systemui.screenshot.AppClipsActivity;
+import com.android.systemui.screenshot.AppClipsTrampolineActivity;
 import com.android.systemui.screenshot.LongScreenshotActivity;
 import com.android.systemui.sensorprivacy.SensorUseStartedActivity;
 import com.android.systemui.sensorprivacy.television.TvSensorPrivacyChangedActivity;
@@ -119,6 +121,18 @@
     @ClassKey(LongScreenshotActivity.class)
     public abstract Activity bindLongScreenshotActivity(LongScreenshotActivity activity);
 
+    /** Inject into AppClipsTrampolineActivity. */
+    @Binds
+    @IntoMap
+    @ClassKey(AppClipsTrampolineActivity.class)
+    public abstract Activity bindAppClipsTrampolineActivity(AppClipsTrampolineActivity activity);
+
+    /** Inject into AppClipsActivity. */
+    @Binds
+    @IntoMap
+    @ClassKey(AppClipsActivity.class)
+    public abstract Activity bindAppClipsActivity(AppClipsActivity activity);
+
     /** Inject into LaunchConversationActivity. */
     @Binds
     @IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 94101df..555f6d3 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -516,6 +516,9 @@
     // TODO(b/266955521): Tracking bug
     @JvmField val SCREENSHOT_DETECTION = unreleasedFlag(1303, "screenshot_detection")
 
+    // TODO(b/251205791): Tracking Bug
+    @JvmField val SCREENSHOT_APP_CLIPS = releasedFlag(1304, "screenshot_app_clips")
+
     // 1400 - columbus
     // TODO(b/254512756): Tracking Bug
     val QUICK_TAP_IN_PCC = releasedFlag(1400, "quick_tap_in_pcc")
@@ -549,7 +552,7 @@
     @JvmField val DUAL_SHADE = releasedFlag(1801, "dual_shade")
 
     // 1900
-    @JvmField val NOTE_TASKS = unreleasedFlag(1900, "keycode_flag")
+    @JvmField val NOTE_TASKS = releasedFlag(1900, "keycode_flag")
 
     // 2000 - device controls
     @Keep @JvmField val USE_APP_PANELS = releasedFlag(2000, "use_app_panels", teamfood = true)
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java
new file mode 100644
index 0000000..f8d86a0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsActivity.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot;
+
+import static com.android.systemui.screenshot.AppClipsTrampolineActivity.ACTION_FINISH_FROM_TRAMPOLINE;
+import static com.android.systemui.screenshot.AppClipsTrampolineActivity.EXTRA_RESULT_RECEIVER;
+import static com.android.systemui.screenshot.AppClipsTrampolineActivity.EXTRA_SCREENSHOT_URI;
+import static com.android.systemui.screenshot.AppClipsTrampolineActivity.PERMISSION_SELF;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageView;
+
+import androidx.activity.ComponentActivity;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.android.settingslib.Utils;
+import com.android.systemui.R;
+
+import javax.inject.Inject;
+
+/**
+ * An {@link Activity} to take a screenshot for the App Clips flow and presenting a screenshot
+ * editing tool.
+ *
+ * <p>An App Clips flow includes:
+ * <ul>
+ *     <li>Checking if calling activity meets the prerequisites. This is done by
+ *     {@link AppClipsTrampolineActivity}.
+ *     <li>Performing the screenshot.
+ *     <li>Showing a screenshot editing tool.
+ *     <li>Returning the screenshot to the {@link AppClipsTrampolineActivity} so that it can return
+ *     the screenshot to the calling activity after explicit user consent.
+ * </ul>
+ *
+ * <p>This {@link Activity} runs in its own separate process to isolate memory intensive image
+ * editing from SysUI process.
+ *
+ * TODO(b/267309532): Polish UI and animations.
+ */
+public final class AppClipsActivity extends ComponentActivity {
+
+    private final AppClipsViewModel.Factory mViewModelFactory;
+    private final BroadcastReceiver mBroadcastReceiver;
+    private final IntentFilter mIntentFilter;
+
+    private View mLayout;
+    private View mRoot;
+    private ImageView mPreview;
+    private CropView mCropView;
+    private MagnifierView mMagnifierView;
+    private Button mSave;
+    private Button mCancel;
+    private AppClipsViewModel mViewModel;
+
+    private ResultReceiver mResultReceiver;
+
+    @Inject
+    public AppClipsActivity(AppClipsViewModel.Factory viewModelFactory) {
+        mViewModelFactory = viewModelFactory;
+
+        mBroadcastReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                // Trampoline activity was dismissed so finish this activity.
+                if (ACTION_FINISH_FROM_TRAMPOLINE.equals(intent.getAction())) {
+                    if (!isFinishing()) {
+                        // Nullify the ResultReceiver so that result cannot be sent as trampoline
+                        // activity is already finishing.
+                        mResultReceiver = null;
+                        finish();
+                    }
+                }
+            }
+        };
+
+        mIntentFilter = new IntentFilter(ACTION_FINISH_FROM_TRAMPOLINE);
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        overridePendingTransition(0, 0);
+        super.onCreate(savedInstanceState);
+
+        // Register the broadcast receiver that informs when the trampoline activity is dismissed.
+        registerReceiver(mBroadcastReceiver, mIntentFilter, PERMISSION_SELF, null,
+                RECEIVER_NOT_EXPORTED);
+
+        Intent intent = getIntent();
+        mResultReceiver = intent.getParcelableExtra(EXTRA_RESULT_RECEIVER, ResultReceiver.class);
+        if (mResultReceiver == null) {
+            setErrorThenFinish(Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED);
+            return;
+        }
+
+        // Inflate layout but don't add it yet as it should be added after the screenshot is ready
+        // for preview.
+        mLayout = getLayoutInflater().inflate(R.layout.app_clips_screenshot, null);
+        mRoot = mLayout.findViewById(R.id.root);
+
+        mSave = mLayout.findViewById(R.id.save);
+        mCancel = mLayout.findViewById(R.id.cancel);
+        mSave.setOnClickListener(this::onClick);
+        mCancel.setOnClickListener(this::onClick);
+
+        mMagnifierView = mLayout.findViewById(R.id.magnifier);
+        mCropView = mLayout.findViewById(R.id.crop_view);
+        mCropView.setCropInteractionListener(mMagnifierView);
+
+        mPreview = mLayout.findViewById(R.id.preview);
+        mPreview.addOnLayoutChangeListener(
+                (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
+                        updateImageDimensions());
+
+        mViewModel = new ViewModelProvider(this, mViewModelFactory).get(AppClipsViewModel.class);
+        mViewModel.getScreenshot().observe(this, this::setScreenshot);
+        mViewModel.getResultLiveData().observe(this, this::setResultThenFinish);
+        mViewModel.getErrorLiveData().observe(this, this::setErrorThenFinish);
+
+        if (savedInstanceState == null) {
+            mViewModel.performScreenshot();
+        }
+    }
+
+    @Override
+    public void finish() {
+        super.finish();
+        overridePendingTransition(0, 0);
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+
+        unregisterReceiver(mBroadcastReceiver);
+
+        // If neither error nor result was set, it implies that the activity is finishing due to
+        // some other reason such as user dismissing this activity using back gesture. Inform error.
+        if (isFinishing() && mViewModel.getErrorLiveData().getValue() == null
+                && mViewModel.getResultLiveData().getValue() == null) {
+            // Set error but don't finish as the activity is already finishing.
+            setError(Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED);
+        }
+    }
+
+    private void setScreenshot(Bitmap screenshot) {
+        // Set background, status and navigation bar colors as the activity is no longer
+        // translucent.
+        int colorBackgroundFloating = Utils.getColorAttr(this,
+                android.R.attr.colorBackgroundFloating).getDefaultColor();
+        mRoot.setBackgroundColor(colorBackgroundFloating);
+
+        BitmapDrawable drawable = new BitmapDrawable(getResources(), screenshot);
+        mPreview.setImageDrawable(drawable);
+        mPreview.setAlpha(1f);
+
+        mMagnifierView.setDrawable(drawable, screenshot.getWidth(), screenshot.getHeight());
+
+        // Screenshot is now available so set content view.
+        setContentView(mLayout);
+    }
+
+    private void onClick(View view) {
+        mSave.setEnabled(false);
+        mCancel.setEnabled(false);
+
+        int id = view.getId();
+        if (id == R.id.save) {
+            saveScreenshotThenFinish();
+        } else {
+            setErrorThenFinish(Intent.CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED);
+        }
+    }
+
+    private void saveScreenshotThenFinish() {
+        Drawable drawable = mPreview.getDrawable();
+        if (drawable == null) {
+            setErrorThenFinish(Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED);
+            return;
+        }
+
+        Rect bounds = mCropView.getCropBoundaries(drawable.getIntrinsicWidth(),
+                drawable.getIntrinsicHeight());
+
+        if (bounds.isEmpty()) {
+            setErrorThenFinish(Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED);
+            return;
+        }
+
+        updateImageDimensions();
+        mViewModel.saveScreenshotThenFinish(drawable, bounds);
+    }
+
+    private void setResultThenFinish(Uri uri) {
+        if (mResultReceiver == null) {
+            return;
+        }
+
+        Bundle data = new Bundle();
+        data.putInt(Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE,
+                Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS);
+        data.putParcelable(EXTRA_SCREENSHOT_URI, uri);
+        try {
+            mResultReceiver.send(Activity.RESULT_OK, data);
+        } catch (Exception e) {
+            // Do nothing.
+        }
+
+        // Nullify the ResultReceiver before finishing to avoid resending the result.
+        mResultReceiver = null;
+        finish();
+    }
+
+    private void setErrorThenFinish(int errorCode) {
+        setError(errorCode);
+        finish();
+    }
+
+    private void setError(int errorCode) {
+        if (mResultReceiver == null) {
+            return;
+        }
+
+        Bundle data = new Bundle();
+        data.putInt(Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, errorCode);
+        try {
+            mResultReceiver.send(RESULT_OK, data);
+        } catch (Exception e) {
+            // Do nothing.
+        }
+
+        // Nullify the ResultReceiver to avoid resending the result.
+        mResultReceiver = null;
+    }
+
+    private void updateImageDimensions() {
+        Drawable drawable = mPreview.getDrawable();
+        if (drawable == null) {
+            return;
+        }
+
+        Rect bounds = drawable.getBounds();
+        float imageRatio = bounds.width() / (float) bounds.height();
+        int previewWidth = mPreview.getWidth() - mPreview.getPaddingLeft()
+                - mPreview.getPaddingRight();
+        int previewHeight = mPreview.getHeight() - mPreview.getPaddingTop()
+                - mPreview.getPaddingBottom();
+        float viewRatio = previewWidth / (float) previewHeight;
+
+        if (imageRatio > viewRatio) {
+            // Image is full width and height is constrained, compute extra padding to inform
+            // CropView.
+            int imageHeight = (int) (previewHeight * viewRatio / imageRatio);
+            int extraPadding = (previewHeight - imageHeight) / 2;
+            mCropView.setExtraPadding(extraPadding, extraPadding);
+            mCropView.setImageWidth(previewWidth);
+        } else {
+            // Image is full height.
+            mCropView.setExtraPadding(mPreview.getPaddingTop(), mPreview.getPaddingBottom());
+            mCropView.setImageWidth((int) (previewHeight * imageRatio));
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java
new file mode 100644
index 0000000..65fb4c9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsCrossProcessHelper.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.appclips;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.view.Display;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.infra.AndroidFuture;
+import com.android.internal.infra.ServiceConnector;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Application;
+
+import javax.inject.Inject;
+
+/** An intermediary singleton object to help communicating with the cross process service. */
+@SysUISingleton
+public class AppClipsCrossProcessHelper {
+
+    private final ServiceConnector<IAppClipsScreenshotHelperService> mProxyConnector;
+
+    @Inject
+    public AppClipsCrossProcessHelper(@Application Context context) {
+        mProxyConnector = new ServiceConnector.Impl<IAppClipsScreenshotHelperService>(context,
+                new Intent(context, AppClipsScreenshotHelperService.class),
+                Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY
+                        | Context.BIND_NOT_VISIBLE, context.getUserId(),
+                IAppClipsScreenshotHelperService.Stub::asInterface);
+    }
+
+    /**
+     * Returns a {@link Bitmap} captured in the SysUI process, {@code null} in case of an error.
+     *
+     * <p>Note: The SysUI process captures a {@link ScreenshotHardwareBufferInternal} which is ok to
+     * pass around but not a {@link Bitmap}.
+     */
+    @Nullable
+    public Bitmap takeScreenshot() {
+        try {
+            AndroidFuture<ScreenshotHardwareBufferInternal> future =
+                    mProxyConnector.postForResult(
+                            service -> service.takeScreenshot(Display.DEFAULT_DISPLAY));
+            return future.get().createBitmapThenCloseBuffer();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperService.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperService.java
new file mode 100644
index 0000000..6f8c365
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperService.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.appclips;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.window.ScreenCapture.ScreenshotHardwareBuffer;
+import android.window.ScreenCapture.ScreenshotSync;
+
+import androidx.annotation.Nullable;
+
+import com.android.systemui.screenshot.AppClipsActivity;
+import com.android.wm.shell.bubbles.Bubbles;
+
+import java.util.Optional;
+
+import javax.inject.Inject;
+
+/**
+ * A helper service that runs in SysUI process and helps {@link AppClipsActivity} which runs in its
+ * own separate process take a screenshot.
+ */
+public class AppClipsScreenshotHelperService extends Service {
+
+    private final Optional<Bubbles> mOptionalBubbles;
+
+    @Inject
+    public AppClipsScreenshotHelperService(Optional<Bubbles> optionalBubbles) {
+        mOptionalBubbles = optionalBubbles;
+    }
+
+    @Nullable
+    @Override
+    public IBinder onBind(Intent intent) {
+        return new IAppClipsScreenshotHelperService.Stub() {
+            @Override
+            @Nullable
+            public ScreenshotHardwareBufferInternal takeScreenshot(int displayId) {
+                if (mOptionalBubbles.isEmpty()) {
+                    return null;
+                }
+
+                ScreenshotSync screenshotSync =
+                        mOptionalBubbles.get().getScreenshotExcludingBubble(displayId);
+                ScreenshotHardwareBuffer screenshotHardwareBuffer = screenshotSync.get();
+                if (screenshotHardwareBuffer == null) {
+                    return null;
+                }
+
+                return new ScreenshotHardwareBufferInternal(screenshotHardwareBuffer);
+            }
+        };
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java
new file mode 100644
index 0000000..d0b7ad3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsService.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.appclips;
+
+import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS;
+
+import android.app.Activity;
+import android.app.Service;
+import android.app.StatusBarManager;
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.IBinder;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.statusbar.IAppClipsService;
+import com.android.systemui.R;
+import com.android.systemui.dagger.qualifiers.Application;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.wm.shell.bubbles.Bubbles;
+
+import java.util.Optional;
+
+import javax.inject.Inject;
+
+/**
+ * A service that communicates with {@link StatusBarManager} to support the
+ * {@link StatusBarManager#canLaunchCaptureContentActivityForNote(Activity)} API.
+ */
+public class AppClipsService extends Service {
+
+    @Application private final Context mContext;
+    private final FeatureFlags mFeatureFlags;
+    private final Optional<Bubbles> mOptionalBubbles;
+    private final DevicePolicyManager mDevicePolicyManager;
+    private final boolean mAreTaskAndTimeIndependentPrerequisitesMet;
+
+    @Inject
+    public AppClipsService(@Application Context context, FeatureFlags featureFlags,
+            Optional<Bubbles> optionalBubbles, DevicePolicyManager devicePolicyManager) {
+        mContext = context;
+        mFeatureFlags = featureFlags;
+        mOptionalBubbles = optionalBubbles;
+        mDevicePolicyManager = devicePolicyManager;
+
+        mAreTaskAndTimeIndependentPrerequisitesMet = checkIndependentVariables();
+    }
+
+    private boolean checkIndependentVariables() {
+        if (!mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)) {
+            return false;
+        }
+
+        if (mOptionalBubbles.isEmpty()) {
+            return false;
+        }
+
+        return isComponentValid();
+    }
+
+    private boolean isComponentValid() {
+        ComponentName componentName;
+        try {
+            componentName = ComponentName.unflattenFromString(
+                    mContext.getString(R.string.config_screenshotAppClipsActivityComponent));
+        } catch (Resources.NotFoundException e) {
+            return false;
+        }
+
+        return componentName != null
+                && !componentName.getPackageName().isEmpty()
+                && !componentName.getClassName().isEmpty();
+    }
+
+    @Nullable
+    @Override
+    public IBinder onBind(Intent intent) {
+        return new IAppClipsService.Stub() {
+            @Override
+            public boolean canLaunchCaptureContentActivityForNote(int taskId) {
+                if (!mAreTaskAndTimeIndependentPrerequisitesMet) {
+                    return false;
+                }
+
+                if (!mOptionalBubbles.get().isAppBubbleTaskId(taskId)) {
+                    return false;
+                }
+
+                return !mDevicePolicyManager.getScreenCaptureDisabled(null);
+            }
+        };
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java
new file mode 100644
index 0000000..4759cc6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivity.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot;
+
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED;
+import static android.content.Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE;
+import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION;
+
+import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS;
+
+import android.app.Activity;
+import android.app.admin.DevicePolicyManager;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Parcel;
+import android.os.ResultReceiver;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.systemui.R;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.notetask.NoteTaskController;
+import com.android.wm.shell.bubbles.Bubbles;
+
+import java.util.Optional;
+
+import javax.inject.Inject;
+
+/**
+ * A trampoline activity that is responsible for:
+ * <ul>
+ *     <li>Performing precondition checks before starting the actual screenshot activity.
+ *     <li>Communicating with the screenshot activity and the calling activity.
+ * </ul>
+ *
+ * <p>As this activity is started in a bubble app, the windowing for this activity is restricted
+ * to the parent bubble app. The screenshot editing activity, see {@link AppClipsActivity}, is
+ * started in a regular activity window using {@link Intent#FLAG_ACTIVITY_NEW_TASK}. However,
+ * {@link Activity#startActivityForResult(Intent, int)} is not compatible with
+ * {@link Intent#FLAG_ACTIVITY_NEW_TASK}. So, this activity acts as a trampoline activity to
+ * abstract the complexity of communication with the screenshot editing activity for a simpler
+ * developer experience.
+ *
+ * TODO(b/267309532): Polish UI and animations.
+ */
+public class AppClipsTrampolineActivity extends Activity {
+
+    private static final String TAG = AppClipsTrampolineActivity.class.getSimpleName();
+    public static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
+    public static final String EXTRA_SCREENSHOT_URI = TAG + "SCREENSHOT_URI";
+    public static final String ACTION_FINISH_FROM_TRAMPOLINE = TAG + "FINISH_FROM_TRAMPOLINE";
+    static final String EXTRA_RESULT_RECEIVER = TAG + "RESULT_RECEIVER";
+
+    private final DevicePolicyManager mDevicePolicyManager;
+    private final FeatureFlags mFeatureFlags;
+    private final Optional<Bubbles> mOptionalBubbles;
+    private final NoteTaskController mNoteTaskController;
+    private final ResultReceiver mResultReceiver;
+
+    private Intent mKillAppClipsBroadcastIntent;
+
+    @Inject
+    public AppClipsTrampolineActivity(DevicePolicyManager devicePolicyManager, FeatureFlags flags,
+            Optional<Bubbles> optionalBubbles, NoteTaskController noteTaskController,
+            @Main Handler mainHandler) {
+        mDevicePolicyManager = devicePolicyManager;
+        mFeatureFlags = flags;
+        mOptionalBubbles = optionalBubbles;
+        mNoteTaskController = noteTaskController;
+
+        mResultReceiver = createResultReceiver(mainHandler);
+    }
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        if (savedInstanceState != null) {
+            return;
+        }
+
+        if (!mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)) {
+            finish();
+            return;
+        }
+
+        if (mOptionalBubbles.isEmpty()) {
+            setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+            return;
+        }
+
+        if (!mOptionalBubbles.get().isAppBubbleTaskId(getTaskId())) {
+            setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED);
+            return;
+        }
+
+        if (mDevicePolicyManager.getScreenCaptureDisabled(null)) {
+            setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN);
+            return;
+        }
+
+        ComponentName componentName;
+        try {
+            componentName = ComponentName.unflattenFromString(
+                    getString(R.string.config_screenshotAppClipsActivityComponent));
+        } catch (Resources.NotFoundException e) {
+            setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+            return;
+        }
+
+        if (componentName == null || componentName.getPackageName().isEmpty()
+                || componentName.getClassName().isEmpty()) {
+            setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+            return;
+        }
+
+        Intent intent = new Intent().setComponent(componentName).addFlags(
+                Intent.FLAG_ACTIVITY_NEW_TASK).putExtra(EXTRA_RESULT_RECEIVER, mResultReceiver);
+        try {
+            // Start the App Clips activity.
+            startActivity(intent);
+
+            // Set up the broadcast intent that will inform the above App Clips activity to finish
+            // when this trampoline activity is finished.
+            mKillAppClipsBroadcastIntent =
+                    new Intent(ACTION_FINISH_FROM_TRAMPOLINE)
+                            .setComponent(componentName)
+                            .setPackage(componentName.getPackageName());
+        } catch (ActivityNotFoundException e) {
+            setErrorResultAndFinish(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+
+        if (isFinishing() && mKillAppClipsBroadcastIntent != null) {
+            sendBroadcast(mKillAppClipsBroadcastIntent, PERMISSION_SELF);
+        }
+    }
+
+    private void setErrorResultAndFinish(int errorCode) {
+        setResult(RESULT_OK,
+                new Intent().putExtra(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, errorCode));
+        finish();
+    }
+
+    private class AppClipsResultReceiver extends ResultReceiver {
+
+        AppClipsResultReceiver(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        protected void onReceiveResult(int resultCode, Bundle resultData) {
+            if (isFinishing()) {
+                // It's too late, trampoline activity is finishing or already finished.
+                // Return early.
+                return;
+            }
+
+            // Package the response that should be sent to the calling activity.
+            Intent convertedData = new Intent();
+            int statusCode = CAPTURE_CONTENT_FOR_NOTE_FAILED;
+            if (resultData != null) {
+                statusCode = resultData.getInt(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE,
+                        CAPTURE_CONTENT_FOR_NOTE_FAILED);
+            }
+            convertedData.putExtra(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, statusCode);
+
+            if (statusCode == CAPTURE_CONTENT_FOR_NOTE_SUCCESS) {
+                Uri uri = resultData.getParcelable(EXTRA_SCREENSHOT_URI, Uri.class);
+                convertedData.setData(uri).addFlags(FLAG_GRANT_READ_URI_PERMISSION);
+            }
+
+            // Broadcast no longer required, setting it to null.
+            mKillAppClipsBroadcastIntent = null;
+
+            // Expand the note bubble before returning the result. As App Clips API is only
+            // available when in a bubble, isInMultiWindowMode is always false below.
+            mNoteTaskController.showNoteTask(false);
+            setResult(RESULT_OK, convertedData);
+            finish();
+        }
+    }
+
+    /**
+     * @return a {@link ResultReceiver} by initializing an {@link AppClipsResultReceiver} and
+     * converting it into a generic {@link ResultReceiver} to pass across a different but trusted
+     * process.
+     */
+    private ResultReceiver createResultReceiver(@Main Handler handler) {
+        AppClipsResultReceiver appClipsResultReceiver = new AppClipsResultReceiver(handler);
+        Parcel parcel = Parcel.obtain();
+        appClipsResultReceiver.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        ResultReceiver resultReceiver  = ResultReceiver.CREATOR.createFromParcel(parcel);
+        parcel.recycle();
+        return resultReceiver;
+    }
+
+    /** This is a test only API for mocking response from {@link AppClipsActivity}. */
+    @VisibleForTesting
+    public ResultReceiver getResultReceiverForTest() {
+        return mResultReceiver;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java
new file mode 100644
index 0000000..5a7b5f9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/AppClipsViewModel.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot;
+
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED;
+
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.HardwareRenderer;
+import android.graphics.RecordingCanvas;
+import android.graphics.Rect;
+import android.graphics.RenderNode;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Process;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.screenshot.appclips.AppClipsCrossProcessHelper;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.time.ZonedDateTime;
+import java.util.UUID;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+
+import javax.inject.Inject;
+
+/** A {@link ViewModel} to help with the App Clips screenshot flow. */
+final class AppClipsViewModel extends ViewModel {
+
+    private final AppClipsCrossProcessHelper mAppClipsCrossProcessHelper;
+    private final ImageExporter mImageExporter;
+    @Main
+    private final Executor mMainExecutor;
+    @Background
+    private final Executor mBgExecutor;
+
+    private final MutableLiveData<Bitmap> mScreenshotLiveData;
+    private final MutableLiveData<Uri> mResultLiveData;
+    private final MutableLiveData<Integer> mErrorLiveData;
+
+    AppClipsViewModel(AppClipsCrossProcessHelper appClipsCrossProcessHelper,
+            ImageExporter imageExporter, @Main Executor mainExecutor,
+            @Background Executor bgExecutor) {
+        mAppClipsCrossProcessHelper = appClipsCrossProcessHelper;
+        mImageExporter = imageExporter;
+        mMainExecutor = mainExecutor;
+        mBgExecutor = bgExecutor;
+
+        mScreenshotLiveData = new MutableLiveData<>();
+        mResultLiveData = new MutableLiveData<>();
+        mErrorLiveData = new MutableLiveData<>();
+    }
+
+    /** Grabs a screenshot and updates the {@link Bitmap} set in screenshot {@link LiveData}. */
+    void performScreenshot() {
+        mBgExecutor.execute(() -> {
+            Bitmap screenshot = mAppClipsCrossProcessHelper.takeScreenshot();
+            mMainExecutor.execute(() -> {
+                if (screenshot == null) {
+                    mErrorLiveData.setValue(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+                } else {
+                    mScreenshotLiveData.setValue(screenshot);
+                }
+            });
+        });
+    }
+
+    /** Returns a {@link LiveData} that holds the captured screenshot. */
+    LiveData<Bitmap> getScreenshot() {
+        return mScreenshotLiveData;
+    }
+
+    /** Returns a {@link LiveData} that holds the {@link Uri} where screenshot is saved. */
+    LiveData<Uri> getResultLiveData() {
+        return mResultLiveData;
+    }
+
+    /**
+     * Returns a {@link LiveData} that holds the error codes for
+     * {@link Intent#EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE}.
+     */
+    LiveData<Integer> getErrorLiveData() {
+        return mErrorLiveData;
+    }
+
+    /**
+     * Saves the provided {@link Drawable} to storage then informs the result {@link Uri} to
+     * {@link LiveData}.
+     */
+    void saveScreenshotThenFinish(Drawable screenshotDrawable, Rect bounds) {
+        mBgExecutor.execute(() -> {
+            // Render the screenshot bitmap in background.
+            Bitmap screenshotBitmap = renderBitmap(screenshotDrawable, bounds);
+
+            // Export and save the screenshot in background.
+            // TODO(b/267310185): Save to work profile UserHandle.
+            ListenableFuture<ImageExporter.Result> exportFuture = mImageExporter.export(
+                    mBgExecutor, UUID.randomUUID(), screenshotBitmap, ZonedDateTime.now(),
+                    Process.myUserHandle());
+
+            // Get the result and update state on main thread.
+            exportFuture.addListener(() -> {
+                try {
+                    ImageExporter.Result result = exportFuture.get();
+                    if (result.uri == null) {
+                        mErrorLiveData.setValue(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+                        return;
+                    }
+
+                    mResultLiveData.setValue(result.uri);
+                } catch (CancellationException | InterruptedException | ExecutionException e) {
+                    mErrorLiveData.setValue(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+                }
+            }, mMainExecutor);
+        });
+    }
+
+    private static Bitmap renderBitmap(Drawable drawable, Rect bounds) {
+        final RenderNode output = new RenderNode("Screenshot save");
+        output.setPosition(0, 0, bounds.width(), bounds.height());
+        RecordingCanvas canvas = output.beginRecording();
+        canvas.translate(-bounds.left, -bounds.top);
+        canvas.clipRect(bounds);
+        drawable.draw(canvas);
+        output.endRecording();
+        return HardwareRenderer.createHardwareBitmap(output, bounds.width(), bounds.height());
+    }
+
+    /** Helper factory to help with injecting {@link AppClipsViewModel}. */
+    static final class Factory implements ViewModelProvider.Factory {
+
+        private final AppClipsCrossProcessHelper mAppClipsCrossProcessHelper;
+        private final ImageExporter mImageExporter;
+        @Main
+        private final Executor mMainExecutor;
+        @Background
+        private final Executor mBgExecutor;
+
+        @Inject
+        Factory(AppClipsCrossProcessHelper appClipsCrossProcessHelper,  ImageExporter imageExporter,
+                @Main Executor mainExecutor, @Background Executor bgExecutor) {
+            mAppClipsCrossProcessHelper = appClipsCrossProcessHelper;
+            mImageExporter = imageExporter;
+            mMainExecutor = mainExecutor;
+            mBgExecutor = bgExecutor;
+        }
+
+        @NonNull
+        @Override
+        public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
+            if (modelClass != AppClipsViewModel.class) {
+                throw new IllegalArgumentException();
+            }
+
+            //noinspection unchecked
+            return (T) new AppClipsViewModel(mAppClipsCrossProcessHelper, mImageExporter,
+                    mMainExecutor, mBgExecutor);
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/IAppClipsScreenshotHelperService.aidl b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/IAppClipsScreenshotHelperService.aidl
new file mode 100644
index 0000000..640e742
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/IAppClipsScreenshotHelperService.aidl
@@ -0,0 +1,29 @@
+/**
+ * Copyright (C) 2023, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.appclips;
+
+import android.os.Bundle;
+
+import com.android.systemui.screenshot.appclips.ScreenshotHardwareBufferInternal;
+
+/**
+ * A helper service that runs in SysUI process and helps {@link AppClipsActivity} which runs in its
+ * own separate process take a screenshot.
+ */
+interface IAppClipsScreenshotHelperService {
+    @nullable ScreenshotHardwareBufferInternal takeScreenshot(in int displayId);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/ScreenshotHardwareBufferInternal.aidl b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/ScreenshotHardwareBufferInternal.aidl
new file mode 100644
index 0000000..3a7b944
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/ScreenshotHardwareBufferInternal.aidl
@@ -0,0 +1,19 @@
+/**
+ * Copyright (C) 2023, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.appclips;
+
+parcelable ScreenshotHardwareBufferInternal;
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/appclips/ScreenshotHardwareBufferInternal.java b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/ScreenshotHardwareBufferInternal.java
new file mode 100644
index 0000000..3b107f1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/appclips/ScreenshotHardwareBufferInternal.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.appclips;
+
+import android.graphics.Bitmap;
+import android.graphics.ColorSpace;
+import android.graphics.ParcelableColorSpace;
+import android.hardware.HardwareBuffer;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.window.ScreenCapture.ScreenshotHardwareBuffer;
+
+/**
+ * An internal version of {@link ScreenshotHardwareBuffer} that helps with parceling the information
+ * necessary for creating a {@link Bitmap}.
+ */
+public class ScreenshotHardwareBufferInternal implements Parcelable {
+
+    public static final Creator<ScreenshotHardwareBufferInternal> CREATOR =
+            new Creator<>() {
+                @Override
+                public ScreenshotHardwareBufferInternal createFromParcel(Parcel in) {
+                    return new ScreenshotHardwareBufferInternal(in);
+                }
+
+                @Override
+                public ScreenshotHardwareBufferInternal[] newArray(int size) {
+                    return new ScreenshotHardwareBufferInternal[size];
+                }
+            };
+    private final HardwareBuffer mHardwareBuffer;
+    private final ParcelableColorSpace mParcelableColorSpace;
+
+    public ScreenshotHardwareBufferInternal(
+            ScreenshotHardwareBuffer screenshotHardwareBuffer) {
+        mHardwareBuffer = screenshotHardwareBuffer.getHardwareBuffer();
+        mParcelableColorSpace = new ParcelableColorSpace(
+                screenshotHardwareBuffer.getColorSpace());
+    }
+
+    private ScreenshotHardwareBufferInternal(Parcel in) {
+        mHardwareBuffer = in.readParcelable(HardwareBuffer.class.getClassLoader(),
+                HardwareBuffer.class);
+        mParcelableColorSpace = in.readParcelable(ParcelableColorSpace.class.getClassLoader(),
+                ParcelableColorSpace.class);
+    }
+
+    /**
+     * Returns a {@link Bitmap} represented by the underlying data and successively closes the
+     * internal {@link HardwareBuffer}. See,
+     * {@link Bitmap#wrapHardwareBuffer(HardwareBuffer, ColorSpace)} and
+     * {@link HardwareBuffer#close()} for more information.
+     */
+    public Bitmap createBitmapThenCloseBuffer() {
+        Bitmap bitmap = Bitmap.wrapHardwareBuffer(mHardwareBuffer,
+                mParcelableColorSpace.getColorSpace());
+        mHardwareBuffer.close();
+        return bitmap;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeParcelable(mHardwareBuffer, flags);
+        dest.writeParcelable(mParcelableColorSpace, flags);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof ScreenshotHardwareBufferInternal)) {
+            return false;
+        }
+
+        ScreenshotHardwareBufferInternal other = (ScreenshotHardwareBufferInternal) obj;
+        return mHardwareBuffer.equals(other.mHardwareBuffer) && mParcelableColorSpace.equals(
+                other.mParcelableColorSpace);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
index fdb0100..22e238c0 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
@@ -24,6 +24,8 @@
 import com.android.systemui.screenshot.ScreenshotPolicyImpl;
 import com.android.systemui.screenshot.ScreenshotProxyService;
 import com.android.systemui.screenshot.TakeScreenshotService;
+import com.android.systemui.screenshot.appclips.AppClipsScreenshotHelperService;
+import com.android.systemui.screenshot.appclips.AppClipsService;
 
 import dagger.Binds;
 import dagger.Module;
@@ -52,4 +54,13 @@
     @Binds
     abstract ImageCapture bindImageCaptureImpl(ImageCaptureImpl capture);
 
+    @Binds
+    @IntoMap
+    @ClassKey(AppClipsScreenshotHelperService.class)
+    abstract Service bindAppClipsScreenshotHelperService(AppClipsScreenshotHelperService service);
+
+    @Binds
+    @IntoMap
+    @ClassKey(AppClipsService.class)
+    abstract Service bindAppClipsService(AppClipsService service);
 }
diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml
index 2c1e681..ed2772a 100644
--- a/packages/SystemUI/tests/AndroidManifest.xml
+++ b/packages/SystemUI/tests/AndroidManifest.xml
@@ -159,6 +159,12 @@
             android:enabled="false"
             tools:replace="android:authorities"
             android:grantUriPermissions="true" />
+
+        <activity
+            android:name="com.android.systemui.screenshot.appclips.AppClipsTrampolineActivityTest$AppClipsTrampolineActivityTestable"
+            android:exported="false"
+            android:permission="com.android.systemui.permission.SELF"
+            android:excludeFromRecents="true" />
     </application>
 
     <instrumentation android:name="android.testing.TestableInstrumentation"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperServiceTest.java
new file mode 100644
index 0000000..6e8f5fe
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsScreenshotHelperServiceTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.appclips;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.content.Intent;
+import android.graphics.ColorSpace;
+import android.hardware.HardwareBuffer;
+import android.os.RemoteException;
+import android.view.Display;
+import android.window.ScreenCapture.ScreenshotHardwareBuffer;
+import android.window.ScreenCapture.ScreenshotSync;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.wm.shell.bubbles.Bubbles;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Optional;
+
+@RunWith(AndroidJUnit4.class)
+public final class AppClipsScreenshotHelperServiceTest extends SysuiTestCase {
+
+    private static final Intent FAKE_INTENT = new Intent();
+    private static final int DEFAULT_DISPLAY = Display.DEFAULT_DISPLAY;
+    private static final HardwareBuffer FAKE_HARDWARE_BUFFER =
+            HardwareBuffer.create(1, 1, HardwareBuffer.RGBA_8888, 1,
+                    HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE);
+    private static final ColorSpace FAKE_COLOR_SPACE = ColorSpace.get(ColorSpace.Named.SRGB);
+    private static final ScreenshotHardwareBufferInternal EXPECTED_SCREENSHOT_BUFFER =
+            new ScreenshotHardwareBufferInternal(
+                    new ScreenshotHardwareBuffer(FAKE_HARDWARE_BUFFER, FAKE_COLOR_SPACE, false,
+                            false));
+
+    @Mock private Optional<Bubbles> mBubblesOptional;
+    @Mock private Bubbles mBubbles;
+    @Mock private ScreenshotHardwareBuffer mScreenshotHardwareBuffer;
+    @Mock private ScreenshotSync mScreenshotSync;
+
+    private AppClipsScreenshotHelperService mAppClipsScreenshotHelperService;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mAppClipsScreenshotHelperService = new AppClipsScreenshotHelperService(mBubblesOptional);
+    }
+
+    @Test
+    public void emptyBubbles_shouldReturnNull() throws RemoteException {
+        when(mBubblesOptional.isEmpty()).thenReturn(true);
+
+        assertThat(getInterface().takeScreenshot(DEFAULT_DISPLAY)).isNull();
+    }
+
+    @Test
+    public void bubblesPresent_screenshotFailed_ShouldReturnNull() throws RemoteException {
+        when(mBubblesOptional.isEmpty()).thenReturn(false);
+        when(mBubblesOptional.get()).thenReturn(mBubbles);
+        when(mBubbles.getScreenshotExcludingBubble(DEFAULT_DISPLAY)).thenReturn(mScreenshotSync);
+        when(mScreenshotSync.get()).thenReturn(null);
+
+        assertThat(getInterface().takeScreenshot(DEFAULT_DISPLAY)).isNull();
+    }
+
+    @Test
+    public void bubblesPresent_screenshotSuccess_shouldReturnScreenshot() throws RemoteException {
+        when(mBubblesOptional.isEmpty()).thenReturn(false);
+        when(mBubblesOptional.get()).thenReturn(mBubbles);
+        when(mBubbles.getScreenshotExcludingBubble(DEFAULT_DISPLAY)).thenReturn(mScreenshotSync);
+        when(mScreenshotSync.get()).thenReturn(mScreenshotHardwareBuffer);
+        when(mScreenshotHardwareBuffer.getHardwareBuffer()).thenReturn(FAKE_HARDWARE_BUFFER);
+        when(mScreenshotHardwareBuffer.getColorSpace()).thenReturn(FAKE_COLOR_SPACE);
+
+        assertThat(getInterface().takeScreenshot(DEFAULT_DISPLAY)).isEqualTo(
+                EXPECTED_SCREENSHOT_BUFFER);
+    }
+
+    private IAppClipsScreenshotHelperService getInterface() {
+        return IAppClipsScreenshotHelperService.Stub.asInterface(
+                mAppClipsScreenshotHelperService.onBind(FAKE_INTENT));
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java
new file mode 100644
index 0000000..b55fe36
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsServiceTest.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.appclips;
+
+import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.statusbar.IAppClipsService;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dagger.qualifiers.Application;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.wm.shell.bubbles.Bubbles;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Optional;
+
+@RunWith(AndroidJUnit4.class)
+public final class AppClipsServiceTest extends SysuiTestCase {
+
+    private static final Intent FAKE_INTENT = new Intent();
+    private static final int FAKE_TASK_ID = 42;
+    private static final String EMPTY = "";
+
+    @Mock @Application private Context mMockContext;
+    @Mock private FeatureFlags mFeatureFlags;
+    @Mock private Optional<Bubbles> mOptionalBubbles;
+    @Mock private Bubbles mBubbles;
+    @Mock private DevicePolicyManager mDevicePolicyManager;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Test
+    public void flagOff_shouldReturnFalse() throws RemoteException {
+        when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(false);
+
+        assertThat(getInterfaceWithRealContext()
+                .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse();
+    }
+
+    @Test
+    public void emptyBubbles_shouldReturnFalse() throws RemoteException {
+        when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+        when(mOptionalBubbles.isEmpty()).thenReturn(true);
+
+        assertThat(getInterfaceWithRealContext()
+                .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse();
+    }
+
+    @Test
+    public void taskIdNotAppBubble_shouldReturnFalse() throws RemoteException {
+        when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+        when(mOptionalBubbles.isEmpty()).thenReturn(false);
+        when(mOptionalBubbles.get()).thenReturn(mBubbles);
+        when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(false);
+
+        assertThat(getInterfaceWithRealContext()
+                .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse();
+    }
+
+    @Test
+    public void dpmScreenshotBlocked_shouldReturnFalse() throws RemoteException {
+        when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+        when(mOptionalBubbles.isEmpty()).thenReturn(false);
+        when(mOptionalBubbles.get()).thenReturn(mBubbles);
+        when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true);
+        when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(true);
+
+        assertThat(getInterfaceWithRealContext()
+                .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse();
+    }
+
+    @Test
+    public void configComponentNameNotValid_shouldReturnFalse() throws RemoteException {
+        when(mMockContext.getString(anyInt())).thenReturn(EMPTY);
+        when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+        when(mOptionalBubbles.isEmpty()).thenReturn(false);
+        when(mOptionalBubbles.get()).thenReturn(mBubbles);
+        when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true);
+        when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(false);
+
+        assertThat(getInterfaceWithMockContext()
+                .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isFalse();
+    }
+
+    @Test
+    public void allPrerequisitesSatisfy_shouldReturnTrue() throws RemoteException {
+        when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+        when(mOptionalBubbles.isEmpty()).thenReturn(false);
+        when(mOptionalBubbles.get()).thenReturn(mBubbles);
+        when(mBubbles.isAppBubbleTaskId(eq((FAKE_TASK_ID)))).thenReturn(true);
+        when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(false);
+
+        assertThat(getInterfaceWithRealContext()
+                .canLaunchCaptureContentActivityForNote(FAKE_TASK_ID)).isTrue();
+    }
+
+    private IAppClipsService getInterfaceWithRealContext() {
+        AppClipsService appClipsService = new AppClipsService(getContext(), mFeatureFlags,
+                mOptionalBubbles, mDevicePolicyManager);
+        return getInterfaceFromService(appClipsService);
+    }
+
+    private IAppClipsService getInterfaceWithMockContext() {
+        AppClipsService appClipsService = new AppClipsService(mMockContext, mFeatureFlags,
+                mOptionalBubbles, mDevicePolicyManager);
+        return getInterfaceFromService(appClipsService);
+    }
+
+    private static IAppClipsService getInterfaceFromService(AppClipsService appClipsService) {
+        IBinder iBinder = appClipsService.onBind(FAKE_INTENT);
+        return IAppClipsService.Stub.asInterface(iBinder);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java
new file mode 100644
index 0000000..295d127
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsTrampolineActivityTest.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.appclips;
+
+import static android.app.Instrumentation.ActivityResult;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_SUCCESS;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED;
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED;
+import static android.content.Intent.EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE;
+
+import static com.android.systemui.flags.Flags.SCREENSHOT_APP_CLIPS;
+import static com.android.systemui.screenshot.AppClipsTrampolineActivity.ACTION_FINISH_FROM_TRAMPOLINE;
+import static com.android.systemui.screenshot.AppClipsTrampolineActivity.EXTRA_SCREENSHOT_URI;
+import static com.android.systemui.screenshot.AppClipsTrampolineActivity.PERMISSION_SELF;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.app.Activity;
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.intercepting.SingleActivityFactory;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.notetask.NoteTaskController;
+import com.android.systemui.screenshot.AppClipsTrampolineActivity;
+import com.android.wm.shell.bubbles.Bubbles;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Optional;
+
+@RunWith(AndroidTestingRunner.class)
+public final class AppClipsTrampolineActivityTest extends SysuiTestCase {
+
+    private static final String TEST_URI_STRING = "www.test-uri.com";
+    private static final Uri TEST_URI = Uri.parse(TEST_URI_STRING);
+    private static final int TIME_OUT = 5000;
+
+    @Mock
+    private DevicePolicyManager mDevicePolicyManager;
+    @Mock
+    private FeatureFlags mFeatureFlags;
+    @Mock
+    private Optional<Bubbles> mOptionalBubbles;
+    @Mock
+    private Bubbles mBubbles;
+    @Mock
+    private NoteTaskController mNoteTaskController;
+    @Main
+    private Handler mMainHandler;
+
+    // Using the deprecated ActivityTestRule and SingleActivityFactory to help with injecting mocks
+    // and getting result from activity both of which are difficult to do in newer APIs.
+    private final SingleActivityFactory<AppClipsTrampolineActivityTestable> mFactory =
+            new SingleActivityFactory<>(AppClipsTrampolineActivityTestable.class) {
+                @Override
+                protected AppClipsTrampolineActivityTestable create(Intent unUsed) {
+                    return new AppClipsTrampolineActivityTestable(mDevicePolicyManager,
+                            mFeatureFlags, mOptionalBubbles, mNoteTaskController, mMainHandler);
+                }
+            };
+
+    @Rule
+    public final ActivityTestRule<AppClipsTrampolineActivityTestable> mActivityRule =
+            new ActivityTestRule<>(mFactory, false, false);
+
+    private Context mContext;
+    private Intent mActivityIntent;
+    private ComponentName mExpectedComponentName;
+    private Intent mKillAppClipsActivityBroadcast;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = getContext();
+        mMainHandler = mContext.getMainThreadHandler();
+
+        mActivityIntent = new Intent(mContext, AppClipsTrampolineActivityTestable.class);
+        mExpectedComponentName = ComponentName.unflattenFromString(
+                mContext.getString(
+                        R.string.config_screenshotAppClipsActivityComponent));
+        mKillAppClipsActivityBroadcast = new Intent(ACTION_FINISH_FROM_TRAMPOLINE)
+                .setComponent(mExpectedComponentName)
+                .setPackage(mExpectedComponentName.getPackageName());
+    }
+
+    @After
+    public void tearDown() {
+        mContext.sendBroadcast(mKillAppClipsActivityBroadcast, PERMISSION_SELF);
+        mActivityRule.finishActivity();
+    }
+
+    @Test
+    public void configComponentName_shouldResolve() {
+        // Verify component name is setup - has package and class name.
+        assertThat(mExpectedComponentName).isNotNull();
+        assertThat(mExpectedComponentName.getPackageName()).isNotEmpty();
+        assertThat(mExpectedComponentName.getClassName()).isNotEmpty();
+
+        // Verify an intent when launched with above component resolves to the same component to
+        // confirm that component from above is available in framework.
+        Intent appClipsActivityIntent = new Intent();
+        appClipsActivityIntent.setComponent(mExpectedComponentName);
+        ResolveInfo resolveInfo = getContext().getPackageManager().resolveActivity(
+                appClipsActivityIntent, PackageManager.ResolveInfoFlags.of(0));
+        ActivityInfo activityInfo = resolveInfo.activityInfo;
+
+        assertThat(activityInfo.packageName).isEqualTo(
+                mExpectedComponentName.getPackageName());
+        assertThat(activityInfo.name).isEqualTo(mExpectedComponentName.getClassName());
+    }
+
+    @Test
+    public void flagOff_shouldFinishWithResultCancel() {
+        when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(false);
+
+        mActivityRule.launchActivity(mActivityIntent);
+
+        assertThat(mActivityRule.getActivityResult().getResultCode())
+                .isEqualTo(Activity.RESULT_CANCELED);
+    }
+
+    @Test
+    public void bubblesEmpty_shouldFinishWithFailed() {
+        when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+        when(mOptionalBubbles.isEmpty()).thenReturn(true);
+
+        mActivityRule.launchActivity(mActivityIntent);
+
+        ActivityResult actualResult = mActivityRule.getActivityResult();
+        assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK);
+        assertThat(getStatusCodeExtra(actualResult.getResultData()))
+                .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+    }
+
+    @Test
+    public void taskIdNotAppBubble_shouldFinishWithWindowModeUnsupported() {
+        when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+        when(mOptionalBubbles.isEmpty()).thenReturn(false);
+        when(mOptionalBubbles.get()).thenReturn(mBubbles);
+        when(mBubbles.isAppBubbleTaskId(anyInt())).thenReturn(false);
+
+        mActivityRule.launchActivity(mActivityIntent);
+
+        ActivityResult actualResult = mActivityRule.getActivityResult();
+        assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK);
+        assertThat(getStatusCodeExtra(actualResult.getResultData()))
+                .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_WINDOW_MODE_UNSUPPORTED);
+    }
+
+    @Test
+    public void dpmScreenshotBlocked_shouldFinishWithBlockedByAdmin() {
+        when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+        when(mOptionalBubbles.isEmpty()).thenReturn(false);
+        when(mOptionalBubbles.get()).thenReturn(mBubbles);
+        when(mBubbles.isAppBubbleTaskId(anyInt())).thenReturn(true);
+        when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(true);
+
+        mActivityRule.launchActivity(mActivityIntent);
+
+        ActivityResult actualResult = mActivityRule.getActivityResult();
+        assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK);
+        assertThat(getStatusCodeExtra(actualResult.getResultData()))
+                .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_BLOCKED_BY_ADMIN);
+    }
+
+    @Test
+    public void startAppClipsActivity_userCanceled_shouldReturnUserCanceled() {
+        mockToSatisfyAllPrerequisites();
+
+        AppClipsTrampolineActivityTestable activity = mActivityRule.launchActivity(mActivityIntent);
+        waitForIdleSync();
+
+        Bundle bundle = new Bundle();
+        bundle.putInt(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE,
+                CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED);
+        activity.getResultReceiverForTest().send(Activity.RESULT_OK, bundle);
+        waitForIdleSync();
+
+        ActivityResult actualResult = mActivityRule.getActivityResult();
+        assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK);
+        assertThat(getStatusCodeExtra(actualResult.getResultData()))
+                .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_USER_CANCELED);
+    }
+
+    @Test
+    public void startAppClipsActivity_shouldReturnSuccess() {
+        mockToSatisfyAllPrerequisites();
+
+        AppClipsTrampolineActivityTestable activity = mActivityRule.launchActivity(mActivityIntent);
+        waitForIdleSync();
+
+        Bundle bundle = new Bundle();
+        bundle.putParcelable(EXTRA_SCREENSHOT_URI, TEST_URI);
+        bundle.putInt(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, CAPTURE_CONTENT_FOR_NOTE_SUCCESS);
+        activity.getResultReceiverForTest().send(Activity.RESULT_OK, bundle);
+        waitForIdleSync();
+
+        ActivityResult actualResult = mActivityRule.getActivityResult();
+        assertThat(actualResult.getResultCode()).isEqualTo(Activity.RESULT_OK);
+        assertThat(getStatusCodeExtra(actualResult.getResultData()))
+                .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_SUCCESS);
+        assertThat(actualResult.getResultData().getData()).isEqualTo(TEST_URI);
+    }
+
+    private void mockToSatisfyAllPrerequisites() {
+        when(mFeatureFlags.isEnabled(SCREENSHOT_APP_CLIPS)).thenReturn(true);
+        when(mOptionalBubbles.isEmpty()).thenReturn(false);
+        when(mOptionalBubbles.get()).thenReturn(mBubbles);
+        when(mBubbles.isAppBubbleTaskId(anyInt())).thenReturn(true);
+        when(mDevicePolicyManager.getScreenCaptureDisabled(eq(null))).thenReturn(false);
+    }
+
+    public static final class AppClipsTrampolineActivityTestable extends
+            AppClipsTrampolineActivity {
+
+        public AppClipsTrampolineActivityTestable(DevicePolicyManager devicePolicyManager,
+                FeatureFlags flags,
+                Optional<Bubbles> optionalBubbles,
+                NoteTaskController noteTaskController,
+                @Main Handler mainHandler) {
+            super(devicePolicyManager, flags, optionalBubbles, noteTaskController, mainHandler);
+        }
+    }
+
+    private static int getStatusCodeExtra(Intent intent) {
+        return intent.getIntExtra(EXTRA_CAPTURE_CONTENT_FOR_NOTE_STATUS_CODE, -100);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java
new file mode 100644
index 0000000..d5af7ce1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot;
+
+import static android.content.Intent.CAPTURE_CONTENT_FOR_NOTE_FAILED;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.net.Uri;
+import android.os.UserHandle;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.screenshot.appclips.AppClipsCrossProcessHelper;
+
+import com.google.common.util.concurrent.Futures;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.time.ZonedDateTime;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+
+@RunWith(AndroidJUnit4.class)
+public final class AppClipsViewModelTest extends SysuiTestCase {
+
+    private static final Bitmap FAKE_BITMAP = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888);
+    private static final Drawable FAKE_DRAWABLE = new ShapeDrawable();
+    private static final Rect FAKE_RECT = new Rect();
+    private static final Uri FAKE_URI = Uri.parse("www.test-uri.com");
+
+    @Mock private AppClipsCrossProcessHelper mAppClipsCrossProcessHelper;
+    @Mock private ImageExporter mImageExporter;
+
+    private com.android.systemui.screenshot.AppClipsViewModel mViewModel;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mViewModel = new AppClipsViewModel.Factory(mAppClipsCrossProcessHelper, mImageExporter,
+                getContext().getMainExecutor(), directExecutor()).create(AppClipsViewModel.class);
+    }
+
+    @Test
+    public void performScreenshot_fails_shouldUpdateErrorWithFailed() {
+        when(mAppClipsCrossProcessHelper.takeScreenshot()).thenReturn(null);
+
+        mViewModel.performScreenshot();
+        waitForIdleSync();
+
+        verify(mAppClipsCrossProcessHelper).takeScreenshot();
+        assertThat(mViewModel.getErrorLiveData().getValue())
+                .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+        assertThat(mViewModel.getResultLiveData().getValue()).isNull();
+    }
+
+    @Test
+    public void performScreenshot_succeeds_shouldUpdateScreenshotWithBitmap() {
+        when(mAppClipsCrossProcessHelper.takeScreenshot()).thenReturn(FAKE_BITMAP);
+
+        mViewModel.performScreenshot();
+        waitForIdleSync();
+
+        verify(mAppClipsCrossProcessHelper).takeScreenshot();
+        assertThat(mViewModel.getErrorLiveData().getValue()).isNull();
+        assertThat(mViewModel.getScreenshot().getValue()).isEqualTo(FAKE_BITMAP);
+    }
+
+    @Test
+    public void saveScreenshot_throwsError_shouldUpdateErrorWithFailed() {
+        when(mImageExporter.export(any(Executor.class), any(UUID.class), eq(null), any(
+                ZonedDateTime.class), any(UserHandle.class))).thenReturn(
+                Futures.immediateFailedFuture(new ExecutionException(new Throwable())));
+
+        mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT);
+        waitForIdleSync();
+
+        assertThat(mViewModel.getErrorLiveData().getValue())
+                .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+        assertThat(mViewModel.getResultLiveData().getValue()).isNull();
+    }
+
+    @Test
+    public void saveScreenshot_failsSilently_shouldUpdateErrorWithFailed() {
+        when(mImageExporter.export(any(Executor.class), any(UUID.class), eq(null), any(
+                ZonedDateTime.class), any(UserHandle.class))).thenReturn(
+                Futures.immediateFuture(new ImageExporter.Result()));
+
+        mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT);
+        waitForIdleSync();
+
+        assertThat(mViewModel.getErrorLiveData().getValue())
+                .isEqualTo(CAPTURE_CONTENT_FOR_NOTE_FAILED);
+        assertThat(mViewModel.getResultLiveData().getValue()).isNull();
+    }
+
+    @Test
+    public void saveScreenshot_succeeds_shouldUpdateResultWithUri() {
+        ImageExporter.Result result = new ImageExporter.Result();
+        result.uri = FAKE_URI;
+        when(mImageExporter.export(any(Executor.class), any(UUID.class), eq(null), any(
+                ZonedDateTime.class), any(UserHandle.class))).thenReturn(
+                Futures.immediateFuture(result));
+
+        mViewModel.saveScreenshotThenFinish(FAKE_DRAWABLE, FAKE_RECT);
+        waitForIdleSync();
+
+        assertThat(mViewModel.getErrorLiveData().getValue()).isNull();
+        assertThat(mViewModel.getResultLiveData().getValue()).isEqualTo(FAKE_URI);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 9d518ac..c6b4c92 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -74,6 +74,7 @@
 import android.testing.TestableLooper;
 import android.util.Pair;
 import android.util.SparseArray;
+import android.view.IWindowManager;
 import android.view.View;
 import android.view.ViewTreeObserver;
 import android.view.WindowManager;
@@ -392,7 +393,8 @@
                 syncExecutor,
                 mock(Handler.class),
                 mTaskViewTransitions,
-                mock(SyncTransactionQueue.class));
+                mock(SyncTransactionQueue.class),
+                mock(IWindowManager.class));
         mBubbleController.setExpandListener(mBubbleExpandListener);
         spyOn(mBubbleController);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
index 6357a09..3179285 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
@@ -20,6 +20,7 @@
 import android.content.pm.LauncherApps;
 import android.os.Handler;
 import android.os.UserManager;
+import android.view.IWindowManager;
 import android.view.WindowManager;
 
 import com.android.internal.statusbar.IStatusBarService;
@@ -72,13 +73,14 @@
             ShellExecutor shellMainExecutor,
             Handler shellMainHandler,
             TaskViewTransitions taskViewTransitions,
-            SyncTransactionQueue syncQueue) {
+            SyncTransactionQueue syncQueue,
+            IWindowManager wmService) {
         super(context, shellInit, shellCommandHandler, shellController, data, Runnable::run,
                 floatingContentCoordinator, dataRepository, statusBarService, windowManager,
                 windowManagerShellWrapper, userManager, launcherApps, bubbleLogger,
                 taskStackListener, shellTaskOrganizer, positioner, displayController,
                 oneHandedOptional, dragAndDropController, shellMainExecutor, shellMainHandler,
-                new SyncExecutor(), taskViewTransitions, syncQueue);
+                new SyncExecutor(), taskViewTransitions, syncQueue, wmService);
         setInflateSynchronously(true);
         onInit();
     }