Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c7865172 authored by William Escande's avatar William Escande
Browse files

Add RadioModeListener + Robolectric tests

Bluetooth listen to 2 different Radios, we can share the code between
them.
This code is not yet active, the SatelliteModeListener is using it
behind a flag in the follow-up change

Add support for Robolectric to add a full coverage with real use case
and add test in TEST_MAPPING

Test: atest ServiceBluetoothRoboTests | no-op change for outside
Bug: 262605980
Bug: 286602847
Change-Id: I718e12fd0fdc8827cc55f2babb1306bcb29df2d4
parent 6507424a
Loading
Loading
Loading
Loading
+23 −1
Original line number Diff line number Diff line
@@ -21,7 +21,8 @@ filegroup {
    srcs: [
        ":statslog-bluetooth-java-gen",
        "src/**/*.java",
        "src/**/*.kt",
        "src/RadioModeListener.kt",
        "src/com/**/*.kt",
    ],
    visibility: [":__subpackages__"],
}
@@ -138,3 +139,24 @@ java_library {
    ],
    min_sdk_version: "Tiramisu",
}

android_robolectric_test {
    name: "ServiceBluetoothRoboTests",
    instrumentation_for: "ServiceBluetoothFakeTestApp",

    srcs: [
        "src/RadioModeListener.kt",
        "src/RadioModeListenerTest.kt",
    ],

    static_libs: [
        "androidx.test.core",
        "mockito-robolectric-prebuilt",
        "platform-test-annotations",
        "testng",
        "truth-prebuilt",
    ],

    upstream: true,
    test_suites: ["general-tests"],
}
+4 −0
Original line number Diff line number Diff line
android_app {
    name: "ServiceBluetoothFakeTestApp",
    sdk_version: "Tiramisu",
}
+19 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!--
    Copyright (C) 2023 The Android Open Source Project

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

         http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
-->

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.android.server.bluetooth" />
+95 −0
Original line number Diff line number Diff line
/*
 * Copyright 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.server.bluetooth

import android.content.ContentResolver
import android.database.ContentObserver
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.util.Log

private const val TAG = "BluetoothRadioModeListener"

/**
 * Listen on radio mode and trigger the callback when it change
 *
 * @param radio: The radio to listen for, eg: Settings.Global.AIRPLANE_MODE_RADIOS
 * @param modeKey: The associated mode key, eg: Settings.Global.AIRPLANE_MODE_ON
 * @param callback: The callback to trigger when there is a mode change, pass new mode as parameter
 * @return The initial value of the radio
 */
internal fun initializeRadioModeListener(
    looper: Looper,
    resolver: ContentResolver,
    radio: String,
    modeKey: String,
    callback: (m: Boolean) -> Unit
): Boolean {
    val observer =
        object : ContentObserver(Handler(looper)) {
            override fun onChange(selfChange: Boolean) {
                callback(getRadioModeValue(resolver, radio, modeKey))
            }
        }

    val notifyForDescendants = false

    resolver.registerContentObserver(
        Settings.Global.getUriFor(radio),
        notifyForDescendants,
        observer
    )
    resolver.registerContentObserver(
        Settings.Global.getUriFor(modeKey),
        notifyForDescendants,
        observer
    )
    return getRadioModeValue(resolver, radio, modeKey)
}

/**
 * Check if Bluetooth is impacted by the radio and fetch global mode status
 *
 * @return weither Bluetooth should consider this radio or not
 */
private fun getRadioModeValue(resolver: ContentResolver, radio: String, modeKey: String): Boolean {
    return if (isSensitive(resolver, radio)) {
        isGlobalModeOn(resolver, modeKey)
    } else {
        Log.d(TAG, "Not sensitive to " + radio + " change. Forced to false")
        false
    }
}

/**
 * *Do not use outside of this file to avoid async issues*
 *
 * @return false if Bluetooth should not listen for mode change related to the {@code radio}
 */
private fun isSensitive(resolver: ContentResolver, radio: String): Boolean {
    val radios = Settings.Global.getString(resolver, radio)
    return radios != null && radios.contains(Settings.Global.RADIO_BLUETOOTH)
}

/**
 * *Do not use outside of this file to avoid async issues*
 *
 * @return whether mode {@code modeKey} is on or off in Global settings
 */
