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

Commit c2261f46 authored by Candice Lo's avatar Candice Lo
Browse files

Fix seekbar flickers when changing the font size

Delaying the time of updating the system font scale to guarantee that
the seekbar status has been updated first.

Bug: 277821437
Test: Manual - Attach video to the bug
Test: atest FontScalingDialogTest
Test: atest FontScalingTileTest
Change-Id: I5f91a77677481efb434478d2b39112ad3c88948d
parent 99a5d2d1
Loading
Loading
Loading
Loading
+60 −16
Original line number Diff line number Diff line
@@ -15,11 +15,12 @@
 */
package com.android.systemui.accessibility.fontscaling

import android.annotation.WorkerThread
import android.content.Context
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.database.ContentObserver
import android.os.Bundle
import android.os.Handler
import android.provider.Settings
import android.util.TypedValue
import android.view.LayoutInflater
@@ -27,13 +28,18 @@ import android.widget.Button
import android.widget.SeekBar
import android.widget.SeekBar.OnSeekBarChangeListener
import android.widget.TextView
import androidx.annotation.MainThread
import androidx.annotation.WorkerThread
import com.android.systemui.R
import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.settings.SecureSettings
import com.android.systemui.util.settings.SystemSettings
import java.util.concurrent.Executor
import com.android.systemui.util.time.SystemClock
import java.util.concurrent.atomic.AtomicInteger
import kotlin.math.roundToInt

