Home | History | Annotate | Download | only in phone
      1 /*
      2  * Copyright (C) 2013 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.phone;
     18 
     19 import com.google.android.collect.Lists;
     20 import com.google.common.base.Preconditions;
     21 
     22 import android.bluetooth.BluetoothAdapter;
     23 import android.bluetooth.BluetoothDevice;
     24 import android.bluetooth.BluetoothHeadset;
     25 import android.bluetooth.BluetoothProfile;
     26 import android.content.BroadcastReceiver;
     27 import android.content.Context;
     28 import android.content.Intent;
     29 import android.content.IntentFilter;
     30 import android.os.SystemClock;
     31 import android.os.SystemProperties;
     32 import android.util.Log;
     33 
     34 import com.android.internal.telephony.CallManager;
     35 import com.android.internal.telephony.Connection;
     36 import com.android.services.telephony.common.Call;
     37 
     38 import java.util.List;
     39 
     40 /**
     41  * Listens to and caches bluetooth headset state.  Used By the AudioRouter for maintaining
     42  * overall audio state for use in the UI layer. Also provides method for connecting the bluetooth
     43  * headset to the phone call.
     44  */
     45 public class BluetoothManager implements CallModeler.Listener {
     46     private static final String LOG_TAG = BluetoothManager.class.getSimpleName();
     47     private static final boolean DBG =
     48             (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
     49     private static final boolean VDBG = (PhoneGlobals.DBG_LEVEL >= 2);
     50 
     51     private final BluetoothAdapter mBluetoothAdapter;
     52     private final CallManager mCallManager;
     53     private final Context mContext;
     54     private final CallModeler mCallModeler;
     55 
     56     private BluetoothHeadset mBluetoothHeadset;
     57     private int mBluetoothHeadsetState = BluetoothProfile.STATE_DISCONNECTED;
     58     private int mBluetoothHeadsetAudioState = BluetoothHeadset.STATE_AUDIO_DISCONNECTED;
     59     private boolean mShowBluetoothIndication = false;
     60     private boolean mBluetoothConnectionPending = false;
     61     private long mBluetoothConnectionRequestTime;
     62 
     63     // Broadcast receiver for various intent broadcasts (see onCreate())
     64     private final BroadcastReceiver mReceiver = new BluetoothBroadcastReceiver();
     65 
     66     private final List<BluetoothIndicatorListener> mListeners = Lists.newArrayList();
     67 
     68     public BluetoothManager(Context context, CallManager callManager, CallModeler callModeler) {
     69         mContext = context;
     70         mCallManager = callManager;
     71         mCallModeler = callModeler;
     72         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
     73 
     74         init(mContext);
     75     }
     76 
     77     /* package */ boolean isBluetoothHeadsetAudioOn() {
     78         return (mBluetoothHeadsetAudioState != BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
     79     }
     80 
     81     //
     82     // Bluetooth helper methods.
     83     //
     84     // - BluetoothAdapter is the Bluetooth system service.  If
     85     //   getDefaultAdapter() returns null
     86     //   then the device is not BT capable.  Use BluetoothDevice.isEnabled()
     87     //   to see if BT is enabled on the device.
     88     //
     89     // - BluetoothHeadset is the API for the control connection to a
     90     //   Bluetooth Headset.  This lets you completely connect/disconnect a
     91     //   headset (which we don't do from the Phone UI!) but also lets you
     92     //   get the address of the currently active headset and see whether
     93     //   it's currently connected.
     94 
     95     /**
     96      * @return true if the Bluetooth on/off switch in the UI should be
     97      *         available to the user (i.e. if the device is BT-capable
     98      *         and a headset is connected.)
     99      */
    100     /* package */ boolean isBluetoothAvailable() {
    101         if (VDBG) log("isBluetoothAvailable()...");
    102 
    103         // There's no need to ask the Bluetooth system service if BT is enabled:
    104         //
    105         //    BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
    106         //    if ((adapter == null) || !adapter.isEnabled()) {
    107         //        if (DBG) log("  ==> FALSE (BT not enabled)");
    108         //        return false;
    109         //    }
    110         //    if (DBG) log("  - BT enabled!  device name " + adapter.getName()
    111         //                 + ", address " + adapter.getAddress());
    112         //
    113         // ...since we already have a BluetoothHeadset instance.  We can just
    114         // call isConnected() on that, and assume it'll be false if BT isn't
    115         // enabled at all.
    116 
    117         // Check if there's a connected headset, using the BluetoothHeadset API.
    118         boolean isConnected = false;
    119         if (mBluetoothHeadset != null) {
    120             List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
    121 
    122             if (deviceList.size() > 0) {
    123                 BluetoothDevice device = deviceList.get(0);
    124                 isConnected = true;
    125 
    126                 if (VDBG) log("  - headset state = " +
    127                               mBluetoothHeadset.getConnectionState(device));
    128                 if (VDBG) log("  - headset address: " + device);
    129                 if (VDBG) log("  - isConnected: " + isConnected);
    130             }
    131         }
    132 
    133         if (VDBG) log("  ==> " + isConnected);
    134         return isConnected;
    135     }
    136 
    137     /**
    138      * @return true if a BT Headset is available, and its audio is currently connected.
    139      */
    140     /* package */ boolean isBluetoothAudioConnected() {
    141         if (mBluetoothHeadset == null) {
    142             if (VDBG) log("isBluetoothAudioConnected: ==> FALSE (null mBluetoothHeadset)");
    143             return false;
    144         }
    145         List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
    146 
    147         if (deviceList.isEmpty()) {
    148             return false;
    149         }
    150         BluetoothDevice device = deviceList.get(0);
    151         boolean isAudioOn = mBluetoothHeadset.isAudioConnected(device);
    152         if (VDBG) log("isBluetoothAudioConnected: ==> isAudioOn = " + isAudioOn);
    153         return isAudioOn;
    154     }
    155 
    156     /**
    157      * Helper method used to control the onscreen "Bluetooth" indication;
    158      * see InCallControlState.bluetoothIndicatorOn.
    159      *
    160      * @return true if a BT device is available and its audio is currently connected,
    161      *              <b>or</b> if we issued a BluetoothHeadset.connectAudio()
    162      *              call within the last 5 seconds (which presumably means
    163      *              that the BT audio connection is currently being set
    164      *              up, and will be connected soon.)
    165      */
    166     /* package */ boolean isBluetoothAudioConnectedOrPending() {
    167         if (isBluetoothAudioConnected()) {
    168             if (VDBG) log("isBluetoothAudioConnectedOrPending: ==> TRUE (really connected)");
    169             return true;
    170         }
    171 
    172         // If we issued a connectAudio() call "recently enough", even
    173         // if BT isn't actually connected yet, let's still pretend BT is
    174         // on.  This makes the onscreen indication more responsive.
    175         if (mBluetoothConnectionPending) {
    176             long timeSinceRequest =
    177                     SystemClock.elapsedRealtime() - mBluetoothConnectionRequestTime;
    178             if (timeSinceRequest < 5000 /* 5 seconds */) {
    179                 if (VDBG) log("isBluetoothAudioConnectedOrPending: ==> TRUE (requested "
    180                              + timeSinceRequest + " msec ago)");
    181                 return true;
    182             } else {
    183                 if (VDBG) log("isBluetoothAudioConnectedOrPending: ==> FALSE (request too old: "
    184                              + timeSinceRequest + " msec ago)");
    185                 mBluetoothConnectionPending = false;
    186                 return false;
    187             }
    188         }
    189 
    190         if (VDBG) log("isBluetoothAudioConnectedOrPending: ==> FALSE");
    191         return false;
    192     }
    193 
    194     /**
    195      * @return true if the onscreen UI should currently be showing the
    196      * special "bluetooth is active" indication in a couple of places (in
    197      * which UI elements turn blue and/or show the bluetooth logo.)
    198      *
    199      * This depends on the BluetoothHeadset state *and* the current
    200      * telephony state; see shouldShowBluetoothIndication().
    201      *
    202      * @see CallCard
    203      * @see NotificationMgr.updateInCallNotification
    204      */
    205     /* package */ boolean showBluetoothIndication() {
    206         return mShowBluetoothIndication;
    207     }
    208 
    209     /**
    210      * Recomputes the mShowBluetoothIndication flag based on the current
    211      * bluetooth state and current telephony state.
    212      *
    213      * This needs to be called any time the bluetooth headset state or the
    214      * telephony state changes.
    215      */
    216     /* package */ void updateBluetoothIndication() {
    217         mShowBluetoothIndication = shouldShowBluetoothIndication(mBluetoothHeadsetState,
    218                                                                  mBluetoothHeadsetAudioState,
    219                                                                  mCallManager);
    220 
    221         notifyListeners(mShowBluetoothIndication);
    222     }
    223 
    224     public void addBluetoothIndicatorListener(BluetoothIndicatorListener listener) {
    225         if (!mListeners.contains(listener)) {
    226             mListeners.add(listener);
    227         }
    228     }
    229 
    230     public void removeBluetoothIndicatorListener(BluetoothIndicatorListener listener) {
    231         if (mListeners.contains(listener)) {
    232             mListeners.remove(listener);
    233         }
    234     }
    235 
    236     private void notifyListeners(boolean showBluetoothOn) {
    237         for (int i = 0; i < mListeners.size(); i++) {
    238             mListeners.get(i).onBluetoothIndicationChange(showBluetoothOn, this);
    239         }
    240     }
    241 
    242     private void init(Context context) {
    243         Preconditions.checkNotNull(context);
    244 
    245         if (mBluetoothAdapter != null) {
    246             mBluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
    247                                     BluetoothProfile.HEADSET);
    248         }
    249 
    250         // Register for misc other intent broadcasts.
    251         IntentFilter intentFilter =
    252                 new IntentFilter(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
    253         intentFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
    254         context.registerReceiver(mReceiver, intentFilter);
    255 
    256         mCallModeler.addListener(this);
    257     }
    258 
    259     private void tearDown() {
    260         if (mBluetoothHeadset != null) {
    261             mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mBluetoothHeadset);
    262             mBluetoothHeadset = null;
    263         }
    264     }
    265 
    266     private BluetoothProfile.ServiceListener mBluetoothProfileServiceListener =
    267              new BluetoothProfile.ServiceListener() {
    268          @Override
    269          public void onServiceConnected(int profile, BluetoothProfile proxy) {
    270              mBluetoothHeadset = (BluetoothHeadset) proxy;
    271              if (VDBG) log("- Got BluetoothHeadset: " + mBluetoothHeadset);
    272          }
    273 
    274          @Override
    275          public void onServiceDisconnected(int profile) {
    276              mBluetoothHeadset = null;
    277          }
    278     };
    279 
    280     /**
    281      * UI policy helper function for the couple of places in the UI that
    282      * have some way of indicating that "bluetooth is in use."
    283      *
    284      * @return true if the onscreen UI should indicate that "bluetooth is in use",
    285      *         based on the specified bluetooth headset state, and the
    286      *         current state of the phone.
    287      * @see showBluetoothIndication()
    288      */
    289     private static boolean shouldShowBluetoothIndication(int bluetoothState,
    290                                                          int bluetoothAudioState,
    291                                                          CallManager cm) {
    292         // We want the UI to indicate that "bluetooth is in use" in two
    293         // slightly different cases:
    294         //
    295         // (a) The obvious case: if a bluetooth headset is currently in
    296         //     use for an ongoing call.
    297         //
    298         // (b) The not-so-obvious case: if an incoming call is ringing,
    299         //     and we expect that audio *will* be routed to a bluetooth
    300         //     headset once the call is answered.
    301 
    302         switch (cm.getState()) {
    303             case OFFHOOK:
    304                 // This covers normal active calls, and also the case if
    305                 // the foreground call is DIALING or ALERTING.  In this
    306                 // case, bluetooth is considered "active" if a headset
    307                 // is connected *and* audio is being routed to it.
    308                 return ((bluetoothState == BluetoothHeadset.STATE_CONNECTED)
    309                         && (bluetoothAudioState == BluetoothHeadset.STATE_AUDIO_CONNECTED));
    310 
    311             case RINGING:
    312                 // If an incoming call is ringing, we're *not* yet routing
    313                 // audio to the headset (since there's no in-call audio
    314                 // yet!)  In this case, if a bluetooth headset is
    315                 // connected at all, we assume that it'll become active
    316                 // once the user answers the phone.
    317                 return (bluetoothState == BluetoothHeadset.STATE_CONNECTED);
    318 
    319             default:  // Presumably IDLE
    320                 return false;
    321         }
    322     }
    323 
    324     private void dumpBluetoothState() {
    325         log("============== dumpBluetoothState() =============");
    326         log("= isBluetoothAvailable: " + isBluetoothAvailable());
    327         log("= isBluetoothAudioConnected: " + isBluetoothAudioConnected());
    328         log("= isBluetoothAudioConnectedOrPending: " + isBluetoothAudioConnectedOrPending());
    329         log("= PhoneApp.showBluetoothIndication: "
    330             + showBluetoothIndication());
    331         log("=");
    332         if (mBluetoothAdapter != null) {
    333             if (mBluetoothHeadset != null) {
    334                 List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
    335 
    336                 if (deviceList.size() > 0) {
    337                     BluetoothDevice device = deviceList.get(0);
    338                     log("= BluetoothHeadset.getCurrentDevice: " + device);
    339                     log("= BluetoothHeadset.State: "
    340                         + mBluetoothHeadset.getConnectionState(device));
    341                     log("= BluetoothHeadset audio connected: " +
    342                         mBluetoothHeadset.isAudioConnected(device));
    343                 }
    344             } else {
    345                 log("= mBluetoothHeadset is null");
    346             }
    347         } else {
    348             log("= mBluetoothAdapter is null; device is not BT capable");
    349         }
    350     }
    351 
    352     /* package */ void connectBluetoothAudio() {
    353         if (VDBG) log("connectBluetoothAudio()...");
    354         if (mBluetoothHeadset != null) {
    355             // TODO(BT) check return
    356             mBluetoothHeadset.connectAudio();
    357         }
    358 
    359         // Watch out: The bluetooth connection doesn't happen instantly;
    360         // the connectAudio() call returns instantly but does its real
    361         // work in another thread.  The mBluetoothConnectionPending flag
    362         // is just a little trickery to ensure that the onscreen UI updates
    363         // instantly. (See isBluetoothAudioConnectedOrPending() above.)
    364         mBluetoothConnectionPending = true;
    365         mBluetoothConnectionRequestTime = SystemClock.elapsedRealtime();
    366     }
    367 
    368     /* package */ void disconnectBluetoothAudio() {
    369         if (VDBG) log("disconnectBluetoothAudio()...");
    370         if (mBluetoothHeadset != null) {
    371             mBluetoothHeadset.disconnectAudio();
    372         }
    373         mBluetoothConnectionPending = false;
    374     }
    375 
    376     /**
    377      * Receiver for misc intent broadcasts the BluetoothManager cares about.
    378      */
    379     private class BluetoothBroadcastReceiver extends BroadcastReceiver {
    380         @Override
    381         public void onReceive(Context context, Intent intent) {
    382             String action = intent.getAction();
    383 
    384             if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
    385                 mBluetoothHeadsetState = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
    386                                                           BluetoothHeadset.STATE_DISCONNECTED);
    387                 if (VDBG) Log.d(LOG_TAG, "mReceiver: HEADSET_STATE_CHANGED_ACTION");
    388                 if (VDBG) Log.d(LOG_TAG, "==> new state: " + mBluetoothHeadsetState);
    389                 // Also update any visible UI if necessary
    390                 updateBluetoothIndication();
    391             } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
    392                 mBluetoothHeadsetAudioState =
    393                         intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
    394                                            BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
    395                 if (VDBG) Log.d(LOG_TAG, "mReceiver: HEADSET_AUDIO_STATE_CHANGED_ACTION");
    396                 if (VDBG) Log.d(LOG_TAG, "==> new state: " + mBluetoothHeadsetAudioState);
    397                 updateBluetoothIndication();
    398             }
    399         }
    400     }
    401 
    402     @Override
    403     public void onDisconnect(Call call) {
    404         updateBluetoothIndication();
    405     }
    406 
    407     @Override
    408     public void onIncoming(Call call) {
    409         // An incoming call can affect bluetooth indicator, so we update it whenever there is
    410         // a change to any of the calls.
    411         updateBluetoothIndication();
    412     }
    413 
    414     @Override
    415     public void onUpdate(List<Call> calls) {
    416         updateBluetoothIndication();
    417     }
    418 
    419     @Override
    420     public void onPostDialAction(Connection.PostDialState state, int callId, String chars, char c) {
    421         // no-op
    422     }
    423 
    424     private void log(String msg) {
    425         Log.d(LOG_TAG, msg);
    426     }
    427 
    428     /* package */ interface BluetoothIndicatorListener {
    429         public void onBluetoothIndicationChange(boolean isConnected, BluetoothManager manager);
    430     }
    431 }
    432