1 /* 2 * 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 package com.android.systemui.accessibility.fontscaling 17 18 import android.content.Context 19 import android.content.pm.ActivityInfo 20 import android.content.res.Configuration 21 import android.database.ContentObserver 22 import android.os.Bundle 23 import android.os.Handler 24 import android.provider.Settings 25 import android.util.TypedValue 26 import android.view.LayoutInflater 27 import android.widget.Button 28 import android.widget.SeekBar 29 import android.widget.TextView 30 import androidx.annotation.MainThread 31 import androidx.annotation.WorkerThread 32 import com.android.systemui.R 33 import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView 34 import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView.OnSeekBarWithIconButtonsChangeListener 35 import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView.OnSeekBarWithIconButtonsChangeListener.ControlUnitType 36 import com.android.systemui.dagger.qualifiers.Background 37 import com.android.systemui.dagger.qualifiers.Main 38 import com.android.systemui.settings.UserTracker 39 import com.android.systemui.statusbar.phone.SystemUIDialog 40 import com.android.systemui.util.concurrency.DelayableExecutor 41 import com.android.systemui.util.settings.SecureSettings 42 import com.android.systemui.util.settings.SystemSettings 43 import com.android.systemui.util.time.SystemClock 44 import java.util.concurrent.atomic.AtomicInteger 45 import kotlin.math.roundToInt 46 47 /** The Dialog that contains a seekbar for changing the font size. */ 48 class FontScalingDialog( 49 context: Context, 50 private val systemSettings: SystemSettings, 51 private val secureSettings: SecureSettings, 52 private val systemClock: SystemClock, 53 private val userTracker: UserTracker, 54 @Main mainHandler: Handler, 55 @Background private val backgroundDelayableExecutor: DelayableExecutor 56 ) : SystemUIDialog(context) { 57 private val MIN_UPDATE_INTERVAL_MS: Long = 800 58 private val CHANGE_BY_SEEKBAR_DELAY_MS: Long = 100 59 private val CHANGE_BY_BUTTON_DELAY_MS: Long = 300 60 private val strEntryValues: Array<String> = 61 context.resources.getStringArray(com.android.settingslib.R.array.entryvalues_font_size) 62 private lateinit var title: TextView 63 private lateinit var doneButton: Button 64 private lateinit var seekBarWithIconButtonsView: SeekBarWithIconButtonsView 65 private var lastProgress: AtomicInteger = AtomicInteger(-1) 66 private var lastUpdateTime: Long = 0 67 private var cancelUpdateFontScaleRunnable: Runnable? = null 68 69 private val configuration: Configuration = Configuration(context.resources.configuration) 70 71 private val fontSizeObserver = 72 object : ContentObserver(mainHandler) { 73 override fun onChange(selfChange: Boolean) { 74 lastUpdateTime = systemClock.elapsedRealtime() 75 } 76 } 77 78 override fun onCreate(savedInstanceState: Bundle?) { 79 setTitle(R.string.font_scaling_dialog_title) 80 setView(LayoutInflater.from(context).inflate(R.layout.font_scaling_dialog, null)) 81 setPositiveButton( 82 R.string.quick_settings_done, 83 /* onClick = */ null, 84 /* dismissOnClick = */ true 85 ) 86 super.onCreate(savedInstanceState) 87 88 title = requireViewById(com.android.internal.R.id.alertTitle) 89 doneButton = requireViewById(com.android.internal.R.id.button1) 90 seekBarWithIconButtonsView = requireViewById(R.id.font_scaling_slider) 91 92 val labelArray = arrayOfNulls<String>(strEntryValues.size) 93 for (i in strEntryValues.indices) { 94 labelArray[i] = 95 context.resources.getString( 96 com.android.settingslib.R.string.font_scale_percentage, 97 (strEntryValues[i].toFloat() * 100).roundToInt() 98 ) 99 } 100 seekBarWithIconButtonsView.setProgressStateLabels(labelArray) 101 102 seekBarWithIconButtonsView.setMax((strEntryValues).size - 1) 103 104 val currentScale = 105 systemSettings.getFloatForUser(Settings.System.FONT_SCALE, 1.0f, userTracker.userId) 106 lastProgress.set(fontSizeValueToIndex(currentScale)) 107 seekBarWithIconButtonsView.setProgress(lastProgress.get()) 108 109 seekBarWithIconButtonsView.setOnSeekBarWithIconButtonsChangeListener( 110 object : OnSeekBarWithIconButtonsChangeListener { 111 override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { 112 // Always provide preview configuration for text first when there is a change 113 // in the seekbar progress. 114 createTextPreview(progress) 115 } 116 117 override fun onStartTrackingTouch(seekBar: SeekBar) { 118 // Do nothing 119 } 120 121 override fun onStopTrackingTouch(seekBar: SeekBar) { 122 // Do nothing 123 } 124 125 override fun onUserInteractionFinalized( 126 seekBar: SeekBar, 127 @ControlUnitType control: Int 128 ) { 129 if (control == ControlUnitType.BUTTON) { 130 // The seekbar progress is changed by icon buttons 131 changeFontSize(seekBar.progress, CHANGE_BY_BUTTON_DELAY_MS) 132 } else { 133 changeFontSize(seekBar.progress, CHANGE_BY_SEEKBAR_DELAY_MS) 134 } 135 } 136 } 137 ) 138 doneButton.setOnClickListener { dismiss() } 139 systemSettings.registerContentObserver(Settings.System.FONT_SCALE, fontSizeObserver) 140 } 141 142 /** 143 * Avoid SeekBar flickers when changing font scale. See the description from Setting at {@link 144 * TextReadingPreviewController#postCommitDelayed} for the reasons of flickers. 145 */ 146 @MainThread 147 fun updateFontScaleDelayed(delayMsFromSource: Long) { 148 doneButton.isEnabled = false 149 150 var delayMs = delayMsFromSource 151 if (systemClock.elapsedRealtime() - lastUpdateTime < MIN_UPDATE_INTERVAL_MS) { 152 delayMs += MIN_UPDATE_INTERVAL_MS 153 } 154 cancelUpdateFontScaleRunnable?.run() 155 cancelUpdateFontScaleRunnable = 156 backgroundDelayableExecutor.executeDelayed({ updateFontScale() }, delayMs) 157 } 158 159 override fun stop() { 160 cancelUpdateFontScaleRunnable?.run() 161 cancelUpdateFontScaleRunnable = null 162 systemSettings.unregisterContentObserver(fontSizeObserver) 163 } 164 165 @MainThread 166 private fun changeFontSize(progress: Int, changedWithDelay: Long) { 167 if (progress != lastProgress.get()) { 168 lastProgress.set(progress) 169 170 if (!fontSizeHasBeenChangedFromTile) { 171 backgroundDelayableExecutor.execute { updateSecureSettingsIfNeeded() } 172 fontSizeHasBeenChangedFromTile = true 173 } 174 175 updateFontScaleDelayed(changedWithDelay) 176 } 177 } 178 179 @WorkerThread 180 private fun fontSizeValueToIndex(value: Float): Int { 181 var lastValue = strEntryValues[0].toFloat() 182 for (i in 1 until strEntryValues.size) { 183 val thisValue = strEntryValues[i].toFloat() 184 if (value < lastValue + (thisValue - lastValue) * .5f) { 185 return i - 1 186 } 187 lastValue = thisValue 188 } 189 return strEntryValues.size - 1 190 } 191 192 override fun onConfigurationChanged(configuration: Configuration) { 193 super.onConfigurationChanged(configuration) 194 195 val configDiff = configuration.diff(this.configuration) 196 this.configuration.setTo(configuration) 197 198 if (configDiff and ActivityInfo.CONFIG_FONT_SCALE != 0) { 199 title.post { 200 title.setTextAppearance(R.style.TextAppearance_Dialog_Title) 201 doneButton.setTextAppearance(R.style.Widget_Dialog_Button) 202 doneButton.isEnabled = true 203 } 204 } 205 } 206 207 @WorkerThread 208 fun updateFontScale() { 209 if ( 210 !systemSettings.putStringForUser( 211 Settings.System.FONT_SCALE, 212 strEntryValues[lastProgress.get()], 213 userTracker.userId 214 ) 215 ) { 216 title.post { doneButton.isEnabled = true } 217 } 218 } 219 220 @WorkerThread 221 fun updateSecureSettingsIfNeeded() { 222 if ( 223 secureSettings.getStringForUser( 224 Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED, 225 userTracker.userId 226 ) != ON 227 ) { 228 secureSettings.putStringForUser( 229 Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED, 230 ON, 231 userTracker.userId 232 ) 233 } 234 } 235 236 /** Provides font size preview for text before putting the final settings to the system. */ 237 fun createTextPreview(index: Int) { 238 val previewConfig = Configuration(configuration) 239 previewConfig.fontScale = strEntryValues[index].toFloat() 240 241 val previewConfigContext = context.createConfigurationContext(previewConfig) 242 previewConfigContext.theme.setTo(context.theme) 243 244 title.setTextSize( 245 TypedValue.COMPLEX_UNIT_PX, 246 previewConfigContext.resources.getDimension(R.dimen.dialog_title_text_size) 247 ) 248 } 249 250 companion object { 251 private const val ON = "1" 252 private const val OFF = "0" 253 private var fontSizeHasBeenChangedFromTile = false 254 } 255 } 256