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