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

Commit 9a68067a authored by Ugo Yu's avatar Ugo Yu
Browse files

Context-aware Bluetooth airplane mode

Do not automatically turn off Bluetooth when airplane mode is turned
on and Bluetooth is in one of the following situations:
  1. Bluetooth A2DP is connected.
  2. Bluetooth Hearing Aid profile is connected.

Bug: 142831248
Test: Manual
Change-Id: I7f10b27102d91cc7a9803f74c88a4af80a294fb9
parent 05c99fe1
Loading
Loading
Loading
Loading
+258 −0
Original line number Diff line number Diff line
/*
 * Copyright 2019 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;

import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothProfile.ServiceListener;
import android.content.Context;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.provider.Settings;
import android.util.Log;
import android.widget.Toast;

import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;

/**
 * The BluetoothAirplaneModeListener handles system airplane mode change callback and checks
 * whether we need to inform BluetoothManagerService on this change.
 *
 * The information of airplane mode turns on would not be passed to the BluetoothManagerService
 * when Bluetooth is on and Bluetooth is in one of the following situations:
 *   1. Bluetooth A2DP is connected.
 *   2. Bluetooth Hearing Aid profile is connected.
 */
class BluetoothAirplaneModeListener {
    private static final String TAG = "BluetoothAirplaneModeListener";
    @VisibleForTesting static final String TOAST_COUNT = "bluetooth_airplane_toast_count";

    private static final int MSG_AIRPLANE_MODE_CHANGED = 0;

    @VisibleForTesting static final int MAX_TOAST_COUNT = 10; // 10 times

    private final BluetoothManagerService mBluetoothManager;
    private final BluetoothAirplaneModeHandler mHandler;
    private AirplaneModeHelper mAirplaneHelper;

    @VisibleForTesting int mToastCount = 0;

    BluetoothAirplaneModeListener(BluetoothManagerService service, Looper looper, Context context) {
        mBluetoothManager = service;

        mHandler = new BluetoothAirplaneModeHandler(looper);
        context.getContentResolver().registerContentObserver(
                Settings.Global.getUriFor(Settings.Global.AIRPLANE_MODE_ON), true,
                mAirplaneModeObserver);
    }

    private final ContentObserver mAirplaneModeObserver = new ContentObserver(null) {
        @Override
        public void onChange(boolean unused) {
            // Post from system main thread to android_io thread.
            Message msg = mHandler.obtainMessage(MSG_AIRPLANE_MODE_CHANGED);
            mHandler.sendMessage(msg);
        }
    };

