1 /*
<lambda>null2 * Copyright (C) 2023 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.annotation.WorkerThread
20 import android.app.ActivityManager
21 import android.content.Context
22 import android.graphics.drawable.Drawable
23 import android.graphics.drawable.Icon
24 import android.util.Dumpable
25 import android.util.Log
26 import android.util.Size
27 import androidx.annotation.MainThread
28 import com.android.app.tracing.coroutines.launchTraced as launch
29 import com.android.internal.R
30 import com.android.internal.widget.NotificationDrawableConsumer
31 import com.android.internal.widget.NotificationIconManager
32 import com.android.systemui.dagger.qualifiers.Application
33 import com.android.systemui.dagger.qualifiers.Background
34 import com.android.systemui.dagger.qualifiers.Main
35 import com.android.systemui.graphics.ImageLoader
36 import com.android.systemui.shade.ShadeDisplayAware
37 import com.android.systemui.statusbar.notification.row.BigPictureIconManager.DrawableState.Empty
38 import com.android.systemui.statusbar.notification.row.BigPictureIconManager.DrawableState.FullImage
39 import com.android.systemui.statusbar.notification.row.BigPictureIconManager.DrawableState.Initial
40 import com.android.systemui.statusbar.notification.row.BigPictureIconManager.DrawableState.PlaceHolder
41 import com.android.systemui.util.Assert
42 import java.io.PrintWriter
43 import javax.inject.Inject
44 import kotlin.math.min
45 import kotlinx.coroutines.CoroutineDispatcher
46 import kotlinx.coroutines.CoroutineScope
47 import kotlinx.coroutines.Job
48 import kotlinx.coroutines.delay
49 import kotlinx.coroutines.withContext
50
51 private const val TAG = "BigPicImageLoader"
52 private const val DEBUG = false
53 private const val FREE_IMAGE_DELAY_MS = 3000L
54
55 /**
56 * A helper class for [com.android.internal.widget.BigPictureNotificationImageView] to lazy-load
57 * images from SysUI. It replaces the placeholder image with the fully loaded one, and vica versa.
58 *
59 * TODO(b/283082473) move the logs to a [com.android.systemui.log.LogBuffer]
60 */
61 @SuppressWarnings("DumpableNotRegistered")
62 class BigPictureIconManager
63 @Inject
64 constructor(
65 @ShadeDisplayAware private val context: Context,
66 private val imageLoader: ImageLoader,
67 private val statsManager: BigPictureStatsManager,
68 @Application private val scope: CoroutineScope,
69 @Main private val mainDispatcher: CoroutineDispatcher,
70 @Background private val bgDispatcher: CoroutineDispatcher,
71 ) : NotificationIconManager, Dumpable {
72
73 private var lastLoadingJob: Job? = null
74 private var drawableConsumer: NotificationDrawableConsumer? = null
75 private var displayedState: DrawableState = Initial
76 private var viewShown = false
77
78 private var maxWidth = getMaxWidth()
79 private var maxHeight = getMaxHeight()
80
81 /**
82 * Called when the displayed state changes of the view.
83 *
84 * @param shown true if the view is shown, and the image needs to be displayed.
85 */
86 fun onViewShown(shown: Boolean) {
87 log("onViewShown:$shown")
88
89 if (this.viewShown != shown) {
90 this.viewShown = shown
91
92 val state = displayedState
93
94 this.lastLoadingJob?.cancel()
95 this.lastLoadingJob =
96 when {
97 skipLazyLoading(state.icon) -> null
98 state is PlaceHolder && shown -> startLoadingJob(state.icon)
99 state is FullImage && !shown ->
100 startFreeImageJob(state.icon, state.drawableSize)
101 else -> null
102 }
103 }
104 }
105
106 /**
107 * Update the maximum width and height allowed for bitmaps, ex. after a configuration change.
108 */
109 fun updateMaxImageSizes() {
110 log("updateMaxImageSizes")
111 maxWidth = getMaxWidth()
112 maxHeight = getMaxHeight()
113 }
114
115 /** Cancels all currently running jobs. */
116 fun cancelJobs() {
117 lastLoadingJob?.cancel()
118 }
119
120 @WorkerThread
121 override fun updateIcon(drawableConsumer: NotificationDrawableConsumer, icon: Icon?): Runnable {
122 this.drawableConsumer = drawableConsumer
123 this.lastLoadingJob?.cancel()
124
125 val drawableAndState = loadImageOrPlaceHolderSync(icon)
126 log("icon updated")
127
128 return Runnable { applyDrawableAndState(drawableAndState) }
129 }
130
131 override fun dump(pw: PrintWriter, args: Array<out String>?) {
132 pw.println("BigPictureIconManager ${getDebugString()}")
133 }
134
135 @WorkerThread
136 private fun loadImageOrPlaceHolderSync(icon: Icon?): Pair<Drawable, DrawableState>? {
137 icon ?: return null
138
139 if (viewShown || skipLazyLoading(icon)) {
140 return loadImageSync(icon)
141 }
142
143 return loadPlaceHolderSync(icon) ?: loadImageSync(icon)
144 }
145
146 @WorkerThread
147 private fun loadImageSync(icon: Icon): Pair<Drawable, DrawableState>? {
148 return imageLoader.loadDrawableSync(icon, context, maxWidth, maxHeight)?.let { drawable ->
149 checkPlaceHolderSizeForDrawable(this.displayedState, drawable)
150 Pair(drawable, FullImage(icon, drawable.intrinsicSize))
151 }
152 }
153
154 private fun checkPlaceHolderSizeForDrawable(
155 displayedState: DrawableState,
156 newDrawable: Drawable,
157 ) {
158 if (displayedState is PlaceHolder) {
159 val (oldWidth, oldHeight) = displayedState.drawableSize
160 val (newWidth, newHeight) = newDrawable.intrinsicSize
161
162 if (oldWidth != newWidth || oldHeight != newHeight) {
163 Log.e(
164 TAG,
165 "Mismatch in dimensions, when replacing PlaceHolder " +
166 "$oldWidth X $oldHeight with Drawable $newWidth X $newHeight.",
167 )
168 }
169 }
170 }
171
172 @WorkerThread
173 private fun loadPlaceHolderSync(icon: Icon): Pair<Drawable, DrawableState>? {
174 return imageLoader
175 .loadSizeSync(icon, context)
176 ?.resizeToMax(maxWidth, maxHeight) // match the dimensions of the fully loaded drawable
177 ?.let { size -> createPlaceHolder(icon, size) }
178 }
179
180 @MainThread
181 private fun applyDrawableAndState(drawableAndState: Pair<Drawable, DrawableState>?) {
182 Assert.isMainThread()
183 drawableConsumer?.setImageDrawable(drawableAndState?.first)
184 displayedState = drawableAndState?.second ?: Empty
185 }
186
187 private fun startLoadingJob(icon: Icon): Job =
188 scope.launch { statsManager.measure { loadImage(icon) } }
189
190 private suspend fun loadImage(icon: Icon) {
191 val drawableAndState = withContext(bgDispatcher) { loadImageSync(icon) }
192 withContext(mainDispatcher) { applyDrawableAndState(drawableAndState) }
193 log("full image loaded")
194 }
195
196 private fun startFreeImageJob(icon: Icon, drawableSize: Size): Job =
197 scope.launch {
198 delay(FREE_IMAGE_DELAY_MS)
199 val drawableAndState = createPlaceHolder(icon, drawableSize)
200 withContext(mainDispatcher) { applyDrawableAndState(drawableAndState) }
201 log("placeholder loaded")
202 }
203
204 private fun createPlaceHolder(icon: Icon, size: Size): Pair<Drawable, DrawableState> {
205 val drawable = PlaceHolderDrawable(width = size.width, height = size.height)
206 val state = PlaceHolder(icon, drawable.intrinsicSize)
207 return Pair(drawable, state)
208 }
209
210 private fun isLowRam(): Boolean {
211 return ActivityManager.isLowRamDeviceStatic()
212 }
213
214 private fun getMaxWidth() =
215 context.resources.getDimensionPixelSize(
216 if (isLowRam()) {
217 R.dimen.notification_big_picture_max_width_low_ram
218 } else {
219 R.dimen.notification_big_picture_max_width
220 }
221 )
222
223 private fun getMaxHeight() =
224 context.resources.getDimensionPixelSize(
225 if (isLowRam()) {
226 R.dimen.notification_big_picture_max_height_low_ram
227 } else {
228 R.dimen.notification_big_picture_max_height
229 }
230 )
231
232 /**
233 * We don't support lazy-loading or set placeholders for bitmap and data based icons, because
234 * they gonna stay in memory anyways.
235 */
236 private fun skipLazyLoading(icon: Icon?): Boolean =
237 when (icon?.type) {
238 Icon.TYPE_BITMAP,
239 Icon.TYPE_ADAPTIVE_BITMAP,
240 Icon.TYPE_DATA,
241 null -> true
242 else -> false
243 }
244
245 private fun log(msg: String) {
246 if (DEBUG) {
247 Log.d(TAG, "$msg state=${getDebugString()}")
248 }
249 }
250
251 private fun getDebugString() =
252 "{ state:$displayedState, hasConsumer:${drawableConsumer != null}, viewShown:$viewShown}"
253
254 private sealed class DrawableState(open val icon: Icon?) {
255 data object Initial : DrawableState(null)
256
257 data object Empty : DrawableState(null)
258
259 data class PlaceHolder(override val icon: Icon, val drawableSize: Size) :
260 DrawableState(icon)
261
262 data class FullImage(override val icon: Icon, val drawableSize: Size) : DrawableState(icon)
263 }
264 }
265
266 /**
267 * @return an image size that conforms to the maxWidth / maxHeight parameters. It can be the same
268 * instance, if the provided size was already small enough.
269 */
Sizenull270 private fun Size.resizeToMax(maxWidth: Int, maxHeight: Int): Size {
271 if (width <= maxWidth && height <= maxHeight) {
272 return this
273 }
274
275 // Calculate the scale factor for both dimensions
276 val wScale =
277 if (maxWidth <= 0) {
278 1.0f
279 } else {
280 maxWidth.toFloat() / width.toFloat()
281 }
282
283 val hScale =
284 if (maxHeight <= 0) {
285 1.0f
286 } else {
287 maxHeight.toFloat() / height.toFloat()
288 }
289
290 // Scale down to the smaller scale factor
291 val scale = min(wScale, hScale)
292 if (scale < 1.0f) {
293 val targetWidth = (width * scale).toInt()
294 val targetHeight = (height * scale).toInt()
295
296 return Size(targetWidth, targetHeight)
297 }
298
299 return this
300 }
301
302 private val Drawable.intrinsicSize
303 get() = Size(/* width= */ intrinsicWidth, /* height= */ intrinsicHeight)
304
component1null305 private operator fun Size.component1() = width
306
307 private operator fun Size.component2() = height
308