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