• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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