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