1 /* 2 * 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.content.Context 20 import android.content.Intent 21 import android.provider.Settings 22 import android.util.Log 23 import androidx.annotation.VisibleForTesting 24 import com.android.internal.jank.InteractionJankMonitor 25 import com.android.systemui.animation.Expandable 26 import com.android.systemui.broadcast.BroadcastSender 27 import com.android.systemui.dagger.SysUISingleton 28 import com.android.systemui.dagger.qualifiers.Application 29 import com.android.systemui.media.controls.data.repository.MediaFilterRepository 30 import com.android.systemui.media.controls.domain.pipeline.MediaDataProcessor 31 import com.android.systemui.media.controls.shared.model.MediaRecModel 32 import com.android.systemui.media.controls.shared.model.MediaRecommendationsModel 33 import com.android.systemui.media.controls.shared.model.SmartspaceMediaData 34 import com.android.systemui.plugins.ActivityStarter 35 import java.net.URISyntaxException 36 import javax.inject.Inject 37 import kotlinx.coroutines.CoroutineScope 38 import kotlinx.coroutines.flow.Flow 39 import kotlinx.coroutines.flow.SharingStarted 40 import kotlinx.coroutines.flow.StateFlow 41 import kotlinx.coroutines.flow.distinctUntilChanged 42 import kotlinx.coroutines.flow.map 43 import kotlinx.coroutines.flow.stateIn 44 45 /** Encapsulates business logic for media recommendation */ 46 @SysUISingleton 47 class MediaRecommendationsInteractor 48 @Inject 49 constructor( 50 @Application applicationScope: CoroutineScope, 51 @Application private val applicationContext: Context, 52 private val repository: MediaFilterRepository, 53 private val mediaDataProcessor: MediaDataProcessor, 54 private val broadcastSender: BroadcastSender, 55 private val activityStarter: ActivityStarter, 56 ) { 57 58 val recommendations: Flow<MediaRecommendationsModel> = <lambda>null59 repository.smartspaceMediaData.map { toRecommendationsModel(it) }.distinctUntilChanged() 60 61 /** Indicates whether the recommendations card is active. */ 62 val isActive: StateFlow<Boolean> = 63 repository.smartspaceMediaData <lambda>null64 .map { it.isActive } 65 .distinctUntilChanged() 66 .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false) 67 68 val onAnyMediaConfigurationChange: Flow<Unit> = repository.onAnyMediaConfigurationChange 69 removeMediaRecommendationsnull70 fun removeMediaRecommendations(key: String, dismissIntent: Intent?, delayMs: Long) { 71 mediaDataProcessor.dismissSmartspaceRecommendation(key, delayMs) 72 if (dismissIntent == null) { 73 Log.w(TAG, "Cannot create dismiss action click action: extras missing dismiss_intent.") 74 return 75 } 76 77 val className = dismissIntent.component?.className 78 if (className == EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME) { 79 // Dismiss the card Smartspace data through Smartspace trampoline activity. 80 applicationContext.startActivity(dismissIntent) 81 } else { 82 broadcastSender.sendBroadcast(dismissIntent) 83 } 84 } 85 startSettingsnull86 fun startSettings() { 87 activityStarter.startActivity(SETTINGS_INTENT, /* dismissShade= */ true) 88 } 89 startClickIntentnull90 fun startClickIntent(expandable: Expandable, intent: Intent) { 91 if (shouldActivityOpenInForeground(intent)) { 92 // Request to unlock the device if the activity needs to be opened in foreground. 93 activityStarter.postStartActivityDismissingKeyguard( 94 intent, 95 0 /* delay */, 96 expandable.activityTransitionController( 97 InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_MEDIA_PLAYER 98 ) 99 ) 100 } else { 101 // Otherwise, open the activity in background directly. 102 applicationContext.startActivity(intent) 103 } 104 } 105 106 /** Returns if the action will open the activity in foreground. */ shouldActivityOpenInForegroundnull107 private fun shouldActivityOpenInForeground(intent: Intent): Boolean { 108 val intentString = intent.extras?.getString(EXTRAS_SMARTSPACE_INTENT) ?: return false 109 try { 110 val wrapperIntent = Intent.parseUri(intentString, Intent.URI_INTENT_SCHEME) 111 return wrapperIntent.getBooleanExtra(KEY_SMARTSPACE_OPEN_IN_FOREGROUND, false) 112 } catch (e: URISyntaxException) { 113 Log.wtf(TAG, "Failed to create intent from URI: $intentString") 114 e.printStackTrace() 115 } 116 return false 117 } 118 toRecommendationsModelnull119 private fun toRecommendationsModel(data: SmartspaceMediaData): MediaRecommendationsModel { 120 val mediaRecs = ArrayList<MediaRecModel>() 121 data.recommendations.forEach { 122 with(it) { mediaRecs.add(MediaRecModel(intent, title, subtitle, icon, extras)) } 123 } 124 return with(data) { 125 MediaRecommendationsModel( 126 key = targetId, 127 uid = getUid(applicationContext), 128 packageName = packageName, 129 instanceId = instanceId, 130 appName = getAppName(applicationContext), 131 dismissIntent = dismissIntent, 132 areRecommendationsValid = isValid(), 133 mediaRecs = mediaRecs, 134 ) 135 } 136 } 137 switchToMediaControlnull138 fun switchToMediaControl(packageName: String) { 139 repository.setMediaFromRecPackageName(packageName) 140 } 141 142 companion object { 143 144 private const val TAG = "MediaRecommendationsInteractor" 145 146 // TODO (b/237284176) : move AGSA reference out. 147 private const val EXTRAS_SMARTSPACE_INTENT = 148 "com.google.android.apps.gsa.smartspace.extra.SMARTSPACE_INTENT" 149 @VisibleForTesting 150 const val EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME = 151 "com.google.android.apps.gsa.staticplugins.opa.smartspace." + 152 "ExportedSmartspaceTrampolineActivity" 153 154 private const val KEY_SMARTSPACE_OPEN_IN_FOREGROUND = "KEY_OPEN_IN_FOREGROUND" 155 156 private val SETTINGS_INTENT = Intent(Settings.ACTION_MEDIA_CONTROLS_SETTINGS) 157 } 158 } 159