• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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