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