• 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.media.controls.domain.pipeline.interactor
18 
19 import android.app.ActivityOptions
20 import android.app.BroadcastOptions
21 import android.app.PendingIntent
22 import android.content.Intent
23 import android.media.session.MediaSession
24 import android.provider.Settings
25 import android.util.Log
26 import com.android.internal.jank.Cuj
27 import com.android.internal.logging.InstanceId
28 import com.android.systemui.ActivityIntentHelper
29 import com.android.systemui.animation.DialogCuj
30 import com.android.systemui.animation.DialogTransitionAnimator
31 import com.android.systemui.animation.Expandable
32 import com.android.systemui.bluetooth.BroadcastDialogController
33 import com.android.systemui.media.controls.data.repository.MediaFilterRepository
34 import com.android.systemui.media.controls.domain.pipeline.MediaDataProcessor
35 import com.android.systemui.media.controls.domain.pipeline.getNotificationActions
36 import com.android.systemui.media.controls.shared.MediaLogger
37 import com.android.systemui.media.controls.shared.model.MediaControlModel
38 import com.android.systemui.media.controls.shared.model.MediaData
39 import com.android.systemui.media.dialog.MediaOutputDialogManager
40 import com.android.systemui.plugins.ActivityStarter
41 import com.android.systemui.statusbar.NotificationLockscreenUserManager
42 import com.android.systemui.statusbar.policy.KeyguardStateController
43 import dagger.assisted.Assisted
44 import dagger.assisted.AssistedInject
45 import kotlinx.coroutines.flow.Flow
46 import kotlinx.coroutines.flow.distinctUntilChanged
47 import kotlinx.coroutines.flow.map
48 
49 /** Encapsulates business logic for single media control. */
50 class MediaControlInteractor
51 @AssistedInject
52 constructor(
53     @Assisted private val instanceId: InstanceId,
54     private val repository: MediaFilterRepository,
55     private val mediaDataProcessor: MediaDataProcessor,
56     private val keyguardStateController: KeyguardStateController,
57     private val activityStarter: ActivityStarter,
58     private val activityIntentHelper: ActivityIntentHelper,
59     private val lockscreenUserManager: NotificationLockscreenUserManager,
60     private val mediaOutputDialogManager: MediaOutputDialogManager,
61     private val broadcastDialogController: BroadcastDialogController,
62     private val mediaLogger: MediaLogger,
63 ) {
64 
65     val mediaControl: Flow<MediaControlModel?> =
66         repository.selectedUserEntries
67             .map { entries -> entries[instanceId]?.let { toMediaControlModel(it) } }
68             .distinctUntilChanged()
69 
70     fun removeMediaControl(
71         token: MediaSession.Token?,
72         instanceId: InstanceId,
73         delayMs: Long,
74     ): Boolean {
75         val dismissed =
76             mediaDataProcessor.dismissMediaData(instanceId, delayMs, userInitiated = true)
77         if (!dismissed) {
78             Log.w(
79                 TAG,
80                 "Manager failed to dismiss media of instanceId=$instanceId, Token uid=${token?.uid}",
81             )
82         }
83         return dismissed
84     }
85 
86     private fun toMediaControlModel(data: MediaData): MediaControlModel {
87         return with(data) {
88             MediaControlModel(
89                 uid = appUid,
90                 packageName = packageName,
91                 instanceId = instanceId,
92                 token = token,
93                 appIcon = appIcon,
94                 clickIntent = clickIntent,
95                 appName = app,
96                 songName = song,
97                 artistName = artist,
98                 showExplicit = isExplicit,
99                 artwork = artwork,
100                 deviceData = device,
101                 semanticActionButtons = semanticActions,
102                 notificationActionButtons = getNotificationActions(data.actions, activityStarter),
103                 actionsToShowInCollapsed = actionsToShowInCompact,
104                 isDismissible = isClearable,
105                 isResume = resumption,
106                 resumeProgress = resumeProgress,
107             )
108         }
109     }
110 
111     fun startSettings() {
112         activityStarter.startActivity(SETTINGS_INTENT, /* dismissShade= */ true)
113     }
114 
115     fun startClickIntent(expandable: Expandable, clickIntent: PendingIntent) {
116         if (!launchOverLockscreen(expandable, clickIntent)) {
117             activityStarter.postStartActivityDismissingKeyguard(
118                 clickIntent,
119                 expandable.activityTransitionController(Cuj.CUJ_SHADE_APP_LAUNCH_FROM_MEDIA_PLAYER),
120             )
121         }
122     }
123 
124     fun startDeviceIntent(deviceIntent: PendingIntent) {
125         if (deviceIntent.isActivity) {
126             if (!launchOverLockscreen(expandable = null, deviceIntent)) {
127                 activityStarter.postStartActivityDismissingKeyguard(deviceIntent)
128             }
129         } else {
130             Log.w(TAG, "Device pending intent of instanceId=$instanceId is not an activity.")
131         }
132     }
133 
134     private fun launchOverLockscreen(
135         expandable: Expandable?,
136         pendingIntent: PendingIntent,
137     ): Boolean {
138         val showOverLockscreen =
139             keyguardStateController.isShowing &&
140                 activityIntentHelper.wouldPendingShowOverLockscreen(
141                     pendingIntent,
142                     lockscreenUserManager.currentUserId,
143                 )
144         if (showOverLockscreen) {
145             try {
146                 if (expandable != null) {
147                     activityStarter.startPendingIntentMaybeDismissingKeyguard(
148                         pendingIntent,
149                         /* intentSentUiThreadCallback = */ null,
150                         expandable.activityTransitionController(
151                             Cuj.CUJ_SHADE_APP_LAUNCH_FROM_MEDIA_PLAYER
152                         ),
153                     )
154                 } else {
155                     val options = BroadcastOptions.makeBasic()
156                     options.isInteractive = true
157                     options.pendingIntentBackgroundActivityStartMode =
158                         ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
159                     pendingIntent.send(options.toBundle())
160                 }
161             } catch (e: PendingIntent.CanceledException) {
162                 Log.e(TAG, "pending intent of $instanceId was canceled")
163             }
164             return true
165         }
166         return false
167     }
168 
169     fun startMediaOutputDialog(
170         expandable: Expandable,
171         packageName: String,
172         token: MediaSession.Token? = null,
173     ) {
174         mediaOutputDialogManager.createAndShowWithController(
175             packageName,
176             true,
177             expandable.dialogController(),
178             token = token,
179         )
180     }
181 
182     fun startBroadcastDialog(expandable: Expandable, broadcastApp: String, packageName: String) {
183         broadcastDialogController.createBroadcastDialogWithController(
184             broadcastApp,
185             packageName,
186             expandable.dialogTransitionController(),
187         )
188     }
189 
190     fun logMediaControlIsBound(artistName: CharSequence, songName: CharSequence) {
191         mediaLogger.logMediaControlIsBound(instanceId, artistName, songName)
192     }
193 
194     private fun Expandable.dialogController(): DialogTransitionAnimator.Controller? {
195         return dialogTransitionController(
196             cuj =
197                 DialogCuj(Cuj.CUJ_SHADE_DIALOG_OPEN, MediaOutputDialogManager.INTERACTION_JANK_TAG)
198         )
199     }
200 
201     companion object {
202         private const val TAG = "MediaControlInteractor"
203         private val SETTINGS_INTENT = Intent(Settings.ACTION_MEDIA_CONTROLS_SETTINGS)
204     }
205 }
206