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