    private class BluetoothAirplaneModeHandler extends Handler {
        BluetoothAirplaneModeHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_AIRPLANE_MODE_CHANGED:
                    handleAirplaneModeChange();
                    break;
                default:
                    Log.e(TAG, "Invalid message: " + msg.what);
                    break;
            }
        }
    }

    /**
     * Call after boot complete
     */
    @VisibleForTesting
    void start(AirplaneModeHelper helper) {
        Log.i(TAG, "start");
        mAirplaneHelper = helper;
        mToastCount = mAirplaneHelper.getSettingsInt(TOAST_COUNT);
    }

    @VisibleForTesting
    boolean shouldPopToast() {
        if (mToastCount >= MAX_TOAST_COUNT) {
            return false;
        }
        mToastCount++;
        mAirplaneHelper.setSettingsInt(TOAST_COUNT, mToastCount);
        return true;
    }

    @VisibleForTesting
    void handleAirplaneModeChange() {
        if (shouldSkipAirplaneModeChange()) {
            Log.i(TAG, "Ignore airplane mode change");
            // We have to store Bluetooth state here, so if user turns off Bluetooth
            // after airplane mode is turned on, we don't forget to turn on Bluetooth
            // when airplane mode turns off.
            mAirplaneHelper.setSettingsInt(Settings.Global.BLUETOOTH_ON,
                    BluetoothManagerService.BLUETOOTH_ON_AIRPLANE);
            if (shouldPopToast()) {
                mAirplaneHelper.showToastMessage();
            }
            return;
        }
        mAirplaneHelper.onAirplaneModeChanged(mBluetoothManager);
    }

    @VisibleForTesting
    boolean shouldSkipAirplaneModeChange() {
        if (mAirplaneHelper == null) {
            return false;
        }
        if (!mAirplaneHelper.isBluetoothOn() || !mAirplaneHelper.isAirplaneModeOn()
                || !mAirplaneHelper.isA2dpOrHearingAidConnected()) {
            return false;
        }
        return true;
    }

    /**
     * Helper class that handles callout and callback methods without
     * complex logic.
     */
    @VisibleForTesting
    public static class AirplaneModeHelper {
        private volatile BluetoothA2dp mA2dp;
        private volatile BluetoothHearingAid mHearingAid;
        private final BluetoothAdapter mAdapter;
        private final Context mContext;

        AirplaneModeHelper(Context context) {
            mAdapter = BluetoothAdapter.getDefaultAdapter();
            mContext = context;

            mAdapter.getProfileProxy(mContext, mProfileServiceListener, BluetoothProfile.A2DP);
            mAdapter.getProfileProxy(mContext, mProfileServiceListener,
                    BluetoothProfile.HEARING_AID);
        }

        private final ServiceListener mProfileServiceListener = new ServiceListener() {
            @Override
            public void onServiceConnected(int profile, BluetoothProfile proxy) {
                // Setup Bluetooth profile proxies
                switch (profile) {
                    case BluetoothProfile.A2DP:
                        mA2dp = (BluetoothA2dp) proxy;
                        break;
                    case BluetoothProfile.HEARING_AID:
                        mHearingAid = (BluetoothHearingAid) proxy;
                        break;
                    default:
                        break;
                }
            }

            @Override
            public void onServiceDisconnected(int profile) {
                // Clear Bluetooth profile proxies
                switch (profile) {
                    case BluetoothProfile.A2DP:
                        mA2dp = null;
                        break;
                    case BluetoothProfile.HEARING_AID:
                        mHearingAid = null;
                        break;
                    default:
                        break;
                }
            }
        };

        @VisibleForTesting
        public boolean isA2dpOrHearingAidConnected() {
            return isA2dpConnected() || isHearingAidConnected();
        }

        @VisibleForTesting
        public boolean isBluetoothOn() {
            final BluetoothAdapter adapter = mAdapter;
            if (adapter == null) {
                return false;
            }
            return adapter.getLeState() == BluetoothAdapter.STATE_ON;
        }

        @VisibleForTesting
        public boolean isAirplaneModeOn() {
            return Settings.Global.getInt(mContext.getContentResolver(),
                    Settings.Global.AIRPLANE_MODE_ON, 0) == 1;
        }

        @VisibleForTesting
        public void onAirplaneModeChanged(BluetoothManagerService managerService) {
            managerService.onAirplaneModeChanged();
        }

        @VisibleForTesting
        public int getSettingsInt(String name) {
            return Settings.Global.getInt(mContext.getContentResolver(),
                    name, 0);
        }

        @VisibleForTesting
        public void setSettingsInt(String name, int value) {
            Settings.Global.putInt(mContext.getContentResolver(),
                    name, value);
        }

        @VisibleForTesting
        public void showToastMessage() {
            Resources r = mContext.getResources();
            final CharSequence text = r.getString(
                    R.string.bluetooth_airplane_mode_toast, 0);
            Toast.makeText(mContext, text, Toast.LENGTH_LONG).show();
        }

        private boolean isA2dpConnected() {
            final BluetoothA2dp a2dp = mA2dp;
            if (a2dp == null) {
                return false;
            }
            return a2dp.getConnectedDevices().size() > 0;
        }

        private boolean isHearingAidConnected() {
            final BluetoothHearingAid hearingAid = mHearingAid;
            if (hearingAid == null) {
                return false;
            }
            return hearingAid.getConnectedDevices().size() > 0;
        }
    };
}
+74 −57
Original line number Diff line number Diff line
@@ -68,6 +68,7 @@ import android.util.Slog;
import android.util.StatsLog;

import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.DumpUtils;
import com.android.server.pm.UserRestrictionsUtils;