/** The Dialog that contains a seekbar for changing the font size. */
@@ -41,17 +47,31 @@ class FontScalingDialog(
    context: Context,
    private val systemSettings: SystemSettings,
    private val secureSettings: SecureSettings,
    @Background private val backgroundExecutor: Executor
    private val systemClock: SystemClock,
    @Main mainHandler: Handler,
    @Background private val backgroundDelayableExecutor: DelayableExecutor
) : SystemUIDialog(context) {
    private val MIN_UPDATE_INTERVAL_MS: Long = 800
    private val CHANGE_BY_SEEKBAR_DELAY_MS: Long = 100
    private val CHANGE_BY_BUTTON_DELAY_MS: Long = 300
    private val strEntryValues: Array<String> =
        context.resources.getStringArray(com.android.settingslib.R.array.entryvalues_font_size)
    private lateinit var title: TextView
    private lateinit var doneButton: Button
    private lateinit var seekBarWithIconButtonsView: SeekBarWithIconButtonsView
    private var lastProgress: Int = -1
    private var lastProgress: AtomicInteger = AtomicInteger(-1)
    private var lastUpdateTime: Long = 0
    private var cancelUpdateFontScaleRunnable: Runnable? = null

    private val configuration: Configuration = Configuration(context.resources.configuration)

    private val fontSizeObserver =
        object : ContentObserver(mainHandler) {
            override fun onChange(selfChange: Boolean) {
                lastUpdateTime = systemClock.elapsedRealtime()
            }
        }

    override fun onCreate(savedInstanceState: Bundle?) {
        setTitle(R.string.font_scaling_dialog_title)
        setView(LayoutInflater.from(context).inflate(R.layout.font_scaling_dialog, null))
@@ -79,8 +99,8 @@ class FontScalingDialog(
        seekBarWithIconButtonsView.setMax((strEntryValues).size - 1)

        val currentScale = systemSettings.getFloat(Settings.System.FONT_SCALE, 1.0f)
        lastProgress = fontSizeValueToIndex(currentScale)
        seekBarWithIconButtonsView.setProgress(lastProgress)
        lastProgress.set(fontSizeValueToIndex(currentScale))
        seekBarWithIconButtonsView.setProgress(lastProgress.get())

        seekBarWithIconButtonsView.setOnSeekBarChangeListener(
            object : OnSeekBarChangeListener {
@@ -89,7 +109,7 @@ class FontScalingDialog(
                override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
                    if (!isTrackingTouch) {
                        // The seekbar progress is changed by icon buttons
                        changeFontSize(progress)
                        changeFontSize(progress, CHANGE_BY_BUTTON_DELAY_MS)
                    } else {
                        // Provide preview configuration for text instead of changing the system
                        // font scale before users release their finger from the seekbar.
@@ -103,26 +123,50 @@ class FontScalingDialog(

                override fun onStopTrackingTouch(seekBar: SeekBar) {
                    isTrackingTouch = false
                    changeFontSize(seekBar.progress)
                    changeFontSize(seekBar.progress, CHANGE_BY_SEEKBAR_DELAY_MS)
                }
            }
        )
        doneButton.setOnClickListener { dismiss() }
        systemSettings.registerContentObserver(Settings.System.FONT_SCALE, fontSizeObserver)
    }

    /**
     * Avoid SeekBar flickers when changing font scale. See the description from Setting at {@link
     * TextReadingPreviewController#postCommitDelayed} for the reasons of flickers.
     */
    @MainThread
    fun updateFontScaleDelayed(delayMsFromSource: Long) {
        var delayMs = delayMsFromSource
        if (systemClock.elapsedRealtime() - lastUpdateTime < MIN_UPDATE_INTERVAL_MS) {
            delayMs += MIN_UPDATE_INTERVAL_MS
        }
        cancelUpdateFontScaleRunnable?.run()
        cancelUpdateFontScaleRunnable =
            backgroundDelayableExecutor.executeDelayed({ updateFontScale() }, delayMs)
    }

    override fun stop() {
        cancelUpdateFontScaleRunnable?.run()
        cancelUpdateFontScaleRunnable = null
        systemSettings.unregisterContentObserver(fontSizeObserver)
    }

    private fun changeFontSize(progress: Int) {
        if (progress != lastProgress) {
    @MainThread
    private fun changeFontSize(progress: Int, changedWithDelay: Long) {
        if (progress != lastProgress.get()) {
            lastProgress.set(progress)

            if (!fontSizeHasBeenChangedFromTile) {
                backgroundExecutor.execute { updateSecureSettingsIfNeeded() }
                backgroundDelayableExecutor.execute { updateSecureSettingsIfNeeded() }
                fontSizeHasBeenChangedFromTile = true
            }

            backgroundExecutor.execute { updateFontScale(strEntryValues[progress]) }

            lastProgress = progress
            updateFontScaleDelayed(changedWithDelay)
        }
    }

    @WorkerThread
    private fun fontSizeValueToIndex(value: Float): Int {
        var lastValue = strEntryValues[0].toFloat()
        for (i in 1 until strEntryValues.size) {
@@ -150,8 +194,8 @@ class FontScalingDialog(
    }

    @WorkerThread
    fun updateFontScale(newScale: String) {
        systemSettings.putString(Settings.System.FONT_SCALE, newScale)
    fun updateFontScale() {
        systemSettings.putString(Settings.System.FONT_SCALE, strEntryValues[lastProgress.get()])
    }

    @WorkerThread
+9 −4
Original line number Diff line number Diff line
@@ -17,7 +17,6 @@ package com.android.systemui.qs.tiles

import android.content.Intent
import android.os.Handler
import android.os.HandlerExecutor
import android.os.Looper
import android.provider.Settings
import android.view.View
@@ -40,8 +39,10 @@ import com.android.systemui.qs.QsEventLogger
import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.qs.tileimpl.QSTileImpl
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.settings.SecureSettings
import com.android.systemui.util.settings.SystemSettings
import com.android.systemui.util.time.SystemClock
import javax.inject.Inject

class FontScalingTile
@@ -50,7 +51,7 @@ constructor(
    host: QSHost,
    uiEventLogger: QsEventLogger,
    @Background backgroundLooper: Looper,
    @Main mainHandler: Handler,
    @Main private val mainHandler: Handler,
    falsingManager: FalsingManager,
    metricsLogger: MetricsLogger,
    statusBarStateController: StatusBarStateController,
@@ -59,7 +60,9 @@ constructor(
    private val dialogLaunchAnimator: DialogLaunchAnimator,
    private val systemSettings: SystemSettings,
    private val secureSettings: SecureSettings,
    private val featureFlags: FeatureFlags
    private val systemClock: SystemClock,
    private val featureFlags: FeatureFlags,
    @Background private val backgroundDelayableExecutor: DelayableExecutor
) :
    QSTileImpl<QSTile.State?>(
        host,
@@ -89,7 +92,9 @@ constructor(
                    mContext,
                    systemSettings,
                    secureSettings,
                    HandlerExecutor(mHandler)
                    systemClock,
                    mainHandler,
                    backgroundDelayableExecutor
                )
            if (view != null) {
                dialogLaunchAnimator.showFromView(
+40 −9
Original line number Diff line number Diff line
@@ -50,10 +50,14 @@ private const val OFF: Int = 0
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
class FontScalingDialogTest : SysuiTestCase() {
    private val MIN_UPDATE_INTERVAL_MS: Long = 800
    private val CHANGE_BY_SEEKBAR_DELAY_MS: Long = 100
    private val CHANGE_BY_BUTTON_DELAY_MS: Long = 300
    private lateinit var fontScalingDialog: FontScalingDialog
    private lateinit var systemSettings: SystemSettings
    private lateinit var secureSettings: SecureSettings
    private lateinit var backgroundExecutor: FakeExecutor
    private lateinit var systemClock: FakeSystemClock
    private lateinit var backgroundDelayableExecutor: FakeExecutor
    private val fontSizeValueArray: Array<String> =
        mContext
            .getResources()
@@ -67,10 +71,20 @@ class FontScalingDialogTest : SysuiTestCase() {
        MockitoAnnotations.initMocks(this)
        val mainHandler = Handler(TestableLooper.get(this).getLooper())
        systemSettings = FakeSettings()
        // Guarantee that the systemSettings always starts with the default font scale.
        systemSettings.putFloat(Settings.System.FONT_SCALE, 1.0f)
        secureSettings = FakeSettings()
        backgroundExecutor = FakeExecutor(FakeSystemClock())
        systemClock = FakeSystemClock()
        backgroundDelayableExecutor = FakeExecutor(systemClock)
        fontScalingDialog =
            spy(FontScalingDialog(mContext, systemSettings, secureSettings, backgroundExecutor))
            FontScalingDialog(
                mContext,
                systemSettings,
                secureSettings,
                systemClock,
                mainHandler,
                backgroundDelayableExecutor
            )
    }

    @Test
@@ -96,9 +110,14 @@ class FontScalingDialogTest : SysuiTestCase() {
        val seekBar: SeekBar = fontScalingDialog.findViewById(R.id.seekbar)!!

        seekBarWithIconButtonsView.setProgress(0)
        backgroundDelayableExecutor.runAllReady()
        backgroundDelayableExecutor.advanceClockToNext()
        backgroundDelayableExecutor.runAllReady()

        iconEndFrame.performClick()
        backgroundExecutor.runAllReady()
        backgroundDelayableExecutor.runAllReady()
        backgroundDelayableExecutor.advanceClockToNext()
        backgroundDelayableExecutor.runAllReady()

        val currentScale = systemSettings.getFloat(Settings.System.FONT_SCALE, /* def= */ 1.0f)
        assertThat(seekBar.getProgress()).isEqualTo(1)
@@ -117,9 +136,14 @@ class FontScalingDialogTest : SysuiTestCase() {
        val seekBar: SeekBar = fontScalingDialog.findViewById(R.id.seekbar)!!

        seekBarWithIconButtonsView.setProgress(fontSizeValueArray.size - 1)
        backgroundDelayableExecutor.runAllReady()
        backgroundDelayableExecutor.advanceClockToNext()
        backgroundDelayableExecutor.runAllReady()

        iconStartFrame.performClick()
        backgroundExecutor.runAllReady()
        backgroundDelayableExecutor.runAllReady()
        backgroundDelayableExecutor.advanceClockToNext()
        backgroundDelayableExecutor.runAllReady()

        val currentScale = systemSettings.getFloat(Settings.System.FONT_SCALE, /* def= */ 1.0f)
        assertThat(seekBar.getProgress()).isEqualTo(fontSizeValueArray.size - 2)
@@ -139,7 +163,7 @@ class FontScalingDialogTest : SysuiTestCase() {

        // Default seekbar progress for font size is 1, set it to another progress 0
        seekBarWithIconButtonsView.setProgress(0)
        backgroundExecutor.runAllReady()
        backgroundDelayableExecutor.runAllReady()

        val currentSettings =
            secureSettings.getInt(
@@ -153,6 +177,7 @@ class FontScalingDialogTest : SysuiTestCase() {

    @Test
    fun dragSeekbar_systemFontSizeSettingsDoesNotChange() {
        fontScalingDialog = spy(fontScalingDialog)
        val slider: SeekBarWithIconButtonsView = spy(SeekBarWithIconButtonsView(mContext))
        whenever(
                fontScalingDialog.findViewById<SeekBarWithIconButtonsView>(R.id.font_scaling_slider)
@@ -169,7 +194,9 @@ class FontScalingDialogTest : SysuiTestCase() {
        // OnSeekBarChangeListener and the seekbar could get updated progress value
        // in onStopTrackingTouch.
        seekBar.progress = 0
        backgroundExecutor.runAllReady()
        backgroundDelayableExecutor.runAllReady()
        backgroundDelayableExecutor.advanceClockToNext()
        backgroundDelayableExecutor.runAllReady()

        // Verify that the scale of font size remains the default value 1.0f.
        var systemScale = systemSettings.getFloat(Settings.System.FONT_SCALE, /* def= */ 1.0f)
@@ -177,7 +204,9 @@ class FontScalingDialogTest : SysuiTestCase() {

        // Simulate releasing the finger from the seekbar.
        seekBarChangeCaptor.value.onStopTrackingTouch(seekBar)
        backgroundExecutor.runAllReady()
        backgroundDelayableExecutor.runAllReady()
        backgroundDelayableExecutor.advanceClockToNext()
        backgroundDelayableExecutor.runAllReady()

        // Verify that the scale of font size has been updated.
        systemScale = systemSettings.getFloat(Settings.System.FONT_SCALE, /* def= */ 1.0f)
@@ -188,6 +217,7 @@ class FontScalingDialogTest : SysuiTestCase() {

    @Test
    fun dragSeekBar_createTextPreview() {
        fontScalingDialog = spy(fontScalingDialog)
        val slider: SeekBarWithIconButtonsView = spy(SeekBarWithIconButtonsView(mContext))
        whenever(
                fontScalingDialog.findViewById<SeekBarWithIconButtonsView>(R.id.font_scaling_slider)
@@ -205,7 +235,8 @@ class FontScalingDialogTest : SysuiTestCase() {
            /* progress= */ 0,
            /* fromUser= */ false
        )
        backgroundExecutor.runAllReady()
        backgroundDelayableExecutor.advanceClockToNext()
        backgroundDelayableExecutor.runAllReady()

        verify(fontScalingDialog).createTextPreview(/* index= */ 0)
        fontScalingDialog.dismiss()
+9 −1
Original line number Diff line number Diff line
@@ -33,10 +33,12 @@ import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.qs.QSHost
import com.android.systemui.qs.QsEventLogger
import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.settings.FakeSettings
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
@@ -61,6 +63,8 @@ class FontScalingTileTest : SysuiTestCase() {
    @Mock private lateinit var uiEventLogger: QsEventLogger

    private lateinit var testableLooper: TestableLooper
    private lateinit var systemClock: FakeSystemClock
    private lateinit var backgroundDelayableExecutor: FakeExecutor
    private lateinit var fontScalingTile: FontScalingTile

    val featureFlags = FakeFeatureFlags()
@@ -70,6 +74,8 @@ class FontScalingTileTest : SysuiTestCase() {
        MockitoAnnotations.initMocks(this)
        testableLooper = TestableLooper.get(this)
        `when`(qsHost.getContext()).thenReturn(mContext)
        systemClock = FakeSystemClock()
        backgroundDelayableExecutor = FakeExecutor(systemClock)

        fontScalingTile =
            FontScalingTile(
@@ -85,7 +91,9 @@ class FontScalingTileTest : SysuiTestCase() {
                dialogLaunchAnimator,
                FakeSettings(),
                FakeSettings(),
                featureFlags
                FakeSystemClock(),
                featureFlags,
                backgroundDelayableExecutor,
            )
        fontScalingTile.initialize()
        testableLooper.processAllMessages()