Merge changes I9d8c2997,I16cd74a6,Ic22cc8a6 into main

* changes:
  [SB][Screen Chips] Show a stop dialog for screen share and screen cast.
  [SB][Screen Chips] Show a stop dialog when tapping on screen record chip
  [SB][Screen Chips] Distinguish share-to-app and cast-to-other-device.
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractorTest.kt
index e87c8ad..899122d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractorTest.kt
@@ -35,6 +35,7 @@
 import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx
 import com.android.systemui.screenrecord.RecordingController
 import com.android.systemui.screenrecord.data.model.ScreenRecordModel
+import com.android.systemui.screenrecord.data.repository.ScreenRecordRepositoryImpl
 import com.android.systemui.statusbar.phone.KeyguardDismissUtil
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
@@ -72,11 +73,18 @@
                 .thenReturn(dialog)
         }
 
+    private val screenRecordRepository =
+        ScreenRecordRepositoryImpl(
+            bgCoroutineContext = testScope.testScheduler,
+            recordingController = recordingController,
+        )
+
     private val underTest =
         ScreenRecordTileUserActionInteractor(
             context,
             testScope.testScheduler,
             testScope.testScheduler,
+            screenRecordRepository,
             recordingController,
             keyguardInteractor,
             keyguardDismissUtil,
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index d3569c6..fe968a7 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -319,6 +319,29 @@
     <string name="screenrecord_save_error">Error saving screen recording</string>
     <!-- A toast message shown when the screen recording cannot be started due to a generic error [CHAR LIMIT=NONE] -->
     <string name="screenrecord_start_error">Error starting screen recording</string>
+    <!-- Title for a dialog shown to the user that will let them stop recording their screen [CHAR LIMIT=50] -->
+    <string name="screenrecord_stop_dialog_title">Stop recording screen?</string>
+    <!-- Text telling a user that they will stop recording their screen if they click the "Stop recording" button [CHAR LIMIT=100] -->
+    <string name="screenrecord_stop_dialog_message">You will stop recording your screen</string>
+    <!-- Button to stop a screen recording [CHAR LIMIT=35] -->
+    <string name="screenrecord_stop_dialog_button">Stop recording</string>
+
+    <!-- Title for a dialog shown to the user that will let them stop sharing their screen to another app on the device [CHAR LIMIT=50] -->
+    <string name="share_to_app_stop_dialog_title">Stop sharing screen?</string>
+    <!-- Text telling a user that they will stop sharing their screen if they click the "Stop sharing" button [CHAR LIMIT=100] -->
+    <string name="share_to_app_stop_dialog_message">You will stop sharing your screen</string>
+    <!-- Button to stop screen sharing [CHAR LIMIT=35] -->
+    <string name="share_to_app_stop_dialog_button">Stop sharing</string>
+
+    <!-- Title for a dialog shown to the user that will let them stop casting their screen to a different device [CHAR LIMIT=50] -->
+    <string name="cast_to_other_device_stop_dialog_title">Stop casting screen?</string>
+    <!-- Text telling a user that they will stop casting their screen to a different device if they click the "Stop casting" button [CHAR LIMIT=100] -->
+    <string name="cast_to_other_device_stop_dialog_message">You will stop casting your screen</string>
+    <!-- Button to stop screen casting to a different device [CHAR LIMIT=35] -->
+    <string name="cast_to_other_device_stop_dialog_button">Stop casting</string>
+
+    <!-- Button to close a dialog without doing any action [CHAR LIMIT=20] -->
+    <string name="close_dialog_button">Close</string>
 
     <!-- Notification title displayed for issue recording [CHAR LIMIT=50]-->
     <string name="issuerecord_title">Issue Recorder</string>
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/model/MediaProjectionState.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/model/MediaProjectionState.kt
index 1d5f6f5..de300b2 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/model/MediaProjectionState.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/model/MediaProjectionState.kt
@@ -20,7 +20,21 @@
 
 /** Represents the state of media projection. */
 sealed interface MediaProjectionState {
-    object NotProjecting : MediaProjectionState
-    object EntireScreen : MediaProjectionState
-    data class SingleTask(val task: RunningTaskInfo) : MediaProjectionState
+    /** There is no media being projected. */
+    data object NotProjecting : MediaProjectionState
+
+    /**
+     * Media is currently being projected.
+     *
+     * @property hostPackage the package name of the app that is receiving the content of the media
+     *   projection (aka which app the phone screen contents are being sent to).
+     */
+    sealed class Projecting(open val hostPackage: String) : MediaProjectionState {
+        /** The entire screen is being projected. */
+        data class EntireScreen(override val hostPackage: String) : Projecting(hostPackage)
+
+        /** Only a single task is being projected. */
+        data class SingleTask(override val hostPackage: String, val task: RunningTaskInfo) :
+            Projecting(hostPackage)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
index 3ce0a1e0..8a9adc7 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepository.kt
@@ -64,6 +64,10 @@
         }
     }
 
+    override suspend fun stopProjecting() {
+        withContext(backgroundDispatcher) { mediaProjectionManager.stopActiveProjection() }
+    }
+
     override val mediaProjectionState: Flow<MediaProjectionState> =
         conflatedCallbackFlow {
                 val callback =
@@ -83,7 +87,9 @@
                             session: ContentRecordingSession?
                         ) {
                             Log.d(TAG, "MediaProjectionManager.Callback#onSessionStarted: $session")
-                            launch { trySendWithFailureLogging(stateForSession(session), TAG) }
+                            launch {
+                                trySendWithFailureLogging(stateForSession(info, session), TAG)
+                            }
                         }
                     }
                 mediaProjectionManager.addCallback(callback, handler)
@@ -95,19 +101,23 @@
                 initialValue = MediaProjectionState.NotProjecting,
             )
 
