1 /*
<lambda>null2  * Copyright 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.server.bluetooth.airplane.test
17 
18 import android.app.ActivityManager
19 import android.bluetooth.BluetoothAdapter
20 import android.content.ContentResolver
21 import android.content.Context
22 import android.content.res.Resources
23 import android.os.Looper
24 import android.os.UserHandle
25 import android.platform.test.flag.junit.FlagsParameterization
26 import android.platform.test.flag.junit.SetFlagsRule
27 import android.provider.Settings
28 import androidx.test.core.app.ApplicationProvider
29 import com.android.bluetooth.flags.Flags
30 import com.android.server.bluetooth.BluetoothAdapterState
31 import com.android.server.bluetooth.Log
32 import com.android.server.bluetooth.airplane.APM_BT_ENABLED_NOTIFICATION
33 import com.android.server.bluetooth.airplane.APM_BT_NOTIFICATION
34 import com.android.server.bluetooth.airplane.APM_ENHANCEMENT
35 import com.android.server.bluetooth.airplane.APM_USER_TOGGLED_BLUETOOTH
36 import com.android.server.bluetooth.airplane.APM_WIFI_BT_NOTIFICATION
37 import com.android.server.bluetooth.airplane.BLUETOOTH_APM_STATE
38 import com.android.server.bluetooth.airplane.WIFI_APM_STATE
39 import com.android.server.bluetooth.airplane.initialize
40 import com.android.server.bluetooth.airplane.isOn
41 import com.android.server.bluetooth.airplane.isOnOverrode
42 import com.android.server.bluetooth.airplane.notifyUserToggledBluetooth
43 import com.android.server.bluetooth.test.disableMode
44 import com.android.server.bluetooth.test.disableSensitive
45 import com.android.server.bluetooth.test.enableMode
46 import com.android.server.bluetooth.test.enableSensitive
47 import com.google.common.truth.Truth.assertThat
48 import kotlin.time.Duration.Companion.minutes
49 import kotlin.time.TestTimeSource
50 import kotlin.time.TimeSource
51 import org.junit.Before
52 import org.junit.Rule
53 import org.junit.Test
54 import org.junit.rules.TestName
55 import org.junit.runner.RunWith
56 import org.robolectric.ParameterizedRobolectricTestRunner
57 import org.robolectric.ParameterizedRobolectricTestRunner.Parameters
58 import org.robolectric.shadows.ShadowToast
59 
60 @RunWith(ParameterizedRobolectricTestRunner::class)
61 @kotlin.time.ExperimentalTime
62 class ModeListenerTest(flags: FlagsParameterization) {
63     companion object {
64         @JvmStatic
65         @Parameters(name = "{0}")
66         fun getParams() =
67             FlagsParameterization.allCombinationsOf(Flags.FLAG_GET_STATE_FROM_SYSTEM_SERVER)
68 
69         internal fun setupAirplaneModeToOn(
70             resolver: ContentResolver,
71             looper: Looper,
72             user: () -> Context,
73             enableEnhancedMode: Boolean
74         ) {
75             enableSensitive(resolver, looper, Settings.Global.AIRPLANE_MODE_RADIOS)
76             enableMode(resolver, looper, Settings.Global.AIRPLANE_MODE_ON)
77             val mode: (m: Boolean) -> Unit = { _: Boolean -> }
78             val notif: (m: String) -> Unit = { _: String -> }
79             val media: () -> Boolean = { -> false }
80             if (enableEnhancedMode) {
81                 Settings.Secure.putInt(resolver, APM_USER_TOGGLED_BLUETOOTH, 1)
82             }
83 
84             initialize(
85                 looper,
86                 resolver,
87                 BluetoothAdapterState(),
88                 mode,
89                 notif,
90                 media,
91                 user,
92                 TimeSource.Monotonic,
93             )
94         }
95 
96         internal fun setupAirplaneModeToOff(resolver: ContentResolver, looper: Looper) {
97             disableSensitive(resolver, looper, Settings.Global.AIRPLANE_MODE_RADIOS)
98             disableMode(resolver, looper, Settings.Global.AIRPLANE_MODE_ON)
99         }
100     }
101 
102     @get:Rule val testName = TestName()
103     @get:Rule val setFlagsRule = SetFlagsRule()
104 
105     init {
106         setFlagsRule.setFlagsParameterization(flags)
107     }
108 
109     private val looper: Looper = Looper.getMainLooper()
110     private val state = BluetoothAdapterState()
111     private val mContext = ApplicationProvider.getApplicationContext<Context>()
112     private val resolver: ContentResolver = mContext.contentResolver
113 
114     private val userContext =
115         mContext.createContextAsUser(UserHandle.of(ActivityManager.getCurrentUser()), 0)
116 
117     private var isMediaProfileConnected = false
118     private lateinit var mode: ArrayList<Boolean>
119     private lateinit var notification: ArrayList<String>
120 
121     @Before
122     public fun setup() {
123         Log.i("AirplaneModeListenerTest", "\t--> setup of " + testName.getMethodName())
124 
125         // Most test will expect the system to be sensitive + off
126         enableSensitive()
127         disableMode()
128 
129         isMediaProfileConnected = false
130         mode = ArrayList()
131         notification = ArrayList()
132     }
133 
134     private fun initializeAirplane() {
135         initialize(
136             looper,
137             resolver,
138             state,
139             this::callback,
140             this::notificationCallback,
141             this::mediaCallback,
142             this::userCallback,
143             TimeSource.Monotonic,
144         )
145     }
146 
147     private fun enableSensitive() {
148         enableSensitive(resolver, looper, Settings.Global.AIRPLANE_MODE_RADIOS)
149     }
150 
151     private fun disableSensitive() {
152         disableSensitive(resolver, looper, Settings.Global.AIRPLANE_MODE_RADIOS)
153     }
154 
155     private fun disableMode() {
156         disableMode(resolver, looper, Settings.Global.AIRPLANE_MODE_ON)
157     }
158 
159     private fun enableMode() {
160         enableMode(resolver, looper, Settings.Global.AIRPLANE_MODE_ON)
161     }
162 
163     private fun callback(newMode: Boolean) = mode.add(newMode)
164 
165     private fun notificationCallback(state: String) = notification.add(state)
166 
167     private fun mediaCallback() = isMediaProfileConnected
168 
169     private fun userCallback() = userContext
170 
171     @Test
172     fun initialize_whenNullSensitive_isOff() {
173         Settings.Global.putString(resolver, Settings.Global.AIRPLANE_MODE_RADIOS, null)
174         enableMode()
175 
176         initializeAirplane()
177 
178         assertThat(isOn).isFalse()
179         assertThat(isOnOverrode).isFalse()
180         assertThat(mode).isEmpty()
181     }
182 
183     @Test
184     fun initialize_whenNotSensitive_isOff() {
185         disableSensitive()
186         enableMode()
187 
188         initializeAirplane()
189 
190         assertThat(isOn).isFalse()
191         assertThat(isOnOverrode).isFalse()
192         assertThat(mode).isEmpty()
193     }
194 
195     @Test
196     fun enable_whenNotSensitive_isOff() {
197         disableSensitive()
198         disableMode()
199 
200         initializeAirplane()
201 
202         enableMode()
203 
204         assertThat(isOn).isFalse()
205         assertThat(isOnOverrode).isFalse()
206         assertThat(mode).isEmpty()
207     }
208 
209     @Test
210     fun initialize_whenSensitive_isOff() {
211         initializeAirplane()
212 
213         assertThat(isOn).isFalse()
214         assertThat(isOnOverrode).isFalse()
215         assertThat(mode).isEmpty()
216     }
217 
218     @Test
219     fun initialize_whenSensitive_isOnOverrode() {
220         enableSensitive()
221         enableMode()
222 
223         initializeAirplane()
224 
225         assertThat(isOn).isTrue()
226         assertThat(isOnOverrode).isTrue()
227         assertThat(mode).isEmpty()
228     }
229 
230     @Test
231     fun initialize_whenApmToggled_isOnOverrode() {
232         enableSensitive()
233         enableMode()
234         Settings.Secure.putInt(userContext.contentResolver, APM_USER_TOGGLED_BLUETOOTH, 1)
235         Settings.Secure.putInt(userContext.contentResolver, BLUETOOTH_APM_STATE, 1)
236 
237         initializeAirplane()
238 
239         assertThat(isOn).isTrue()
240         assertThat(isOnOverrode).isFalse()
241         assertThat(mode).isEmpty()
242     }
243 
244     @Test
245     fun toggleSensitive_whenEnabled_isOnOverrode() {
246         enableSensitive()
247         enableMode()
248 
249         initializeAirplane()
250 
251         disableSensitive()
252         enableSensitive()
253 
254         assertThat(isOnOverrode).isTrue()
255         assertThat(mode).containsExactly(false, true)
256     }
257 
258     @Test
259     fun toggleEnable_whenSensitive_isOffOnOff() {
260         initializeAirplane()
261 
262         enableMode()
263         disableMode()
264 
265         assertThat(isOnOverrode).isFalse()
266         assertThat(mode).containsExactly(true, false)
267     }
268 
269     @Test
270     fun disable_whenDisabled_discardUpdate() {
271         initializeAirplane()
272 
273         disableMode()
274 
275         assertThat(isOnOverrode).isFalse()
276         assertThat(mode).isEmpty()
277     }
278 
279     @Test
280     fun disable_whenBluetoothOn_discardUpdate() {
281         initializeAirplane()
282         enableMode()
283 
284         state.set(BluetoothAdapter.STATE_ON)
285         disableMode()
286 
287         assertThat(isOnOverrode).isFalse()
288         assertThat(mode).containsExactly(true)
289     }
290 
291     @Test
292     fun enabled_whenEnabled_discardOnChange() {
293         enableSensitive()
294         enableMode()
295 
296         initializeAirplane()
297 
298         enableMode()
299 
300         assertThat(isOnOverrode).isTrue()
301         assertThat(mode).isEmpty()
302     }
303 
304     @Test
305     fun changeContent_whenDisabled_discard() {
306         initializeAirplane()
307 
308         disableSensitive()
309         enableMode()
310 
311         assertThat(isOnOverrode).isFalse()
312         // As opposed to the bare RadioModeListener, similar consecutive event are discarded
313         assertThat(mode).isEmpty()
314     }
315 
316     @Test
317     fun triggerOverride_whenNoOverride_turnOff() {
318         initializeAirplane()
319 
320         state.set(BluetoothAdapter.STATE_ON)
321 
322         enableMode()
323 
324         assertThat(isOnOverrode).isTrue()
325         assertThat(mode).containsExactly(true)
326         assertThat(ShadowToast.shownToastCount()).isEqualTo(0)
327     }
328 
329     @Test
330     fun triggerOverride_whenMedia_staysOn() {
331         initializeAirplane()
332 
333         state.set(BluetoothAdapter.STATE_ON)
334         isMediaProfileConnected = true
335 
336         enableMode()
337 
338         assertThat(isOnOverrode).isFalse()
339         assertThat(mode).isEmpty()
340 
341         assertThat(ShadowToast.shownToastCount()).isEqualTo(1)
342         assertThat(ShadowToast.getTextOfLatestToast())
343             .isEqualTo(
344                 mContext.getString(
345                     Resources.getSystem()
346                         .getIdentifier("bluetooth_airplane_mode_toast", "string", "android")
347                 )
348             )
349     }
350 
351     @Test
352     fun triggerOverride_whenApmEnhancementNotTrigger_turnOff() {
353         initializeAirplane()
354 
355         state.set(BluetoothAdapter.STATE_ON)
356         Settings.Global.putInt(resolver, APM_ENHANCEMENT, 0)
357 
358         enableMode()
359 
360         assertThat(isOnOverrode).isTrue()
361         assertThat(isOn).isTrue()
362         assertThat(mode).containsExactly(true)
363     }
364 
365     @Test
366     fun triggerOverride_whenApmEnhancementNotTriggerButMedia_staysOn() {
367         initializeAirplane()
368 
369         state.set(BluetoothAdapter.STATE_ON)
370         Settings.Global.putInt(resolver, APM_ENHANCEMENT, 0)
371         isMediaProfileConnected = true
372 
373         enableMode()
374 
375         assertThat(isOnOverrode).isFalse()
376         assertThat(isOn).isTrue()
377         assertThat(mode).isEmpty()
378     }
379 
380     @Test
381     fun triggerOverride_whenApmEnhancementWasToggled_turnOff() {
382         initializeAirplane()
383 
384         state.set(BluetoothAdapter.STATE_ON)
385         Settings.Secure.putInt(userContext.contentResolver, APM_USER_TOGGLED_BLUETOOTH, 1)
386 
387         enableMode()
388 
389         assertThat(isOnOverrode).isTrue()
390         assertThat(isOn).isTrue()
391         assertThat(mode).containsExactly(true)
392     }
393 
394     @Test
395     fun triggerOverride_whenApmEnhancementWasToggled_staysOnWithBtNotification() {
396         initializeAirplane()
397 
398         state.set(BluetoothAdapter.STATE_ON)
399         Settings.Secure.putInt(userContext.contentResolver, APM_USER_TOGGLED_BLUETOOTH, 1)
400         Settings.Secure.putInt(userContext.contentResolver, BLUETOOTH_APM_STATE, 1)
401 
402         enableMode()
403 
404         assertThat(isOnOverrode).isFalse()
405         assertThat(isOn).isTrue()
406         assertThat(mode).isEmpty()
407         assertThat(notification).containsExactly(APM_BT_NOTIFICATION)
408     }
409 
410     @Test
411     fun triggerOverride_whenApmEnhancementWasToggledAndWifiOn_staysOnWithBtWifiNotification() {
412         initializeAirplane()
413 
414         state.set(BluetoothAdapter.STATE_ON)
415         Settings.Secure.putInt(userContext.contentResolver, APM_USER_TOGGLED_BLUETOOTH, 1)
416         Settings.Secure.putInt(userContext.contentResolver, BLUETOOTH_APM_STATE, 1)
417 
418         Settings.Global.putInt(resolver, Settings.Global.WIFI_ON, 1)
419         Settings.Secure.putInt(userContext.contentResolver, WIFI_APM_STATE, 1)
420 
421         enableMode()
422 
423         assertThat(isOnOverrode).isFalse()
424         assertThat(mode).isEmpty()
425         assertThat(notification).containsExactly(APM_WIFI_BT_NOTIFICATION)
426     }
427 
428     @Test
429     fun triggerOverride_whenApmEnhancementWasToggledAndWifiNotOn_staysOnWithBtNotification() {
430         initializeAirplane()
431 
432         state.set(BluetoothAdapter.STATE_ON)
433         Settings.Secure.putInt(userContext.contentResolver, APM_USER_TOGGLED_BLUETOOTH, 1)
434         Settings.Secure.putInt(userContext.contentResolver, BLUETOOTH_APM_STATE, 1)
435 
436         Settings.Global.putInt(resolver, Settings.Global.WIFI_ON, 1)
437 
438         enableMode()
439 
440         assertThat(isOnOverrode).isFalse()
441         assertThat(mode).isEmpty()
442         assertThat(notification).containsExactly(APM_BT_NOTIFICATION)
443     }
444 
445     @Test
446     fun showToast_inLoop_stopNotifyWhenMaxToastReached() {
447         initializeAirplane()
448 
449         state.set(BluetoothAdapter.STATE_ON)
450         isMediaProfileConnected = true
451 
452         repeat(30) {
453             enableMode()
454             disableMode()
455         }
456 
457         assertThat(isOnOverrode).isFalse()
458         assertThat(mode).isEmpty()
459         assertThat(notification).isEmpty()
460 
461         assertThat(ShadowToast.shownToastCount())
462             .isEqualTo(com.android.server.bluetooth.airplane.ToastNotification.MAX_TOAST_COUNT)
463     }
464 
465     @Test
466     fun userToggleBluetooth_whenNoSession_nothingHappen() {
467         initializeAirplane()
468 
469         notifyUserToggledBluetooth(resolver, userContext, false)
470 
471         assertThat(isOnOverrode).isFalse()
472         assertThat(mode).isEmpty()
473         assertThat(notification).isEmpty()
474         assertThat(ShadowToast.shownToastCount()).isEqualTo(0)
475     }
476 
477     @Test
478     fun userToggleBluetooth_whenSessionButNoApm_noNotificationAndNoSettingSave() {
479         initializeAirplane()
480         Settings.Global.putInt(resolver, APM_ENHANCEMENT, 0)
481 
482         enableMode()
483         notifyUserToggledBluetooth(resolver, userContext, true)
484 
485         assertThat(isOnOverrode).isTrue()
486         assertThat(mode).containsExactly(true)
487         assertThat(notification).isEmpty()
488         assertThat(ShadowToast.shownToastCount()).isEqualTo(0)
489         assertThat(Settings.Secure.getInt(userContext.contentResolver, BLUETOOTH_APM_STATE, 0))
490             .isEqualTo(0)
491         assertThat(
492                 Settings.Secure.getInt(userContext.contentResolver, APM_USER_TOGGLED_BLUETOOTH, 0)
493             )
494             .isEqualTo(0)
495     }
496 
497     @Test
498     fun userToggleBluetooth_whenSession_noNotificationAndSettingSaved() {
499         initializeAirplane()
500 
501         enableMode()
502         notifyUserToggledBluetooth(resolver, userContext, false)
503 
504         assertThat(isOnOverrode).isTrue()
505         assertThat(mode).containsExactly(true)
506         assertThat(notification).isEmpty()
507         assertThat(ShadowToast.shownToastCount()).isEqualTo(0)
508         assertThat(Settings.Secure.getInt(userContext.contentResolver, BLUETOOTH_APM_STATE, 0))
509             .isEqualTo(0)
510         assertThat(
511                 Settings.Secure.getInt(userContext.contentResolver, APM_USER_TOGGLED_BLUETOOTH, 0)
512             )
513             .isEqualTo(1)
514     }
515 
516     @Test
517     fun userToggleBluetooth_whenSession_notificationAndSettingSaved() {
518         initializeAirplane()
519 
520         enableMode()
521         notifyUserToggledBluetooth(resolver, userContext, true)
522 
523         assertThat(isOnOverrode).isTrue()
524         assertThat(mode).containsExactly(true)
525         assertThat(notification).containsExactly(APM_BT_ENABLED_NOTIFICATION)
526         assertThat(ShadowToast.shownToastCount()).isEqualTo(0)
527         assertThat(Settings.Secure.getInt(userContext.contentResolver, BLUETOOTH_APM_STATE, 0))
528             .isEqualTo(1)
529         assertThat(
530                 Settings.Secure.getInt(userContext.contentResolver, APM_USER_TOGGLED_BLUETOOTH, 0)
531             )
532             .isEqualTo(1)
533     }
534 
535     @Test
536     fun userToggleTwiceBluetooth_whenSession_notificationAndSettingSaved() {
537         initializeAirplane()
538 
539         enableMode()
540         notifyUserToggledBluetooth(resolver, userContext, true)
541         notifyUserToggledBluetooth(resolver, userContext, false)
542 
543         assertThat(isOnOverrode).isTrue()
544         assertThat(mode).containsExactly(true)
545         assertThat(notification).containsExactly(APM_BT_ENABLED_NOTIFICATION)
546         assertThat(ShadowToast.shownToastCount()).isEqualTo(0)
547         assertThat(Settings.Secure.getInt(userContext.contentResolver, BLUETOOTH_APM_STATE, 0))
548             .isEqualTo(0)
549         assertThat(
550                 Settings.Secure.getInt(userContext.contentResolver, APM_USER_TOGGLED_BLUETOOTH, 0)
551             )
552             .isEqualTo(1)
553     }
554 
555     @Test
556     fun userToggleBluetooth_whenSessionButNoApm_noNotificationAndNoSettingSave_skipTime() {
557         val timesource = TestTimeSource()
558         initialize(
559             looper,
560             resolver,
561             state,
562             this::callback,
563             this::notificationCallback,
564             this::mediaCallback,
565             this::userCallback,
566             timesource,
567         )
568         Settings.Global.putInt(resolver, APM_ENHANCEMENT, 0)
569 
570         enableMode()
571         timesource += 2.minutes
572         notifyUserToggledBluetooth(resolver, userContext, true)
573 
574         assertThat(isOnOverrode).isTrue()
575         assertThat(mode).containsExactly(true)
576         assertThat(notification).isEmpty()
577         assertThat(ShadowToast.shownToastCount()).isEqualTo(0)
578         assertThat(Settings.Secure.getInt(userContext.contentResolver, BLUETOOTH_APM_STATE, 0))
579             .isEqualTo(0)
580         assertThat(
581                 Settings.Secure.getInt(userContext.contentResolver, APM_USER_TOGGLED_BLUETOOTH, 0)
582             )
583             .isEqualTo(0)
584     }
585 
586     @Test
587     fun initialize_firstTime_apmSettingIsSet() {
588         initializeAirplane()
589         assertThat(Settings.Global.getInt(resolver, APM_ENHANCEMENT, 0)).isEqualTo(1)
590     }
591 
592     @Test
593     fun initialize_secondTime_apmSettingIsNotOverride() {
594         val settingValue = 42
595         Settings.Global.putInt(resolver, APM_ENHANCEMENT, settingValue)
596 
597         initializeAirplane()
598 
599         assertThat(Settings.Global.getInt(resolver, APM_ENHANCEMENT, 0)).isEqualTo(settingValue)
600     }
601 }
602