1 /*
2  * Copyright (C) 2020 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.models.player
18 
19 import android.app.PendingIntent
20 import android.graphics.drawable.Drawable
21 import android.graphics.drawable.Icon
22 import android.media.session.MediaSession
23 import com.android.internal.logging.InstanceId
24 import com.android.systemui.R
25 
26 /** State of a media view. */
27 data class MediaData(
28     val userId: Int,
29     val initialized: Boolean = false,
30     /** App name that will be displayed on the player. */
31     val app: String?,
32     /** App icon shown on player. */
33     val appIcon: Icon?,
34     /** Artist name. */
35     val artist: CharSequence?,
36     /** Song name. */
37     val song: CharSequence?,
38     /** Album artwork. */
39     val artwork: Icon?,
40     /** List of generic action buttons for the media player, based on notification actions */
41     val actions: List<MediaAction>,
42     /** Same as above, but shown on smaller versions of the player, like in QQS or keyguard. */
43     val actionsToShowInCompact: List<Int>,
44     /**
45      * Semantic actions buttons, based on the PlaybackState of the media session. If present, these
46      * actions will be preferred in the UI over [actions]
47      */
48     val semanticActions: MediaButton? = null,
49     /** Package name of the app that's posting the media. */
50     val packageName: String,
51     /** Unique media session identifier. */
52     val token: MediaSession.Token?,
53     /** Action to perform when the player is tapped. This is unrelated to {@link #actions}. */
54     val clickIntent: PendingIntent?,
55     /** Where the media is playing: phone, headphones, ear buds, remote session. */
56     val device: MediaDeviceData?,
57     /**
58      * When active, a player will be displayed on keyguard and quick-quick settings. This is
59      * unrelated to the stream being playing or not, a player will not be active if timed out, or in
60      * resumption mode.
61      */
62     var active: Boolean,
63     /** Action that should be performed to restart a non active session. */
64     var resumeAction: Runnable?,
65     /** Playback location: one of PLAYBACK_LOCAL, PLAYBACK_CAST_LOCAL, or PLAYBACK_CAST_REMOTE */
66     var playbackLocation: Int = PLAYBACK_LOCAL,
67     /**
68      * Indicates that this player is a resumption player (ie. It only shows a play actions which
69      * will start the app and start playing).
70      */
71     var resumption: Boolean = false,
72     /**
73      * Notification key for cancelling a media player after a timeout (when not using resumption.)
74      */
75     val notificationKey: String? = null,
76     var hasCheckedForResume: Boolean = false,
77 
78     /** If apps do not report PlaybackState, set as null to imply 'undetermined' */
79     val isPlaying: Boolean? = null,
80 
81     /** Set from the notification and used as fallback when PlaybackState cannot be determined */
82     val isClearable: Boolean = true,
83 
84     /** Timestamp when this player was last active. */
85     var lastActive: Long = 0L,
86 
87     /** Instance ID for logging purposes */
88     val instanceId: InstanceId,
89 
90     /** The UID of the app, used for logging */
91     val appUid: Int,
92 
93     /** Whether explicit indicator exists */
94     val isExplicit: Boolean = false,
95 
96     /** Track progress (0 - 1) to display for players where [resumption] is true */
97     val resumeProgress: Double? = null,
98 ) {
99     companion object {
100         /** Media is playing on the local device */
101         const val PLAYBACK_LOCAL = 0
102         /** Media is cast but originated on the local device */
103         const val PLAYBACK_CAST_LOCAL = 1
104         /** Media is from a remote cast notification */
105         const val PLAYBACK_CAST_REMOTE = 2
106     }
107 
108     fun isLocalSession(): Boolean {
109         return playbackLocation == PLAYBACK_LOCAL
110     }
111 }
112 
113 /** Contains [MediaAction] objects which represent specific buttons in the UI */
114 data class MediaButton(
115     /** Play/pause button */
116     val playOrPause: MediaAction? = null,
117     /** Next button, or custom action */
118     val nextOrCustom: MediaAction? = null,
119     /** Previous button, or custom action */
120     val prevOrCustom: MediaAction? = null,
121     /** First custom action space */
122     val custom0: MediaAction? = null,
123     /** Second custom action space */
124     val custom1: MediaAction? = null,
125     /** Whether to reserve the empty space when the nextOrCustom is null */
126     val reserveNext: Boolean = false,
127     /** Whether to reserve the empty space when the prevOrCustom is null */
128     val reservePrev: Boolean = false
129 ) {
130     fun getActionById(id: Int): MediaAction? {
131         return when (id) {
132             R.id.actionPlayPause -> playOrPause
133             R.id.actionNext -> nextOrCustom
134             R.id.actionPrev -> prevOrCustom
135             R.id.action0 -> custom0
136             R.id.action1 -> custom1
137             else -> null
138         }
139     }
140 }
141 
142 /** State of a media action. */
143 data class MediaAction(
144     val icon: Drawable?,
145     val action: Runnable?,
146     val contentDescription: CharSequence?,
147     val background: Drawable?,
148 
149     // Rebind Id is used to detect identical rebinds and ignore them. It is intended
150     // to prevent continuously looping animations from restarting due to the arrival
151     // of repeated media notifications that are visually identical.
152     val rebindId: Int? = null
153 )
154 
155 /** State of the media device. */
156 data class MediaDeviceData
157 @JvmOverloads
158 constructor(
159     /** Whether or not to enable the chip */
160     val enabled: Boolean,
161 
162     /** Device icon to show in the chip */
163     val icon: Drawable?,
164 
165     /** Device display name */
166     val name: CharSequence?,
167 
168     /** Optional intent to override the default output switcher for this control */
169     val intent: PendingIntent? = null,
170 
171     /** Unique id for this device */
172     val id: String? = null,
173 
174     /** Whether or not to show the broadcast button */
175     val showBroadcastButton: Boolean
176 ) {
177     /**
178      * Check whether [MediaDeviceData] objects are equal in all fields except the icon. The icon is
179      * ignored because it can change by reference frequently depending on the device type's
180      * implementation, but this is not usually relevant unless other info has changed
181      */
182     fun equalsWithoutIcon(other: MediaDeviceData?): Boolean {
183         if (other == null) {
184             return false
185         }
186 
187         return enabled == other.enabled &&
188             name == other.name &&
189             intent == other.intent &&
190             id == other.id &&
191             showBroadcastButton == other.showBroadcastButton
192     }
193 }
194