-    private suspend fun stateForSession(session: ContentRecordingSession?): MediaProjectionState {
+    private suspend fun stateForSession(
+        info: MediaProjectionInfo,
+        session: ContentRecordingSession?
+    ): MediaProjectionState {
         if (session == null) {
             return MediaProjectionState.NotProjecting
         }
+
+        val hostPackage = info.packageName
         if (session.contentToRecord == RECORD_CONTENT_DISPLAY || session.tokenToRecord == null) {
-            return MediaProjectionState.EntireScreen
+            return MediaProjectionState.Projecting.EntireScreen(hostPackage)
         }
         val matchingTask =
             tasksRepository.findRunningTaskFromWindowContainerToken(
                 checkNotNull(session.tokenToRecord)
-            )
-                ?: return MediaProjectionState.EntireScreen
-        return MediaProjectionState.SingleTask(matchingTask)
+            ) ?: return MediaProjectionState.Projecting.EntireScreen(hostPackage)
+        return MediaProjectionState.Projecting.SingleTask(hostPackage, matchingTask)
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepository.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepository.kt
index 21300db..50182d7 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionRepository.kt
@@ -26,6 +26,9 @@
     /** Switches the task that should be projected. */
     suspend fun switchProjectedTask(task: RunningTaskInfo)
 
+    /** Stops the currently active projection. */
+    suspend fun stopProjecting()
+
     /** Represents the current [MediaProjectionState]. */
     val mediaProjectionState: Flow<MediaProjectionState>
 }
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt
index c232d4d..118639c 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/domain/interactor/TaskSwitchInteractor.kt
@@ -57,7 +57,7 @@
         mediaProjectionRepository.mediaProjectionState.flatMapLatest { projectionState ->
             Log.d(TAG, "MediaProjectionState -> $projectionState")
             when (projectionState) {
-                is MediaProjectionState.SingleTask -> {
+                is MediaProjectionState.Projecting.SingleTask -> {
                     val projectedTask = projectionState.task
                     tasksRepository.foregroundTask.map { foregroundTask ->
                         if (hasForegroundTaskSwitched(projectedTask, foregroundTask)) {
@@ -67,7 +67,7 @@
                         }
                     }
                 }
-                is MediaProjectionState.EntireScreen,
+                is MediaProjectionState.Projecting.EntireScreen,
                 is MediaProjectionState.NotProjecting -> {
                     flowOf(TaskSwitchState.NotProjectingTask)
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt
index 79720c1..5637115 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt
@@ -35,6 +35,7 @@
 import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
 import com.android.systemui.screenrecord.RecordingController
 import com.android.systemui.screenrecord.data.model.ScreenRecordModel
+import com.android.systemui.screenrecord.data.repository.ScreenRecordRepository
 import com.android.systemui.statusbar.phone.KeyguardDismissUtil
 import javax.inject.Inject
 import kotlin.coroutines.CoroutineContext
@@ -47,6 +48,7 @@
     @Application private val context: Context,
     @Main private val mainContext: CoroutineContext,
     @Background private val backgroundContext: CoroutineContext,
+    private val screenRecordRepository: ScreenRecordRepository,
     private val recordingController: RecordingController,
     private val keyguardInteractor: KeyguardInteractor,
     private val keyguardDismissUtil: KeyguardDismissUtil,
@@ -65,8 +67,7 @@
                             Log.d(TAG, "Cancelling countdown")
                             withContext(backgroundContext) { recordingController.cancelCountdown() }
                         }
-                        is ScreenRecordModel.Recording ->
-                            withContext(backgroundContext) { recordingController.stopRecording() }
+                        is ScreenRecordModel.Recording -> screenRecordRepository.stopRecording()
                         is ScreenRecordModel.DoingNothing ->
                             withContext(mainContext) {
                                 showPrompt(action.expandable, user.identifier)
@@ -122,8 +123,7 @@
                             controller,
                             animateBackgroundBoundsChange = true,
                         )
-                    }
-                        ?: dialog.show()
+                    } ?: dialog.show()
                 } else {
                     dialog.show()
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepository.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepository.kt
index d59d220..9eeb3b9 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepository.kt
@@ -28,6 +28,7 @@
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.withContext
 
 /**
  * Repository storing information about the state of screen recording.
@@ -38,6 +39,9 @@
 interface ScreenRecordRepository {
     /** The current screen recording state. Note that this is a cold flow. */
     val screenRecordState: Flow<ScreenRecordModel>
+
+    /** Stops the recording. */
+    suspend fun stopRecording()
 }
 
 @SysUISingleton
@@ -90,4 +94,8 @@
             ScreenRecordModel.DoingNothing
         }
     }
