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

Commit 387efcdd authored by Lyn Han's avatar Lyn Han Committed by Android Build Coastguard Worker
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
(cherry picked from https://googleplex-android-review.googlesource.com/q/commit:56a930e723c4d896928e55d3fe3c31c768b21b38)
Merged-In: I3674060e2925acb326f0b120c09b2663112e7cba
Change-Id: I3674060e2925acb326f0b120c09b2663112e7cba
parent ca8d1e06
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)
        }
    }

    /**