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