blob: 31cd5d519d87ffeb7b66b20b0aa71791b10000e7 [file] [log] [blame]
Ugo Yufd8efab2019-10-16 20:27:23 +08001/*
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
17package com.android.server;
18
19import android.bluetooth.BluetoothA2dp;
20import android.bluetooth.BluetoothAdapter;
21import android.bluetooth.BluetoothHearingAid;
22import android.bluetooth.BluetoothProfile;
23import android.bluetooth.BluetoothProfile.ServiceListener;
24import android.content.Context;
25import android.content.res.Resources;
26import android.database.ContentObserver;
27import android.os.Handler;
28import android.os.Looper;
29import android.os.Message;
30import android.provider.Settings;
31import android.util.Log;
32import android.widget.Toast;
33
34import com.android.internal.R;
35import com.android.internal.annotations.VisibleForTesting;
36
37/**
38 * The BluetoothAirplaneModeListener handles system airplane mode change callback and checks
39 * whether we need to inform BluetoothManagerService on this change.
40 *
41 * The information of airplane mode turns on would not be passed to the BluetoothManagerService
42 * when Bluetooth is on and Bluetooth is in one of the following situations:
43 * 1. Bluetooth A2DP is connected.
44 * 2. Bluetooth Hearing Aid profile is connected.
45 */
46class BluetoothAirplaneModeListener {
47 private static final String TAG = "BluetoothAirplaneModeListener";
48 @VisibleForTesting static final String TOAST_COUNT = "bluetooth_airplane_toast_count";
49
50 private static final int MSG_AIRPLANE_MODE_CHANGED = 0;
51
52 @VisibleForTesting static final int MAX_TOAST_COUNT = 10; // 10 times
53
54 private final BluetoothManagerService mBluetoothManager;
55 private final BluetoothAirplaneModeHandler mHandler;
56 private AirplaneModeHelper mAirplaneHelper;
57
58 @VisibleForTesting int mToastCount = 0;
59
60 BluetoothAirplaneModeListener(BluetoothManagerService service, Looper looper, Context context) {
61 mBluetoothManager = service;
62
63 mHandler = new BluetoothAirplaneModeHandler(looper);
64 context.getContentResolver().registerContentObserver(
65 Settings.Global.getUriFor(Settings.Global.AIRPLANE_MODE_ON), true,
66 mAirplaneModeObserver);
67 }
68
69 private final ContentObserver mAirplaneModeObserver = new ContentObserver(null) {
70 @Override
71 public void onChange(boolean unused) {
72 // Post from system main thread to android_io thread.
73 Message msg = mHandler.obtainMessage(MSG_AIRPLANE_MODE_CHANGED);
74 mHandler.sendMessage(msg);
75 }
76 };
77
78 private class BluetoothAirplaneModeHandler extends Handler {
79 BluetoothAirplaneModeHandler(Looper looper) {
80 super(looper);
81 }
82
83 @Override
84 public void handleMessage(Message msg) {
85 switch (msg.what) {
86 case MSG_AIRPLANE_MODE_CHANGED:
87 handleAirplaneModeChange();
88 break;
89 default:
90 Log.e(TAG, "Invalid message: " + msg.what);
91 break;
92 }
93 }
94 }
95
96 /**
97 * Call after boot complete
98 */
99 @VisibleForTesting
100 void start(AirplaneModeHelper helper) {
101 Log.i(TAG, "start");
102 mAirplaneHelper = helper;
103 mToastCount = mAirplaneHelper.getSettingsInt(TOAST_COUNT);
104 }
105
106 @VisibleForTesting
107 boolean shouldPopToast() {
108 if (mToastCount >= MAX_TOAST_COUNT) {
109 return false;
110 }
111 mToastCount++;
112 mAirplaneHelper.setSettingsInt(TOAST_COUNT, mToastCount);
113 return true;
114 }
115
116 @VisibleForTesting
117 void handleAirplaneModeChange() {
118 if (shouldSkipAirplaneModeChange()) {
119 Log.i(TAG, "Ignore airplane mode change");
120 // We have to store Bluetooth state here, so if user turns off Bluetooth
121 // after airplane mode is turned on, we don't forget to turn on Bluetooth
122 // when airplane mode turns off.
123 mAirplaneHelper.setSettingsInt(Settings.Global.BLUETOOTH_ON,
124 BluetoothManagerService.BLUETOOTH_ON_AIRPLANE);
125 if (shouldPopToast()) {
126 mAirplaneHelper.showToastMessage();
127 }
128 return;
129 }
130 mAirplaneHelper.onAirplaneModeChanged(mBluetoothManager);
131 }
132
133 @VisibleForTesting
134 boolean shouldSkipAirplaneModeChange() {
135 if (mAirplaneHelper == null) {
136 return false;
137 }
138 if (!mAirplaneHelper.isBluetoothOn() || !mAirplaneHelper.isAirplaneModeOn()
139 || !mAirplaneHelper.isA2dpOrHearingAidConnected()) {
140 return false;
141 }
142 return true;
143 }
144
145 /**
146 * Helper class that handles callout and callback methods without
147 * complex logic.
148 */
149 @VisibleForTesting
150 public static class AirplaneModeHelper {
151 private volatile BluetoothA2dp mA2dp;
152 private volatile BluetoothHearingAid mHearingAid;
153 private final BluetoothAdapter mAdapter;
154 private final Context mContext;
155
156 AirplaneModeHelper(Context context) {
157 mAdapter = BluetoothAdapter.getDefaultAdapter();
158 mContext = context;
159
160 mAdapter.getProfileProxy(mContext, mProfileServiceListener, BluetoothProfile.A2DP);
161 mAdapter.getProfileProxy(mContext, mProfileServiceListener,
162 BluetoothProfile.HEARING_AID);
163 }
164
165 private final ServiceListener mProfileServiceListener = new ServiceListener() {
166 @Override
167 public void onServiceConnected(int profile, BluetoothProfile proxy) {
168 // Setup Bluetooth profile proxies
169 switch (profile) {
170 case BluetoothProfile.A2DP:
171 mA2dp = (BluetoothA2dp) proxy;
172 break;
173 case BluetoothProfile.HEARING_AID:
174 mHearingAid = (BluetoothHearingAid) proxy;
175 break;
176 default:
177 break;
178 }
179 }
180
181 @Override
182 public void onServiceDisconnected(int profile) {
183 // Clear Bluetooth profile proxies
184 switch (profile) {
185 case BluetoothProfile.A2DP:
186 mA2dp = null;
187 break;
188 case BluetoothProfile.HEARING_AID:
189 mHearingAid = null;
190 break;
191 default:
192 break;
193 }
194 }
195 };
196
197 @VisibleForTesting
198 public boolean isA2dpOrHearingAidConnected() {
199 return isA2dpConnected() || isHearingAidConnected();
200 }
201
202 @VisibleForTesting
203 public boolean isBluetoothOn() {
204 final BluetoothAdapter adapter = mAdapter;
205 if (adapter == null) {
206 return false;
207 }
208 return adapter.getLeState() == BluetoothAdapter.STATE_ON;
209 }
210
211 @VisibleForTesting
212 public boolean isAirplaneModeOn() {
213 return Settings.Global.getInt(mContext.getContentResolver(),
214 Settings.Global.AIRPLANE_MODE_ON, 0) == 1;
215 }
216
217 @VisibleForTesting
218 public void onAirplaneModeChanged(BluetoothManagerService managerService) {
219 managerService.onAirplaneModeChanged();
220 }
221
222 @VisibleForTesting
223 public int getSettingsInt(String name) {
224 return Settings.Global.getInt(mContext.getContentResolver(),
225 name, 0);
226 }
227
228 @VisibleForTesting
229 public void setSettingsInt(String name, int value) {
230 Settings.Global.putInt(mContext.getContentResolver(),
231 name, value);
232 }
233
234 @VisibleForTesting
235 public void showToastMessage() {
236 Resources r = mContext.getResources();
237 final CharSequence text = r.getString(
238 R.string.bluetooth_airplane_mode_toast, 0);
239 Toast.makeText(mContext, text, Toast.LENGTH_LONG).show();
240 }
241
242 private boolean isA2dpConnected() {
243 final BluetoothA2dp a2dp = mA2dp;
244 if (a2dp == null) {
245 return false;
246 }
247 return a2dp.getConnectedDevices().size() > 0;
248 }
249
250 private boolean isHearingAidConnected() {
251 final BluetoothHearingAid hearingAid = mHearingAid;
252 if (hearingAid == null) {
253 return false;
254 }
255 return hearingAid.getConnectedDevices().size() > 0;
256 }
257 };
258}