• 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.PendingIntent
21 import android.content.Context
22 import android.graphics.drawable.Icon
23 import android.media.session.MediaController
24 import android.media.session.PlaybackState
25 import android.os.BadParcelableException
26 import android.util.Log
27 import com.android.systemui.biometrics.Utils.toBitmap
28 import com.android.systemui.media.controls.shared.model.MediaData
29 
30 private const val TAG = "MediaProcessingHelper"
31 
32 /**
33  * Compares [new] media data to [old] media data.
34  *
35  * @param context Context
36  * @param newController media controller of the new media data.
37  * @param new new media data.
38  * @param old old media data.
39  * @return whether new and old contain same data
40  */
41 fun isSameMediaData(
42     context: Context,
43     newController: MediaController,
44     new: MediaData,
45     old: MediaData?,
46 ): Boolean {
47     if (old == null) return false
48 
49     return new.userId == old.userId &&
50         new.app == old.app &&
51         new.artist == old.artist &&
52         new.song == old.song &&
53         new.packageName == old.packageName &&
54         new.isExplicit == old.isExplicit &&
55         new.appUid == old.appUid &&
56         new.notificationKey == old.notificationKey &&
57         new.isPlaying == old.isPlaying &&
58         new.isClearable == old.isClearable &&
59         new.playbackLocation == old.playbackLocation &&
60         new.device == old.device &&
61         new.initialized == old.initialized &&
62         new.resumption == old.resumption &&
63         new.token == old.token &&
64         new.resumeProgress == old.resumeProgress &&
65         areClickIntentsEqual(new.clickIntent, old.clickIntent) &&
66         areActionsEqual(context, newController, new, old) &&
67         areIconsEqual(context, new.artwork, old.artwork) &&
68         areIconsEqual(context, new.appIcon, old.appIcon)
69 }
70 
71 /** Returns whether actions lists are equal. */
areCustomActionListsEqualnull72 fun areCustomActionListsEqual(
73     first: List<PlaybackState.CustomAction>?,
74     second: List<PlaybackState.CustomAction>?,
75 ): Boolean {
76     // Same object, or both null
77     if (first === second) {
78         return true
79     }
80 
81     // Only one null, or different number of actions
82     if ((first == null || second == null) || (first.size != second.size)) {
83         return false
84     }
85 
86     // Compare individual actions
87     first.asSequence().zip(second.asSequence()).forEach { (firstAction, secondAction) ->
88         if (!areCustomActionsEqual(firstAction, secondAction)) {
89             return false
90         }
91     }
92     return true
93 }
94 
areCustomActionsEqualnull95 private fun areCustomActionsEqual(
96     firstAction: PlaybackState.CustomAction,
97     secondAction: PlaybackState.CustomAction,
98 ): Boolean {
99     if (
100         firstAction.action != secondAction.action ||
101             firstAction.name != secondAction.name ||
102             firstAction.icon != secondAction.icon
103     ) {
104         return false
105     }
106 
107     if ((firstAction.extras == null) != (secondAction.extras == null)) {
108         return false
109     }
110     if (firstAction.extras != null) {
111         firstAction.extras.keySet().forEach { key ->
112             try {
113                 if (firstAction.extras[key] != secondAction.extras[key]) {
114                     return false
115                 }
116             } catch (e: BadParcelableException) {
117                 Log.e(TAG, "Cannot unparcel extras", e)
118                 return false
119             }
120         }
121     }
122     return true
123 }
124 
125 @WorkerThread
areIconsEqualnull126 private fun areIconsEqual(context: Context, new: Icon?, old: Icon?): Boolean {
127     if (new == old) return true
128     if (new == null || old == null || new.type != old.type) return false
129     return if (new.type == Icon.TYPE_BITMAP || new.type == Icon.TYPE_ADAPTIVE_BITMAP) {
130         if (new.bitmap.isRecycled || old.bitmap.isRecycled) {
131             Log.e(TAG, "Cannot compare recycled bitmap")
132             return false
133         }
134         new.bitmap.sameAs(old.bitmap)
135     } else {
136         val newDrawable = new.loadDrawable(context)
137         val oldDrawable = old.loadDrawable(context)
138 
139         return newDrawable?.toBitmap()?.sameAs(oldDrawable?.toBitmap()) ?: false
140     }
141 }
142 
areActionsEqualnull143 private fun areActionsEqual(
144     context: Context,
145     newController: MediaController,
146     new: MediaData,
147     old: MediaData,
148 ): Boolean {
149     // TODO(b/360196209): account for actions generated from media3
150     val oldState = MediaController(context, old.token!!).playbackState
151     return if (
152         new.semanticActions == null &&
153             old.semanticActions == null &&
154             new.actions.size == old.actions.size
155     ) {
156         var same = true
157         new.actions.asSequence().zip(old.actions.asSequence()).forEach {
158             if (
159                 it.first.actionIntent?.intent != it.second.actionIntent?.intent ||
160                     it.first.icon != it.second.icon ||
161                     it.first.contentDescription != it.second.contentDescription
162             ) {
163                 same = false
164                 return@forEach
165             }
166         }
167         same
168     } else if (new.semanticActions != null && old.semanticActions != null) {
169         oldState?.actions == newController.playbackState?.actions &&
170             areCustomActionListsEqual(
171                 oldState?.customActions,
172                 newController.playbackState?.customActions,
173             )
174     } else {
175         false
176     }
177 }
178 
areClickIntentsEqualnull179 private fun areClickIntentsEqual(newIntent: PendingIntent?, oldIntent: PendingIntent?): Boolean {
180     return newIntent == oldIntent
181 }
182