+
+    override suspend fun stopRecording() {
+        withContext(bgCoroutineContext) { recordingController.stopRecording() }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/domain/interactor/OngoingActivityChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/domain/interactor/OngoingActivityChipInteractor.kt
index c3d37fb..086a32d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/domain/interactor/OngoingActivityChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/domain/interactor/OngoingActivityChipInteractor.kt
@@ -16,11 +16,37 @@
 
 package com.android.systemui.statusbar.chips.domain.interactor
 
+import android.view.View
+import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
+import com.android.systemui.statusbar.phone.SystemUIDialog
 import kotlinx.coroutines.flow.StateFlow
 
 /** Interface for an interactor that knows the state of a single type of ongoing activity chip. */
 interface OngoingActivityChipInteractor {
     /** A flow modeling the chip that should be shown. */
     val chip: StateFlow<OngoingActivityChipModel>
+
+    companion object {
+        /** Creates a chip click listener that launches a dialog created by [dialogDelegate]. */
+        fun createDialogLaunchOnClickListener(
+            dialogDelegate: SystemUIDialog.Delegate,
+            dialogTransitionAnimator: DialogTransitionAnimator,
+        ): View.OnClickListener {
+            return View.OnClickListener { view ->
+                val dialog = dialogDelegate.createDialog()
+                val launchableView =
+                    view.requireViewById<ChipBackgroundContainer>(
+                        R.id.ongoing_activity_chip_background
+                    )
+                // TODO(b/343699052): This makes a beautiful animate-in, but the
+                //  animate-out looks odd because the dialog animates back into the chip
+                //  but then the chip disappears. If we aren't able to address
+                //  b/343699052 in time for launch, we should just use `dialog.show`.
+                dialogTransitionAnimator.showFromView(dialog, launchableView)
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt
index ac16d26..6611434 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractor.kt
@@ -16,6 +16,9 @@
 
 package com.android.systemui.statusbar.chips.mediaprojection.domain.interactor
 
+import android.content.pm.PackageManager
+import androidx.annotation.DrawableRes
+import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.dagger.SysUISingleton
@@ -24,7 +27,12 @@
 import com.android.systemui.mediaprojection.data.repository.MediaProjectionRepository
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.chips.domain.interactor.OngoingActivityChipInteractor
+import com.android.systemui.statusbar.chips.domain.interactor.OngoingActivityChipInteractor.Companion.createDialogLaunchOnClickListener
 import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndCastToOtherDeviceDialogDelegate
+import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndShareToAppDialogDelegate
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.util.Utils
 import com.android.systemui.util.time.SystemClock
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -32,6 +40,7 @@
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
 
 /**
  * Interactor for media-projection-related chips in the status bar.
@@ -47,33 +56,88 @@
 class MediaProjectionChipInteractor
 @Inject
 constructor(
-    @Application scope: CoroutineScope,
-    mediaProjectionRepository: MediaProjectionRepository,
-    val systemClock: SystemClock,
+    @Application private val scope: CoroutineScope,
+    private val mediaProjectionRepository: MediaProjectionRepository,
+    private val packageManager: PackageManager,
+    private val systemClock: SystemClock,
+    private val dialogFactory: SystemUIDialog.Factory,
+    private val dialogTransitionAnimator: DialogTransitionAnimator,
 ) : OngoingActivityChipInteractor {
     override val chip: StateFlow<OngoingActivityChipModel> =
         mediaProjectionRepository.mediaProjectionState
             .map { state ->
                 when (state) {
                     is MediaProjectionState.NotProjecting -> OngoingActivityChipModel.Hidden
-                    is MediaProjectionState.EntireScreen,
-                    is MediaProjectionState.SingleTask -> {
-                        // TODO(b/332662551): Distinguish between cast-to-other-device and
-                        // share-to-app.
-                        OngoingActivityChipModel.Shown(
-                            icon =
-                                Icon.Resource(
-                                    R.drawable.ic_cast_connected,
-                                    ContentDescription.Resource(R.string.accessibility_casting)
-                                ),
-                            // TODO(b/332662551): See if we can use a MediaProjection API to fetch
-                            // this time.
-                            startTimeMs = systemClock.elapsedRealtime()
-                        ) {
-                            // TODO(b/332662551): Implement the pause dialog.
+                    is MediaProjectionState.Projecting -> {
+                        if (isProjectionToOtherDevice(state.hostPackage)) {
+                            createCastToOtherDeviceChip()
+                        } else {
+                            createShareToAppChip()
                         }
                     }
                 }
             }
             .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+
+    /** Stops the currently active projection. */
+    fun stopProjecting() {
+        scope.launch { mediaProjectionRepository.stopProjecting() }
+    }
+
+    /**
+     * Returns true iff projecting to the given [packageName] means that we're projecting to a
+     * *different* device (as opposed to projecting to some application on *this* device).
+     */
+    private fun isProjectionToOtherDevice(packageName: String?): Boolean {
+        // The [isHeadlessRemoteDisplayProvider] check approximates whether a projection is to a
+        // different device or the same device, because headless remote display packages are the
+        // only kinds of packages that do cast-to-other-device. This isn't exactly perfect,
+        // because it means that any projection by those headless remote display packages will be
+        // marked as going to a different device, even if that isn't always true. See b/321078669.
+        return Utils.isHeadlessRemoteDisplayProvider(packageManager, packageName)
+    }
+
+    private fun createCastToOtherDeviceChip(): OngoingActivityChipModel.Shown {
+        return OngoingActivityChipModel.Shown(
+            icon =
+                Icon.Resource(
+                    CAST_TO_OTHER_DEVICE_ICON,
+                    ContentDescription.Resource(R.string.accessibility_casting)
+                ),
+            // TODO(b/332662551): Maybe use a MediaProjection API to fetch this time.
+            startTimeMs = systemClock.elapsedRealtime(),
+            createDialogLaunchOnClickListener(
+                castToOtherDeviceDialogDelegate,
+                dialogTransitionAnimator,
+            ),
+        )
+    }
+
+    private val castToOtherDeviceDialogDelegate =
+        EndCastToOtherDeviceDialogDelegate(
+            dialogFactory,
+            this@MediaProjectionChipInteractor,
+        )
+
+    private fun createShareToAppChip(): OngoingActivityChipModel.Shown {
+        return OngoingActivityChipModel.Shown(
+            // TODO(b/332662551): Use the right content description.
+            icon = Icon.Resource(SHARE_TO_APP_ICON, contentDescription = null),
+            // TODO(b/332662551): Maybe use a MediaProjection API to fetch this time.
+            startTimeMs = systemClock.elapsedRealtime(),
+            createDialogLaunchOnClickListener(shareToAppDialogDelegate, dialogTransitionAnimator),
+        )
+    }
+
+    private val shareToAppDialogDelegate =
+        EndShareToAppDialogDelegate(
+            dialogFactory,
+            this@MediaProjectionChipInteractor,
+        )
+
+    companion object {
+        // TODO(b/332662551): Use the right icon.
+        @DrawableRes val SHARE_TO_APP_ICON = R.drawable.ic_screenshot_share
+        @DrawableRes val CAST_TO_OTHER_DEVICE_ICON = R.drawable.ic_cast_connected
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegate.kt
new file mode 100644
index 0000000..33cec97
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegate.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.chips.mediaprojection.ui.view
+
+import android.os.Bundle
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractor
+import com.android.systemui.statusbar.phone.SystemUIDialog
+
+/** A dialog that lets the user stop an ongoing cast-screen-to-other-device event. */
+class EndCastToOtherDeviceDialogDelegate(
+    private val systemUIDialogFactory: SystemUIDialog.Factory,
+    private val interactor: MediaProjectionChipInteractor,
+) : SystemUIDialog.Delegate {
+    override fun createDialog(): SystemUIDialog {
+        return systemUIDialogFactory.create(this)
+    }
+
+    override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
+        with(dialog) {
+            setIcon(MediaProjectionChipInteractor.CAST_TO_OTHER_DEVICE_ICON)
+            setTitle(R.string.cast_to_other_device_stop_dialog_title)
+            // TODO(b/332662551): Use a different message if they're sharing just a single app.
+            setMessage(R.string.cast_to_other_device_stop_dialog_message)
+            // No custom on-click, because the dialog will automatically be dismissed when the
+            // button is clicked anyway.
+            setNegativeButton(R.string.close_dialog_button, /* onClick= */ null)
+            setPositiveButton(R.string.cast_to_other_device_stop_dialog_button) { _, _ ->
+                interactor.stopProjecting()
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegate.kt
new file mode 100644
index 0000000..3a863b1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegate.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.chips.mediaprojection.ui.view
+
+import android.os.Bundle
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractor
+import com.android.systemui.statusbar.phone.SystemUIDialog
+
+/** A dialog that lets the user stop an ongoing share-screen-to-app event. */
+class EndShareToAppDialogDelegate(
+    private val systemUIDialogFactory: SystemUIDialog.Factory,
+    private val interactor: MediaProjectionChipInteractor,
+) : SystemUIDialog.Delegate {
+    override fun createDialog(): SystemUIDialog {
+        return systemUIDialogFactory.create(this)
+    }
+
+    override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
+        with(dialog) {
+            setIcon(MediaProjectionChipInteractor.SHARE_TO_APP_ICON)
+            setTitle(R.string.share_to_app_stop_dialog_title)
+            // TODO(b/332662551): Use a different message if they're sharing just a single app.
+            setMessage(R.string.share_to_app_stop_dialog_message)
+            // No custom on-click, because the dialog will automatically be dismissed when the
+            // button is clicked anyway.
+            setNegativeButton(R.string.close_dialog_button, /* onClick= */ null)
+            setPositiveButton(R.string.share_to_app_stop_dialog_button) { _, _ ->
+                interactor.stopProjecting()
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
index 585ff5f..4959b09 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.chips.screenrecord.domain.interactor
 
+import androidx.annotation.DrawableRes
+import com.android.systemui.animation.DialogTransitionAnimator
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -23,7 +25,10 @@
 import com.android.systemui.screenrecord.data.model.ScreenRecordModel
 import com.android.systemui.screenrecord.data.repository.ScreenRecordRepository
 import com.android.systemui.statusbar.chips.domain.interactor.OngoingActivityChipInteractor
+import com.android.systemui.statusbar.chips.domain.interactor.OngoingActivityChipInteractor.Companion.createDialogLaunchOnClickListener
 import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.screenrecord.ui.view.EndScreenRecordingDialogDelegate
+import com.android.systemui.statusbar.phone.SystemUIDialog
 import com.android.systemui.util.time.SystemClock
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -31,15 +36,18 @@
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
 
 /** Interactor for the screen recording chip shown in the status bar. */
 @SysUISingleton
 class ScreenRecordChipInteractor
 @Inject
 constructor(
-    @Application scope: CoroutineScope,
-    screenRecordRepository: ScreenRecordRepository,
-    val systemClock: SystemClock,
+    @Application private val scope: CoroutineScope,
+    private val screenRecordRepository: ScreenRecordRepository,
+    private val systemClock: SystemClock,
+    private val dialogFactory: SystemUIDialog.Factory,
+    private val dialogTransitionAnimator: DialogTransitionAnimator,
 ) : OngoingActivityChipInteractor {
     override val chip: StateFlow<OngoingActivityChipModel> =
         screenRecordRepository.screenRecordState
@@ -51,16 +59,29 @@
                     is ScreenRecordModel.Recording ->
                         OngoingActivityChipModel.Shown(
                             // TODO(b/332662551): Also provide a content description.
-                            icon =
-                                Icon.Resource(
-                                    R.drawable.stat_sys_screen_record,
-                                    contentDescription = null
-                                ),
-                            startTimeMs = systemClock.elapsedRealtime()
-                        ) {
-                            // TODO(b/332662551): Implement the pause dialog.
-                        }
+                            icon = Icon.Resource(ICON, contentDescription = null),
+                            startTimeMs = systemClock.elapsedRealtime(),
+                            createDialogLaunchOnClickListener(
+                                dialogDelegate,
+                                dialogTransitionAnimator
+                            ),
+                        )
                 }
             }
             .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+
+    /** Stops the recording. */
+    fun stopRecording() {
+        scope.launch { screenRecordRepository.stopRecording() }
+    }
+
+    private val dialogDelegate =
+        EndScreenRecordingDialogDelegate(
+            dialogFactory,
+            this@ScreenRecordChipInteractor,
+        )
+
+    companion object {
+        @DrawableRes val ICON = R.drawable.ic_screenrecord
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt
new file mode 100644
index 0000000..b8e8cfa
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegate.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.chips.screenrecord.ui.view
+
+import android.os.Bundle
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.ScreenRecordChipInteractor
+import com.android.systemui.statusbar.phone.SystemUIDialog
+
+/** A dialog that lets the user stop an ongoing screen recording. */
+class EndScreenRecordingDialogDelegate(
+    private val systemUIDialogFactory: SystemUIDialog.Factory,
+    private val interactor: ScreenRecordChipInteractor,
+) : SystemUIDialog.Delegate {
+
+    override fun createDialog(): SystemUIDialog {
+        return systemUIDialogFactory.create(this)
+    }
+
+    override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
+        with(dialog) {
+            setIcon(ScreenRecordChipInteractor.ICON)
+            setTitle(R.string.screenrecord_stop_dialog_title)
+            // TODO(b/332662551): Use a different message if they're sharing just a single app.
+            setMessage(R.string.screenrecord_stop_dialog_message)
+            // No custom on-click, because the dialog will automatically be dismissed when the
+            // button is clicked anyway.
+            setNegativeButton(R.string.close_dialog_button, /* onClick= */ null)
+            setPositiveButton(R.string.screenrecord_stop_dialog_button) { _, _ ->
+                interactor.stopRecording()
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
index a2ec1f2..44b5baf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
@@ -94,7 +94,7 @@
                                 is OngoingActivityChipModel.Shown -> {
                                     IconViewBinder.bind(chipModel.icon, chipIconView)
                                     ChipChronometerBinder.bind(chipModel.startTimeMs, chipTimeView)
-                                    // TODO(b/332662551): Attach click listener to chip
+                                    chipView.setOnClickListener(chipModel.onClickListener)
 
                                     listener.onOngoingActivityStatusChanged(
                                         hasOngoingActivity = true
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
index b7fefc0..c0d411b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/data/repository/MediaProjectionManagerRepositoryTest.kt
@@ -16,7 +16,9 @@
 
 package com.android.systemui.mediaprojection.data.repository
 
+import android.media.projection.MediaProjectionInfo
 import android.os.Binder
+import android.os.UserHandle
 import android.view.ContentRecordingSession
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -26,6 +28,7 @@
 import com.android.systemui.mediaprojection.data.model.MediaProjectionState
 import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
 import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createToken
+import com.android.systemui.mediaprojection.taskswitcher.FakeMediaProjectionManager.Companion.createDisplaySession
 import com.android.systemui.mediaprojection.taskswitcher.fakeActivityTaskManager
 import com.android.systemui.mediaprojection.taskswitcher.fakeMediaProjectionManager
 import com.android.systemui.mediaprojection.taskswitcher.taskSwitcherKosmos
@@ -33,6 +36,7 @@
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.verify
 
 @RunWith(AndroidJUnit4::class)
 @SmallTest
@@ -55,7 +59,8 @@
             fakeActivityTaskManager.addRunningTasks(task)
             repo.switchProjectedTask(task)
 
-            assertThat(state).isEqualTo(MediaProjectionState.SingleTask(task))
+            assertThat(state).isInstanceOf(MediaProjectionState.Projecting.SingleTask::class.java)
+            assertThat((state as MediaProjectionState.Projecting.SingleTask).task).isEqualTo(task)
         }
 
     @Test
@@ -97,7 +102,7 @@
                 session = ContentRecordingSession.createDisplaySession(/* displayToMirror= */ 123)
             )
 
-            assertThat(state).isEqualTo(MediaProjectionState.EntireScreen)
+            assertThat(state).isInstanceOf(MediaProjectionState.Projecting.EntireScreen::class.java)
         }
 
     @Test
@@ -110,7 +115,27 @@
                 session = ContentRecordingSession.createTaskSession(taskWindowContainerToken)
             )
 
-            assertThat(state).isEqualTo(MediaProjectionState.EntireScreen)
+            assertThat(state).isInstanceOf(MediaProjectionState.Projecting.EntireScreen::class.java)
+        }
+
+    @Test
+    fun mediaProjectionState_entireScreen_hasHostPackage() =
+        testScope.runTest {
+            val state by collectLastValue(repo.mediaProjectionState)
+
+            val info =
+                MediaProjectionInfo(
+                    /* packageName= */ "com.media.projection.repository.test",
+                    /* handle= */ UserHandle.getUserHandleForUid(UserHandle.myUserId()),
+                    /* launchCookie = */ null,
+                )
+            fakeMediaProjectionManager.dispatchOnSessionSet(
+                info = info,
+                session = createDisplaySession(),
+            )
+
+            assertThat((state as MediaProjectionState.Projecting.EntireScreen).hostPackage)
+                .isEqualTo("com.media.projection.repository.test")
         }
 
     @Test
@@ -125,6 +150,39 @@
                 session = ContentRecordingSession.createTaskSession(token.asBinder())
             )
 
-            assertThat(state).isEqualTo(MediaProjectionState.SingleTask(task))
+            assertThat(state).isInstanceOf(MediaProjectionState.Projecting.SingleTask::class.java)
+            assertThat((state as MediaProjectionState.Projecting.SingleTask).task).isEqualTo(task)
+        }
+
+    @Test
+    fun mediaProjectionState_singleTask_hasHostPackage() =
+        testScope.runTest {
+            val state by collectLastValue(repo.mediaProjectionState)
+
+            val token = createToken()
+            val task = createTask(taskId = 1, token = token)
+            fakeActivityTaskManager.addRunningTasks(task)
+
+            val info =
+                MediaProjectionInfo(
+                    /* packageName= */ "com.media.projection.repository.test",
+                    /* handle= */ UserHandle.getUserHandleForUid(UserHandle.myUserId()),
+                    /* launchCookie = */ null,
+                )
+            fakeMediaProjectionManager.dispatchOnSessionSet(
+                info = info,
+                session = ContentRecordingSession.createTaskSession(token.asBinder())
+            )
+
+            assertThat((state as MediaProjectionState.Projecting.SingleTask).hostPackage)
+                .isEqualTo("com.media.projection.repository.test")
+        }
+
+    @Test
+    fun stopProjecting_invokesManager() =
+        testScope.runTest {
+            repo.stopProjecting()
+
+            verify(fakeMediaProjectionManager.mediaProjectionManager).stopActiveProjection()
         }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepositoryTest.kt
index b77a15b..61ea437 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepositoryTest.kt
@@ -119,4 +119,12 @@
 
             assertThat(lastModel).isEqualTo(isRecording)
         }
+
+    @Test
+    fun stopRecording_invokesController() =
+        testScope.runTest {
+            underTest.stopRecording()
+
+            verify(recordingController).stopRecording()
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/domain/interactor/OngoingActivityChipInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/domain/interactor/OngoingActivityChipInteractorTest.kt
new file mode 100644
index 0000000..abb6e2b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/domain/interactor/OngoingActivityChipInteractorTest.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.chips.domain.interactor
+
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.chips.domain.interactor.OngoingActivityChipInteractor.Companion.createDialogLaunchOnClickListener
+import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import kotlin.test.Test
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+class OngoingActivityChipInteractorTest : SysuiTestCase() {
+    private val mockSystemUIDialog = mock<SystemUIDialog>()
+    private val dialogDelegate = SystemUIDialog.Delegate { mockSystemUIDialog }
+    private val dialogTransitionAnimator = mock<DialogTransitionAnimator>()
+
+    private val chipBackgroundView = mock<ChipBackgroundContainer>()
+    private val chipView =
+        mock<View>().apply {
+            whenever(
+                    this.requireViewById<ChipBackgroundContainer>(
+                        R.id.ongoing_activity_chip_background
+                    )
+                )
+                .thenReturn(chipBackgroundView)
+        }
+
+    @Test
+    fun createDialogLaunchOnClickListener_showsDialogOnClick() {
+        val clickListener =
+            createDialogLaunchOnClickListener(dialogDelegate, dialogTransitionAnimator)
+
+        // Dialogs must be created on the main thread
+        context.mainExecutor.execute {
+            clickListener.onClick(chipView)
+            verify(dialogTransitionAnimator)
+                .showFromView(
+                    eq(mockSystemUIDialog),
+                    eq(chipBackgroundView),
+                    eq(null),
+                    anyBoolean(),
+                )
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
index 0f33b9d..a4505a9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
@@ -16,8 +16,15 @@
 
 package com.android.systemui.statusbar.chips.mediaprojection.domain.interactor
 
+import android.Manifest
+import android.content.Intent
+import android.content.packageManager
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.mockDialogTransitionAnimator
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.Kosmos
@@ -27,11 +34,24 @@
 import com.android.systemui.mediaprojection.taskswitcher.FakeActivityTaskManager.Companion.createTask
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
-import com.android.systemui.statusbar.chips.ui.viewmodel.mediaProjectionChipInteractor
+import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndCastToOtherDeviceDialogDelegate
+import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndShareToAppDialogDelegate
+import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
 import com.android.systemui.util.time.fakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import kotlin.test.Test
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.doAnswer
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
 
 @SmallTest
 class MediaProjectionChipInteractorTest : SysuiTestCase() {
@@ -40,6 +60,30 @@
     private val mediaProjectionRepo = kosmos.fakeMediaProjectionRepository
     private val systemClock = kosmos.fakeSystemClock
 
+    private val mockCastDialog = mock<SystemUIDialog>()
+    private val mockShareDialog = mock<SystemUIDialog>()
+
+    private val chipBackgroundView = mock<ChipBackgroundContainer>()
+    private val chipView =
+        mock<View>().apply {
+            whenever(
+                    this.requireViewById<ChipBackgroundContainer>(
+                        R.id.ongoing_activity_chip_background
+                    )
+                )
+                .thenReturn(chipBackgroundView)
+        }
+
+    @Before
+    fun setUp() {
+        setUpPackageManagerForMediaProjection(kosmos)
+
+        whenever(kosmos.mockSystemUIDialogFactory.create(any<EndCastToOtherDeviceDialogDelegate>()))
+            .thenReturn(mockCastDialog)
+        whenever(kosmos.mockSystemUIDialogFactory.create(any<EndShareToAppDialogDelegate>()))
+            .thenReturn(mockShareDialog)
+    }
+
     private val underTest = kosmos.mediaProjectionChipInteractor
 
     @Test
@@ -53,12 +97,15 @@
         }
 
     @Test
-    fun chip_singleTaskState_isShownWithIcon() =
+    fun chip_singleTaskState_otherDevicesPackage_castToOtherDeviceChipShown() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
             mediaProjectionRepo.mediaProjectionState.value =
-                MediaProjectionState.SingleTask(createTask(taskId = 1))
+                MediaProjectionState.Projecting.SingleTask(
+                    CAST_TO_OTHER_DEVICES_PACKAGE,
+                    createTask(taskId = 1)
+                )
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
             val icon = (latest as OngoingActivityChipModel.Shown).icon
@@ -66,11 +113,12 @@
         }
 
     @Test
-    fun chip_entireScreenState_isShownWithIcon() =
+    fun chip_entireScreenState_otherDevicesPackage_castToOtherDeviceChipShown() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
-            mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.EntireScreen
+            mediaProjectionRepo.mediaProjectionState.value =
+                MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
             val icon = (latest as OngoingActivityChipModel.Shown).icon
@@ -78,12 +126,39 @@
         }
 
     @Test
+    fun chip_singleTaskState_normalPackage_shareToAppChipShown() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            mediaProjectionRepo.mediaProjectionState.value =
+                MediaProjectionState.Projecting.SingleTask(NORMAL_PACKAGE, createTask(taskId = 1))
+
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+            val icon = (latest as OngoingActivityChipModel.Shown).icon
+            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_screenshot_share)
+        }
+
+    @Test
+    fun chip_entireScreenState_normalPackage_shareToAppChipShown() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            mediaProjectionRepo.mediaProjectionState.value =
+                MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
+
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+            val icon = (latest as OngoingActivityChipModel.Shown).icon
+            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_screenshot_share)
+        }
+
+    @Test
     fun chip_timeResetsOnEachNewShare() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
             systemClock.setElapsedRealtime(1234)
-            mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.EntireScreen
+            mediaProjectionRepo.mediaProjectionState.value =
+                MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
             assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(1234)
@@ -92,9 +167,99 @@
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
 
             systemClock.setElapsedRealtime(5678)
-            mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.EntireScreen
+            mediaProjectionRepo.mediaProjectionState.value =
+                MediaProjectionState.Projecting.SingleTask(
+                    CAST_TO_OTHER_DEVICES_PACKAGE,
+                    createTask(taskId = 1)
+                )
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
             assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(5678)
         }
+
+    @Test
+    fun chip_castToOtherDevice_clickListenerShowsCastDialog() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+            mediaProjectionRepo.mediaProjectionState.value =
+                MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
+
+            val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+
+            // Dialogs must be created on the main thread
+            context.mainExecutor.execute {
+                clickListener.onClick(chipView)
+                verify(kosmos.mockDialogTransitionAnimator)
+                    .showFromView(
+                        eq(mockCastDialog),
+                        eq(chipBackgroundView),
+                        eq(null),
+                        anyBoolean(),
+                    )
+            }
+        }
+
+    @Test
+    fun chip_shareToApp_clickListenerShowsShareDialog() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+            mediaProjectionRepo.mediaProjectionState.value =
+                MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
+
+            val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+
+            // Dialogs must be created on the main thread
+            context.mainExecutor.execute {
+                clickListener.onClick(chipView)
+                verify(kosmos.mockDialogTransitionAnimator)
+                    .showFromView(
+                        eq(mockShareDialog),
+                        eq(chipBackgroundView),
+                        eq(null),
+                        anyBoolean(),
+                    )
+            }
+        }
+
+    companion object {
+        const val CAST_TO_OTHER_DEVICES_PACKAGE = "other.devices.package"
+        const val NORMAL_PACKAGE = "some.normal.package"
+
+        /**
+         * Sets up [kosmos.packageManager] so that [CAST_TO_OTHER_DEVICES_PACKAGE] is marked as a
+         * package that casts to other devices, and [NORMAL_PACKAGE] is *not* marked as casting to
+         * other devices.
+         */
+        fun setUpPackageManagerForMediaProjection(kosmos: Kosmos) {
+            kosmos.packageManager.apply {
+                whenever(
+                        this.checkPermission(
+                            Manifest.permission.REMOTE_DISPLAY_PROVIDER,
+                            CAST_TO_OTHER_DEVICES_PACKAGE
+                        )
+                    )
+                    .thenReturn(PackageManager.PERMISSION_GRANTED)
+                whenever(
+                        this.checkPermission(
+                            Manifest.permission.REMOTE_DISPLAY_PROVIDER,
+                            NORMAL_PACKAGE
+                        )
+                    )
+                    .thenReturn(PackageManager.PERMISSION_DENIED)
+
+                doAnswer {
+                        // See Utils.isHeadlessRemoteDisplayProvider
+                        if (
+                            (it.arguments[0] as Intent).`package` == CAST_TO_OTHER_DEVICES_PACKAGE
+                        ) {
+                            emptyList()
+                        } else {
+                            listOf(mock<ResolveInfo>())
+                        }
+                    }
+                    .whenever(this)
+                    .queryIntentActivities(any(), anyInt())
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegateTest.kt
new file mode 100644
index 0000000..9a2f545
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndCastToOtherDeviceDialogDelegateTest.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.chips.mediaprojection.ui.view
+
+import android.content.DialogInterface
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.mediaProjectionChipInteractor
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class EndCastToOtherDeviceDialogDelegateTest : SysuiTestCase() {
+    private val kosmos = Kosmos()
+    private val sysuiDialog = mock<SystemUIDialog>()
+    private val sysuiDialogFactory = kosmos.mockSystemUIDialogFactory
+    private val underTest =
+        EndCastToOtherDeviceDialogDelegate(
+            sysuiDialogFactory,
+            kosmos.mediaProjectionChipInteractor,
+        )
+
+    @Before
+    fun setUp() {
+        whenever(sysuiDialogFactory.create(eq(underTest), eq(context))).thenReturn(sysuiDialog)
+    }
+
+    @Test
+    fun icon() {
+        underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+        verify(sysuiDialog).setIcon(R.drawable.ic_cast_connected)
+    }
+
+    @Test
+    fun title() {
+        underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+        verify(sysuiDialog).setTitle(R.string.cast_to_other_device_stop_dialog_title)
+    }
+
+    @Test
+    fun message() {
+        underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+        verify(sysuiDialog).setMessage(R.string.cast_to_other_device_stop_dialog_message)
+    }
+
+    @Test
+    fun negativeButton() {
+        underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+        verify(sysuiDialog).setNegativeButton(R.string.close_dialog_button, null)
+    }
+
+    @Test
+    fun positiveButton() =
+        kosmos.testScope.runTest {
+            underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+            val clickListener = argumentCaptor<DialogInterface.OnClickListener>()
+
+            // Verify the button has the right text
+            verify(sysuiDialog)
+                .setPositiveButton(
+                    eq(R.string.cast_to_other_device_stop_dialog_button),
+                    clickListener.capture()
+                )
+
+            // Verify that clicking the button stops the recording
+            assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isFalse()
+
+            clickListener.firstValue.onClick(mock<DialogInterface>(), 0)
+            runCurrent()
+
+            assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isTrue()
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegateTest.kt
new file mode 100644
index 0000000..1d6e866
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndShareToAppDialogDelegateTest.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.chips.mediaprojection.ui.view
+
+import android.content.DialogInterface
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.mediaProjectionChipInteractor
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class EndShareToAppDialogDelegateTest : SysuiTestCase() {
+    private val kosmos = Kosmos()
+    private val sysuiDialog = mock<SystemUIDialog>()
+    private val sysuiDialogFactory = kosmos.mockSystemUIDialogFactory
+    private val underTest =
+        EndShareToAppDialogDelegate(
+            sysuiDialogFactory,
+            kosmos.mediaProjectionChipInteractor,
+        )
+
+    @Before
+    fun setUp() {
+        whenever(sysuiDialogFactory.create(eq(underTest), eq(context))).thenReturn(sysuiDialog)
+    }
+
+    @Test
+    fun icon() {
+        underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+        verify(sysuiDialog).setIcon(R.drawable.ic_screenshot_share)
+    }
+
+    @Test
+    fun title() {
+        underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+        verify(sysuiDialog).setTitle(R.string.share_to_app_stop_dialog_title)
+    }
+
+    @Test
+    fun message() {
+        underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+        verify(sysuiDialog).setMessage(R.string.share_to_app_stop_dialog_message)
+    }
+
+    @Test
+    fun negativeButton() {
+        underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+        verify(sysuiDialog).setNegativeButton(R.string.close_dialog_button, null)
+    }
+
+    @Test
+    fun positiveButton() =
+        kosmos.testScope.runTest {
+            underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+            val clickListener = argumentCaptor<DialogInterface.OnClickListener>()
+
+            // Verify the button has the right text
+            verify(sysuiDialog)
+                .setPositiveButton(
+                    eq(R.string.share_to_app_stop_dialog_button),
+                    clickListener.capture()
+                )
+
+            // Verify that clicking the button stops the recording
+            assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isFalse()
+
+            clickListener.firstValue.onClick(mock<DialogInterface>(), 0)
+            runCurrent()
+
+            assertThat(kosmos.fakeMediaProjectionRepository.stopProjectingInvoked).isTrue()
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt
index 25efaf1..f6c3adb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt
@@ -16,8 +16,10 @@
 
 package com.android.systemui.statusbar.chips.screenrecord.domain.interactor
 
+import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.mockDialogTransitionAnimator
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.Kosmos
@@ -26,11 +28,21 @@
 import com.android.systemui.screenrecord.data.model.ScreenRecordModel
 import com.android.systemui.screenrecord.data.repository.screenRecordRepository
 import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
 import com.android.systemui.statusbar.chips.ui.viewmodel.screenRecordChipInteractor
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
 import com.android.systemui.util.time.fakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import kotlin.test.Test
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
 
 @SmallTest
 class ScreenRecordChipInteractorTest : SysuiTestCase() {
@@ -38,9 +50,27 @@
     private val testScope = kosmos.testScope
     private val screenRecordRepo = kosmos.screenRecordRepository
     private val systemClock = kosmos.fakeSystemClock
+    private val mockSystemUIDialog = mock<SystemUIDialog>()
+
+    private val chipBackgroundView = mock<ChipBackgroundContainer>()
+    private val chipView =
+        mock<View>().apply {
+            whenever(
+                    this.requireViewById<ChipBackgroundContainer>(
+                        R.id.ongoing_activity_chip_background
+                    )
+                )
+                .thenReturn(chipBackgroundView)
+        }
 
     private val underTest = kosmos.screenRecordChipInteractor
 
+    @Before
+    fun setUp() {
+        whenever(kosmos.mockSystemUIDialogFactory.create(any<SystemUIDialog.Delegate>()))
+            .thenReturn(mockSystemUIDialog)
+    }
+
     @Test
     fun chip_doingNothingState_isHidden() =
         testScope.runTest {
@@ -70,7 +100,7 @@
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
             val icon = (latest as OngoingActivityChipModel.Shown).icon
-            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.stat_sys_screen_record)
+            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_screenrecord)
         }
 
     @Test
@@ -93,4 +123,25 @@
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
             assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(5678)
         }
+
+    @Test
+    fun chip_clickListenerShowsDialog() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
+
+            val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+
+            // Dialogs must be created on the main thread
+            context.mainExecutor.execute {
+                clickListener.onClick(chipView)
+                verify(kosmos.mockDialogTransitionAnimator)
+                    .showFromView(
+                        eq(mockSystemUIDialog),
+                        eq(chipBackgroundView),
+                        eq(null),
+                        anyBoolean(),
+                    )
+            }
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt
new file mode 100644
index 0000000..bca6763
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.chips.screenrecord.ui.view
+
+import android.content.DialogInterface
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
+import com.android.systemui.screenrecord.data.repository.screenRecordRepository
+import com.android.systemui.statusbar.chips.ui.viewmodel.screenRecordChipInteractor
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+class EndScreenRecordingDialogDelegateTest : SysuiTestCase() {
+    private val kosmos = Kosmos()
+
+    private val sysuiDialog = mock<SystemUIDialog>()
+    private val sysuiDialogFactory = kosmos.mockSystemUIDialogFactory
+
+    private val underTest =
+        EndScreenRecordingDialogDelegate(
+            sysuiDialogFactory,
+            kosmos.screenRecordChipInteractor,
+        )
+
+    @Before
+    fun setUp() {
+        whenever(sysuiDialogFactory.create(eq(underTest), eq(context))).thenReturn(sysuiDialog)
+    }
+
+    @Test
+    fun icon() {
+        underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+        verify(sysuiDialog).setIcon(R.drawable.ic_screenrecord)
+    }
+
+    @Test
+    fun title() {
+        underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+        verify(sysuiDialog).setTitle(R.string.screenrecord_stop_dialog_title)
+    }
+
+    @Test
+    fun message() {
+        underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+        verify(sysuiDialog).setMessage(R.string.screenrecord_stop_dialog_message)
+    }
+
+    @Test
+    fun negativeButton() {
+        underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+        verify(sysuiDialog).setNegativeButton(R.string.close_dialog_button, null)
+    }
+
+    @Test
+    fun positiveButton() =
+        kosmos.testScope.runTest {
+            underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
+
+            val clickListener = argumentCaptor<DialogInterface.OnClickListener>()
+
+            // Verify the button has the right text
+            verify(sysuiDialog)
+                .setPositiveButton(
+                    eq(R.string.screenrecord_stop_dialog_button),
+                    clickListener.capture()
+                )
+
+            // Verify that clicking the button stops the recording
+            assertThat(kosmos.screenRecordRepository.stopRecordingInvoked).isFalse()
+
+            clickListener.firstValue.onClick(mock<DialogInterface>(), 0)
+            runCurrent()
+
+            assertThat(kosmos.screenRecordRepository.stopRecordingInvoked).isTrue()
+        }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
index 121229c..6712963 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
@@ -30,8 +30,11 @@
 import com.android.systemui.screenrecord.data.model.ScreenRecordModel
 import com.android.systemui.screenrecord.data.repository.screenRecordRepository
 import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
+import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.NORMAL_PACKAGE
+import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 
 @SmallTest
@@ -46,6 +49,11 @@
 
     private val underTest = kosmos.ongoingActivityChipsViewModel
 
+    @Before
+    fun setUp() {
+        setUpPackageManagerForMediaProjection(kosmos)
+    }
+
     @Test
     fun chip_allHidden_hidden() =
         testScope.runTest {
@@ -91,7 +99,8 @@
     fun chip_screenRecordShowAndMediaProjectionShow_screenRecordShown() =
         testScope.runTest {
             screenRecordState.value = ScreenRecordModel.Recording
-            mediaProjectionState.value = MediaProjectionState.EntireScreen
+            mediaProjectionState.value =
+                MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
             callState.value = OngoingActivityChipModel.Hidden
 
             val latest by collectLastValue(underTest.chip)
@@ -103,7 +112,8 @@
     fun chip_mediaProjectionShowAndCallShow_mediaProjectionShown() =
         testScope.runTest {
             screenRecordState.value = ScreenRecordModel.DoingNothing
-            mediaProjectionState.value = MediaProjectionState.EntireScreen
+            mediaProjectionState.value =
+                MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
             val callChip =
                 OngoingActivityChipModel.Shown(
                     Icon.Resource(R.drawable.ic_call, ContentDescription.Loaded("icon")),
@@ -113,7 +123,7 @@
 
             val latest by collectLastValue(underTest.chip)
 
-            assertIsMediaProjectionChip(latest)
+            assertIsShareToAppChip(latest)
         }
 
     @Test
@@ -152,10 +162,14 @@
             assertThat(latest).isEqualTo(callChip)
 
             // WHEN the higher priority media projection chip is added
-            mediaProjectionState.value = MediaProjectionState.SingleTask(createTask(taskId = 1))
+            mediaProjectionState.value =
+                MediaProjectionState.Projecting.SingleTask(
+                    NORMAL_PACKAGE,
+                    createTask(taskId = 1),
+                )
 
             // THEN the higher priority media projection chip is used
-            assertIsMediaProjectionChip(latest)
+            assertIsShareToAppChip(latest)
 
             // WHEN the higher priority screen record chip is added
             screenRecordState.value = ScreenRecordModel.Recording
@@ -169,7 +183,8 @@
         testScope.runTest {
             // WHEN all chips are active
             screenRecordState.value = ScreenRecordModel.Recording
-            mediaProjectionState.value = MediaProjectionState.EntireScreen
+            mediaProjectionState.value =
+                MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
 
             val callChip =
                 OngoingActivityChipModel.Shown(
@@ -187,7 +202,7 @@
             screenRecordState.value = ScreenRecordModel.DoingNothing
 
             // THEN the lower priority media projection is used
-            assertIsMediaProjectionChip(latest)
+            assertIsShareToAppChip(latest)
 
             // WHEN the higher priority media projection is removed
             mediaProjectionState.value = MediaProjectionState.NotProjecting
@@ -200,13 +215,13 @@
         fun assertIsScreenRecordChip(latest: OngoingActivityChipModel?) {
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
             val icon = (latest as OngoingActivityChipModel.Shown).icon
-            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.stat_sys_screen_record)
+            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_screenrecord)
         }
 
-        fun assertIsMediaProjectionChip(latest: OngoingActivityChipModel?) {
+        fun assertIsShareToAppChip(latest: OngoingActivityChipModel?) {
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
             val icon = (latest as OngoingActivityChipModel.Shown).icon
-            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_cast_connected)
+            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_screenshot_share)
         }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
index c9fe449..cdb2b88 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
@@ -37,8 +37,10 @@
 import com.android.systemui.screenrecord.data.model.ScreenRecordModel
 import com.android.systemui.screenrecord.data.repository.screenRecordRepository
 import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
-import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsMediaProjectionChip
+import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.NORMAL_PACKAGE
+import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractorTest.Companion.setUpPackageManagerForMediaProjection
 import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsScreenRecordChip
+import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipsViewModelTest.Companion.assertIsShareToAppChip
 import com.android.systemui.statusbar.chips.ui.viewmodel.ongoingActivityChipsViewModel
 import com.android.systemui.statusbar.data.model.StatusBarMode
 import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository.Companion.DISPLAY_ID
@@ -55,6 +57,7 @@
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 
 @SmallTest
@@ -77,6 +80,11 @@
             kosmos.applicationCoroutineScope,
         )
 
+    @Before
+    fun setUp() {
+        setUpPackageManagerForMediaProjection(kosmos)
+    }
+
     @Test
     fun isTransitioningFromLockscreenToOccluded_started_isTrue() =
         testScope.runTest {
@@ -405,9 +413,9 @@
             assertThat(latest).isEqualTo(OngoingActivityChipModel.Hidden)
 
             kosmos.fakeMediaProjectionRepository.mediaProjectionState.value =
-                MediaProjectionState.EntireScreen
+                MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
 
-            assertIsMediaProjectionChip(latest)
+            assertIsShareToAppChip(latest)
         }
 
     private fun activeNotificationsStore(notifications: List<ActiveNotificationModel>) =
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/DialogTransitionAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/DialogTransitionAnimatorKosmos.kt
index 62e56be..976a19c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/DialogTransitionAnimatorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/DialogTransitionAnimatorKosmos.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.jank.interactionJankMonitor
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.util.mockito.mock
 
 val Kosmos.dialogTransitionAnimator by Fixture {
     fakeDialogTransitionAnimator(
@@ -29,3 +30,5 @@
         interactionJankMonitor = interactionJankMonitor,
     )
 }
+
+val Kosmos.mockDialogTransitionAnimator by Fixture { mock<DialogTransitionAnimator>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/FakeMediaProjectionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/FakeMediaProjectionRepository.kt
index c4365c9..d631f92 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/FakeMediaProjectionRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/mediaprojection/data/repository/FakeMediaProjectionRepository.kt
@@ -25,4 +25,10 @@
 
     override val mediaProjectionState: MutableStateFlow<MediaProjectionState> =
         MutableStateFlow(MediaProjectionState.NotProjecting)
+
+    var stopProjectingInvoked = false
+
+    override suspend fun stopProjecting() {
+        stopProjectingInvoked = true
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/screenrecord/data/repository/FakeScreenRecordRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/screenrecord/data/repository/FakeScreenRecordRepository.kt
index fb0e368..30b4763 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/screenrecord/data/repository/FakeScreenRecordRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/screenrecord/data/repository/FakeScreenRecordRepository.kt
@@ -22,4 +22,10 @@
 class FakeScreenRecordRepository : ScreenRecordRepository {
     override val screenRecordState: MutableStateFlow<ScreenRecordModel> =
         MutableStateFlow(ScreenRecordModel.DoingNothing)
+
+    var stopRecordingInvoked = false
+
+    override suspend fun stopRecording() {
+        stopRecordingInvoked = true
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorKosmos.kt
new file mode 100644
index 0000000..062b448
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorKosmos.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.chips.mediaprojection.domain.interactor
+
+import android.content.packageManager
+import com.android.systemui.animation.mockDialogTransitionAnimator
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
+import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
+import com.android.systemui.util.time.fakeSystemClock
+
+val Kosmos.mediaProjectionChipInteractor: MediaProjectionChipInteractor by
+    Kosmos.Fixture {
+        MediaProjectionChipInteractor(
+            scope = applicationCoroutineScope,
+            mediaProjectionRepository = fakeMediaProjectionRepository,
+            packageManager = packageManager,
+            systemClock = fakeSystemClock,
+            dialogFactory = mockSystemUIDialogFactory,
+            dialogTransitionAnimator = mockDialogTransitionAnimator,
+        )
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt
index 88bde2e..51ec540 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelKosmos.kt
@@ -16,13 +16,14 @@
 
 package com.android.systemui.statusbar.chips.ui.viewmodel
 
+import com.android.systemui.animation.mockDialogTransitionAnimator
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.mediaprojection.data.repository.fakeMediaProjectionRepository
 import com.android.systemui.screenrecord.data.repository.screenRecordRepository
-import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractor
+import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.mediaProjectionChipInteractor
 import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.ScreenRecordChipInteractor
+import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
 import com.android.systemui.util.time.fakeSystemClock
 
 val Kosmos.screenRecordChipInteractor: ScreenRecordChipInteractor by
@@ -30,15 +31,8 @@
         ScreenRecordChipInteractor(
             scope = applicationCoroutineScope,
             screenRecordRepository = screenRecordRepository,
-            systemClock = fakeSystemClock,
-        )
-    }
-
-val Kosmos.mediaProjectionChipInteractor: MediaProjectionChipInteractor by
-    Kosmos.Fixture {
-        MediaProjectionChipInteractor(
-            scope = applicationCoroutineScope,
-            mediaProjectionRepository = fakeMediaProjectionRepository,
+            dialogFactory = mockSystemUIDialogFactory,
+            dialogTransitionAnimator = mockDialogTransitionAnimator,
             systemClock = fakeSystemClock,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryKosmos.kt
index 3bb9580..1851c89 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/SystemUIDialogFactoryKosmos.kt
@@ -21,8 +21,9 @@
 import com.android.systemui.broadcast.broadcastDispatcher
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.model.sysUiState
+import com.android.systemui.util.mockito.mock
 
-val Kosmos.systemUIDialogFactory by
+val Kosmos.systemUIDialogFactory: SystemUIDialogFactory by
     Kosmos.Fixture {
         SystemUIDialogFactory(
             applicationContext,
@@ -32,3 +33,6 @@
             dialogTransitionAnimator,
         )
     }
+
+val Kosmos.mockSystemUIDialogFactory: SystemUIDialog.Factory by
+    Kosmos.Fixture { mock<SystemUIDialog.Factory>() }