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