• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
18 
19 import android.annotation.WorkerThread
20 import android.app.ActivityOptions
21 import android.app.BroadcastOptions
22 import android.app.Notification
23 import android.app.PendingIntent
24 import android.content.Context
25 import android.graphics.drawable.Animatable
26 import android.graphics.drawable.Icon
27 import android.media.session.MediaController
28 import android.media.session.PlaybackState
29 import android.service.notification.StatusBarNotification
30 import android.util.Log
31 import androidx.media.utils.MediaConstants
32 import com.android.systemui.Flags
33 import com.android.systemui.media.NotificationMediaManager.isConnectingState
34 import com.android.systemui.media.NotificationMediaManager.isPlayingState
35 import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl.Companion.MAX_COMPACT_ACTIONS
36 import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl.Companion.MAX_NOTIFICATION_ACTIONS
37 import com.android.systemui.media.controls.shared.MediaControlDrawables
38 import com.android.systemui.media.controls.shared.model.MediaAction
39 import com.android.systemui.media.controls.shared.model.MediaButton
40 import com.android.systemui.media.controls.shared.model.MediaNotificationAction
41 import com.android.systemui.plugins.ActivityStarter
42 import com.android.systemui.res.R
43 import com.android.systemui.util.kotlin.logI
44 
45 private const val TAG = "MediaActions"
46 
47 /**
48  * Generates action button info for this media session based on the PlaybackState
49  *
50  * @param packageName Package name for the media app
51  * @param controller MediaController for the current session
52  * @return a Pair consisting of a list of media actions, and a list of ints representing which of
53  *   those actions should be shown in the compact player
54  */
55 @WorkerThread
56 fun createActionsFromState(
57     context: Context,
58     packageName: String,
59     controller: MediaController,
60 ): MediaButton? {
61     val state = controller.playbackState ?: return null
62     // First, check for standard actions
63     val playOrPause =
64         if (isConnectingState(state.state)) {
65             // Spinner needs to be animating to render anything. Start it here.
66             val drawable =
67                 context.getDrawable(com.android.internal.R.drawable.progress_small_material)
68             (drawable as Animatable).start()
69             MediaAction(
70                 drawable,
71                 null, // no action to perform when clicked
72                 context.getString(R.string.controls_media_button_connecting),
73                 if (Flags.mediaControlsUiUpdate()) {
74                     context.getDrawable(R.drawable.ic_media_connecting_button_container)
75                 } else {
76                     context.getDrawable(R.drawable.ic_media_connecting_container)
77                 },
78                 // Specify a rebind id to prevent the spinner from restarting on later binds.
79                 com.android.internal.R.drawable.progress_small_material,
80             )
81         } else if (isPlayingState(state.state)) {
82             getStandardAction(context, controller, state.actions, PlaybackState.ACTION_PAUSE)
83         } else {
84             getStandardAction(context, controller, state.actions, PlaybackState.ACTION_PLAY)
85         }
86     val prevButton =
87         getStandardAction(context, controller, state.actions, PlaybackState.ACTION_SKIP_TO_PREVIOUS)
88     val nextButton =
89         getStandardAction(context, controller, state.actions, PlaybackState.ACTION_SKIP_TO_NEXT)
90 
91     // Then, create a way to build any custom actions that will be needed
92     val customActions =
93         state.customActions
94             .asSequence()
95             .filterNotNull()
96             .map { getCustomAction(context, packageName, controller, it) }
97             .iterator()
98     fun nextCustomAction() = if (customActions.hasNext()) customActions.next() else null
99 
100     // Finally, assign the remaining button slots: play/pause A B C D
101     // A = previous, else custom action (if not reserved)
102     // B = next, else custom action (if not reserved)
103     // C and D are always custom actions
104     val reservePrev =
105         controller.extras?.getBoolean(
106             MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV
107         ) == true
108     val reserveNext =
109         controller.extras?.getBoolean(
110             MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT
111         ) == true
112 
113     val prevOrCustom =
114         if (prevButton != null) {
115             prevButton
116         } else if (!reservePrev) {
117             nextCustomAction()
118         } else {
119             null
120         }
121 
122     val nextOrCustom =
123         if (nextButton != null) {
124             nextButton
125         } else if (!reserveNext) {
126             nextCustomAction()
127         } else {
128             null
129         }
130 
131     return MediaButton(
132         playOrPause,
133         nextOrCustom,
134         prevOrCustom,
135         nextCustomAction(),
136         nextCustomAction(),
137         reserveNext,
138         reservePrev,
139     )
140 }
141 
142 /**
143  * Create a [MediaAction] for a given action and media session
144  *
145  * @param controller MediaController for the session
146  * @param stateActions The actions included with the session's [PlaybackState]
147  * @param action A [PlaybackState.Actions] value representing what action to generate. One of:
148  *   [PlaybackState.ACTION_PLAY] [PlaybackState.ACTION_PAUSE]
149  *   [PlaybackState.ACTION_SKIP_TO_PREVIOUS] [PlaybackState.ACTION_SKIP_TO_NEXT]
150  * @return A [MediaAction] with correct values set, or null if the state doesn't support it
151  */
getStandardActionnull152 private fun getStandardAction(
153     context: Context,
154     controller: MediaController,
155     stateActions: Long,
156     @PlaybackState.Actions action: Long,
157 ): MediaAction? {
158     if (!includesAction(stateActions, action)) {
159         return null
160     }
161 
162     return when (action) {
163         PlaybackState.ACTION_PLAY -> {
164             MediaAction(
165                 if (Flags.mediaControlsUiUpdate()) {
166                     context.getDrawable(R.drawable.ic_media_play_button)
167                 } else {
168                     context.getDrawable(R.drawable.ic_media_play)
169                 },
170                 { controller.transportControls.play() },
171                 context.getString(R.string.controls_media_button_play),
172                 if (Flags.mediaControlsUiUpdate()) {
173                     context.getDrawable(R.drawable.ic_media_play_button_container)
174                 } else {
175                     context.getDrawable(R.drawable.ic_media_play_container)
176                 },
177             )
178         }
179         PlaybackState.ACTION_PAUSE -> {
180             MediaAction(
181                 if (Flags.mediaControlsUiUpdate()) {
182                     context.getDrawable(R.drawable.ic_media_pause_button)
183                 } else {
184                     context.getDrawable(R.drawable.ic_media_pause)
185                 },
186                 { controller.transportControls.pause() },
187                 context.getString(R.string.controls_media_button_pause),
188                 if (Flags.mediaControlsUiUpdate()) {
189                     context.getDrawable(R.drawable.ic_media_pause_button_container)
190                 } else {
191                     context.getDrawable(R.drawable.ic_media_pause_container)
192                 },
193             )
194         }
195         PlaybackState.ACTION_SKIP_TO_PREVIOUS -> {
196             MediaAction(
197                 MediaControlDrawables.getPrevIcon(context),
198                 { controller.transportControls.skipToPrevious() },
199                 context.getString(R.string.controls_media_button_prev),
200                 null,
201             )
202         }
203         PlaybackState.ACTION_SKIP_TO_NEXT -> {
204             MediaAction(
205                 MediaControlDrawables.getNextIcon(context),
206                 { controller.transportControls.skipToNext() },
207                 context.getString(R.string.controls_media_button_next),
208                 null,
209             )
210         }
211         else -> null
212     }
213 }
214 
215 /** Get a [MediaAction] representing a [PlaybackState.CustomAction] */
getCustomActionnull216 private fun getCustomAction(
217     context: Context,
218     packageName: String,
219     controller: MediaController,
220     customAction: PlaybackState.CustomAction,
221 ): MediaAction {
222     return MediaAction(
223         Icon.createWithResource(packageName, customAction.icon).loadDrawable(context),
224         { controller.transportControls.sendCustomAction(customAction, customAction.extras) },
225         customAction.name,
226         null,
227     )
228 }
229 
230 /** Check whether the actions from a [PlaybackState] include a specific action */
includesActionnull231 private fun includesAction(stateActions: Long, @PlaybackState.Actions action: Long): Boolean {
232     if (
233         (action == PlaybackState.ACTION_PLAY || action == PlaybackState.ACTION_PAUSE) &&
234             (stateActions and PlaybackState.ACTION_PLAY_PAUSE > 0L)
235     ) {
236         return true
237     }
238     return (stateActions and action != 0L)
239 }
240 
241 /** Generate action buttons based on notification actions */
createActionsFromNotificationnull242 fun createActionsFromNotification(
243     context: Context,
244     sbn: StatusBarNotification,
245 ): Pair<List<MediaNotificationAction>, List<Int>> {
246     val notif = sbn.notification
247     val actionIcons: MutableList<MediaNotificationAction> = ArrayList()
248     val actions = notif.actions
249     var actionsToShowCollapsed =
250         notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList()
251             ?: mutableListOf()
252     if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) {
253         Log.e(
254             TAG,
255             "Too many compact actions for ${sbn.key}, limiting to first $MAX_COMPACT_ACTIONS",
256         )
257         actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS)
258     }
259 
260     actions?.let {
261         if (it.size > MAX_NOTIFICATION_ACTIONS) {
262             Log.w(
263                 TAG,
264                 "Too many notification actions for ${sbn.key}, " +
265                     "limiting to first $MAX_NOTIFICATION_ACTIONS",
266             )
267         }
268 
269         for ((index, action) in it.take(MAX_NOTIFICATION_ACTIONS).withIndex()) {
270             if (action.getIcon() == null) {
271                 logI(TAG) { "No icon for action $index ${action.title}" }
272                 actionsToShowCollapsed.remove(index)
273                 continue
274             }
275 
276             val themeText =
277                 com.android.settingslib.Utils.getColorAttr(
278                         context,
279                         com.android.internal.R.attr.textColorPrimary,
280                     )
281                     .defaultColor
282 
283             val mediaActionIcon =
284                 when (action.getIcon().type) {
285                         Icon.TYPE_RESOURCE ->
286                             Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId())
287                         else -> action.getIcon()
288                     }
289                     .setTint(themeText)
290                     .loadDrawable(context)
291 
292             val mediaAction =
293                 MediaNotificationAction(
294                     action.isAuthenticationRequired,
295                     action.actionIntent,
296                     mediaActionIcon,
297                     action.title,
298                 )
299             actionIcons.add(mediaAction)
300         }
301     }
302     return Pair(actionIcons, actionsToShowCollapsed)
303 }
304 
305 /**
306  * Converts [MediaNotificationAction] list into [MediaAction] list
307  *
308  * @param actions list of [MediaNotificationAction]
309  * @param activityStarter starter for activities
310  * @return list of [MediaAction]
311  */
getNotificationActionsnull312 fun getNotificationActions(
313     actions: List<MediaNotificationAction>,
314     activityStarter: ActivityStarter,
315 ): List<MediaAction> {
316     return actions.map { action ->
317         val runnable =
318             action.actionIntent?.let { actionIntent ->
319                 Runnable {
320                     when {
321                         actionIntent.isActivity ->
322                             activityStarter.startPendingIntentDismissingKeyguard(
323                                 action.actionIntent
324                             )
325                         action.isAuthenticationRequired ->
326                             activityStarter.dismissKeyguardThenExecute(
327                                 { sendPendingIntent(action.actionIntent) },
328                                 {},
329                                 true,
330                             )
331                         else -> sendPendingIntent(actionIntent)
332                     }
333                 }
334             }
335         MediaAction(action.icon, runnable, action.contentDescription, background = null)
336     }
337 }
338 
sendPendingIntentnull339 private fun sendPendingIntent(intent: PendingIntent): Boolean {
340     return try {
341         intent.send(
342             BroadcastOptions.makeBasic()
343                 .apply {
344                     setInteractive(true)
345                     setPendingIntentBackgroundActivityStartMode(
346                         ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
347                     )
348                 }
349                 .toBundle()
350         )
351         true
352     } catch (e: PendingIntent.CanceledException) {
353         Log.d(TAG, "Intent canceled", e)
354         false
355     }
356 }
357