Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 56a930e7 authored by Lyn Han's avatar Lyn Han
Browse files

Add timeout to loadImage to prevent shade freeze

Problem: notifications do not show until a few seconds
after the shade opens, caused by a blocking call to load
ic_brightness_full.xml for the brightness slider

Call stack that blocks UI:
- The UI (produceState in BrightnessSlider.kt) waits for the icon
  to load before it can finish drawing.
- The ViewModel's loadImage function calls imageLoader.loadDrawable.
- imageLoader switches to a background thread but then calls a
  synchronous loadDrawableSync that hangs while trying to load the xml.
- Because the background thread is frozen, the UI thread, which is
  waiting for the result, is also frozen.

Solution:
The fix wraps the imageLoader call in a 500ms timeout.
If loading the icon takes too long, we cancel and return null,
so the UI remains responsive and renders notifications promptly
on shade open.

Fixes: 423364554
Test: BrightnessSliderViewModelTest.kt
Test: open shade, notifs show with no delay, brightness slider has no    regressions; screenrecord in bug
Flag: EXEMPT bug fix
Change-Id: I3674060e2925acb326f0b120c09b2663112e7cba
parent 25929b52
Loading
Loading
Loading
Loading
+35 −1
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.brightness.ui.viewmodel

import android.graphics.drawable.Icon
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.display.BrightnessUtils
@@ -27,6 +28,7 @@ import com.android.systemui.brightness.shared.model.GammaBrightness
import com.android.systemui.brightness.shared.model.LinearBrightness
import com.android.systemui.classifier.domain.interactor.falsingInteractor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.graphics.ImageLoader
import com.android.systemui.graphics.imageLoader
import com.android.systemui.haptics.slider.sliderHapticsViewModelFactory
import com.android.systemui.kosmos.Kosmos
@@ -37,11 +39,15 @@ import com.android.systemui.settings.brightness.domain.interactor.brightnessMirr
import com.android.systemui.settings.brightness.ui.brightnessWarningToast
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.doSuspendableAnswer
import org.mockito.kotlin.mock

@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -225,7 +231,35 @@ class BrightnessSliderViewModelTest : SysuiTestCase() {
        }
    }

    private fun Kosmos.create(supportsMirror: Boolean = true): BrightnessSliderViewModel {
    @Test
    fun loadImage_timesOutAndReturnsNull_whenLoaderHangs() =
        with(kosmos) {
            testScope.runTest {
                // GIVEN: a mock ImageLoader that simulates a long-running operation
                val hangingImageLoader: ImageLoader = mock {
                    onBlocking {
                        loadDrawable(any<Icon>(), any(), any(), any(), any())
                    } doSuspendableAnswer
                        {
                            delay(10_000)
                            mock<android.graphics.drawable.Drawable>()
                        }
                }

                val underTest = create(imageLoader = hangingImageLoader)

                // WHEN: we load the image
                val loadedIcon = underTest.loadImage(R.drawable.ic_brightness_full, context)

                // THEN: return null due to timeout
                assertThat(loadedIcon).isNull()
            }
        }

    private fun Kosmos.create(
        supportsMirror: Boolean = true,
        imageLoader: ImageLoader = this.imageLoader,
    ): BrightnessSliderViewModel {
        return BrightnessSliderViewModel(
            screenBrightnessInteractor,
            brightnessPolicyEnforcementInteractor,
+8 −7
Original line number Diff line number Diff line
@@ -83,7 +83,6 @@ import com.android.compose.modifiers.padding
import com.android.compose.modifiers.thenIf
import com.android.compose.theme.LocalAndroidColorScheme
import com.android.compose.ui.graphics.drawInOverlay
import com.android.systemui.Flags
import com.android.systemui.biometrics.Utils.toBitmap
import com.android.systemui.brightness.shared.model.GammaBrightness
import com.android.systemui.brightness.ui.compose.AnimationSpecs.IconAppearSpec
@@ -115,7 +114,7 @@ fun BrightnessSlider(
    gammaValue: Int,
    valueRange: IntRange,
    iconResProvider: (Float) -> Int,
    imageLoader: suspend (Int, Context) -> Icon.Loaded,
    imageLoader: suspend (Int, Context) -> Icon.Loaded?,
    restriction: PolicyRestriction,
    onRestrictedClick: (PolicyRestriction.Restricted) -> Unit,
    onDrag: (Int) -> Unit,
@@ -165,12 +164,14 @@ fun BrightnessSlider(
            key1 = iconRes,
            key2 = context,
        ) {
            val icon = imageLoader(iconRes, context)
            // toBitmap is Drawable?.() -> Bitmap? and handles null internally.
            val bitmap = icon.drawable.toBitmap()!!.asImageBitmap()
            val icon: Icon.Loaded? = imageLoader(iconRes, context)
            if (icon != null) {
                val bitmap = icon.drawable.toBitmap()?.asImageBitmap()
                if (bitmap != null) {
                    this@produceState.value = BitmapPainter(bitmap)
                }

            }
        }
    val activeIconColor = colors.activeTickColor
    val inactiveIconColor = colors.inactiveTickColor
    // Offset from the right
+11 −8
Original line number Diff line number Diff line
@@ -41,6 +41,7 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.withTimeoutOrNull

/**
 * View Model for a brightness slider.
@@ -102,14 +103,16 @@ constructor(
        falsingInteractor.isFalseTouch(Classifier.BRIGHTNESS_SLIDER)
    }

    suspend fun loadImage(@DrawableRes resId: Int, context: Context): Icon.Loaded {
        return imageLoader
    suspend fun loadImage(@DrawableRes resId: Int, context: Context): Icon.Loaded? {
        return withTimeoutOrNull(500L) {
            imageLoader
                .loadDrawable(
                    android.graphics.drawable.Icon.createWithResource(context, resId),
                    maxHeight = 200,
                    maxWidth = 200,
            )!!
            .asIcon(null, resId)
                )
                ?.asIcon(null, resId)
        }
    }

    /**