diff options
5 files changed, 464 insertions, 57 deletions
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index f78a53e294aa..e15999fb6e0a 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -5398,6 +5398,10 @@ <!-- Description of media type: presentation file, such as PPT. The 'extension' variable is the file name extension. [CHAR LIMIT=32] --> <string name="mime_type_presentation_ext"><xliff:g id="extension" example="PDF">%1$s</xliff:g> presentation</string> + <!-- Strings for Bluetooth service --> + <!-- toast message informing user that Bluetooth stays on after airplane mode is turned on. [CHAR LIMIT=NONE] --> + <string name="bluetooth_airplane_mode_toast">Bluetooth will stay on during airplane mode</string> + <!-- Strings for car --> <!-- String displayed when loading a user in the car [CHAR LIMIT=30] --> <string name="car_loading_profile">Loading</string> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 32749a202a00..39423c55d973 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3803,6 +3803,9 @@ <java-symbol type="string" name="mime_type_presentation" /> <java-symbol type="string" name="mime_type_presentation_ext" /> + <!-- For Bluetooth service --> + <java-symbol type="string" name="bluetooth_airplane_mode_toast" /> + <!-- For high refresh rate displays --> <java-symbol type="integer" name="config_defaultPeakRefreshRate" /> <java-symbol type="integer" name="config_defaultRefreshRateInZone" /> diff --git a/services/core/java/com/android/server/BluetoothAirplaneModeListener.java b/services/core/java/com/android/server/BluetoothAirplaneModeListener.java new file mode 100644 index 000000000000..31cd5d519d87 --- /dev/null +++ b/services/core/java/com/android/server/BluetoothAirplaneModeListener.java @@ -0,0 +1,258 @@ +/* + * 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; + } + }; +} diff --git a/services/core/java/com/android/server/BluetoothManagerService.java b/services/core/java/com/android/server/BluetoothManagerService.java index cdfd310a85ae..fa8eda54e53c 100644 --- a/services/core/java/com/android/server/BluetoothManagerService.java +++ b/services/core/java/com/android/server/BluetoothManagerService.java @@ -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,68 +261,65 @@ class BluetoothManagerService extends IBluetoothManager.Stub { } }; - private final ContentObserver mAirplaneModeObserver = new ContentObserver(null) { - @Override - public void onChange(boolean unused) { - synchronized (this) { - if (isBluetoothPersistedStateOn()) { - if (isAirplaneModeOn()) { - persistBluetoothSetting(BLUETOOTH_ON_AIRPLANE); - } else { - persistBluetoothSetting(BLUETOOTH_ON_BLUETOOTH); - } + public void onAirplaneModeChanged() { + synchronized (this) { + if (isBluetoothPersistedStateOn()) { + if (isAirplaneModeOn()) { + persistBluetoothSetting(BLUETOOTH_ON_AIRPLANE); + } else { + persistBluetoothSetting(BLUETOOTH_ON_BLUETOOTH); } + } - int st = BluetoothAdapter.STATE_OFF; - try { - mBluetoothLock.readLock().lock(); - if (mBluetooth != null) { - st = mBluetooth.getState(); - } - } catch (RemoteException e) { - Slog.e(TAG, "Unable to call getState", e); - return; - } finally { - mBluetoothLock.readLock().unlock(); + int st = BluetoothAdapter.STATE_OFF; + try { + mBluetoothLock.readLock().lock(); + if (mBluetooth != null) { + st = mBluetooth.getState(); } + } catch (RemoteException e) { + Slog.e(TAG, "Unable to call getState", e); + return; + } finally { + mBluetoothLock.readLock().unlock(); + } - Slog.d(TAG, - "Airplane Mode change - current state: " + BluetoothAdapter.nameForState( - st) + ", isAirplaneModeOn()=" + isAirplaneModeOn()); + Slog.d(TAG, + "Airplane Mode change - current state: " + BluetoothAdapter.nameForState( + st) + ", isAirplaneModeOn()=" + isAirplaneModeOn()); - if (isAirplaneModeOn()) { - // Clear registered LE apps to force shut-off - clearBleApps(); + if (isAirplaneModeOn()) { + // Clear registered LE apps to force shut-off + clearBleApps(); - // If state is BLE_ON make sure we trigger disableBLE - if (st == BluetoothAdapter.STATE_BLE_ON) { - try { - mBluetoothLock.readLock().lock(); - if (mBluetooth != null) { - addActiveLog( - BluetoothProtoEnums.ENABLE_DISABLE_REASON_AIRPLANE_MODE, - mContext.getPackageName(), false); - mBluetooth.onBrEdrDown(); - mEnable = false; - mEnableExternal = false; - } - } catch (RemoteException e) { - Slog.e(TAG, "Unable to call onBrEdrDown", e); - } finally { - mBluetoothLock.readLock().unlock(); + // If state is BLE_ON make sure we trigger disableBLE + if (st == BluetoothAdapter.STATE_BLE_ON) { + try { + mBluetoothLock.readLock().lock(); + if (mBluetooth != null) { + addActiveLog( + BluetoothProtoEnums.ENABLE_DISABLE_REASON_AIRPLANE_MODE, + mContext.getPackageName(), false); + mBluetooth.onBrEdrDown(); + mEnable = false; + mEnableExternal = false; } - } else if (st == BluetoothAdapter.STATE_ON) { - sendDisableMsg(BluetoothProtoEnums.ENABLE_DISABLE_REASON_AIRPLANE_MODE, - mContext.getPackageName()); + } catch (RemoteException e) { + Slog.e(TAG, "Unable to call onBrEdrDown", e); + } finally { + mBluetoothLock.readLock().unlock(); } - } else if (mEnableExternal) { - sendEnableMsg(mQuietEnableExternal, - BluetoothProtoEnums.ENABLE_DISABLE_REASON_AIRPLANE_MODE, + } else if (st == BluetoothAdapter.STATE_ON) { + sendDisableMsg(BluetoothProtoEnums.ENABLE_DISABLE_REASON_AIRPLANE_MODE, mContext.getPackageName()); } + } else if (mEnableExternal) { + sendEnableMsg(mQuietEnableExternal, + BluetoothProtoEnums.ENABLE_DISABLE_REASON_AIRPLANE_MODE, + mContext.getPackageName()); } } - }; + } 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 (persist) { - persistBluetoothSetting(BLUETOOTH_OFF); + if (!isBluetoothPersistedStateOnAirplane()) { + if (persist) { + persistBluetoothSetting(BLUETOOTH_OFF); + } + mEnableExternal = false; } - 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)); + } } /** diff --git a/services/tests/servicestests/src/com/android/server/BluetoothAirplaneModeListenerTest.java b/services/tests/servicestests/src/com/android/server/BluetoothAirplaneModeListenerTest.java new file mode 100644 index 000000000000..968a402ff3b7 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/BluetoothAirplaneModeListenerTest.java @@ -0,0 +1,125 @@ +/* + * 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()); + } +} |