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