@@ -138,7 +139,8 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
    // Bluetooth persisted setting is on
    // but Airplane mode will affect Bluetooth state at start up
    // and Airplane mode will have higher priority.
    private static final int BLUETOOTH_ON_AIRPLANE = 2;
    @VisibleForTesting
    static final int BLUETOOTH_ON_AIRPLANE = 2;

    private static final int SERVICE_IBLUETOOTH = 1;
    private static final int SERVICE_IBLUETOOTHGATT = 2;
@@ -159,6 +161,8 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
    private boolean mBinding;
    private boolean mUnbinding;

    private BluetoothAirplaneModeListener mBluetoothAirplaneModeListener;

    // used inside handler thread
    private boolean mQuietEnable = false;
    private boolean mEnable;
@@ -257,9 +261,7 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
                }
            };

    private final ContentObserver mAirplaneModeObserver = new ContentObserver(null) {
        @Override
        public void onChange(boolean unused) {
    public void onAirplaneModeChanged() {
        synchronized (this) {
            if (isBluetoothPersistedStateOn()) {
                if (isAirplaneModeOn()) {
@@ -318,7 +320,6 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
            }
        }
    }
    };

    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
@@ -430,9 +431,8 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
                Settings.Global.getString(mContentResolver, Settings.Global.AIRPLANE_MODE_RADIOS);
        if (airplaneModeRadios == null || airplaneModeRadios.contains(
                Settings.Global.RADIO_BLUETOOTH)) {
            mContentResolver.registerContentObserver(
                    Settings.Global.getUriFor(Settings.Global.AIRPLANE_MODE_ON), true,
                    mAirplaneModeObserver);
            mBluetoothAirplaneModeListener = new BluetoothAirplaneModeListener(
                    this, IoThread.get().getLooper(), context);
        }

        int systemUiUid = -1;
@@ -478,6 +478,17 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
        return state != BLUETOOTH_OFF;
    }

    private boolean isBluetoothPersistedStateOnAirplane() {
        if (!supportBluetoothPersistedState()) {
            return false;
        }
        int state = Settings.Global.getInt(mContentResolver, Settings.Global.BLUETOOTH_ON, -1);
        if (DBG) {
            Slog.d(TAG, "Bluetooth persisted state: " + state);
        }
        return state == BLUETOOTH_ON_AIRPLANE;
    }

    /**
     *  Returns true if the Bluetooth saved state is BLUETOOTH_ON_BLUETOOTH
     */
@@ -954,10 +965,12 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
        }

        synchronized (mReceiver) {
            if (!isBluetoothPersistedStateOnAirplane()) {
                if (persist) {
                    persistBluetoothSetting(BLUETOOTH_OFF);
                }
                mEnableExternal = false;
            }
            sendDisableMsg(BluetoothProtoEnums.ENABLE_DISABLE_REASON_APPLICATION_REQUEST,
                    packageName);
        }
