• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.statusbar.chips.sharetoapp.ui.viewmodel
18 
19 import android.content.Context
20 import androidx.annotation.DrawableRes
21 import com.android.internal.jank.Cuj
22 import com.android.systemui.CoreStartable
23 import com.android.systemui.animation.DialogCuj
24 import com.android.systemui.animation.DialogTransitionAnimator
25 import com.android.systemui.common.shared.model.ContentDescription
26 import com.android.systemui.common.shared.model.Icon
27 import com.android.systemui.dagger.SysUISingleton
28 import com.android.systemui.dagger.qualifiers.Application
29 import com.android.systemui.log.LogBuffer
30 import com.android.systemui.log.core.LogLevel
31 import com.android.systemui.res.R
32 import com.android.systemui.statusbar.chips.StatusBarChipLogTags.pad
33 import com.android.systemui.statusbar.chips.StatusBarChipsLog
34 import com.android.systemui.statusbar.chips.mediaprojection.domain.interactor.MediaProjectionChipInteractor
35 import com.android.systemui.statusbar.chips.mediaprojection.domain.model.MediaProjectionStopDialogModel
36 import com.android.systemui.statusbar.chips.mediaprojection.domain.model.ProjectionChipModel
37 import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper
38 import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndGenericShareToAppDialogDelegate
39 import com.android.systemui.statusbar.chips.sharetoapp.ui.view.EndShareScreenToAppDialogDelegate
40 import com.android.systemui.statusbar.chips.ui.model.ColorsModel
41 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
42 import com.android.systemui.statusbar.chips.ui.viewmodel.ChipTransitionHelper
43 import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel
44 import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickCallback
45 import com.android.systemui.statusbar.chips.ui.viewmodel.OngoingActivityChipViewModel.Companion.createDialogLaunchOnClickListener
46 import com.android.systemui.statusbar.chips.uievents.StatusBarChipsUiEventLogger
47 import com.android.systemui.util.kotlin.sample
48 import com.android.systemui.util.time.SystemClock
49 import javax.inject.Inject
50 import kotlinx.coroutines.CoroutineScope
51 import kotlinx.coroutines.flow.MutableStateFlow
52 import kotlinx.coroutines.flow.SharingStarted
53 import kotlinx.coroutines.flow.StateFlow
54 import kotlinx.coroutines.flow.asStateFlow
55 import kotlinx.coroutines.flow.combine
56 import kotlinx.coroutines.flow.map
57 import kotlinx.coroutines.flow.stateIn
58 import kotlinx.coroutines.launch
59 
60 /**
61  * View model for the share-to-app chip, shown when sharing your phone screen content to another app
62  * on the same device. (Triggered from within each individual app.)
63  */
64 @SysUISingleton
65 class ShareToAppChipViewModel
66 @Inject
67 constructor(
68     @Application private val scope: CoroutineScope,
69     private val context: Context,
70     private val mediaProjectionChipInteractor: MediaProjectionChipInteractor,
71     private val systemClock: SystemClock,
72     private val endMediaProjectionDialogHelper: EndMediaProjectionDialogHelper,
73     private val dialogTransitionAnimator: DialogTransitionAnimator,
74     @StatusBarChipsLog private val logger: LogBuffer,
75     private val uiEventLogger: StatusBarChipsUiEventLogger,
76 ) : OngoingActivityChipViewModel, CoreStartable {
77     // There can only be 1 active cast-to-other-device chip at a time, so we can re-use the ID.
78     private val instanceId = uiEventLogger.createNewInstanceId()
79 
80     private val _stopDialogToShow: MutableStateFlow<MediaProjectionStopDialogModel> =
81         MutableStateFlow(MediaProjectionStopDialogModel.Hidden)
82 
83     /**
84      * Represents the current state of the media projection stop dialog. Emits
85      * [MediaProjectionStopDialogModel.Shown] when the dialog should be displayed, and
86      * [MediaProjectionStopDialogModel.Hidden] when it is dismissed.
87      */
88     val stopDialogToShow: StateFlow<MediaProjectionStopDialogModel> =
89         _stopDialogToShow.asStateFlow()
90 
91     /**
92      * Emits a [MediaProjectionStopDialogModel] based on the current projection state when a
93      * projectionStartedDuringCallAndActivePostCallEvent event is emitted. If projecting, determines
94      * the appropriate dialog type to show. Otherwise, emits a hidden dialog state.
95      */
96     private val stopDialogDueToCallEndedState: StateFlow<MediaProjectionStopDialogModel> =
97         mediaProjectionChipInteractor.projectionStartedDuringCallAndActivePostCallEvent
98             .sample(mediaProjectionChipInteractor.projection) { _, currentProjection ->
99                 when (currentProjection) {
100                     is ProjectionChipModel.NotProjecting -> MediaProjectionStopDialogModel.Hidden
101                     is ProjectionChipModel.Projecting -> {
102                         when (currentProjection.receiver) {
103                             ProjectionChipModel.Receiver.ShareToApp -> {
104                                 when (currentProjection.contentType) {
105                                     ProjectionChipModel.ContentType.Screen ->
106                                         createShareScreenToAppStopDialog(currentProjection)
107                                     ProjectionChipModel.ContentType.Audio ->
108                                         createGenericShareScreenToAppStopDialog()
109                                 }
110                             }
111                             ProjectionChipModel.Receiver.CastToOtherDevice ->
112                                 MediaProjectionStopDialogModel.Hidden
113                         }
114                     }
115                 }
116             }
117             .stateIn(scope, SharingStarted.WhileSubscribed(), MediaProjectionStopDialogModel.Hidden)
118 
119     /**
120      * Initializes background flow collector during SysUI startup for events determining the
121      * visibility of media projection stop dialogs.
122      */
123     override fun start() {
124         if (com.android.media.projection.flags.Flags.showStopDialogPostCallEnd()) {
125             scope.launch {
126                 stopDialogDueToCallEndedState.collect { event -> _stopDialogToShow.value = event }
127             }
128         }
129     }
130 
131     private val internalChip =
132         mediaProjectionChipInteractor.projection
133             .map { projectionModel ->
134                 when (projectionModel) {
135                     is ProjectionChipModel.NotProjecting -> OngoingActivityChipModel.Inactive()
136                     is ProjectionChipModel.Projecting -> {
137                         when (projectionModel.receiver) {
138                             ProjectionChipModel.Receiver.ShareToApp -> {
139                                 when (projectionModel.contentType) {
140                                     ProjectionChipModel.ContentType.Screen ->
141                                         createShareScreenToAppChip(projectionModel)
142                                     ProjectionChipModel.ContentType.Audio ->
143                                         createIconOnlyShareToAppChip()
144                                 }
145                             }
146                             ProjectionChipModel.Receiver.CastToOtherDevice ->
147                                 OngoingActivityChipModel.Inactive()
148                         }
149                     }
150                 }
151             }
152             // See b/347726238 for [SharingStarted.Lazily] reasoning.
153             .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Inactive())
154 
155     private val chipTransitionHelper = ChipTransitionHelper(scope)
156 
157     override val chip: StateFlow<OngoingActivityChipModel> =
158         combine(chipTransitionHelper.createChipFlow(internalChip), stopDialogToShow) {
159                 currentChip,
160                 stopDialog ->
161                 if (
162                     com.android.media.projection.flags.Flags.showStopDialogPostCallEnd() &&
163                         stopDialog is MediaProjectionStopDialogModel.Shown
164                 ) {
165                     logger.log(
166                         TAG,
167                         LogLevel.INFO,
168                         {},
169                         { "Hiding the chip as stop dialog is being shown" },
170                     )
171                     OngoingActivityChipModel.Inactive()
172                 } else {
173                     currentChip
174                 }
175             }
176             .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Inactive())
177 
178     /**
179      * Notifies this class that the user just stopped a screen recording from the dialog that's
180      * shown when you tap the recording chip.
181      */
182     fun onRecordingStoppedFromDialog() {
183         // When a screen recording is active, share-to-app is also active (screen recording is just
184         // a special case of share-to-app, where the specific app receiving the share is System UI).
185         // When a screen recording is stopped, we immediately hide the screen recording chip in
186         // [com.android.systemui.statusbar.chips.screenrecord.ui.viewmodel.ScreenRecordChipViewModel].
187         // We *also* need to immediately hide the share-to-app chip so it doesn't briefly show.
188         // See b/350891338.
189         chipTransitionHelper.onActivityStoppedFromDialog()
190     }
191 
192     /** Called when the stop dialog is dismissed or cancelled. */
193     private fun onStopDialogDismissed() {
194         logger.log(TAG, LogLevel.INFO, {}, { "The media projection stop dialog was dismissed" })
195         _stopDialogToShow.value = MediaProjectionStopDialogModel.Hidden
196     }
197 
198     /** Stops the currently active projection. */
199     private fun stopProjectingFromDialog() {
200         logger.log(TAG, LogLevel.INFO, {}, { "Stop sharing requested from dialog" })
201         chipTransitionHelper.onActivityStoppedFromDialog()
202         mediaProjectionChipInteractor.stopProjecting()
203     }
204 
205     private fun createShareScreenToAppStopDialog(
206         projectionModel: ProjectionChipModel.Projecting
207     ): MediaProjectionStopDialogModel {
208         val dialogDelegate = createShareScreenToAppDialogDelegate(projectionModel)
209         return MediaProjectionStopDialogModel.Shown(
210             dialogDelegate,
211             onDismissAction = ::onStopDialogDismissed,
212         )
213     }
214 
215     private fun createGenericShareScreenToAppStopDialog(): MediaProjectionStopDialogModel {
216         val dialogDelegate = createGenericShareToAppDialogDelegate()
217         return MediaProjectionStopDialogModel.Shown(
218             dialogDelegate,
219             onDismissAction = ::onStopDialogDismissed,
220         )
221     }
222 
223     private fun createShareScreenToAppChip(
224         state: ProjectionChipModel.Projecting
225     ): OngoingActivityChipModel.Active {
226         return OngoingActivityChipModel.Active.Timer(
227             key = KEY,
228             isImportantForPrivacy = true,
229             icon =
230                 OngoingActivityChipModel.ChipIcon.SingleColorIcon(
231                     Icon.Resource(
232                         SHARE_TO_APP_ICON,
233                         ContentDescription.Resource(R.string.share_to_app_chip_accessibility_label),
234                     )
235                 ),
236             colors = ColorsModel.Red,
237             // TODO(b/332662551): Maybe use a MediaProjection API to fetch this time.
238             startTimeMs = systemClock.elapsedRealtime(),
239             onClickListenerLegacy =
240                 createDialogLaunchOnClickListener(
241                     createShareScreenToAppDialogDelegate(state),
242                     dialogTransitionAnimator,
243                     DIALOG_CUJ,
244                     instanceId = instanceId,
245                     uiEventLogger = uiEventLogger,
246                     logger = logger,
247                     tag = TAG,
248                 ),
249             clickBehavior =
250                 OngoingActivityChipModel.ClickBehavior.ExpandAction(
251                     onClick =
252                         createDialogLaunchOnClickCallback(
253                             createShareScreenToAppDialogDelegate(state),
254                             dialogTransitionAnimator,
255                             DIALOG_CUJ,
256                             instanceId = instanceId,
257                             uiEventLogger = uiEventLogger,
258                             logger = logger,
259                             tag = TAG,
260                         )
261                 ),
262             instanceId = instanceId,
263         )
264     }
265 
266     private fun createIconOnlyShareToAppChip(): OngoingActivityChipModel.Active {
267         return OngoingActivityChipModel.Active.IconOnly(
268             key = KEY,
269             isImportantForPrivacy = true,
270             icon =
271                 OngoingActivityChipModel.ChipIcon.SingleColorIcon(
272                     Icon.Resource(
273                         SHARE_TO_APP_ICON,
274                         ContentDescription.Resource(
275                             R.string.share_to_app_chip_accessibility_label_generic
276                         ),
277                     )
278                 ),
279             colors = ColorsModel.Red,
280             onClickListenerLegacy =
281                 createDialogLaunchOnClickListener(
282                     createGenericShareToAppDialogDelegate(),
283                     dialogTransitionAnimator,
284                     DIALOG_CUJ_AUDIO_ONLY,
285                     instanceId = instanceId,
286                     uiEventLogger = uiEventLogger,
287                     logger = logger,
288                     tag = TAG,
289                 ),
290             clickBehavior =
291                 OngoingActivityChipModel.ClickBehavior.ExpandAction(
292                     createDialogLaunchOnClickCallback(
293                         createGenericShareToAppDialogDelegate(),
294                         dialogTransitionAnimator,
295                         DIALOG_CUJ_AUDIO_ONLY,
296                         instanceId = instanceId,
297                         uiEventLogger = uiEventLogger,
298                         logger = logger,
299                         tag = TAG,
300                     )
301                 ),
302             instanceId = instanceId,
303         )
304     }
305 
306     private fun createShareScreenToAppDialogDelegate(state: ProjectionChipModel.Projecting) =
307         EndShareScreenToAppDialogDelegate(
308             endMediaProjectionDialogHelper,
309             context,
310             stopAction = this::stopProjectingFromDialog,
311             state,
312         )
313 
314     private fun createGenericShareToAppDialogDelegate() =
315         EndGenericShareToAppDialogDelegate(
316             endMediaProjectionDialogHelper,
317             context,
318             stopAction = this::stopProjectingFromDialog,
319         )
320 
321     companion object {
322         const val KEY = "ShareToApp"
323         @DrawableRes val SHARE_TO_APP_ICON = R.drawable.ic_present_to_all
324         private val DIALOG_CUJ =
325             DialogCuj(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, tag = "Share to app")
326         private val DIALOG_CUJ_AUDIO_ONLY =
327             DialogCuj(Cuj.CUJ_STATUS_BAR_LAUNCH_DIALOG_FROM_CHIP, tag = "Share to app audio only")
328         private val TAG = "ShareToAppVM".pad()
329     }
330 }
331