1 /*
<lambda>null2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11  * KIND, either express or implied. See the License for the specific language governing
12  * permissions and limitations under the License.
13  *
14  */
15 
16 package com.android.systemui.statusbar.notification.icon.ui.viewmodel
17 
18 import android.graphics.drawable.Icon
19 import androidx.collection.ArrayMap
20 import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
21 import com.android.systemui.util.kotlin.mapValuesNotNullTo
22 
23 /** Encapsulates the collection of notification icons present on the device. */
24 data class NotificationIconsViewData(
25     /** Icons that are visible in the container. */
26     val visibleIcons: List<NotificationIconInfo> = emptyList(),
27     /** Limit applied to the [visibleIcons]; can be interpreted different based on [limitType]. */
28     val iconLimit: Int = visibleIcons.size,
29     /** How [iconLimit] is applied to [visibleIcons]. */
30     val limitType: LimitType = LimitType.MaximumAmount,
31 ) {
32     // TODO(b/305739416): This can be removed once we are no longer looking up the StatusBarIconView
33     //  instances from outside of the view-binder layer. Ideally, we would just use MaximumAmount,
34     //  and apply it directly to the list of visibleIcons by truncating the list to that amount.
35     //  At the time of this writing, we cannot do that because looking up the SBIV can fail, and so
36     //  we need additional icons to fall-back to.
37     /** Determines how a limit to the icons is to be applied. */
38     enum class LimitType {
39         /** The [Int] limit is a maximum amount of icons to be displayed. */
40         MaximumAmount,
41         /**
42          * The [Int] limit is a maximum index into the
43          * [list of visible icons][NotificationIconsViewData.visibleIcons] to be displayed; any
44          * icons beyond that index should be omitted.
45          */
46         MaximumIndex,
47     }
48 
49     /** The difference between two [NotificationIconsViewData]s. */
50     data class Diff(
51         /** Icons added in the newer dataset. */
52         val added: List<String> = emptyList(),
53         /** Icons removed from the older dataset. */
54         val removed: List<String> = emptyList(),
55         /**
56          * Groups whose icon was replaced with a single new notification icon. The key of the [Map]
57          * is the notification group key, and the value is the new icon.
58          *
59          * Specifically, this models a difference where the older dataset had notification groups
60          * with a single icon in the set, and the newer dataset has a single, different icon for the
61          * same group. A view binder can use this information for special animations for this
62          * specific change.
63          */
64         val groupReplacements: Map<String, String> = emptyMap(),
65     )
66 
67     companion object {
68         /**
69          * Returns an [NotificationIconsViewData.Diff] calculated from a [new] and [previous][prev]
70          * [NotificationIconsViewData] state.
71          */
72         fun computeDifference(
73             new: NotificationIconsViewData,
74             prev: NotificationIconsViewData
75         ): Diff {
76             val prevKeys = prev.visibleIcons.asSequence().map { it.notifKey }.toSet()
77             val newKeys = new.visibleIcons.asSequence().map { it.notifKey }.toSet()
78             val added: List<String> = newKeys.mapNotNull { key -> key.takeIf { it !in prevKeys } }
79             val removed: List<NotificationIconInfo> =
80                 prev.visibleIcons.filter { it.notifKey !in newKeys }
81             val groupsToShow: Set<IconGroupInfo> =
82                 new.visibleIcons.asSequence().map { it.groupInfo }.toSet()
83             val replacements: ArrayMap<String, String> =
84                 removed
85                     .asSequence()
86                     .filter { keyToRemove -> keyToRemove.groupInfo in groupsToShow }
87                     .groupBy { it.groupInfo.groupKey }
88                     .mapValuesNotNullTo(ArrayMap()) { (_, vs) ->
89                         vs.takeIf { it.size == 1 }?.get(0)?.notifKey
90                     }
91             return Diff(added, removed.map { it.notifKey }, replacements)
92         }
93     }
94 }
95 
96 /** An Icon, and keys for unique identification. */
97 data class NotificationIconInfo(
98     val sourceIcon: Icon,
99     val notifKey: String,
100     val groupKey: String,
101 )
102 
103 /**
104  * Construct an [NotificationIconInfo] out of an [ActiveNotificationModel], or return `null` if one
105  * cannot be created due to missing information.
106  */
ActiveNotificationModelnull107 fun ActiveNotificationModel.toIconInfo(sourceIcon: Icon?): NotificationIconInfo? {
108     return sourceIcon?.let {
109         groupKey?.let { groupKey ->
110             NotificationIconInfo(
111                 sourceIcon = sourceIcon,
112                 notifKey = key,
113                 groupKey = groupKey,
114             )
115         }
116     }
117 }
118 
119 private val NotificationIconInfo.groupInfo: IconGroupInfo
120     get() = IconGroupInfo(sourceIcon, groupKey)
121 
122 private data class IconGroupInfo(
123     val sourceIcon: Icon,
124     val groupKey: String,
125 ) {
126 
equalsnull127     override fun equals(other: Any?): Boolean {
128         if (this === other) return true
129         if (javaClass != other?.javaClass) return false
130 
131         other as IconGroupInfo
132 
133         if (groupKey != other.groupKey) return false
134         return sourceIcon.sameAs(other.sourceIcon)
135     }
136 
hashCodenull137     override fun hashCode(): Int {
138         var result = groupKey.hashCode()
139         result = 31 * result + sourceIcon.type.hashCode()
140         when (sourceIcon.type) {
141             Icon.TYPE_BITMAP,
142             Icon.TYPE_ADAPTIVE_BITMAP -> {
143                 result = 31 * result + sourceIcon.bitmap.hashCode()
144             }
145             Icon.TYPE_DATA -> {
146                 result = 31 * result + sourceIcon.dataLength.hashCode()
147                 result = 31 * result + sourceIcon.dataOffset.hashCode()
148             }
149             Icon.TYPE_RESOURCE -> {
150                 result = 31 * result + sourceIcon.resId.hashCode()
151                 result = 31 * result + sourceIcon.resPackage.hashCode()
152             }
153             Icon.TYPE_URI,
154             Icon.TYPE_URI_ADAPTIVE_BITMAP -> {
155                 result = 31 * result + sourceIcon.uriString.hashCode()
156             }
157         }
158         return result
159     }
160 }
161