1 /*
<lambda>null2 * Copyright (C) 2025 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.statusbar.notification.row
18
19 import android.content.Context
20 import android.graphics.drawable.Drawable
21 import android.graphics.drawable.Icon
22 import androidx.annotation.VisibleForTesting
23 import com.android.app.tracing.traceSection
24 import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi
25 import com.android.systemui.statusbar.notification.row.shared.IconData
26 import com.android.systemui.statusbar.notification.row.shared.ImageModel
27 import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider
28 import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider.ImageSizeClass
29 import com.android.systemui.statusbar.notification.row.shared.ImageModelProvider.ImageTransform
30 import java.util.Date
31
32 /**
33 * A class used as part of the notification content inflation process to generate image models,
34 * resolve image content for active models, and manage a generational index of images to reduce
35 * image load overhead.
36 *
37 * Each round of inflation follows this process:
38 * 1. Instantiate a [newInstance] of this class using the current index.
39 * 2. Call [useForContentModel] and use that object to generate [ImageModel] objects.
40 * 3. [ImageModel] objects may be stored in a content model, which may be used in Flows or States.
41 * 4. On the background thread, call [loadImagesSynchronously] to ensure all models have images.
42 * 5. In case of success, call [getNewImageIndex] and if the result is not null, replace the
43 * original index with this one that is a generation newer. In case the inflation failed, the
44 * [RowImageInflater] can be discarded and while all newly resolved images will be discarded, no
45 * change will have been made to the previous index.
46 */
47 interface RowImageInflater {
48 /**
49 * This returns an [ImageModelProvider] that can be used for getting models of images for use in
50 * a content model. Calling this method marks the inflater as being used, which means that
51 * instead of [getNewImageIndex] returning the previous index, it will now suddenly return
52 * nothing unless until other models are provided. This behavior allows the implicit absence of
53 * calls to evict unused images from the new index.
54 *
55 * NOTE: right now there is only one inflation process which uses this to access images. In the
56 * future we will likely have more. In that case, we will need this method and the
57 * [ImageModelIndex] to support the concept of different optional inflation lanes.
58 *
59 * Here's an example to illustrate why this would be necessary:
60 * 1. We inflate just the general content and save the index with 6 images.
61 * 2. Later, we inflate just the AOD RON content and save the index with 3 images, discarding
62 * the 3 from the general content.
63 * 3. Later, we reinflate the general content and have to reload 3 images that should've been in
64 * the index.
65 */
66 fun useForContentModel(): ImageModelProvider
67
68 /**
69 * Synchronously load all drawables that are not in the index, and ensure the [ImageModel]s
70 * previously returned by an [ImageModelProvider] all provide access to those drawables.
71 */
72 fun loadImagesSynchronously(context: Context)
73
74 /**
75 * Get the next generation of the [ImageModelIndex] for this row. This will return the previous
76 * index if this inflater was never used.
77 */
78 fun getNewImageIndex(): ImageModelIndex?
79
80 companion object {
81 @Suppress("NOTHING_TO_INLINE")
82 @JvmStatic
83 inline fun featureFlagEnabled() = PromotedNotificationUi.isEnabled
84
85 @JvmStatic
86 fun newInstance(previousIndex: ImageModelIndex?, reinflating: Boolean): RowImageInflater =
87 if (featureFlagEnabled()) {
88 RowImageInflaterImpl(previousIndex, reinflating)
89 } else {
90 RowImageInflaterStub
91 }
92 }
93 }
94
95 /** A no-op implementation that does nothing */
96 private object ImageModelProviderStub : ImageModelProvider {
getImageModelnull97 override fun getImageModel(
98 icon: Icon,
99 sizeClass: ImageSizeClass,
100 transform: ImageTransform,
101 ): ImageModel? = null
102 }
103
104 /** A no-op implementation that does nothing */
105 private object RowImageInflaterStub : RowImageInflater {
106 override fun useForContentModel(): ImageModelProvider = ImageModelProviderStub
107
108 override fun loadImagesSynchronously(context: Context) = Unit
109
110 override fun getNewImageIndex(): ImageModelIndex? = null
111 }
112
113 class RowImageInflaterImpl(private val previousIndex: ImageModelIndex?, val reinflating: Boolean) :
114 RowImageInflater {
115 private val providedImages = mutableListOf<LazyImage>()
116
117 /**
118 * For now there is only one way we use this, so we don't need to track which "model" it was
119 * used for. If in the future we use it for more models, then we can do that, and also track the
120 * parts of the index that should or shouldn't be copied.
121 */
122 private var wasUsed = false
123
124 /** Gets the ImageModelProvider that is used for inflating the content model. */
useForContentModelnull125 override fun useForContentModel(): ImageModelProvider {
126 wasUsed = true
127 return object : ImageModelProvider {
128 override fun getImageModel(
129 icon: Icon,
130 sizeClass: ImageSizeClass,
131 transform: ImageTransform,
132 ): ImageModel? {
133 val iconData = IconData.fromIcon(icon) ?: return null
134 // if we've already provided an equivalent image, return it again.
135 providedImages.firstOrNull(iconData, sizeClass, transform)?.let {
136 return it
137 }
138 // create and return a new entry
139 return LazyImage(iconData, sizeClass, transform).also { newImage ->
140 // ensure all entries are stored
141 providedImages.add(newImage)
142 // load the image result from the index into our new object
143 previousIndex
144 // skip the cached image when we are "reinflating" to avoid stale content
145 // being displayed from the same URI after the app updated the notif
146 ?.takeUnless { reinflating }
147 ?.findImage(iconData, sizeClass, transform)
148 ?.let {
149 // copy the result into our new object
150 newImage.result = it
151 }
152 }
153 }
154 }
155 }
156
loadImagesSynchronouslynull157 override fun loadImagesSynchronously(context: Context) {
158 traceSection("RowImageInflater.loadImageDrawablesSync") {
159 providedImages.forEach { lazyImage ->
160 if (lazyImage.result == null) {
161 lazyImage.result = lazyImage.load(context)
162 }
163 }
164 }
165 }
166
loadnull167 private fun LazyImage.load(context: Context): ImageResult {
168 traceSection("LazyImage.load") {
169 // TODO: use the sizeClass to load the drawable into a correctly sized bitmap,
170 // and be sure to respect [lazyImage.transform.requiresSoftwareBitmapInput]
171 val iconDrawable =
172 icon.toIcon().loadDrawable(context)
173 ?: return ImageResult.Empty("Icon.loadDrawable() returned null for $icon")
174 return transform.transformDrawable(iconDrawable)?.let { ImageResult.Image(it) }
175 ?: return ImageResult.Empty("Transform ${transform.key} returned null")
176 }
177 }
178
getNewImageIndexnull179 override fun getNewImageIndex(): ImageModelIndex? =
180 if (wasUsed) ImageModelIndex(providedImages) else previousIndex
181 }
182
183 class ImageModelIndex internal constructor(data: Collection<LazyImage>) {
184 private val images = data.toMutableList()
185
186 fun findImage(
187 icon: IconData,
188 sizeClass: ImageSizeClass,
189 transform: ImageTransform,
190 ): ImageResult? = images.firstOrNull(icon, sizeClass, transform)?.result
191
192 @VisibleForTesting
193 val contentsForTesting: MutableList<LazyImage>
194 get() = images
195 }
196
firstOrNullnull197 private fun Collection<LazyImage>.firstOrNull(
198 icon: IconData,
199 sizeClass: ImageSizeClass,
200 transform: ImageTransform,
201 ): LazyImage? = firstOrNull {
202 it.sizeClass == sizeClass && it.icon == icon && it.transform == transform
203 }
204
205 data class LazyImage(
206 val icon: IconData,
207 val sizeClass: ImageSizeClass,
208 val transform: ImageTransform,
209 var result: ImageResult? = null,
210 ) : ImageModel {
211 override val drawable: Drawable?
212 get() = (result as? ImageResult.Image)?.drawable
213 }
214
215 /** The result of attempting to load an image. */
216 sealed interface ImageResult {
217 /** Indicates a null result from the image loading process, with a reason for debugging */
218 data class Empty(val reason: String, val time: Date = Date()) : ImageResult
219
220 /** Stores the drawable result of loading an image */
221 data class Image(val drawable: Drawable) : ImageResult
222 }
223