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

Commit 4558febc authored by William Escande's avatar William Escande
Browse files

SystemServer: Extract ActiveLog management

This class was bloating the managerService and it can easily removed to
handle everything on its own.
This also have the advantage of now being tested thoroughly (test does
not check the content of the log, to not become change detector)

Bug: 319685396
Test: atest ServiceBluetoothRoboTests
Flag: Exempt refactor
Change-Id: I87a9ebf2b9e2add4bc77f82890c8a7e0234b30df
parent dd572237
Loading
Loading
Loading
Loading
+6 −0
Original line number Original line Diff line number Diff line
@@ -77,6 +77,7 @@ java_library {
    srcs: [
    srcs: [
        ":statslog-bluetooth-java-gen",
        ":statslog-bluetooth-java-gen",
        "src/**/*.java",
        "src/**/*.java",
        "src/ActiveLog.kt",
        "src/AdapterBinder.kt",
        "src/AdapterBinder.kt",
        "src/AdapterState.kt",
        "src/AdapterState.kt",
        "src/AutoOnFeature.kt",
        "src/AutoOnFeature.kt",
@@ -188,6 +189,8 @@ android_robolectric_test {


    srcs: [
    srcs: [
        ":statslog-bluetooth-java-gen",
        ":statslog-bluetooth-java-gen",
        "src/ActiveLog.kt",
        "src/ActiveLogTest.kt",
        "src/AdapterState.kt",
        "src/AdapterState.kt",
        "src/AdapterStateTest.kt",
        "src/AdapterStateTest.kt",
        "src/AutoOnFeature.kt",
        "src/AutoOnFeature.kt",
@@ -209,6 +212,9 @@ android_robolectric_test {
    static_libs: [
    static_libs: [
        "androidx.test.core",
        "androidx.test.core",
        "androidx.test.ext.truth",
        "androidx.test.ext.truth",
        "bluetooth-manager-service-proto-java-gen",
        "bluetooth-nano-protos",
        "bluetooth-proto-enums-java-gen",
        "bluetooth_flags_java_lib",
        "bluetooth_flags_java_lib",
        "flag-junit",
        "flag-junit",
        "kotlin-test",
        "kotlin-test",
+152 −0
Original line number Original line 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("ActiveLogs")

package com.android.server.bluetooth

import android.bluetooth.BluetoothProtoEnums.ENABLE_DISABLE_REASON_AIRPLANE_MODE
import android.bluetooth.BluetoothProtoEnums.ENABLE_DISABLE_REASON_APPLICATION_REQUEST
import android.bluetooth.BluetoothProtoEnums.ENABLE_DISABLE_REASON_CRASH
import android.bluetooth.BluetoothProtoEnums.ENABLE_DISABLE_REASON_DISALLOWED
import android.bluetooth.BluetoothProtoEnums.ENABLE_DISABLE_REASON_FACTORY_RESET
import android.bluetooth.BluetoothProtoEnums.ENABLE_DISABLE_REASON_RESTARTED
import android.bluetooth.BluetoothProtoEnums.ENABLE_DISABLE_REASON_RESTORE_USER_SETTING
import android.bluetooth.BluetoothProtoEnums.ENABLE_DISABLE_REASON_SATELLITE_MODE
import android.bluetooth.BluetoothProtoEnums.ENABLE_DISABLE_REASON_START_ERROR
import android.bluetooth.BluetoothProtoEnums.ENABLE_DISABLE_REASON_SYSTEM_BOOT
import android.bluetooth.BluetoothProtoEnums.ENABLE_DISABLE_REASON_USER_SWITCH
import android.os.Binder
import android.util.proto.ProtoOutputStream
import androidx.annotation.VisibleForTesting
import com.android.bluetooth.BluetoothStatsLog
import com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_ENABLED_STATE_CHANGED
import com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_ENABLED_STATE_CHANGED__STATE__DISABLED
import com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_ENABLED_STATE_CHANGED__STATE__ENABLED
import com.android.bluetooth.BluetoothStatsLog.BLUETOOTH_ENABLED_STATE_CHANGED__STATE__UNKNOWN
import com.android.server.BluetoothManagerServiceDumpProto as BtProto
import java.io.PrintWriter

private const val TAG = "ActiveLogs"

object ActiveLogs {
    @VisibleForTesting internal const val MAX_ENTRIES_STORED = 20
    @VisibleForTesting
    internal val activeLogs: ArrayDeque<ActiveLog> = ArrayDeque(MAX_ENTRIES_STORED)

    @JvmStatic
    fun dump(writer: PrintWriter) {
        if (activeLogs.isEmpty()) {
            writer.println("Bluetooth never enabled!")
        } else {
            writer.println("Enable log:")
            activeLogs.forEach { writer.println("  $it") }
        }
    }

    @JvmStatic
    fun dumpProto(proto: ProtoOutputStream) {
        val token = proto.start(BtProto.ACTIVE_LOGS)
        activeLogs.forEach { it.dump(proto) }
        proto.end(token)
    }

    @JvmStatic
    @JvmOverloads
    fun add(
        reason: Int,
        enable: Boolean,
        packageName: String = "BluetoothSystemServer",
        isBle: Boolean = false
    ) {
        val last = activeLogs.lastOrNull()
        if (activeLogs.size == MAX_ENTRIES_STORED) {
            activeLogs.removeFirst()
        }
        activeLogs.addLast(ActiveLog(reason, packageName, enable, isBle))
        val state =
            if (enable) BLUETOOTH_ENABLED_STATE_CHANGED__STATE__ENABLED
            else BLUETOOTH_ENABLED_STATE_CHANGED__STATE__DISABLED
        val lastState: Int
        val timeSinceLastChanged: Long
        if (last != null) {
            lastState =
                if (last.enable) BLUETOOTH_ENABLED_STATE_CHANGED__STATE__ENABLED
                else BLUETOOTH_ENABLED_STATE_CHANGED__STATE__DISABLED
            timeSinceLastChanged = System.currentTimeMillis() - last.timestamp
        } else {
            lastState = BLUETOOTH_ENABLED_STATE_CHANGED__STATE__UNKNOWN
            timeSinceLastChanged = 0
        }

        BluetoothStatsLog.write_non_chained(
            BLUETOOTH_ENABLED_STATE_CHANGED,
            Binder.getCallingUid(),
            null,
            state,
            reason,
            packageName,
            lastState,
            timeSinceLastChanged
        )
    }
}

@VisibleForTesting
internal class ActiveLog(
    private val reason: Int,
    private val packageName: String,
    val enable: Boolean,
    private val isBle: Boolean,
) {
    val timestamp = System.currentTimeMillis()

    init {
        Log.d(TAG, this.toString())
    }

    override fun toString() =
        Log.timeToStringWithZone(timestamp) +
            " \tPackage [$packageName] requested to [" +
            (if (enable) "Enable" else "Disable") +
            (if (isBle) "Ble" else "") +
            "]. \tReason is " +
            getEnableDisableReasonString(reason)

    fun dump(proto: ProtoOutputStream) {
        proto.write(BtProto.ActiveLog.TIMESTAMP_MS, timestamp)
        proto.write(BtProto.ActiveLog.ENABLE, enable)
        proto.write(BtProto.ActiveLog.PACKAGE_NAME, packageName)
        proto.write(BtProto.ActiveLog.REASON, reason)
    }
}

private fun getEnableDisableReasonString(reason: Int): String {
    return when (reason) {
        ENABLE_DISABLE_REASON_APPLICATION_REQUEST -> "APPLICATION_REQUEST"
        ENABLE_DISABLE_REASON_AIRPLANE_MODE -> "AIRPLANE_MODE"
        ENABLE_DISABLE_REASON_DISALLOWED -> "DISALLOWED"
        ENABLE_DISABLE_REASON_RESTARTED -> "RESTARTED"
        ENABLE_DISABLE_REASON_START_ERROR -> "START_ERROR"
        ENABLE_DISABLE_REASON_SYSTEM_BOOT -> "SYSTEM_BOOT"
        ENABLE_DISABLE_REASON_CRASH -> "CRASH"
        ENABLE_DISABLE_REASON_USER_SWITCH -> "USER_SWITCH"
        ENABLE_DISABLE_REASON_RESTORE_USER_SETTING -> "RESTORE_USER_SETTING"
        ENABLE_DISABLE_REASON_FACTORY_RESET -> "FACTORY_RESET"
        ENABLE_DISABLE_REASON_SATELLITE_MODE -> "SATELLITE MODE"
        else -> "UNKNOWN[$reason]"
    }
}
+151 −0
Original line number Original line Diff line number Diff line
/*
 * 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.
 */
package com.android.server.bluetooth.test

import android.bluetooth.BluetoothProtoEnums.ENABLE_DISABLE_REASON_AIRPLANE_MODE
import android.bluetooth.BluetoothProtoEnums.ENABLE_DISABLE_REASON_APPLICATION_REQUEST
import android.bluetooth.BluetoothProtoEnums.ENABLE_DISABLE_REASON_CRASH
import android.bluetooth.BluetoothProtoEnums.ENABLE_DISABLE_REASON_DISALLOWED
import android.bluetooth.BluetoothProtoEnums.ENABLE_DISABLE_REASON_FACTORY_RESET
import android.bluetooth.BluetoothProtoEnums.ENABLE_DISABLE_REASON_RESTARTED
import android.bluetooth.BluetoothProtoEnums.ENABLE_DISABLE_REASON_RESTORE_USER_SETTING
import android.bluetooth.BluetoothProtoEnums.ENABLE_DISABLE_REASON_SATELLITE_MODE
import android.bluetooth.BluetoothProtoEnums.ENABLE_DISABLE_REASON_START_ERROR
import android.bluetooth.BluetoothProtoEnums.ENABLE_DISABLE_REASON_SYSTEM_BOOT
import android.bluetooth.BluetoothProtoEnums.ENABLE_DISABLE_REASON_USER_SWITCH
import android.util.proto.ProtoOutputStream
import com.android.server.bluetooth.ActiveLogs
import com.android.server.bluetooth.Log
import com.google.common.truth.Truth.assertThat
import java.io.PrintWriter
import java.io.StringWriter
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

@RunWith(RobolectricTestRunner::class)
@kotlinx.coroutines.ExperimentalCoroutinesApi
class ActiveLogTest {

    @JvmField @Rule val testName = TestName()

    @Before
    fun setUp() {
        Log.i("ActiveLogTest", "\t--> setup of " + testName.getMethodName())
        ActiveLogs.activeLogs.clear()
    }

    @Test
    fun dump_whenNoActiveLog_indicateNeverEnabled() {
        val stringWriter = StringWriter()
        val writer = PrintWriter(stringWriter)

        ActiveLogs.dump(writer)

        assertThat(stringWriter.toString()).isEqualTo("Bluetooth never enabled!\n")
    }

    @Test
    fun dump_whenActiveLog_indicateAll() {
        val numberOfLogEntry = 3
        for (i in 1..numberOfLogEntry) {
            ActiveLogs.add(ENABLE_DISABLE_REASON_START_ERROR, false)
        }
        val stringWriter = StringWriter()
        val writer = PrintWriter(stringWriter)

        ActiveLogs.dump(writer)

        assertThat(stringWriter.toString()).matches("Enable log:\n(.*\n){$numberOfLogEntry}")
    }

    @Test
    fun dump_overflowQueue_indicateFirstEntries() {
        for (i in 1..ActiveLogs.MAX_ENTRIES_STORED * 2) {
            ActiveLogs.add(ENABLE_DISABLE_REASON_START_ERROR, false)
        }
        val stringWriter = StringWriter()
        val writer = PrintWriter(stringWriter)

        ActiveLogs.dump(writer)

        assertThat(stringWriter.toString())
            .matches("Enable log:\n(.*\n){${ActiveLogs.MAX_ENTRIES_STORED}}")
    }

    @Test
    fun dump_differentState_logsVariation() {
        ActiveLogs.add(ENABLE_DISABLE_REASON_START_ERROR, false)
        ActiveLogs.add(ENABLE_DISABLE_REASON_START_ERROR, true, "Foo", true)
        ActiveLogs.add(ENABLE_DISABLE_REASON_START_ERROR, true)
        val stringWriter = StringWriter()
        val writer = PrintWriter(stringWriter)

        ActiveLogs.dump(writer)

        assertThat(stringWriter.toString())
            .matches("Enable log:\n.*Disable.*\n.*EnableBle.*\n.*Enable.*\n")
    }

    @Test
    fun dump_allReason_stringIsKnown() {
        ActiveLogs.add(ENABLE_DISABLE_REASON_APPLICATION_REQUEST, false)
        ActiveLogs.add(ENABLE_DISABLE_REASON_AIRPLANE_MODE, false)
        ActiveLogs.add(ENABLE_DISABLE_REASON_DISALLOWED, false)
        ActiveLogs.add(ENABLE_DISABLE_REASON_RESTARTED, false)
        ActiveLogs.add(ENABLE_DISABLE_REASON_START_ERROR, false)
        ActiveLogs.add(ENABLE_DISABLE_REASON_SYSTEM_BOOT, false)
        ActiveLogs.add(ENABLE_DISABLE_REASON_CRASH, false)
        ActiveLogs.add(ENABLE_DISABLE_REASON_USER_SWITCH, false)
        ActiveLogs.add(ENABLE_DISABLE_REASON_RESTORE_USER_SETTING, false)
        ActiveLogs.add(ENABLE_DISABLE_REASON_FACTORY_RESET, false)
        ActiveLogs.add(ENABLE_DISABLE_REASON_SATELLITE_MODE, false)
        ActiveLogs.add(42, false)
        val stringWriter = StringWriter()
        val writer = PrintWriter(stringWriter)

        ActiveLogs.dump(writer)
        assertThat(stringWriter.toString())
            .matches(
                "Enable log:\n" +
                    ".*APPLICATION_REQUEST\n" +
                    ".*AIRPLANE_MODE\n" +
                    ".*DISALLOWED\n" +
                    ".*RESTARTED\n" +
                    ".*START_ERROR\n" +
                    ".*SYSTEM_BOOT\n" +
                    ".*CRASH\n" +
                    ".*USER_SWITCH\n" +
                    ".*RESTORE_USER_SETTING\n" +
                    ".*FACTORY_RESET\n" +
                    ".*SATELLITE MODE\n" +
                    ".*UNKNOWN\\[\\d+\\]\n"
            )
    }

    @Test
    fun protoDump() {
        ActiveLogs.add(ENABLE_DISABLE_REASON_APPLICATION_REQUEST, false)

        val proto = ProtoOutputStream()
        ActiveLogs.dumpProto(proto)

        assertThat(proto.getRawSize()).isEqualTo(48)
    }
}
+5 −2
Original line number Original line Diff line number Diff line
@@ -20,7 +20,6 @@ import android.bluetooth.IBluetoothCallback
import android.content.AttributionSource
import android.content.AttributionSource
import android.os.IBinder
import android.os.IBinder
import android.os.RemoteException
import android.os.RemoteException
import com.android.server.bluetooth.BluetoothManagerService.timeToLog


class AdapterBinder(rawBinder: IBinder) {
class AdapterBinder(rawBinder: IBinder) {
    private val TAG = "AdapterBinder"
    private val TAG = "AdapterBinder"
@@ -28,7 +27,11 @@ class AdapterBinder(rawBinder: IBinder) {
    private val createdAt = System.currentTimeMillis()
    private val createdAt = System.currentTimeMillis()


    override fun toString(): String =
    override fun toString(): String =
        "[Binder=" + adapterBinder.hashCode() + ", createdAt=" + timeToLog(createdAt) + "]"
        "[Binder=" +
            adapterBinder.hashCode() +
            ", createdAt=" +
            Log.timeToStringWithZone(createdAt) +
            "]"


    @Throws(RemoteException::class)
    @Throws(RemoteException::class)
    fun disable(source: AttributionSource) {
    fun disable(source: AttributionSource) {
+25 −23
Original line number Original line Diff line number Diff line
@@ -16,38 +16,40 @@
package com.android.server.bluetooth
package com.android.server.bluetooth


import android.util.Log
import android.util.Log
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter


private const val SYSTEM_SERVER_TAG = "BluetoothSystemServer"
private const val SYSTEM_SERVER_TAG = "BluetoothSystemServer"


public class Log private constructor() {
object Log {
    companion object {


    // Kotlin could shorten below method by having a Throwable? that is default to null but the
    // Kotlin could shorten below method by having a Throwable? that is default to null but the
    // current implementation of util.Log is behaving differently depending if it is called with
    // current implementation of util.Log is behaving differently depending if it is called with
    // 2 or 3 parameters. We do not want to change the behavior in this class, just add a common
    // 2 or 3 parameters. We do not want to change the behavior in this class, just add a common
    // TAG to all the Bluetooth System Server logs.
    // TAG to all the Bluetooth System Server logs.


        @JvmStatic
    @JvmStatic fun v(subtag: String, msg: String) = Log.v(SYSTEM_SERVER_TAG, "${subtag}: ${msg}")
        fun v(subtag: String, msg: String) = Log.v(SYSTEM_SERVER_TAG, "${subtag}: ${msg}")


        @JvmStatic
    @JvmStatic fun d(subtag: String, msg: String) = Log.d(SYSTEM_SERVER_TAG, "${subtag}: ${msg}")
        fun d(subtag: String, msg: String) = Log.d(SYSTEM_SERVER_TAG, "${subtag}: ${msg}")


        @JvmStatic
    @JvmStatic fun i(subtag: String, msg: String) = Log.i(SYSTEM_SERVER_TAG, "${subtag}: ${msg}")
        fun i(subtag: String, msg: String) = Log.i(SYSTEM_SERVER_TAG, "${subtag}: ${msg}")


        @JvmStatic
    @JvmStatic fun w(subtag: String, msg: String) = Log.w(SYSTEM_SERVER_TAG, "${subtag}: ${msg}")
        fun w(subtag: String, msg: String) = Log.w(SYSTEM_SERVER_TAG, "${subtag}: ${msg}")


    @JvmStatic
    @JvmStatic
    fun w(subtag: String, msg: String, tr: Throwable) =
    fun w(subtag: String, msg: String, tr: Throwable) =
        Log.w(SYSTEM_SERVER_TAG, "${subtag}: ${msg}", tr)
        Log.w(SYSTEM_SERVER_TAG, "${subtag}: ${msg}", tr)


        @JvmStatic
    @JvmStatic fun e(subtag: String, msg: String) = Log.e(SYSTEM_SERVER_TAG, "${subtag}: ${msg}")
        fun e(subtag: String, msg: String) = Log.e(SYSTEM_SERVER_TAG, "${subtag}: ${msg}")


    @JvmStatic
    @JvmStatic
    fun e(subtag: String, msg: String, tr: Throwable) =
    fun e(subtag: String, msg: String, tr: Throwable) =
        Log.e(SYSTEM_SERVER_TAG, "${subtag}: ${msg}", tr)
        Log.e(SYSTEM_SERVER_TAG, "${subtag}: ${msg}", tr)
    }

    @JvmStatic
    fun timeToStringWithZone(timestamp: Long) =
        DateTimeFormatter.ofPattern("MM-dd HH:mm:ss.SSS")
            .withZone(ZoneId.systemDefault())
            .format(Instant.ofEpochMilli(timestamp))
}
}
Loading