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