private fun isGlobalModeOn(resolver: ContentResolver, modeKey: String): Boolean {
    return Settings.Global.getInt(resolver, modeKey, 0) == 1
}
+218 −0
Original line number Diff line number Diff line
/*
 * Copyright 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.server.bluetooth.test

import android.content.ContentResolver
import android.content.Context
import android.os.Looper
import android.provider.Settings
import androidx.test.core.app.ApplicationProvider
import com.android.server.bluetooth.initializeRadioModeListener
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.times
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf

private const val RADIO = "my_awesome_radio"
private const val MODE_KEY = "is_awesome_radio_enabled"
private const val RADIO_BLUETOOTH = Settings.Global.RADIO_BLUETOOTH

internal fun enableSensitive(resolver: ContentResolver, looper: Looper, radio: String) {
    Settings.Global.putString(resolver, radio, "foo," + RADIO_BLUETOOTH + ",bar")
    shadowOf(looper).idle()
}

internal fun disableSensitive(resolver: ContentResolver, looper: Looper, radio: String) {
    Settings.Global.putString(resolver, radio, "foo,bar")
    shadowOf(looper).idle()
}

internal fun disableMode(resolver: ContentResolver, looper: Looper, modeKey: String) {
    Settings.Global.putInt(resolver, modeKey, 0)
    shadowOf(looper).idle()
}

internal fun enableMode(resolver: ContentResolver, looper: Looper, modeKey: String) {
    Settings.Global.putInt(resolver, modeKey, 1)
    shadowOf(looper).idle()
}

@RunWith(RobolectricTestRunner::class)
class RadioModeListenerTest {
    private val resolver: ContentResolver =
        ApplicationProvider.getApplicationContext<Context>().getContentResolver()

    private val looper: Looper = Looper.getMainLooper()

    private lateinit var mode: ArrayList<Boolean>

    @Before
    public fun setup() {
        mode = ArrayList()
    }

    private fun startListener(): Boolean {
        return initializeRadioModeListener(looper, resolver, RADIO, MODE_KEY, this::callback)
    }

    private fun enableSensitive() {
        enableSensitive(resolver, looper, RADIO)
    }

    private fun disableSensitive() {
        disableSensitive(resolver, looper, RADIO)
    }

    private fun disableMode() {
        disableMode(resolver, looper, MODE_KEY)
    }

    private fun enableMode() {
        enableMode(resolver, looper, MODE_KEY)
    }

    private fun callback(newMode: Boolean) = mode.add(newMode)

    @Test
    fun initialize_whenNullSensitive_isOff() {
        Settings.Global.putString(resolver, RADIO, null)
        enableMode()

        val initialValue = startListener()

        assertThat(initialValue).isFalse()
        assertThat(mode).isEmpty()
    }

    @Test
    fun initialize_whenNotSensitive_isOff() {
        disableSensitive()
        enableMode()

        val initialValue = startListener()

        assertThat(initialValue).isFalse()
        assertThat(mode).isEmpty()
    }

    @Test
    fun enable_whenNotSensitive_isOff() {
        disableSensitive()
        disableMode()

        val initialValue = startListener()

        enableMode()

        assertThat(initialValue).isFalse()
        assertThat(mode).containsExactly(false)
    }

    @Test
    fun initialize_whenSensitive_isOff() {
        enableSensitive()
        disableMode()

        val initialValue = startListener()

        assertThat(initialValue).isFalse()
        assertThat(mode).isEmpty()
    }

    @Test
    fun initialize_whenSensitive_isOn() {
        enableSensitive()
        enableMode()

        val initialValue = startListener()

        assertThat(initialValue).isTrue()
        assertThat(mode).isEmpty()
    }

    @Test
    fun toggleSensitive_whenEnabled_isOnOffOn() {
        enableSensitive()
        enableMode()

        val initialValue = startListener()

        disableSensitive()
        enableSensitive()

        assertThat(initialValue).isTrue()
        assertThat(mode).containsExactly(false, true)
    }

    @Test
    fun toggleEnable_whenSensitive_isOffOnOff() {
        enableSensitive()
        disableMode()

        val initialValue = startListener()

        enableMode()
        disableMode()

        assertThat(initialValue).isFalse()
        assertThat(mode).containsExactly(true, false)
    }

    @Test
    fun disable_whenDisabled_isDicarded() {
        enableSensitive()
        disableMode()

        val initialValue = startListener()

        disableMode()
        disableMode()

        assertThat(initialValue).isFalse()
        assertThat(mode).isEmpty()
    }

    @Test
    fun enabled_whenEnabled_isDiscarded() {
        enableSensitive()
        enableMode()

        val initialValue = startListener()

        enableMode()
        enableMode()

        assertThat(initialValue).isTrue()
        assertThat(mode).isEmpty()
    }

    @Test
    fun changeContent_whenDisabled_noDiscard() {
        enableSensitive()
        disableMode()

        val initialValue = startListener()

        disableSensitive() // The value is changed but the result is still false
        enableMode() // The value is changed but the result is still false

        assertThat(initialValue).isFalse()
        assertThat(mode).containsExactly(false, false)
    }
}