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.res.Configuration
19 import android.os.Handler
20 import android.provider.Settings
21 import android.testing.TestableLooper
22 import android.view.LayoutInflater
23 import android.view.ViewGroup
24 import android.widget.Button
25 import android.widget.SeekBar
26 import androidx.test.ext.junit.runners.AndroidJUnit4
27 import androidx.test.filters.SmallTest
28 import com.android.systemui.SysuiTestCase
29 import com.android.systemui.animation.DialogTransitionAnimator
30 import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView
31 import com.android.systemui.common.ui.view.SeekBarWithIconButtonsView.OnSeekBarWithIconButtonsChangeListener
32 import com.android.systemui.model.SysUiState
33 import com.android.systemui.res.R
34 import com.android.systemui.settings.UserTracker
35 import com.android.systemui.statusbar.phone.SystemUIDialog
36 import com.android.systemui.statusbar.phone.SystemUIDialog.DEFAULT_DISMISS_ON_DEVICE_LOCK
37 import com.android.systemui.statusbar.phone.SystemUIDialogManager
38 import com.android.systemui.util.concurrency.FakeExecutor
39 import com.android.systemui.util.settings.FakeSettings
40 import com.android.systemui.util.settings.SecureSettings
41 import com.android.systemui.util.settings.SystemSettings
42 import com.android.systemui.util.time.FakeSystemClock
43 import com.google.common.truth.Truth.assertThat
44 import org.junit.Before
45 import org.junit.Test
46 import org.junit.runner.RunWith
47 import org.mockito.ArgumentMatchers.any
48 import org.mockito.ArgumentMatchers.anyBoolean
49 import org.mockito.ArgumentMatchers.anyLong
50 import org.mockito.Mock
51 import org.mockito.Mockito.spy
52 import org.mockito.Mockito.verify
53 import org.mockito.MockitoAnnotations
54 import org.mockito.Mockito.`when` as whenever
55 
56 private const val ON: Int = 1
57 private const val OFF: Int = 0
58 
59 /** Tests for [FontScalingDialogDelegate]. */
60 @SmallTest
61 @RunWith(AndroidJUnit4::class)
62 @TestableLooper.RunWithLooper(setAsMainLooper = true)
63 class FontScalingDialogDelegateTest : SysuiTestCase() {
64     private lateinit var fontScalingDialogDelegate: FontScalingDialogDelegate
65     private lateinit var dialog: SystemUIDialog
66     private lateinit var systemSettings: SystemSettings
67     private lateinit var secureSettings: SecureSettings
68     private lateinit var systemClock: FakeSystemClock
69     private lateinit var backgroundDelayableExecutor: FakeExecutor
70     private lateinit var testableLooper: TestableLooper
71     private val fontSizeValueArray: Array<String> =
72         mContext
73             .getResources()
74             .getStringArray(com.android.settingslib.R.array.entryvalues_font_size)
75 
76     @Mock private lateinit var dialogManager: SystemUIDialogManager
77     @Mock private lateinit var dialogFactory: SystemUIDialog.Factory
78     @Mock private lateinit var userTracker: UserTracker
79     @Mock private lateinit var sysuiState: SysUiState
80     @Mock private lateinit var mDialogTransitionAnimator: DialogTransitionAnimator
81 
82     @Before
setUpnull83     fun setUp() {
84         MockitoAnnotations.initMocks(this)
85         testableLooper = TestableLooper.get(this)
86         val mainHandler = Handler(testableLooper.looper)
87         systemSettings = FakeSettings()
88         // Guarantee that the systemSettings always starts with the default font scale.
89         systemSettings.putFloatForUser(Settings.System.FONT_SCALE, 1.0f, userTracker.userId)
90         secureSettings = FakeSettings()
91         systemClock = FakeSystemClock()
92         backgroundDelayableExecutor = FakeExecutor(systemClock)
93         whenever(sysuiState.setFlag(anyLong(), anyBoolean())).thenReturn(sysuiState)
94 
95         fontScalingDialogDelegate =
96             spy(
97                 FontScalingDialogDelegate(
98                     mContext,
99                     dialogFactory,
100                     LayoutInflater.from(mContext),
101                     systemSettings,
102                     secureSettings,
103                     systemClock,
104                     userTracker,
105                     mainHandler,
106                     backgroundDelayableExecutor
107                 )
108             )
109 
110         dialog =
111             SystemUIDialog(
112                 mContext,
113                 0,
114                 DEFAULT_DISMISS_ON_DEVICE_LOCK,
115                 dialogManager,
116                 sysuiState,
117                 fakeBroadcastDispatcher,
118                 mDialogTransitionAnimator,
119                 fontScalingDialogDelegate
120             )
121 
122         whenever(dialogFactory.create(any(), any())).thenReturn(dialog)
123     }
124 
125     @Test
showTheDialog_seekbarIsShowingCorrectProgressnull126     fun showTheDialog_seekbarIsShowingCorrectProgress() {
127         dialog.show()
128 
129         val seekBar: SeekBar = dialog.findViewById<SeekBar>(R.id.seekbar)!!
130         val progress: Int = seekBar.getProgress()
131         val currentScale =
132             systemSettings.getFloatForUser(
133                 Settings.System.FONT_SCALE,
134                 /* def= */ 1.0f,
135                 userTracker.userId
136             )
137 
138         assertThat(currentScale).isEqualTo(fontSizeValueArray[progress].toFloat())
139 
140         dialog.dismiss()
141     }
142 
143     @Test
progressIsZero_clickIconEnd_seekBarProgressIncreaseOne_fontSizeScalednull144     fun progressIsZero_clickIconEnd_seekBarProgressIncreaseOne_fontSizeScaled() {
145         dialog.show()
146 
147         val iconEndFrame: ViewGroup = dialog.findViewById(R.id.icon_end_frame)!!
148         val seekBarWithIconButtonsView: SeekBarWithIconButtonsView =
149             dialog.findViewById(R.id.font_scaling_slider)!!
150         val seekBar: SeekBar = dialog.findViewById(R.id.seekbar)!!
151 
152         seekBarWithIconButtonsView.setProgress(0)
153         backgroundDelayableExecutor.runAllReady()
154         backgroundDelayableExecutor.advanceClockToNext()
155         backgroundDelayableExecutor.runAllReady()
156 
157         iconEndFrame.performClick()
158         backgroundDelayableExecutor.runAllReady()
159         backgroundDelayableExecutor.advanceClockToNext()
160         backgroundDelayableExecutor.runAllReady()
161 
162         val currentScale =
163             systemSettings.getFloatForUser(
164                 Settings.System.FONT_SCALE,
165                 /* def= */ 1.0f,
166                 userTracker.userId
167             )
168         assertThat(seekBar.getProgress()).isEqualTo(1)
169         assertThat(currentScale).isEqualTo(fontSizeValueArray[1].toFloat())
170 
171         dialog.dismiss()
172     }
173 
174     @Test
progressIsMax_clickIconStart_seekBarProgressDecreaseOne_fontSizeScalednull175     fun progressIsMax_clickIconStart_seekBarProgressDecreaseOne_fontSizeScaled() {
176         dialog.show()
177 
178         val iconStartFrame: ViewGroup = dialog.findViewById(R.id.icon_start_frame)!!
179         val seekBarWithIconButtonsView: SeekBarWithIconButtonsView =
180             dialog.findViewById(R.id.font_scaling_slider)!!
181         val seekBar: SeekBar = dialog.findViewById(R.id.seekbar)!!
182 
183         seekBarWithIconButtonsView.setProgress(fontSizeValueArray.size - 1)
184         backgroundDelayableExecutor.runAllReady()
185         backgroundDelayableExecutor.advanceClockToNext()
186         backgroundDelayableExecutor.runAllReady()
187 
188         iconStartFrame.performClick()
189         backgroundDelayableExecutor.runAllReady()
190         backgroundDelayableExecutor.advanceClockToNext()
191         backgroundDelayableExecutor.runAllReady()
192 
193         val currentScale =
194             systemSettings.getFloatForUser(
195                 Settings.System.FONT_SCALE,
196                 /* def= */ 1.0f,
197                 userTracker.userId
198             )
199         assertThat(seekBar.getProgress()).isEqualTo(fontSizeValueArray.size - 2)
200         assertThat(currentScale)
201             .isEqualTo(fontSizeValueArray[fontSizeValueArray.size - 2].toFloat())
202 
203         dialog.dismiss()
204     }
205 
206     @Test
progressChanged_keyWasNotSetBefore_fontScalingHasBeenChangedIsOnnull207     fun progressChanged_keyWasNotSetBefore_fontScalingHasBeenChangedIsOn() {
208         dialog.show()
209 
210         val iconStartFrame: ViewGroup = dialog.findViewById(R.id.icon_start_frame)!!
211         secureSettings.putIntForUser(
212             Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED,
213             OFF,
214             userTracker.userId
215         )
216 
217         // Default seekbar progress for font size is 1, click start icon to decrease the progress
218         iconStartFrame.performClick()
219         backgroundDelayableExecutor.runAllReady()
220         backgroundDelayableExecutor.advanceClockToNext()
221         backgroundDelayableExecutor.runAllReady()
222 
223         val currentSettings =
224             secureSettings.getIntForUser(
225                 Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED,
226                 /* def = */ OFF,
227                 userTracker.userId
228             )
229         assertThat(currentSettings).isEqualTo(ON)
230 
231         dialog.dismiss()
232     }
233 
234     @Test
dragSeekbar_systemFontSizeSettingsDoesNotChangenull235     fun dragSeekbar_systemFontSizeSettingsDoesNotChange() {
236         dialog.show()
237 
238         val slider = dialog.findViewById<SeekBarWithIconButtonsView>(R.id.font_scaling_slider)!!
239         val changeListener = slider.onSeekBarWithIconButtonsChangeListener
240 
241         val seekBar: SeekBar = slider.findViewById(R.id.seekbar)!!
242 
243         // Default seekbar progress for font size is 1, simulate dragging to 0 without
244         // releasing the finger.
245         changeListener.onStartTrackingTouch(seekBar)
246         // Update seekbar progress. This will trigger onProgressChanged in the
247         // OnSeekBarChangeListener and the seekbar could get updated progress value
248         // in onStopTrackingTouch.
249         seekBar.progress = 0
250         backgroundDelayableExecutor.runAllReady()
251         backgroundDelayableExecutor.advanceClockToNext()
252         backgroundDelayableExecutor.runAllReady()
253 
254         // Verify that the scale of font size remains the default value 1.0f.
255         var systemScale =
256             systemSettings.getFloatForUser(
257                 Settings.System.FONT_SCALE,
258                 /* def= */ 1.0f,
259                 userTracker.userId
260             )
261         assertThat(systemScale).isEqualTo(1.0f)
262 
263         // Simulate releasing the finger from the seekbar.
264         changeListener.onStopTrackingTouch(seekBar)
265         backgroundDelayableExecutor.runAllReady()
266         backgroundDelayableExecutor.advanceClockToNext()
267         backgroundDelayableExecutor.runAllReady()
268 
269         // SeekBar interaction is finalized.
270         changeListener.onUserInteractionFinalized(
271             seekBar,
272             OnSeekBarWithIconButtonsChangeListener.ControlUnitType.SLIDER
273         )
274         backgroundDelayableExecutor.runAllReady()
275         backgroundDelayableExecutor.advanceClockToNext()
276         backgroundDelayableExecutor.runAllReady()
277 
278         // Verify that the scale of font size has been updated.
279         systemScale =
280             systemSettings.getFloatForUser(
281                 Settings.System.FONT_SCALE,
282                 /* def= */ 1.0f,
283                 userTracker.userId
284             )
285         assertThat(systemScale).isEqualTo(fontSizeValueArray[0].toFloat())
286 
287         dialog.dismiss()
288     }
289 
290     @Test
dragSeekBar_createTextPreviewnull291     fun dragSeekBar_createTextPreview() {
292         dialog.show()
293         val slider = dialog.findViewById<SeekBarWithIconButtonsView>(R.id.font_scaling_slider)!!
294         val changeListener = slider.onSeekBarWithIconButtonsChangeListener
295 
296         val seekBar: SeekBar = slider.findViewById(R.id.seekbar)!!
297 
298         // Default seekbar progress for font size is 1, simulate dragging to 0 without
299         // releasing the finger
300         changeListener.onStartTrackingTouch(seekBar)
301         changeListener.onProgressChanged(seekBar, /* progress= */ 0, /* fromUser= */ false)
302         backgroundDelayableExecutor.advanceClockToNext()
303         backgroundDelayableExecutor.runAllReady()
304 
305         verify(fontScalingDialogDelegate).createTextPreview(/* index= */ 0)
306         dialog.dismiss()
307     }
308 
309     @Test
changeFontSize_buttonIsDisabledBeforeFontSizeChangeFinishesnull310     fun changeFontSize_buttonIsDisabledBeforeFontSizeChangeFinishes() {
311         dialog.show()
312 
313         val iconEndFrame: ViewGroup = dialog.findViewById(R.id.icon_end_frame)!!
314         val doneButton: Button = dialog.findViewById(com.android.internal.R.id.button1)!!
315 
316         iconEndFrame.performClick()
317         backgroundDelayableExecutor.runAllReady()
318         backgroundDelayableExecutor.advanceClockToNext()
319         backgroundDelayableExecutor.runAllReady()
320 
321         // Verify that the button is disabled before receiving onConfigurationChanged
322         assertThat(doneButton.isEnabled).isFalse()
323 
324         val config = Configuration()
325         config.fontScale = 1.15f
326         dialog.onConfigurationChanged(config)
327         testableLooper.processAllMessages()
328         assertThat(doneButton.isEnabled).isTrue()
329     }
330 }
331