Home | History | Annotate | Download | only in telecom
      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 {
     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", null /*lock*/) {
    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", null /*lock*/) {
    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 
    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 
    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
    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
    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
    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 
    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      */
    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
    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 
    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
    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      */
    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
    384     public void setBluetoothHeadsetForTesting(BluetoothHeadsetProxy bluetoothHeadsetProxy) {
    385         mBluetoothHeadset = bluetoothHeadsetProxy;
    386     }
    387 
    388     /**
    389      * Set mBluetoothState for testing.
    390      * @param state
    391      */
    392     @VisibleForTesting
    393     public void setInternalBluetoothState(int state) {
    394         mBluetoothState = state;
    395     }
    396 }
    397