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

Commit 75cd7e4e authored by William Escande's avatar William Escande
Browse files

SystemServer: Kt implementation of airplane mode

Test: atest ServiceBluetoothRoboTests
Bug: 286260653
Bug: 286602847
Bug: 309033118
Change-Id: I60c5070b477a6a07334122de5395e498d4546228
parent e976df30
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -7,6 +7,13 @@ flag {
    bug: "303552318"
}

flag {
    name: "use_new_airplane_mode"
    namespace: "bluetooth"
    description: "Use the new implemention of airplane mode"
    bug: "309033118"
}

flag {
    name: "use_new_satellite_mode"
    namespace: "bluetooth"
+4 −0
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ filegroup {
        "src/**/*.java",
        "src/AdapterState.kt",
        "src/RadioModeListener.kt",
        "src/airplane/ModeListener.kt",
        "src/com/**/*.kt",
        "src/satellite/ModeListener.kt",
    ],
@@ -157,10 +158,13 @@ android_robolectric_test {
    instrumentation_for: "ServiceBluetoothFakeTestApp",

    srcs: [
        ":statslog-bluetooth-java-gen",
        "src/AdapterState.kt",
        "src/AdapterStateTest.kt",
        "src/RadioModeListener.kt",
        "src/RadioModeListenerTest.kt",
        "src/airplane/ModeListener.kt",
        "src/airplane/ModeListenerTest.kt",
        "src/satellite/ModeListener.kt",
        "src/satellite/ModeListenerTest.kt",
    ],
+352 −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.
 */
@file:JvmName("AirplaneModeListener")

package com.android.server.bluetooth.airplane

import android.bluetooth.BluetoothAdapter.STATE_ON
import android.bluetooth.BluetoothAdapter.STATE_TURNING_OFF
import android.bluetooth.BluetoothAdapter.STATE_TURNING_ON
import android.content.ContentResolver
import android.content.Context
import android.content.res.Resources
import android.os.Looper
import android.provider.Settings
import android.util.Log
import android.widget.Toast
import com.android.bluetooth.BluetoothStatsLog
import com.android.server.bluetooth.BluetoothAdapterState
import com.android.server.bluetooth.initializeRadioModeListener
import kotlin.time.Duration.Companion.minutes
import kotlin.time.TimeMark
import kotlin.time.TimeSource

private const val TAG = "BluetoothAirplaneModeListener"

/** @return true if Bluetooth state is impacted by airplane mode */
public var isOn = false
    private set

/**
 * The airplane ModeListener handles system airplane mode change and checks whether it need to
 * trigger the callback or not.
 *
 * <p>The information of airplane mode being turns on would not be passed when Bluetooth is on and
 * one of the following situations is met:
 * <ul>
 * <li> "Airplane Enhancement Mode" is enabled and the user asked for Bluetooth to be on previously
 * <li> A media profile is connected (one of A2DP | Hearing Aid | Le Audio)
 * </ul>
 */
@kotlin.time.ExperimentalTime
public fun initialize(
    looper: Looper,
    systemResolver: ContentResolver,
    state: BluetoothAdapterState,
    modeCallback: (m: Boolean) -> Unit,
    notificationCallback: (state: String) -> Unit,
    mediaCallback: () -> Boolean,
    userCallback: () -> Context,
    timeSource: TimeSource,
) {

    // Wifi got support for "Airplane Enhancement Mode" prior to Bluetooth.
    // In order for Wifi to be aware that Bluetooth also support the feature, Bluetooth need to set
    // the APM_ENHANCEMENT settings to `1`.
    // Value will be set to DEFAULT_APM_ENHANCEMENT_STATE only if the APM_ENHANCEMENT is not set.
    Settings.Global.putInt(
        systemResolver,
        APM_ENHANCEMENT,
        Settings.Global.getInt(systemResolver, APM_ENHANCEMENT, DEFAULT_APM_ENHANCEMENT_STATE)
    )

    val airplaneModeAtBoot =
        initializeRadioModeListener(
            looper,
            systemResolver,
            Settings.Global.AIRPLANE_MODE_RADIOS,
            Settings.Global.AIRPLANE_MODE_ON,
            fun(newMode: Boolean) {
                val previousMode = isOn
                val isBluetoothOn = state.oneOf(STATE_ON, STATE_TURNING_ON, STATE_TURNING_OFF)
                val isMediaConnected = isBluetoothOn && mediaCallback()

                isOn =
                    airplaneModeValueOverride(
                        systemResolver,
                        newMode,
                        isBluetoothOn,
                        notificationCallback,
                        userCallback,
                        isMediaConnected,
                    )

                AirplaneMetricSession.handleModeChange(
                    newMode,
                    isBluetoothOn,
                    notificationCallback,
                    userCallback,
                    isMediaConnected,
                    timeSource.markNow(),
                )

                if (previousMode == isOn) {
                    Log.d(TAG, "Ignore airplane mode change because is already: $isOn")
                    return
                }

                Log.i(TAG, "Trigger callback with state: $isOn")
                modeCallback(isOn)
            }
        )

    isOn =
        airplaneModeValueOverride(
            systemResolver,
            airplaneModeAtBoot,
            null, // Do not provide a Bluetooth on / off as we want to evaluate override
            null, // Do not provide a notification callback as we want to keep the boot silent
            userCallback,
            false,
        )

    // Bluetooth is always off during initialize, and no media profile can be connected
    AirplaneMetricSession.handleModeChange(
        airplaneModeAtBoot,
        false,
        notificationCallback,
        userCallback,
        false,
        timeSource.markNow(),
    )
    Log.i(TAG, "Initialized successfully with state: $isOn")
}

@kotlin.time.ExperimentalTime
public fun notifyUserToggledBluetooth(
    resolver: ContentResolver,
    userContext: Context,
    isBluetoothOn: Boolean,
) {
    AirplaneMetricSession.notifyUserToggledBluetooth(resolver, userContext, isBluetoothOn)
}

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

private fun airplaneModeValueOverride(
    resolver: ContentResolver,
    currentAirplaneMode: Boolean,
    currentBluetoothStatus: Boolean?,
    sendAirplaneModeNotification: ((state: String) -> Unit)?,
    getUser: () -> Context,
    isMediaConnected: Boolean,
): Boolean {
    // Airplane mode is being disabled or bluetooth was not on: no override
    if (!currentAirplaneMode || currentBluetoothStatus == false) {
        return currentAirplaneMode
    }
    // If "Airplane Enhancement Mode" is on and the user already used the feature …
    if (isApmEnhancementEnabled(resolver) && hasUserToggledApm(getUser)) {
        // … Staying on only depend on its last action in airplane mode
        if (isBluetoothOnAPM(getUser)) {
            Log.i(TAG, "Bluetooth stay on during airplane mode because of last user action")

            val isWifiOn = isWifiOnApm(resolver, getUser)
            sendAirplaneModeNotification?.invoke(
                if (isWifiOn) APM_WIFI_BT_NOTIFICATION else APM_BT_NOTIFICATION
            )
            return false
        }
        return true
    }
    // … Else, staying on only depend on media profile being connected or not
    //
    // Note: Once the "Airplane Enhancement Mode" has been used, media override no longer apply
    //       This has been done on purpose to avoid complexe scenario like:
    //           1. User wants Bt off according to "Airplane Enhancement Mode"
    //           2. User swithes airplane while there is media => so Bt stays on
    //           3. User turns airplane off, stops media and toggles airplane back on
    //       Should we turn Bt off like asked initialy ? Or keep it `on` like the toggle ?
    if (isMediaConnected) {
        Log.i(TAG, "Bluetooth stay on during airplane mode because media profile are connected")
        ToastNotification.displayIfNeeded(resolver, getUser)
        return false
    }
    return true
}

internal class ToastNotification private constructor() {
    companion object {
        private const val TOAST_COUNT = "bluetooth_airplane_toast_count"
        internal const val MAX_TOAST_COUNT = 10

        private fun userNeedToBeNotified(resolver: ContentResolver): Boolean {
            val currentToastCount = Settings.Global.getInt(resolver, TOAST_COUNT, 0)
            if (currentToastCount >= MAX_TOAST_COUNT) {
                return false
            }
            Settings.Global.putInt(resolver, TOAST_COUNT, currentToastCount + 1)
            return true
        }

        fun displayIfNeeded(resolver: ContentResolver, getUser: () -> Context) {
            if (!userNeedToBeNotified(resolver)) {
                Log.d(TAG, "Dismissed Toast notification")
                return
            }
            val userContext = getUser()
            val r = userContext.getResources()
            val text: CharSequence =
                r.getString(
                    Resources.getSystem()
                        .getIdentifier("bluetooth_airplane_mode_toast", "string", "android")
                )
            Toast.makeText(userContext, text, Toast.LENGTH_LONG).show()
            Log.d(TAG, "Displayed Toast notification")
        }
    }
}

@kotlin.time.ExperimentalTime
private class AirplaneMetricSession(
    private val isBluetoothOnBeforeApmToggle: Boolean,
    private val sendAirplaneModeNotification: (state: String) -> Unit,
    private val isMediaProfileConnectedBeforeApmToggle: Boolean,
    private val sessionStartTime: TimeMark,
) {
    companion object {
        private var session: AirplaneMetricSession? = null

        fun handleModeChange(
            isAirplaneModeOn: Boolean,
            isBluetoothOn: Boolean,
            sendAirplaneModeNotification: (state: String) -> Unit,
            getUser: () -> Context,
            isMediaProfileConnected: Boolean,
            startTime: TimeMark,
        ) {
            if (isAirplaneModeOn) {
                session =
                    AirplaneMetricSession(
                        isBluetoothOn,
                        sendAirplaneModeNotification,
                        isMediaProfileConnected,
                        startTime,
                    )
            } else {
                session?.let { it.terminate(getUser, isBluetoothOn) }
                session = null
            }
        }

        fun notifyUserToggledBluetooth(
            resolver: ContentResolver,
            userContext: Context,
            isBluetoothOn: Boolean,
        ) {
            session?.let { it.notifyUserToggledBluetooth(resolver, userContext, isBluetoothOn) }
        }
    }

    private val isBluetoothOnAfterApmToggle = !isOn
    private var userToggledBluetoothDuringApm = false
    private var userToggledBluetoothDuringApmWithinMinute = false

    fun notifyUserToggledBluetooth(
        resolver: ContentResolver,
        userContext: Context,
        isBluetoothOn: Boolean,
    ) {
        val isFirstToggle = !userToggledBluetoothDuringApm
        userToggledBluetoothDuringApm = true

        if (isFirstToggle) {
            val oneMinute = sessionStartTime + 1.minutes
            userToggledBluetoothDuringApmWithinMinute = !oneMinute.hasPassedNow()
        }

        if (isApmEnhancementEnabled(resolver)) {
            // Set "Airplane Enhancement Mode" settings for a specific user
            setUserSettingsSecure(userContext, BLUETOOTH_APM_STATE, if (isBluetoothOn) 1 else 0)
            setUserSettingsSecure(userContext, APM_USER_TOGGLED_BLUETOOTH, 1)

            if (isBluetoothOn) {
                sendAirplaneModeNotification(APM_BT_ENABLED_NOTIFICATION)
            }
        }
    }

    /** Log current airplaneSession. Session cannot be re-use */
    fun terminate(getUser: () -> Context, isBluetoothOn: Boolean) {
        BluetoothStatsLog.write(
            BluetoothStatsLog.AIRPLANE_MODE_SESSION_REPORTED,
            BluetoothStatsLog.AIRPLANE_MODE_SESSION_REPORTED__PACKAGE_NAME__BLUETOOTH,
            isBluetoothOnBeforeApmToggle,
            isBluetoothOnAfterApmToggle,
            isBluetoothOn,
            hasUserToggledApm(getUser),
            userToggledBluetoothDuringApm,
            userToggledBluetoothDuringApmWithinMinute,
            isMediaProfileConnectedBeforeApmToggle,
        )
    }
}

// Notification Id for when the airplane mode is turn on but Bluetooth stay on
internal const val APM_BT_NOTIFICATION = "apm_bt_notification"

// Notification Id for when the airplane mode is turn on but Bluetooth and Wifi stay on
internal const val APM_WIFI_BT_NOTIFICATION = "apm_wifi_bt_notification"

// Notification Id for when the Bluetooth is turned back on durin airplane mode
internal const val APM_BT_ENABLED_NOTIFICATION = "apm_bt_enabled_notification"

// Whether the "Airplane Enhancement Mode" is enabled
internal const val APM_ENHANCEMENT = "apm_enhancement_enabled"

// Whether the user has already toggled and used the "Airplane Enhancement Mode" feature
internal const val APM_USER_TOGGLED_BLUETOOTH = "apm_user_toggled_bluetooth"

// Whether Bluetooth should remain on in airplane mode
internal const val BLUETOOTH_APM_STATE = "bluetooth_apm_state"

// Whether Wifi should remain on in airplane mode
internal const val WIFI_APM_STATE = "wifi_apm_state"

private fun setUserSettingsSecure(userContext: Context, name: String, value: Int) =
    Settings.Secure.putInt(userContext.contentResolver, name, value)

// Define if the "Airplane Enhancement Mode" feature is enabled by default. `0` == disabled
private const val DEFAULT_APM_ENHANCEMENT_STATE = 1

/** Airplane Enhancement Mode: Indicate if the feature is enabled or not. */
private fun isApmEnhancementEnabled(resolver: ContentResolver) =
    Settings.Global.getInt(resolver, APM_ENHANCEMENT, DEFAULT_APM_ENHANCEMENT_STATE) == 1

/** Airplane Enhancement Mode: Return true if the wifi should stays on during airplane mode */
private fun isWifiOnApm(resolver: ContentResolver, getUser: () -> Context) =
    Settings.Global.getInt(resolver, Settings.Global.WIFI_ON, 0) != 0 &&
        Settings.Secure.getInt(getUser().contentResolver, WIFI_APM_STATE, 0) == 1

/** Airplane Enhancement Mode: Return true if this user already toggled (aka used) the feature */
private fun hasUserToggledApm(getUser: () -> Context) =
    Settings.Secure.getInt(getUser().contentResolver, APM_USER_TOGGLED_BLUETOOTH, 0) == 1

/** Airplane Enhancement Mode: Return true if the bluetooth should stays on during airplane mode */
private fun isBluetoothOnAPM(getUser: () -> Context) =
    Settings.Secure.getInt(getUser().contentResolver, BLUETOOTH_APM_STATE, 0) == 1
+531 −0

File added.

Preview size limit exceeded, changes collapsed.

+75 −13
Original line number Diff line number Diff line
@@ -80,9 +80,11 @@ import com.android.bluetooth.flags.FeatureFlags;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.BluetoothManagerServiceDumpProto;
import com.android.server.bluetooth.airplane.AirplaneModeListener;
import com.android.server.bluetooth.satellite.SatelliteModeListener;

import kotlin.Unit;
import kotlin.time.TimeSource;

import java.io.FileDescriptor;
import java.io.FileOutputStream;
@@ -216,6 +218,7 @@ class BluetoothManagerService {

    private List<Integer> mSupportedProfileList = new ArrayList<>();

    // TODO(b/309033118): remove BluetoothAirplaneModeListener once use_new_airplane_mode ship
    private final BluetoothAirplaneModeListener mBluetoothAirplaneModeListener;

    // TODO(b/303552318): remove BluetoothNotificationManager once airplane_ressources_in_app ship
@@ -225,11 +228,15 @@ class BluetoothManagerService {
    private BluetoothSatelliteModeListener mBluetoothSatelliteModeListener;

    private final boolean mUseNewSatelliteMode;
    private final boolean mUseNewAirplaneMode;

    // used inside handler thread
    private boolean mQuietEnable = false;
    private boolean mEnable = false;
    private boolean mShutdownInProgress = false;

    private Context mCurrentUserContext = null;

    static String timeToLog(long timestamp) {
        return DateTimeFormatter.ofPattern("MM-dd HH:mm:ss.SSS")
                .withZone(ZoneId.systemDefault())
@@ -449,7 +456,7 @@ class BluetoothManagerService {
    private static final Object ON_SWITCH_USER_TOKEN = new Object();

    @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
    void onAirplaneModeChanged(boolean isAirplaneModeOn) {
    Unit onAirplaneModeChanged(boolean isAirplaneModeOn) {
        mHandler.postDelayed(
                () ->
                        delayModeChangedIfNeeded(
@@ -458,6 +465,7 @@ class BluetoothManagerService {
                                "onAirplaneModeChanged"),
                ON_AIRPLANE_MODE_CHANGED_TOKEN,
                0);
        return Unit.INSTANCE;
    }

    // TODO(b/289584302): Update to private once use_new_satellite_mode is enabled
@@ -676,7 +684,7 @@ class BluetoothManagerService {
        // Observe BLE scan only mode settings change.
        registerForBleScanModeChange();

        if (!mFeatureFlags.airplaneRessourcesInApp()) {
        if (!mFeatureFlags.airplaneRessourcesInApp() && !mFeatureFlags.useNewAirplaneMode()) {
            mBluetoothNotificationManager = new BluetoothNotificationManager(mContext);
        }

@@ -740,9 +748,15 @@ class BluetoothManagerService {
            mEnableExternal = true;
        }

        // Caching is necessary to prevent caller requiring the READ_DEVICE_CONFIG permission
        mUseNewAirplaneMode = mFeatureFlags.useNewAirplaneMode();
        if (mUseNewAirplaneMode) {
            mBluetoothAirplaneModeListener = null;
        } else {
            mBluetoothAirplaneModeListener =
                    new BluetoothAirplaneModeListener(
                            this, mLooper, mContext, mBluetoothNotificationManager, mFeatureFlags);
        }

        // Caching is necessary to prevent caller requiring the READ_DEVICE_CONFIG permission
        mUseNewSatelliteMode = mFeatureFlags.useNewSatelliteMode();
@@ -760,6 +774,9 @@ class BluetoothManagerService {

    /** Returns true if airplane mode is currently on */
    private boolean isAirplaneModeOn() {
        if (mUseNewAirplaneMode) {
            return AirplaneModeListener.isOn();
        }
        return mBluetoothAirplaneModeListener.isAirplaneModeOn();
    }

@@ -983,6 +1000,10 @@ class BluetoothManagerService {
        return mIsHearingAidProfileSupported;
    }

    Context getCurrentUserContext() {
        return mCurrentUserContext;
    }

    boolean isMediaProfileConnected() {
        if (mAdapter == null || !mState.oneOf(STATE_ON)) {
            return false;
@@ -1284,7 +1305,18 @@ class BluetoothManagerService {
        synchronized (mReceiver) {
            mQuietEnableExternal = false;
            mEnableExternal = true;
            if (!mUseNewAirplaneMode) {
                mBluetoothAirplaneModeListener.notifyUserToggledBluetooth(true);
            } else {
                // TODO(b/288450479): Remove clearCallingIdentity when threading is fixed
                final long callingIdentity = Binder.clearCallingIdentity();
                try {
                    AirplaneModeListener.notifyUserToggledBluetooth(
                            mContentResolver, mCurrentUserContext, true);
                } finally {
                    Binder.restoreCallingIdentity(callingIdentity);
                }
            }
            sendEnableMsg(
                    false,
                    BluetoothProtoEnums.ENABLE_DISABLE_REASON_APPLICATION_REQUEST,
@@ -1307,7 +1339,18 @@ class BluetoothManagerService {
        }

        synchronized (mReceiver) {
            if (!mUseNewAirplaneMode) {
                mBluetoothAirplaneModeListener.notifyUserToggledBluetooth(false);
            } else {
                // TODO(b/288450479): Remove clearCallingIdentity when threading is fixed
                final long callingIdentity = Binder.clearCallingIdentity();
                try {
                    AirplaneModeListener.notifyUserToggledBluetooth(
                            mContentResolver, mCurrentUserContext, false);
                } finally {
                    Binder.restoreCallingIdentity(callingIdentity);
                }
            }

            if (persist) {
                persistBluetoothSetting(BLUETOOTH_OFF);
@@ -1437,15 +1480,28 @@ class BluetoothManagerService {
     * Send enable message and set adapter name and address. Called when the boot phase becomes
     * PHASE_SYSTEM_SERVICES_READY.
     */
    void handleOnBootPhase() {
        mHandler.post(() -> internalHandleOnBootPhase());
    void handleOnBootPhase(UserHandle userHandle) {
        mHandler.post(() -> internalHandleOnBootPhase(userHandle));
    }

    private void internalHandleOnBootPhase() {
    private void internalHandleOnBootPhase(UserHandle userHandle) {
        if (DBG) {
            Log.d(TAG, "Bluetooth boot completed");
        }

        if (mUseNewAirplaneMode) {
            mCurrentUserContext = mContext.createContextAsUser(userHandle, 0);
            AirplaneModeListener.initialize(
                    mLooper,
                    mContentResolver,
                    mState,
                    this::onAirplaneModeChanged,
                    this::sendAirplaneModeNotification,
                    this::isMediaProfileConnected,
                    this::getCurrentUserContext,
                    TimeSource.Monotonic.INSTANCE);
        }

        if (mUseNewSatelliteMode) {
            SatelliteModeListener.initialize(
                    mLooper, mContentResolver, this::onSatelliteModeChanged);
@@ -1471,9 +1527,11 @@ class BluetoothManagerService {
            mHandler.sendEmptyMessage(MESSAGE_GET_NAME_AND_ADDRESS);
        }

        if (!mUseNewAirplaneMode) {
            mBluetoothAirplaneModeListener.start(new BluetoothModeChangeHelper(mContext));
            setApmEnhancementState();
        }
    }

    /** set APM enhancement feature state */
    @VisibleForTesting
@@ -2273,11 +2331,15 @@ class BluetoothManagerService {
                        Log.d(TAG, "MESSAGE_USER_SWITCHED");
                    }
                    mHandler.removeMessages(MESSAGE_USER_SWITCHED);
                    if (!mFeatureFlags.airplaneRessourcesInApp()) {
                    if (!mFeatureFlags.airplaneRessourcesInApp() && !mUseNewAirplaneMode) {
                        mBluetoothNotificationManager.createNotificationChannels();
                    }
                    UserHandle userTo = (UserHandle) msg.obj;

                    if (mUseNewAirplaneMode) {
                        mCurrentUserContext = mContext.createContextAsUser(userTo, 0);
                    }

                    /* disable and enable BT when detect a user switch */
                    if (mAdapter != null && mState.oneOf(STATE_ON)) {
                        restartForNewUser(userTo);
Loading