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

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

SystemServer: AutoOn: Add basic feature

Basic implementation without handling of special cases

Bug: 323060869
Bug: 316946334
Test: atest ServiceBluetoothRoboTests
Change-Id: Ie20680220384152a6608563fadfc872534fde685
parent 759108b3
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ filegroup {
        ":statslog-bluetooth-java-gen",
        "src/**/*.java",
        "src/AdapterState.kt",
        "src/AutoOnFeature.kt",
        "src/Log.kt",
        "src/RadioModeListener.kt",
        "src/airplane/ModeListener.kt",
@@ -160,6 +161,8 @@ android_robolectric_test {
        ":statslog-bluetooth-java-gen",
        "src/AdapterState.kt",
        "src/AdapterStateTest.kt",
        "src/AutoOnFeature.kt",
        "src/AutoOnFeatureTest.kt",
        "src/Log.kt",
        "src/LogTest.kt",
        "src/RadioModeListener.kt",
+6 −0
Original line number Diff line number Diff line
@@ -22,4 +22,10 @@ CLASSPATH+=":$ROOT/out/soong/.intermediates/packages/modules/Bluetooth/android/a
CLASSPATH+=":$ROOT/out/soong/.intermediates/packages/modules/Bluetooth/flags/bluetooth_flags_java_lib/android_common_apex33/turbine-combined/bluetooth_flags_java_lib.jar"
CLASSPATH+=":$ROOT/out/soong/.intermediates/frameworks/libs/modules-utils/java/com/android/modules/utils/modules-utils-shell-command-handler/android_common_apex33/turbine-combined/modules-utils-shell-command-handler.jar"

CLASSPATH+=":$ROOT/out/soong/.intermediates/prebuilts/misc/common/androidx-test/androidx.test.core/android_common/combined/androidx.test.core.jar"
CLASSPATH+=":$ROOT/out/soong/.intermediates/external/truth/truth/android_common/turbine-combined/truth.jar"
CLASSPATH+=":$ROOT/out/soong/.intermediates/external/junit/junit/android_common/turbine-combined/junit.jar"
CLASSPATH+=":$ROOT/out/soong/.intermediates/prebuilts/tools/common/m2/mockito-robolectric-prebuilt/android_common/turbine-combined/mockito-robolectric-prebuilt.jar"
CLASSPATH+=":$ROOT/out/soong/.intermediates/external/robolectric/Robolectric_all_upstream/linux_glibc_common/6da2cb1db6f5106f6cbbfb5faa1ac779/javac-header/Robolectric_all_upstream.jar"

echo "$CLASSPATH"
+130 −0
Original line number Diff line number Diff line
/*
 * Copyright 2024 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.
 */

@file:JvmName("AutoOnFeature")

package com.android.server.bluetooth

import android.bluetooth.BluetoothAdapter.STATE_ON
import android.content.ContentResolver
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import androidx.annotation.VisibleForTesting
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.temporal.ChronoUnit
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration

private const val TAG = "AutoOnFeature"

public fun resetAutoOnTimerForUser(
    looper: Looper,
    context: Context,
    state: BluetoothAdapterState,
    callback_on: () -> Unit
) {
    // Remove any previous timer
    timer?.cancel()
    timer = null

    if (!isFeatureEnabledForUser(context.contentResolver)) {
        Log.d(TAG, "Not Enabled for current user: ${context.getUser()}")
        return
    }
    if (state.oneOf(STATE_ON)) {
        Log.d(TAG, "Bluetooth already in ${state}, no need for timer")
        return
    }

    timer = Timer.start(looper, callback_on)
}

public fun notifyBluetoothOn() {
    timer?.cancel()
    timer = null
}

////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////// PRIVATE METHODS /////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////

@VisibleForTesting internal var timer: Timer? = null

@VisibleForTesting
internal class Timer
private constructor(
    looper: Looper,
    callback_on: () -> Unit,
    private val now: LocalDateTime,
    private val target: LocalDateTime,
    private val timeToSleep: Duration
) {
    private val handler = Handler(looper)

    init {
        handler.postDelayed(
            {
                Log.i(TAG, "[${this}]: Bluetooth restarting now")
                callback_on()
                cancel()
                // Set global instance to null to prevent further action. Job is done here
                timer = null
            },
            timeToSleep.inWholeMilliseconds
        )
        Log.i(TAG, "[${this}]: Scheduling next Bluetooth restart")
    }

    companion object {

        fun start(looper: Looper, callback_on: () -> Unit): Timer {
            val now = LocalDateTime.now()
            val target = nextTimeout(now)
            val timeToSleep =
                now.until(target, ChronoUnit.NANOS).toDuration(DurationUnit.NANOSECONDS)

            return Timer(looper, callback_on, now, target, timeToSleep)
        }

        /** Return a LocalDateTime for tomorrow 5 am */
        private fun nextTimeout(now: LocalDateTime) =
            LocalDateTime.of(now.toLocalDate(), LocalTime.of(5, 0)).plusDays(1)
    }

    /** Stop timer and reset storage */
    @VisibleForTesting
    internal fun cancel() {
        Log.i(TAG, "[${this}]: Cancelling timer")
        handler.removeCallbacksAndMessages(null)
    }

    override fun toString() = "Timer scheduled ${now} for target=${target} (=${timeToSleep} delay)."
}

@VisibleForTesting internal val USER_SETTINGS_KEY = "bluetooth_automatic_turn_on"

/**
 * *Do not use outside of this file to avoid async issues*
 *
 * @return whether the auto on feature is enabled for this user
 */
private fun isFeatureEnabledForUser(resolver: ContentResolver): Boolean {
    return Settings.Secure.getInt(resolver, USER_SETTINGS_KEY, 0) == 1
}
+154 −0
Original line number Diff line number Diff line
/*
 * Copyright 2024 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.bluetooth.BluetoothAdapter
import android.content.Context
import android.os.Looper
import android.provider.Settings
import androidx.test.core.app.ApplicationProvider
import com.android.server.bluetooth.BluetoothAdapterState
import com.android.server.bluetooth.Log
import com.android.server.bluetooth.USER_SETTINGS_KEY
import com.android.server.bluetooth.notifyBluetoothOn
import com.android.server.bluetooth.resetAutoOnTimerForUser
import com.android.server.bluetooth.timer
import com.google.common.truth.Expect
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestName
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf

@RunWith(RobolectricTestRunner::class)
@kotlinx.coroutines.ExperimentalCoroutinesApi
class AutoOnFeatureTest {
    private val looper = Looper.getMainLooper()
    private val state = BluetoothAdapterState()
    private val context = ApplicationProvider.getApplicationContext<Context>()
    private val resolver = context.contentResolver

    private var callback_count = 0

    @JvmField @Rule val testName = TestName()
    @JvmField @Rule val expect = Expect.create()

    @Before
    fun setUp() {
        Log.i("AutoOnFeatureTest", "\t--> setUp(${testName.getMethodName()})")

        enableUserSettings()
    }

    @After
    fun tearDown() {
        callback_count = 0
        timer?.cancel()
        timer = null
    }

    private fun setupTimer() {
        resetAutoOnTimerForUser(looper, context, state, this::callback_on)
    }

    private fun enableUserSettings() {
        Settings.Secure.putInt(resolver, USER_SETTINGS_KEY, 1)
        shadowOf(looper).idle()
    }

    private fun disableUserSettings() {
        Settings.Secure.putInt(resolver, USER_SETTINGS_KEY, 0)
        shadowOf(looper).idle()
    }

    private fun restoreSettings() {
        Settings.Secure.putString(resolver, USER_SETTINGS_KEY, null)
        shadowOf(looper).idle()
    }

    private fun callback_on() {
        callback_count++
    }

    @Test
    fun setupTimer_whenItWasNeverUsed_isNotScheduled() {
        restoreSettings()

        setupTimer()

        expect.that(timer).isNull()
        expect.that(callback_count).isEqualTo(0)
    }

    @Test
    fun setupTimer_whenBtOn_isNotScheduled() {
        state.set(BluetoothAdapter.STATE_ON)

        setupTimer()

        state.set(BluetoothAdapter.STATE_OFF)
        expect.that(timer).isNull()
        expect.that(callback_count).isEqualTo(0)
    }

    @Test
    fun setupTimer_whenBtOffAndUserEnabled_isScheduled() {
        setupTimer()

        expect.that(timer).isNotNull()
    }

    @Test
    fun setupTimer_whenBtOffAndUserEnabled_triggerCallback() {
        setupTimer()

        shadowOf(looper).runToEndOfTasks()
        expect.that(callback_count).isEqualTo(1)
        expect.that(timer).isNull()
    }

    @Test
    fun setupTimer_whenAlreadySetup_triggerCallbackOnce() {
        setupTimer()
        setupTimer()
        setupTimer()

        shadowOf(looper).runToEndOfTasks()
        expect.that(callback_count).isEqualTo(1)
        expect.that(timer).isNull()
    }

    @Test
    fun notifyBluetoothOn_whenNoTimer_noCrash() {
        notifyBluetoothOn()

        assertThat(timer).isNull()
    }

    @Test
    fun notifyBluetoothOn_whenTimer_isNotScheduled() {
        setupTimer()
        notifyBluetoothOn()

        shadowOf(looper).runToEndOfTasks()
        expect.that(callback_count).isEqualTo(0)
        expect.that(timer).isNull()
    }
}
+37 −0
Original line number Diff line number Diff line
@@ -77,6 +77,7 @@ import com.android.bluetooth.flags.FeatureFlags;
import com.android.bluetooth.flags.Flags;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.expresslog.Counter;
import com.android.server.BluetoothManagerServiceDumpProto;
import com.android.server.bluetooth.airplane.AirplaneModeListener;
import com.android.server.bluetooth.satellite.SatelliteModeListener;
@@ -737,6 +738,18 @@ class BluetoothManagerService {
            mBluetoothSatelliteModeListener =
                    new BluetoothSatelliteModeListener(this, mLooper, mContext);
        }

        { // AutoOn feature initialization of flag guarding
            final boolean autoOnFlag = Flags.autoOnFeature();
            final boolean autoOnProperty =
                    SystemProperties.getBoolean("bluetooth.server.automatic_turn_on", false);
            Log.d(TAG, "AutoOnFeature status: flag=" + autoOnFlag + ", property=" + autoOnProperty);

            mDeviceConfigAllowAutoOn = autoOnFlag && autoOnProperty;
            if (mDeviceConfigAllowAutoOn) {
                Counter.logIncrement("bluetooth.value_auto_on_supported");
            }
        }
    }

    IBluetoothManager.Stub getBinder() {
@@ -1176,6 +1189,12 @@ class BluetoothManagerService {
        }
    }

    private Unit enableFromAutoOn() {
        Counter.logIncrement("bluetooth.value_auto_on_triggered");
        enable("BluetoothSystemServer/AutoOn");
        return Unit.INSTANCE;
    }

    boolean enableNoAutoConnect(String packageName) {
        if (isSatelliteModeOn()) {
            Log.d(TAG, "enableNoAutoConnect(" + packageName + "): Blocked by satellite mode");
@@ -2129,8 +2148,15 @@ class BluetoothManagerService {
            return;
        }

        if (prevState == STATE_ON) {
            autoOnSetupTimer();
        }

        // Notify all proxy objects first of adapter state change
        if (newState == STATE_ON) {
            if (mDeviceConfigAllowAutoOn) {
                AutoOnFeature.notifyBluetoothOn();
            }
            sendBluetoothOnCallback();
        } else if (newState == STATE_OFF) {
            // If Bluetooth is off, send service down event to proxy objects, and unbind
@@ -2640,6 +2666,17 @@ class BluetoothManagerService {
        return BluetoothAdapter.BT_SNOOP_LOG_MODE_DISABLED;
    }

    private final boolean mDeviceConfigAllowAutoOn;

    private void autoOnSetupTimer() {
        if (!mDeviceConfigAllowAutoOn) {
            Log.d(TAG, "No support for AutoOn feature: Not creating a timer");
            return;
        }
        AutoOnFeature.resetAutoOnTimerForUser(
                mLooper, mCurrentUserContext, mState, this::enableFromAutoOn);
    }

    /**
     * Check if BLE is supported by this platform
     *