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