1 /*
2  * Copyright (C) 2014 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.telecom;
18 
19 import android.bluetooth.BluetoothDevice;
20 import android.bluetooth.BluetoothHeadset;
21 import android.bluetooth.BluetoothProfile;
22 import android.content.BroadcastReceiver;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.IntentFilter;
26 import android.os.Handler;
27 import android.os.Looper;
28 import android.os.SystemClock;
29 
30 import com.android.internal.annotations.VisibleForTesting;
31 import com.android.internal.util.IndentingPrintWriter;
32 
33 import java.util.List;
34 
35 /**
36  * Listens to and caches bluetooth headset state.  Used By the CallAudioManager for maintaining
37  * overall audio state. Also provides method for connecting the bluetooth headset to the phone call.
38  */
39 public class BluetoothManager {
40     public static final int BLUETOOTH_UNINITIALIZED = 0;
41     public static final int BLUETOOTH_DISCONNECTED = 1;
42     public static final int BLUETOOTH_DEVICE_CONNECTED = 2;
43     public static final int BLUETOOTH_AUDIO_PENDING = 3;
44     public static final int BLUETOOTH_AUDIO_CONNECTED = 4;
45 
46     public interface BluetoothStateListener {
onBluetoothStateChange(int oldState, int newState)47         void onBluetoothStateChange(int oldState, int newState);
48     }
49 
50     private final BluetoothProfile.ServiceListener mBluetoothProfileServiceListener =
51             new BluetoothProfile.ServiceListener() {
52                 @Override
53                 public void onServiceConnected(int profile, BluetoothProfile proxy) {
54                     Log.startSession("BMSL.oSC");
55                     try {
56                         if (profile == BluetoothProfile.HEADSET) {
57                             mBluetoothHeadset = new BluetoothHeadsetProxy((BluetoothHeadset) proxy);
58                             Log.v(this, "- Got BluetoothHeadset: " + mBluetoothHeadset);
59                         } else {
60                             Log.w(this, "Connected to non-headset bluetooth service. Not changing" +
61                                     " bluetooth headset.");
62                         }
63                         updateListenerOfBluetoothState(true);
64                     } finally {
65                         Log.endSession();
66                     }
67                 }
68 
69                 @Override
70                 public void onServiceDisconnected(int profile) {
71                     Log.startSession("BMSL.oSD");
72                     try {
73                         mBluetoothHeadset = null;
74                         Log.v(this, "Lost BluetoothHeadset: " + mBluetoothHeadset);
75                         updateListenerOfBluetoothState(false);
76                     } finally {
77                         Log.endSession();
78                     }
79                 }
80            };
81 
82     /**
83      * Receiver for misc intent broadcasts the BluetoothManager cares about.
84      */
85     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
86         @Override
87         public void onReceive(Context context, Intent intent) {
88             Log.startSession("BM.oR");
89             try {
90                 String action = intent.getAction();
91 
92                 if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
93                     int bluetoothHeadsetState = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
94                             BluetoothHeadset.STATE_DISCONNECTED);
95                     Log.i(this, "mReceiver: HEADSET_STATE_CHANGED_ACTION");
96                     Log.i(this, "==> new state: %s ", bluetoothHeadsetState);
97                     updateListenerOfBluetoothState(
98                             bluetoothHeadsetState == BluetoothHeadset.STATE_CONNECTING);
99                 } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
100                     int bluetoothHeadsetAudioState =
101                             intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
102                                     BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
103                     Log.i(this, "mReceiver: HEADSET_AUDIO_STATE_CHANGED_ACTION");
104                     Log.i(this, "==> new state: %s", bluetoothHeadsetAudioState);
105                     updateListenerOfBluetoothState(
106                             bluetoothHeadsetAudioState ==
107                                     BluetoothHeadset.STATE_AUDIO_CONNECTING
108                             || bluetoothHeadsetAudioState ==
109                                     BluetoothHeadset.STATE_AUDIO_CONNECTED);
110                 }
111             } finally {
112                 Log.endSession();
113             }
114         }
115     };
116 
117     private final Handler mHandler = new Handler(Looper.getMainLooper());
118 
119     private final BluetoothAdapterProxy mBluetoothAdapter;
120     private BluetoothStateListener mBluetoothStateListener;
121 
122     private BluetoothHeadsetProxy mBluetoothHeadset;
123     private long mBluetoothConnectionRequestTime;
124     private final Runnable mBluetoothConnectionTimeout = new Runnable("BM.cBA") {
125         @Override
126         public void loggedRun() {
127             if (!isBluetoothAudioConnected()) {
128                 Log.v(this, "Bluetooth audio inexplicably disconnected within 5 seconds of " +
129                         "connection. Updating UI.");
130             }
131             updateListenerOfBluetoothState(false);
132         }
133     };
134 
135     private final Runnable mRetryConnectAudio = new Runnable("BM.rCA") {
136         @Override
137         public void loggedRun() {
138             Log.i(this, "Retrying connecting to bluetooth audio.");
139             if (!mBluetoothHeadset.connectAudio()) {
140                 Log.w(this, "Retry of bluetooth audio connection failed. Giving up.");
141             } else {
142                 setBluetoothStatePending();
143             }
144         }
145     };
146 
147     private final Context mContext;
148     private int mBluetoothState = BLUETOOTH_UNINITIALIZED;
149 
BluetoothManager(Context context, BluetoothAdapterProxy bluetoothAdapterProxy)150     public BluetoothManager(Context context, BluetoothAdapterProxy bluetoothAdapterProxy) {
151         mBluetoothAdapter = bluetoothAdapterProxy;
152         mContext = context;
153 
154         if (mBluetoothAdapter != null) {
155             mBluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
156                                     BluetoothProfile.HEADSET);
157         }
158 
159         // Register for misc other intent broadcasts.
160         IntentFilter intentFilter =
161                 new IntentFilter(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
162         intentFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
163         context.registerReceiver(mReceiver, intentFilter);
164     }
165 
setBluetoothStateListener(BluetoothStateListener bluetoothStateListener)166     public void setBluetoothStateListener(BluetoothStateListener bluetoothStateListener) {
167         mBluetoothStateListener = bluetoothStateListener;
168     }
169 
170     //
171     // Bluetooth helper methods.
172     //
173     // - BluetoothAdapter is the Bluetooth system service.  If
174     //   getDefaultAdapter() returns null
175     //   then the device is not BT capable.  Use BluetoothDevice.isEnabled()
176     //   to see if BT is enabled on the device.
177     //
178     // - BluetoothHeadset is the API for the control connection to a
179     //   Bluetooth Headset.  This lets you completely connect/disconnect a
180     //   headset (which we don't do from the Phone UI!) but also lets you
181     //   get the address of the currently active headset and see whether
182     //   it's currently connected.
183 
184     /**
185      * @return true if the Bluetooth on/off switch in the UI should be
186      *         available to the user (i.e. if the device is BT-capable
187      *         and a headset is connected.)
188      */
189     @VisibleForTesting
isBluetoothAvailable()190     public boolean isBluetoothAvailable() {
191         Log.v(this, "isBluetoothAvailable()...");
192 
193         // There's no need to ask the Bluetooth system service if BT is enabled:
194         //
195         //    BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
196         //    if ((adapter == null) || !adapter.isEnabled()) {
197         //        Log.d(this, "  ==> FALSE (BT not enabled)");
198         //        return false;
199         //    }
200         //    Log.d(this, "  - BT enabled!  device name " + adapter.getName()
201         //                 + ", address " + adapter.getAddress());
202         //
203         // ...since we already have a BluetoothHeadset instance.  We can just
204         // call isConnected() on that, and assume it'll be false if BT isn't
205         // enabled at all.
206 
207         // Check if there's a connected headset, using the BluetoothHeadset API.
208         boolean isConnected = false;
209         if (mBluetoothHeadset != null) {
210             List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
211 
212             if (deviceList.size() > 0) {
213                 isConnected = true;
214                 for (int i = 0; i < deviceList.size(); i++) {
215                     BluetoothDevice device = deviceList.get(i);
216                     Log.v(this, "state = " + mBluetoothHeadset.getConnectionState(device)
217                             + "for headset: " + device);
218                 }
219             }
220         }
221 
222         Log.v(this, "  ==> " + isConnected);
223         return isConnected;
224     }
225 
226     /**
227      * @return true if a BT Headset is available, and its audio is currently connected.
228      */
229     @VisibleForTesting
isBluetoothAudioConnected()230     public boolean isBluetoothAudioConnected() {
231         if (mBluetoothHeadset == null) {
232             Log.v(this, "isBluetoothAudioConnected: ==> FALSE (null mBluetoothHeadset)");
233             return false;
234         }
235         List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
236 
237         if (deviceList.isEmpty()) {
238             return false;
239         }
240         for (int i = 0; i < deviceList.size(); i++) {
241             BluetoothDevice device = deviceList.get(i);
242             boolean isAudioOn = mBluetoothHeadset.isAudioConnected(device);
243             Log.v(this, "isBluetoothAudioConnected: ==> isAudioOn = " + isAudioOn
244                     + "for headset: " + device);
245             if (isAudioOn) {
246                 return true;
247             }
248         }
249         return false;
250     }
251 
252     /**
253      * Helper method used to control the onscreen "Bluetooth" indication;
254      *
255      * @return true if a BT device is available and its audio is currently connected,
256      *              <b>or</b> if we issued a BluetoothHeadset.connectAudio()
257      *              call within the last 5 seconds (which presumably means
258      *              that the BT audio connection is currently being set
259      *              up, and will be connected soon.)
260      */
261     @VisibleForTesting
isBluetoothAudioConnectedOrPending()262     public boolean isBluetoothAudioConnectedOrPending() {
263         if (isBluetoothAudioConnected()) {
264             Log.v(this, "isBluetoothAudioConnectedOrPending: ==> TRUE (really connected)");
265             return true;
266         }
267 
268         // If we issued a connectAudio() call "recently enough", even
269         // if BT isn't actually connected yet, let's still pretend BT is
270         // on.  This makes the onscreen indication more responsive.
271         if (isBluetoothAudioPending()) {
272             long timeSinceRequest =
273                     SystemClock.elapsedRealtime() - mBluetoothConnectionRequestTime;
274             Log.v(this, "isBluetoothAudioConnectedOrPending: ==> TRUE (requested "
275                     + timeSinceRequest + " msec ago)");
276             return true;
277         }
278 
279         Log.v(this, "isBluetoothAudioConnectedOrPending: ==> FALSE");
280         return false;
281     }
282 
isBluetoothAudioPending()283     private boolean isBluetoothAudioPending() {
284         return mBluetoothState == BLUETOOTH_AUDIO_PENDING;
285     }
286 
287     /**
288      * Notified audio manager of a change to the bluetooth state.
289      */
updateListenerOfBluetoothState(boolean canBePending)290     private void updateListenerOfBluetoothState(boolean canBePending) {
291         int newState;
292         if (isBluetoothAudioConnected()) {
293             newState = BLUETOOTH_AUDIO_CONNECTED;
294         } else if (canBePending && isBluetoothAudioPending()) {
295             newState = BLUETOOTH_AUDIO_PENDING;
296         } else if (isBluetoothAvailable()) {
297             newState = BLUETOOTH_DEVICE_CONNECTED;
298         } else {
299             newState = BLUETOOTH_DISCONNECTED;
300         }
301         if (mBluetoothState != newState) {
302             mBluetoothStateListener.onBluetoothStateChange(mBluetoothState, newState);
303             mBluetoothState = newState;
304         }
305     }
306 
307     @VisibleForTesting
connectBluetoothAudio()308     public void connectBluetoothAudio() {
309         Log.v(this, "connectBluetoothAudio()...");
310         if (mBluetoothHeadset != null) {
311             if (!mBluetoothHeadset.connectAudio()) {
312                 mHandler.postDelayed(mRetryConnectAudio.prepare(),
313                         Timeouts.getRetryBluetoothConnectAudioBackoffMillis(
314                                 mContext.getContentResolver()));
315             }
316         }
317         // The call to connectAudio is asynchronous and may take some time to complete. However,
318         // if connectAudio() returns false, we know that it has failed and therefore will
319         // schedule a retry to happen some time later. We set bluetooth state to pending now and
320         // show bluetooth as connected in the UI, but confirmation that we are connected will
321         // arrive through mReceiver.
322         setBluetoothStatePending();
323     }
324 
setBluetoothStatePending()325     private void setBluetoothStatePending() {
326         mBluetoothState = BLUETOOTH_AUDIO_PENDING;
327         mBluetoothConnectionRequestTime = SystemClock.elapsedRealtime();
328         mHandler.removeCallbacks(mBluetoothConnectionTimeout.getRunnableToCancel());
329         mBluetoothConnectionTimeout.cancel();
330         // If the mBluetoothConnectionTimeout runnable has run, the session had been cleared...
331         // Create a new Session before putting it back in the queue to possibly run again.
332         mHandler.postDelayed(mBluetoothConnectionTimeout.prepare(),
333                 Timeouts.getBluetoothPendingTimeoutMillis(mContext.getContentResolver()));
334     }
335 
336     @VisibleForTesting
disconnectBluetoothAudio()337     public void disconnectBluetoothAudio() {
338         Log.v(this, "disconnectBluetoothAudio()...");
339         if (mBluetoothHeadset != null) {
340             mBluetoothState = BLUETOOTH_DEVICE_CONNECTED;
341             mBluetoothHeadset.disconnectAudio();
342         } else {
343             mBluetoothState = BLUETOOTH_DISCONNECTED;
344         }
345         mHandler.removeCallbacks(mBluetoothConnectionTimeout.getRunnableToCancel());
346         mBluetoothConnectionTimeout.cancel();
347     }
348 
349     /**
350      * Dumps the state of the {@link BluetoothManager}.
351      *
352      * @param pw The {@code IndentingPrintWriter} to write the state to.
353      */
dump(IndentingPrintWriter pw)354     public void dump(IndentingPrintWriter pw) {
355         pw.println("isBluetoothAvailable: " + isBluetoothAvailable());
356         pw.println("isBluetoothAudioConnected: " + isBluetoothAudioConnected());
357         pw.println("isBluetoothAudioConnectedOrPending: " + isBluetoothAudioConnectedOrPending());
358 
359         if (mBluetoothAdapter != null) {
360             if (mBluetoothHeadset != null) {
361                 List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
362 
363                 if (deviceList.size() > 0) {
364                     BluetoothDevice device = deviceList.get(0);
365                     pw.println("BluetoothHeadset.getCurrentDevice: " + device);
366                     pw.println("BluetoothHeadset.State: "
367                             + mBluetoothHeadset.getConnectionState(device));
368                     pw.println("BluetoothHeadset audio connected: " +
369                             mBluetoothHeadset.isAudioConnected(device));
370                 }
371             } else {
372                 pw.println("mBluetoothHeadset is null");
373             }
374         } else {
375             pw.println("mBluetoothAdapter is null; device is not BT capable");
376         }
377     }
378 
379     /**
380      * Set the bluetooth headset proxy for testing purposes.
381      * @param bluetoothHeadsetProxy
382      */
383     @VisibleForTesting
setBluetoothHeadsetForTesting(BluetoothHeadsetProxy bluetoothHeadsetProxy)384     public void setBluetoothHeadsetForTesting(BluetoothHeadsetProxy bluetoothHeadsetProxy) {
385         mBluetoothHeadset = bluetoothHeadsetProxy;
386     }
387 
388     /**
389      * Set mBluetoothState for testing.
390      * @param state
391      */
392     @VisibleForTesting
setInternalBluetoothState(int state)393     public void setInternalBluetoothState(int state) {
394         mBluetoothState = state;
395     }
396 }
397