1 /* 2 * Copyright 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.bluetooth; 18 19 import android.annotation.RequiresPermission; 20 import android.content.Context; 21 import android.content.pm.PackageManager; 22 import android.content.res.Resources; 23 import android.database.ContentObserver; 24 import android.os.Handler; 25 import android.os.Looper; 26 import android.os.Message; 27 import android.os.SystemClock; 28 import android.provider.Settings; 29 import android.util.Log; 30 31 import com.android.bluetooth.BluetoothStatsLog; 32 import com.android.internal.annotations.VisibleForTesting; 33 34 /** 35 * The BluetoothAirplaneModeListener handles system airplane mode change callback and checks 36 * whether we need to inform BluetoothManagerService on this change. 37 * 38 * The information of airplane mode turns on would not be passed to the BluetoothManagerService 39 * when Bluetooth is on and Bluetooth is in one of the following situations: 40 * 1. Bluetooth A2DP is connected. 41 * 2. Bluetooth Hearing Aid profile is connected. 42 * 3. Bluetooth LE Audio is connected 43 */ 44 public class BluetoothAirplaneModeListener { 45 private static final String TAG = "BluetoothAirplaneModeListener"; 46 @VisibleForTesting static final String TOAST_COUNT = "bluetooth_airplane_toast_count"; 47 48 // keeps track of whether wifi should remain on in airplane mode 49 public static final String WIFI_APM_STATE = "wifi_apm_state"; 50 // keeps track of whether wifi and bt remains on notification was shown 51 public static final String APM_WIFI_BT_NOTIFICATION = "apm_wifi_bt_notification"; 52 // keeps track of whether bt remains on notification was shown 53 public static final String APM_BT_NOTIFICATION = "apm_bt_notification"; 54 // keeps track of whether airplane mode enhancement feature is enabled 55 public static final String APM_ENHANCEMENT = "apm_enhancement_enabled"; 56 // keeps track of whether user changed bt state in airplane mode 57 public static final String APM_USER_TOGGLED_BLUETOOTH = "apm_user_toggled_bluetooth"; 58 // keeps track of whether bt should remain on in airplane mode 59 public static final String BLUETOOTH_APM_STATE = "bluetooth_apm_state"; 60 // keeps track of whether user enabling bt notification was shown 61 public static final String APM_BT_ENABLED_NOTIFICATION = "apm_bt_enabled_notification"; 62 63 private static final int MSG_AIRPLANE_MODE_CHANGED = 0; 64 public static final int NOTIFICATION_NOT_SHOWN = 0; 65 public static final int NOTIFICATION_SHOWN = 1; 66 public static final int UNUSED = 0; 67 public static final int USED = 1; 68 69 @VisibleForTesting static final int MAX_TOAST_COUNT = 10; // 10 times 70 71 /* Tracks the bluetooth state before entering airplane mode*/ 72 private boolean mIsBluetoothOnBeforeApmToggle = false; 73 /* Tracks the bluetooth state after entering airplane mode*/ 74 private boolean mIsBluetoothOnAfterApmToggle = false; 75 /* Tracks whether user toggled bluetooth in airplane mode */ 76 private boolean mUserToggledBluetoothDuringApm = false; 77 /* Tracks whether user toggled bluetooth in airplane mode within one minute */ 78 private boolean mUserToggledBluetoothDuringApmWithinMinute = false; 79 /* Tracks whether media profile was connected before entering airplane mode */ 80 private boolean mIsMediaProfileConnectedBeforeApmToggle = false; 81 /* Tracks when airplane mode has been enabled */ 82 private long mApmEnabledTime = 0; 83 84 private final BluetoothManagerService mBluetoothManager; 85 private final BluetoothAirplaneModeHandler mHandler; 86 private final Context mContext; 87 private BluetoothModeChangeHelper mAirplaneHelper; 88 private BluetoothNotificationManager mNotificationManager; 89 90 @VisibleForTesting int mToastCount = 0; 91 BluetoothAirplaneModeListener(BluetoothManagerService service, Looper looper, Context context, BluetoothNotificationManager notificationManager)92 BluetoothAirplaneModeListener(BluetoothManagerService service, Looper looper, Context context, 93 BluetoothNotificationManager notificationManager) { 94 mBluetoothManager = service; 95 mNotificationManager = notificationManager; 96 mContext = context; 97 98 mHandler = new BluetoothAirplaneModeHandler(looper); 99 context.getContentResolver().registerContentObserver( 100 Settings.Global.getUriFor(Settings.Global.AIRPLANE_MODE_ON), true, 101 mAirplaneModeObserver); 102 } 103 104 private final ContentObserver mAirplaneModeObserver = new ContentObserver(null) { 105 @Override 106 public void onChange(boolean unused) { 107 // Post from system main thread to android_io thread. 108 Message msg = mHandler.obtainMessage(MSG_AIRPLANE_MODE_CHANGED); 109 mHandler.sendMessage(msg); 110 } 111 }; 112 113 private class BluetoothAirplaneModeHandler extends Handler { BluetoothAirplaneModeHandler(Looper looper)114 BluetoothAirplaneModeHandler(Looper looper) { 115 super(looper); 116 } 117 118 @Override handleMessage(Message msg)119 public void handleMessage(Message msg) { 120 switch (msg.what) { 121 case MSG_AIRPLANE_MODE_CHANGED: 122 handleAirplaneModeChange(); 123 break; 124 default: 125 Log.e(TAG, "Invalid message: " + msg.what); 126 break; 127 } 128 } 129 } 130 131 /** 132 * Call after boot complete 133 */ 134 @VisibleForTesting start(BluetoothModeChangeHelper helper)135 void start(BluetoothModeChangeHelper helper) { 136 Log.i(TAG, "start"); 137 mAirplaneHelper = helper; 138 mToastCount = mAirplaneHelper.getSettingsInt(TOAST_COUNT); 139 } 140 141 @VisibleForTesting shouldPopToast()142 boolean shouldPopToast() { 143 if (mToastCount >= MAX_TOAST_COUNT) { 144 return false; 145 } 146 mToastCount++; 147 mAirplaneHelper.setSettingsInt(TOAST_COUNT, mToastCount); 148 return true; 149 } 150 151 @VisibleForTesting 152 @RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED) handleAirplaneModeChange()153 void handleAirplaneModeChange() { 154 if (mAirplaneHelper == null) { 155 return; 156 } 157 if (mAirplaneHelper.isAirplaneModeOn()) { 158 mApmEnabledTime = SystemClock.elapsedRealtime(); 159 mIsBluetoothOnBeforeApmToggle = mAirplaneHelper.isBluetoothOn(); 160 mIsBluetoothOnAfterApmToggle = shouldSkipAirplaneModeChange(); 161 mIsMediaProfileConnectedBeforeApmToggle = mAirplaneHelper.isMediaProfileConnected(); 162 if (mIsBluetoothOnAfterApmToggle) { 163 Log.i(TAG, "Ignore airplane mode change"); 164 // Airplane mode enabled when Bluetooth is being used for audio/headering aid. 165 // Bluetooth is not disabled in such case, only state is changed to 166 // BLUETOOTH_ON_AIRPLANE mode. 167 mAirplaneHelper.setSettingsInt(Settings.Global.BLUETOOTH_ON, 168 BluetoothManagerService.BLUETOOTH_ON_AIRPLANE); 169 if (!isApmEnhancementEnabled() || !isBluetoothToggledOnApm()) { 170 if (shouldPopToast()) { 171 mAirplaneHelper.showToastMessage(); 172 } 173 } else { 174 if (isWifiEnabledOnApm() && isFirstTimeNotification(APM_WIFI_BT_NOTIFICATION)) { 175 try { 176 sendApmNotification("bluetooth_and_wifi_stays_on_title", 177 "bluetooth_and_wifi_stays_on_message", 178 APM_WIFI_BT_NOTIFICATION); 179 } catch (Exception e) { 180 Log.e(TAG, 181 "APM enhancement BT and Wi-Fi stays on notification not shown"); 182 } 183 } else if (!isWifiEnabledOnApm() && isFirstTimeNotification( 184 APM_BT_NOTIFICATION)) { 185 try { 186 sendApmNotification("bluetooth_stays_on_title", 187 "bluetooth_stays_on_message", 188 APM_BT_NOTIFICATION); 189 } catch (Exception e) { 190 Log.e(TAG, "APM enhancement BT stays on notification not shown"); 191 } 192 } 193 } 194 return; 195 } 196 } else { 197 BluetoothStatsLog.write(BluetoothStatsLog.AIRPLANE_MODE_SESSION_REPORTED, 198 BluetoothStatsLog.AIRPLANE_MODE_SESSION_REPORTED__PACKAGE_NAME__BLUETOOTH, 199 mIsBluetoothOnBeforeApmToggle, 200 mIsBluetoothOnAfterApmToggle, 201 mAirplaneHelper.isBluetoothOn(), 202 isBluetoothToggledOnApm(), 203 mUserToggledBluetoothDuringApm, 204 mUserToggledBluetoothDuringApmWithinMinute, 205 mIsMediaProfileConnectedBeforeApmToggle); 206 mUserToggledBluetoothDuringApm = false; 207 mUserToggledBluetoothDuringApmWithinMinute = false; 208 } 209 mAirplaneHelper.onAirplaneModeChanged(mBluetoothManager); 210 } 211 212 @VisibleForTesting shouldSkipAirplaneModeChange()213 boolean shouldSkipAirplaneModeChange() { 214 boolean apmEnhancementUsed = isApmEnhancementEnabled() && isBluetoothToggledOnApm(); 215 216 // APM feature disabled or user has not used the feature yet by changing BT state in APM 217 // BT will only remain on in APM when media profile is connected 218 if (!apmEnhancementUsed && mAirplaneHelper.isBluetoothOn() 219 && mAirplaneHelper.isMediaProfileConnected()) { 220 return true; 221 } 222 // APM feature enabled and user has used the feature by changing BT state in APM 223 // BT will only remain on in APM based on user's last action in APM 224 if (apmEnhancementUsed && mAirplaneHelper.isBluetoothOn() 225 && mAirplaneHelper.isBluetoothOnAPM()) { 226 return true; 227 } 228 // APM feature enabled and user has not used the feature yet by changing BT state in APM 229 // BT will only remain on in APM if the default value is set to on 230 if (isApmEnhancementEnabled() && !isBluetoothToggledOnApm() 231 && mAirplaneHelper.isBluetoothOn() 232 && mAirplaneHelper.isBluetoothOnAPM()) { 233 return true; 234 } 235 return false; 236 } 237 isApmEnhancementEnabled()238 private boolean isApmEnhancementEnabled() { 239 return mAirplaneHelper.getSettingsInt(APM_ENHANCEMENT) == 1; 240 } 241 isBluetoothToggledOnApm()242 private boolean isBluetoothToggledOnApm() { 243 return mAirplaneHelper.getSettingsSecureInt(APM_USER_TOGGLED_BLUETOOTH, UNUSED) == USED; 244 } 245 isWifiEnabledOnApm()246 private boolean isWifiEnabledOnApm() { 247 return mAirplaneHelper.getSettingsInt(Settings.Global.WIFI_ON) != 0 248 && mAirplaneHelper.getSettingsSecureInt(WIFI_APM_STATE, 0) == 1; 249 } 250 isFirstTimeNotification(String name)251 private boolean isFirstTimeNotification(String name) { 252 return mAirplaneHelper.getSettingsSecureInt( 253 name, NOTIFICATION_NOT_SHOWN) == NOTIFICATION_NOT_SHOWN; 254 } 255 256 /** 257 * Helper method to send APM notification 258 */ sendApmNotification(String titleId, String messageId, String notificationState)259 public void sendApmNotification(String titleId, String messageId, String notificationState) 260 throws PackageManager.NameNotFoundException { 261 String btPackageName = mAirplaneHelper.getBluetoothPackageName(); 262 if (btPackageName == null) { 263 Log.e(TAG, "Unable to find Bluetooth package name with " 264 + "APM notification resources"); 265 return; 266 } 267 Resources resources = mContext.getPackageManager() 268 .getResourcesForApplication(btPackageName); 269 int title = resources.getIdentifier(titleId, "string", btPackageName); 270 int message = resources.getIdentifier(messageId, "string", btPackageName); 271 mNotificationManager.sendApmNotification( 272 resources.getString(title), resources.getString(message)); 273 mAirplaneHelper.setSettingsSecureInt(notificationState, 274 NOTIFICATION_SHOWN); 275 } 276 277 /** 278 * Helper method to update whether user toggled Bluetooth in airplane mode 279 */ updateBluetoothToggledTime()280 public void updateBluetoothToggledTime() { 281 if (!mUserToggledBluetoothDuringApm) { 282 mUserToggledBluetoothDuringApmWithinMinute = 283 SystemClock.elapsedRealtime() - mApmEnabledTime < 60000; 284 } 285 mUserToggledBluetoothDuringApm = true; 286 } 287 } 288