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.internal.R
29 import com.android.internal.widget.NotificationDrawableConsumer
30 import com.android.internal.widget.NotificationIconManager
31 import com.android.systemui.dagger.qualifiers.Application
32 import com.android.systemui.dagger.qualifiers.Background
33 import com.android.systemui.dagger.qualifiers.Main
34 import com.android.systemui.graphics.ImageLoader
35 import com.android.systemui.statusbar.notification.row.BigPictureIconManager.DrawableState.Empty
36 import com.android.systemui.statusbar.notification.row.BigPictureIconManager.DrawableState.FullImage
37 import com.android.systemui.statusbar.notification.row.BigPictureIconManager.DrawableState.Initial
38 import com.android.systemui.statusbar.notification.row.BigPictureIconManager.DrawableState.PlaceHolder
39 import com.android.systemui.util.Assert
40 import java.io.PrintWriter
41 import javax.inject.Inject
42 import kotlin.math.min
43 import kotlinx.coroutines.CoroutineDispatcher
44 import kotlinx.coroutines.CoroutineScope
45 import kotlinx.coroutines.Job
46 import kotlinx.coroutines.delay
47 import kotlinx.coroutines.launch
48 import kotlinx.coroutines.withContext
49 
50 private const val TAG = "BigPicImageLoader"
51 private const val DEBUG = false
52 private const val FREE_IMAGE_DELAY_MS = 3000L
53 
54 /**
55  * A helper class for [com.android.internal.widget.BigPictureNotificationImageView] to lazy-load
56  * images from SysUI. It replaces the placeholder image with the fully loaded one, and vica versa.
57  *
58  * TODO(b/283082473) move the logs to a [com.android.systemui.log.LogBuffer]
59  */
60 @SuppressWarnings("DumpableNotRegistered")
61 class BigPictureIconManager
62 @Inject
63 constructor(
64     private val context: Context,
65     private val imageLoader: ImageLoader,
66     private val statsManager: BigPictureStatsManager,
67     @Application private val scope: CoroutineScope,
68     @Main private val mainDispatcher: CoroutineDispatcher,
69     @Background private val bgDispatcher: CoroutineDispatcher
70 ) : NotificationIconManager, Dumpable {
71 
72     private var lastLoadingJob: Job? = null
73     private var drawableConsumer: NotificationDrawableConsumer? = null
74     private var displayedState: DrawableState = Initial
75     private var viewShown = false
76 
77     private var maxWidth = getMaxWidth()
78     private var maxHeight = getMaxHeight()
79 
80     /**
81      * Called when the displayed state changes of the view.
82      *
83      * @param shown true if the view is shown, and the image needs to be displayed.
84      */
85     fun onViewShown(shown: Boolean) {
86         log("onViewShown:$shown")
87 
88         if (this.viewShown != shown) {
89             this.viewShown = shown
90 
91             val state = displayedState
92 
93             this.lastLoadingJob?.cancel()
94             this.lastLoadingJob =
95                 when {
96                     skipLazyLoading(state.icon) -> null
97                     state is PlaceHolder && shown -> startLoadingJob(state.icon)
98                     state is FullImage && !shown ->
99                         startFreeImageJob(state.icon, state.drawableSize)
100                     else -> null
101                 }
102         }
103     }
104 
105     /**
106      * Update the maximum width and height allowed for bitmaps, ex. after a configuration change.
107      */
108     fun updateMaxImageSizes() {
109         log("updateMaxImageSizes")
110         maxWidth = getMaxWidth()
111         maxHeight = getMaxHeight()
112     }
113 
114     /** Cancels all currently running jobs. */
115     fun cancelJobs() {
116         lastLoadingJob?.cancel()
117     }
118 
119     @WorkerThread
120     override fun updateIcon(drawableConsumer: NotificationDrawableConsumer, icon: Icon?): Runnable {
121         this.drawableConsumer = drawableConsumer
122         this.lastLoadingJob?.cancel()
123 
124         val drawableAndState = loadImageOrPlaceHolderSync(icon)
125         log("icon updated")
126 
127         return Runnable { applyDrawableAndState(drawableAndState) }
128     }
129 
130     override fun dump(pw: PrintWriter, args: Array<out String>?) {
131         pw.println("BigPictureIconManager ${getDebugString()}")
132     }
133 
134     @WorkerThread
135     private fun loadImageOrPlaceHolderSync(icon: Icon?): Pair<Drawable, DrawableState>? {
136         icon ?: return null
137 
138         if (viewShown || skipLazyLoading(icon)) {
139             return loadImageSync(icon)
140         }
141 
142         return loadPlaceHolderSync(icon) ?: loadImageSync(icon)
143     }
144 
145     @WorkerThread
146     private fun loadImageSync(icon: Icon): Pair<Drawable, DrawableState>? {
147         return imageLoader.loadDrawableSync(icon, context, maxWidth, maxHeight)?.let { drawable ->
148             checkPlaceHolderSizeForDrawable(this.displayedState, drawable)
149             Pair(drawable, FullImage(icon, drawable.intrinsicSize))
150         }
151     }
152 
153     private fun checkPlaceHolderSizeForDrawable(
154         displayedState: DrawableState,
155         newDrawable: Drawable
156     ) {
157         if (displayedState is PlaceHolder) {
158             val (oldWidth, oldHeight) = displayedState.drawableSize
159             val (newWidth, newHeight) = newDrawable.intrinsicSize
160 
161             if (oldWidth != newWidth || oldHeight != newHeight) {
162                 Log.e(
163                     TAG,
164                     "Mismatch in dimensions, when replacing PlaceHolder " +
165                         "$oldWidth X $oldHeight with Drawable $newWidth X $newHeight."
166                 )
167             }
168         }
169     }
170 
171     @WorkerThread
172     private fun loadPlaceHolderSync(icon: Icon): Pair<Drawable, DrawableState>? {
173         return imageLoader
174             .loadSizeSync(icon, context)
175             ?.resizeToMax(maxWidth, maxHeight) // match the dimensions of the fully loaded drawable
176             ?.let { size -> createPlaceHolder(icon, size) }
177     }
178 
179     @MainThread
180     private fun applyDrawableAndState(drawableAndState: Pair<Drawable, DrawableState>?) {
181         Assert.isMainThread()
182         drawableConsumer?.setImageDrawable(drawableAndState?.first)
183         displayedState = drawableAndState?.second ?: Empty
184     }
185 
186     private fun startLoadingJob(icon: Icon): Job = scope.launch {
187         statsManager.measure { loadImage(icon) }
188     }
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         data object Empty : DrawableState(null)
257         data class PlaceHolder(override val icon: Icon, val drawableSize: Size) :
258             DrawableState(icon)
259         data class FullImage(override val icon: Icon, val drawableSize: Size) : DrawableState(icon)
260     }
261 }
262 
263 /**
264  * @return an image size that conforms to the maxWidth / maxHeight parameters. It can be the same
265  *   instance, if the provided size was already small enough.
266  */
Sizenull267 private fun Size.resizeToMax(maxWidth: Int, maxHeight: Int): Size {
268     if (width <= maxWidth && height <= maxHeight) {
269         return this
270     }
271 
272     // Calculate the scale factor for both dimensions
273     val wScale =
274         if (maxWidth <= 0) {
275             1.0f
276         } else {
277             maxWidth.toFloat() / width.toFloat()
278         }
279 
280     val hScale =
281         if (maxHeight <= 0) {
282             1.0f
283         } else {
284             maxHeight.toFloat() / height.toFloat()
285         }
286 
287     // Scale down to the smaller scale factor
288     val scale = min(wScale, hScale)
289     if (scale < 1.0f) {
290         val targetWidth = (width * scale).toInt()
291         val targetHeight = (height * scale).toInt()
292 
293         return Size(targetWidth, targetHeight)
294     }
295 
296     return this
297 }
298 
299 private val Drawable.intrinsicSize
300     get() = Size(/*width=*/ intrinsicWidth, /*height=*/ intrinsicHeight)
301 
component1null302 private operator fun Size.component1() = width
303 
304 private operator fun Size.component2() = height
305