@@ -1185,6 +1198,10 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
            Message getMsg = mHandler.obtainMessage(MESSAGE_GET_NAME_AND_ADDRESS);
            mHandler.sendMessage(getMsg);
        }
        if (mBluetoothAirplaneModeListener != null) {
            mBluetoothAirplaneModeListener.start(
                    new BluetoothAirplaneModeListener.AirplaneModeHelper(mContext));
        }
    }

    /**
+125 −0
Original line number Diff line number Diff line
/*
 * Copyright 2019 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;

import static org.mockito.Mockito.*;

import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.os.Looper;
import android.provider.Settings;

import androidx.test.InstrumentationRegistry;
import androidx.test.filters.MediumTest;
import androidx.test.runner.AndroidJUnit4;

import com.android.server.BluetoothAirplaneModeListener.AirplaneModeHelper;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;

@MediumTest
@RunWith(AndroidJUnit4.class)
public class BluetoothAirplaneModeListenerTest {
    private Context mContext;
    private BluetoothAirplaneModeListener mBluetoothAirplaneModeListener;
    private BluetoothAdapter mBluetoothAdapter;
    private AirplaneModeHelper mHelper;

    @Mock BluetoothManagerService mBluetoothManagerService;

    @Before
    public void setUp() throws Exception {
        mContext = InstrumentationRegistry.getTargetContext();

        mHelper = mock(AirplaneModeHelper.class);
        when(mHelper.getSettingsInt(BluetoothAirplaneModeListener.TOAST_COUNT))
                .thenReturn(BluetoothAirplaneModeListener.MAX_TOAST_COUNT);
        doNothing().when(mHelper).setSettingsInt(anyString(), anyInt());
        doNothing().when(mHelper).showToastMessage();
        doNothing().when(mHelper).onAirplaneModeChanged(any(BluetoothManagerService.class));

        mBluetoothAirplaneModeListener = new BluetoothAirplaneModeListener(
                    mBluetoothManagerService, Looper.getMainLooper(), mContext);
        mBluetoothAirplaneModeListener.start(mHelper);
    }

    @Test
    public void testIgnoreOnAirplanModeChange() {
        Assert.assertFalse(mBluetoothAirplaneModeListener.shouldSkipAirplaneModeChange());

        when(mHelper.isBluetoothOn()).thenReturn(true);
        Assert.assertFalse(mBluetoothAirplaneModeListener.shouldSkipAirplaneModeChange());

        when(mHelper.isA2dpOrHearingAidConnected()).thenReturn(true);
        Assert.assertFalse(mBluetoothAirplaneModeListener.shouldSkipAirplaneModeChange());

        when(mHelper.isAirplaneModeOn()).thenReturn(true);
        Assert.assertTrue(mBluetoothAirplaneModeListener.shouldSkipAirplaneModeChange());
    }

    @Test
    public void testHandleAirplaneModeChange_InvokeAirplaneModeChanged() {
        mBluetoothAirplaneModeListener.handleAirplaneModeChange();
        verify(mHelper).onAirplaneModeChanged(mBluetoothManagerService);
    }

    @Test
    public void testHandleAirplaneModeChange_NotInvokeAirplaneModeChanged_NotPopToast() {
        mBluetoothAirplaneModeListener.mToastCount = BluetoothAirplaneModeListener.MAX_TOAST_COUNT;
        when(mHelper.isBluetoothOn()).thenReturn(true);
        when(mHelper.isA2dpOrHearingAidConnected()).thenReturn(true);
        when(mHelper.isAirplaneModeOn()).thenReturn(true);
        mBluetoothAirplaneModeListener.handleAirplaneModeChange();

        verify(mHelper).setSettingsInt(Settings.Global.BLUETOOTH_ON,
                BluetoothManagerService.BLUETOOTH_ON_AIRPLANE);
        verify(mHelper, times(0)).showToastMessage();
        verify(mHelper, times(0)).onAirplaneModeChanged(mBluetoothManagerService);
    }

    @Test
    public void testHandleAirplaneModeChange_NotInvokeAirplaneModeChanged_PopToast() {
        mBluetoothAirplaneModeListener.mToastCount = 0;
        when(mHelper.isBluetoothOn()).thenReturn(true);
        when(mHelper.isA2dpOrHearingAidConnected()).thenReturn(true);
        when(mHelper.isAirplaneModeOn()).thenReturn(true);
        mBluetoothAirplaneModeListener.handleAirplaneModeChange();

        verify(mHelper).setSettingsInt(Settings.Global.BLUETOOTH_ON,
                BluetoothManagerService.BLUETOOTH_ON_AIRPLANE);
        verify(mHelper).showToastMessage();
        verify(mHelper, times(0)).onAirplaneModeChanged(mBluetoothManagerService);
    }

    @Test
    public void testIsPopToast_PopToast() {
        mBluetoothAirplaneModeListener.mToastCount = 0;
        Assert.assertTrue(mBluetoothAirplaneModeListener.shouldPopToast());
        verify(mHelper).setSettingsInt(BluetoothAirplaneModeListener.TOAST_COUNT, 1);
    }

    @Test
    public void testIsPopToast_NotPopToast() {
        mBluetoothAirplaneModeListener.mToastCount = BluetoothAirplaneModeListener.MAX_TOAST_COUNT;
        Assert.assertFalse(mBluetoothAirplaneModeListener.shouldPopToast());
        verify(mHelper, times(0)).setSettingsInt(anyString(), anyInt());
    }
}