Move BluetoothManager to telecomm.

Copy over bluetooth manager from teleservice and add usage in
CallAudioManager (for audio routing), Ringer (ringtone routing),
InCallTonePlayer (tone routing).

Change-Id: I015961aebf42389a7f4cf3a5f89ec194d6ca64e2
Bug: 13242863
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 79b0f49..80a6042 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -21,15 +21,15 @@
     <!-- Prevents the activity manager from delaying any activity-start
          requests by this package, including requests immediately after
          the user presses "home". -->
-    <!-- TODO(gilad): Better understand/document this use case. -->
-    <uses-permission android:name="android.permission.STOP_APP_SWITCHES" />
-    <uses-permission android:name="android.permission.READ_CALL_LOG" />
-    <uses-permission android:name="android.permission.WRITE_CALL_LOG" />
-    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+    <uses-permission android:name="android.permission.BLUETOOTH" />
+    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
     <uses-permission android:name="android.permission.CALL_PRIVILEGED" />
-
+    <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
     <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
+    <uses-permission android:name="android.permission.READ_CALL_LOG" />
+    <uses-permission android:name="android.permission.STOP_APP_SWITCHES" />
     <uses-permission android:name="android.permission.VIBRATE" />
+    <uses-permission android:name="android.permission.WRITE_CALL_LOG" />
 
     <!-- Declare which SDK level this application was built against. This is needed so that IDEs
          can check for incompatible APIs. -->
diff --git a/src/com/android/telecomm/BluetoothManager.java b/src/com/android/telecomm/BluetoothManager.java
new file mode 100644
index 0000000..efdb724
--- /dev/null
+++ b/src/com/android/telecomm/BluetoothManager.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.telecomm;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothProfile;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.SystemClock;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Listens to and caches bluetooth headset state.  Used By the CallAudioManager for maintaining
+ * overall audio state. Also provides method for connecting the bluetooth headset to the phone call.
+ */
+public class BluetoothManager {
+
+    private final BluetoothProfile.ServiceListener mBluetoothProfileServiceListener =
+            new BluetoothProfile.ServiceListener() {
+                @Override
+                public void onServiceConnected(int profile, BluetoothProfile proxy) {
+                    mBluetoothHeadset = (BluetoothHeadset) proxy;
+                    Log.v(this, "- Got BluetoothHeadset: " + mBluetoothHeadset);
+                }
+
+                @Override
+                public void onServiceDisconnected(int profile) {
+                    mBluetoothHeadset = null;
+                }
+           };
+
+    /**
+     * Receiver for misc intent broadcasts the BluetoothManager cares about.
+     */
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+
+            if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
+                int bluetoothHeadsetState = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
+                                                          BluetoothHeadset.STATE_DISCONNECTED);
+                Log.d(this, "mReceiver: HEADSET_STATE_CHANGED_ACTION");
+                Log.d(this, "==> new state: %s ", bluetoothHeadsetState);
+                updateBluetoothState();
+            } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
+                int bluetoothHeadsetAudioState =
+                        intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
+                                           BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
+                Log.d(this, "mReceiver: HEADSET_AUDIO_STATE_CHANGED_ACTION");
+                Log.d(this, "==> new state: %s", bluetoothHeadsetAudioState);
+                updateBluetoothState();
+            }
+        }
+    };
+
+    private final BluetoothAdapter mBluetoothAdapter;
+    private final CallAudioManager mCallAudioManager;
+
+    private BluetoothHeadset mBluetoothHeadset;
+    private boolean mBluetoothConnectionPending = false;
+    private long mBluetoothConnectionRequestTime;
+
+
+    public BluetoothManager(Context context, CallAudioManager callAudioManager) {
+        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+        mCallAudioManager = callAudioManager;
+
+        if (mBluetoothAdapter != null) {
+            mBluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
+                                    BluetoothProfile.HEADSET);
+        }
+
+        // Register for misc other intent broadcasts.
+        IntentFilter intentFilter =
+                new IntentFilter(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
+        intentFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
+        context.registerReceiver(mReceiver, intentFilter);
+    }
+
+    //
+    // Bluetooth helper methods.
+    //
+    // - BluetoothAdapter is the Bluetooth system service.  If
+    //   getDefaultAdapter() returns null
+    //   then the device is not BT capable.  Use BluetoothDevice.isEnabled()
+    //   to see if BT is enabled on the device.
+    //
+    // - BluetoothHeadset is the API for the control connection to a
+    //   Bluetooth Headset.  This lets you completely connect/disconnect a
+    //   headset (which we don't do from the Phone UI!) but also lets you
+    //   get the address of the currently active headset and see whether
+    //   it's currently connected.
+
+    /**
+     * @return true if the Bluetooth on/off switch in the UI should be
+     *         available to the user (i.e. if the device is BT-capable
+     *         and a headset is connected.)
+     */
+    boolean isBluetoothAvailable() {
+        Log.v(this, "isBluetoothAvailable()...");
+
+        // There's no need to ask the Bluetooth system service if BT is enabled:
+        //
+        //    BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+        //    if ((adapter == null) || !adapter.isEnabled()) {
+        //        Log.d(this, "  ==> FALSE (BT not enabled)");
+        //        return false;
+        //    }
+        //    Log.d(this, "  - BT enabled!  device name " + adapter.getName()
+        //                 + ", address " + adapter.getAddress());
+        //
+        // ...since we already have a BluetoothHeadset instance.  We can just
+        // call isConnected() on that, and assume it'll be false if BT isn't
+        // enabled at all.
+
+        // Check if there's a connected headset, using the BluetoothHeadset API.
+        boolean isConnected = false;
+        if (mBluetoothHeadset != null) {
+            List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
+
+            if (deviceList.size() > 0) {
+                BluetoothDevice device = deviceList.get(0);
+                isConnected = true;
+
+                Log.v(this, "  - headset state = " + mBluetoothHeadset.getConnectionState(device));
+                Log.v(this, "  - headset address: " + device);
+                Log.v(this, "  - isConnected: " + isConnected);
+            }
+        }
+
+        Log.v(this, "  ==> " + isConnected);
+        return isConnected;
+    }
+
+    /**
+     * @return true if a BT Headset is available, and its audio is currently connected.
+     */
+    boolean isBluetoothAudioConnected() {
+        if (mBluetoothHeadset == null) {
+            Log.v(this, "isBluetoothAudioConnected: ==> FALSE (null mBluetoothHeadset)");
+            return false;
+        }
+        List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
+
+        if (deviceList.isEmpty()) {
+            return false;
+        }
+        BluetoothDevice device = deviceList.get(0);
+        boolean isAudioOn = mBluetoothHeadset.isAudioConnected(device);
+        Log.v(this, "isBluetoothAudioConnected: ==> isAudioOn = " + isAudioOn);
+        return isAudioOn;
+    }
+
+    /**
+     * Helper method used to control the onscreen "Bluetooth" indication;
+     *
+     * @return true if a BT device is available and its audio is currently connected,
+     *              <b>or</b> if we issued a BluetoothHeadset.connectAudio()
+     *              call within the last 5 seconds (which presumably means
+     *              that the BT audio connection is currently being set
+     *              up, and will be connected soon.)
+     */
+    /* package */ boolean isBluetoothAudioConnectedOrPending() {
+        if (isBluetoothAudioConnected()) {
+            Log.v(this, "isBluetoothAudioConnectedOrPending: ==> TRUE (really connected)");
+            return true;
+        }
+
+        // If we issued a connectAudio() call "recently enough", even
+        // if BT isn't actually connected yet, let's still pretend BT is
+        // on.  This makes the onscreen indication more responsive.
+        if (mBluetoothConnectionPending) {
+            long timeSinceRequest =
+                    SystemClock.elapsedRealtime() - mBluetoothConnectionRequestTime;
+            if (timeSinceRequest < 5000 /* 5 seconds */) {
+                Log.v(this, "isBluetoothAudioConnectedOrPending: ==> TRUE (requested "
+                             + timeSinceRequest + " msec ago)");
+                return true;
+            } else {
+                Log.v(this, "isBluetoothAudioConnectedOrPending: ==> FALSE (request too old: "
+                             + timeSinceRequest + " msec ago)");
+                mBluetoothConnectionPending = false;
+                return false;
+            }
+        }
+
+        Log.v(this, "isBluetoothAudioConnectedOrPending: ==> FALSE");
+        return false;
+    }
+
+    /**
+     * Notified audio manager of a change to the bluetooth state.
+     */
+    void updateBluetoothState() {
+        mCallAudioManager.onBluetoothStateChange(this);
+    }
+
+    void connectBluetoothAudio() {
+        Log.v(this, "connectBluetoothAudio()...");
+        if (mBluetoothHeadset != null) {
+            mBluetoothHeadset.connectAudio();
+        }
+
+        // Watch out: The bluetooth connection doesn't happen instantly;
+        // the connectAudio() call returns instantly but does its real
+        // work in another thread.  The mBluetoothConnectionPending flag
+        // is just a little trickery to ensure that the onscreen UI updates
+        // instantly. (See isBluetoothAudioConnectedOrPending() above.)
+        mBluetoothConnectionPending = true;
+        mBluetoothConnectionRequestTime = SystemClock.elapsedRealtime();
+    }
+
+    void disconnectBluetoothAudio() {
+        Log.v(this, "disconnectBluetoothAudio()...");
+        if (mBluetoothHeadset != null) {
+            mBluetoothHeadset.disconnectAudio();
+        }
+        mBluetoothConnectionPending = false;
+    }
+
+    private void dumpBluetoothState() {
+        Log.d(this, "============== dumpBluetoothState() =============");
+        Log.d(this, "= isBluetoothAvailable: " + isBluetoothAvailable());
+        Log.d(this, "= isBluetoothAudioConnected: " + isBluetoothAudioConnected());
+        Log.d(this, "= isBluetoothAudioConnectedOrPending: " +
+                isBluetoothAudioConnectedOrPending());
+        Log.d(this, "=");
+        if (mBluetoothAdapter != null) {
+            if (mBluetoothHeadset != null) {
+                List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
+
+                if (deviceList.size() > 0) {
+                    BluetoothDevice device = deviceList.get(0);
+                    Log.d(this, "= BluetoothHeadset.getCurrentDevice: " + device);
+                    Log.d(this, "= BluetoothHeadset.State: "
+                        + mBluetoothHeadset.getConnectionState(device));
+                    Log.d(this, "= BluetoothHeadset audio connected: " +
+                        mBluetoothHeadset.isAudioConnected(device));
+                }
+            } else {
+                Log.d(this, "= mBluetoothHeadset is null");
+            }
+        } else {
+            Log.d(this, "= mBluetoothAdapter is null; device is not BT capable");
+        }
+    }
+}
diff --git a/src/com/android/telecomm/CallAudioManager.java b/src/com/android/telecomm/CallAudioManager.java
index d9ff8a5..fb8ed9e 100644
--- a/src/com/android/telecomm/CallAudioManager.java
+++ b/src/com/android/telecomm/CallAudioManager.java
@@ -31,6 +31,7 @@
 
     private final AudioManager mAudioManager;
     private final WiredHeadsetManager mWiredHeadsetManager;
+    private final BluetoothManager mBluetoothManager;
     private CallAudioState mAudioState;
     private int mAudioFocusStreamType;
     private boolean mIsRinging;
@@ -41,7 +42,8 @@
         Context context = TelecommApp.getInstance();
         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
         mWiredHeadsetManager = new WiredHeadsetManager(this);
-        mAudioState = getInitialAudioState();
+        mBluetoothManager = new BluetoothManager(context, this);
+        mAudioState = getInitialAudioState(null);
         mAudioFocusStreamType = STREAM_NONE;
     }
 
@@ -54,7 +56,7 @@
         updateAudioStreamAndMode();
         if (CallsManager.getInstance().getCalls().size() == 1) {
             Log.v(this, "first call added, reseting system audio to default state");
-            setInitialAudioState();
+            setInitialAudioState(call);
         } else if (!call.isIncoming()) {
             // Unmute new outgoing call.
             setSystemAudioState(false, mAudioState.route, mAudioState.supportedRouteMask);
@@ -65,7 +67,7 @@
     public void onCallRemoved(Call call) {
         if (CallsManager.getInstance().getCalls().isEmpty()) {
             Log.v(this, "all calls removed, reseting system audio to default state");
-            setInitialAudioState();
+            setInitialAudioState(null);
         }
         updateAudioStreamAndMode();
     }
@@ -77,8 +79,18 @@
 
     @Override
     public void onIncomingCallAnswered(Call call) {
-        // Unmute new incoming call.
-        setSystemAudioState(false, mAudioState.route, mAudioState.supportedRouteMask);
+        int route = mAudioState.route;
+
+        // We do two things:
+        // (1) If this is the first call, then we can to turn on bluetooth if available.
+        // (2) Unmute the audio for the new incoming call.
+        boolean isOnlyCall = CallsManager.getInstance().getCalls().size() == 1;
+        if (isOnlyCall && mBluetoothManager.isBluetoothAvailable()) {
+            mBluetoothManager.connectBluetoothAudio();
+            route = CallAudioState.ROUTE_BLUETOOTH;
+        }
+
+        setSystemAudioState(false /* isMute */, route, mAudioState.supportedRouteMask);
     }
 
     @Override
@@ -170,6 +182,30 @@
         setSystemAudioState(mAudioState.isMuted, newRoute, calculateSupportedRoutes());
     }
 
+    /**
+     * Updates the audio routing according to the bluetooth state.
+     */
+    void onBluetoothStateChange(BluetoothManager bluetoothManager) {
+        int newRoute = mAudioState.route;
+        if (bluetoothManager.isBluetoothAudioConnectedOrPending()) {
+            newRoute = CallAudioState.ROUTE_BLUETOOTH;
+        } else if (mAudioState.route == CallAudioState.ROUTE_BLUETOOTH) {
+            newRoute = CallAudioState.ROUTE_WIRED_OR_EARPIECE;
+            // Do not switch to speaker when bluetooth disconnects.
+            mWasSpeakerOn = false;
+        }
+
+        setSystemAudioState(mAudioState.isMuted, newRoute, calculateSupportedRoutes());
+    }
+
+    boolean isBluetoothAudioOn() {
+        return mBluetoothManager.isBluetoothAudioConnected();
+    }
+
+    boolean isBluetoothDeviceAvailable() {
+        return mBluetoothManager.isBluetoothAvailable();
+    }
+
     private void setSystemAudioState(boolean isMuted, int route, int supportedRouteMask) {
         CallAudioState oldAudioState = mAudioState;
         mAudioState = new CallAudioState(isMuted, route, supportedRouteMask);
@@ -182,18 +218,16 @@
         }
 
         // Audio route.
-        if (mAudioState.route == CallAudioState.ROUTE_SPEAKER) {
-            if (!mAudioManager.isSpeakerphoneOn()) {
-                Log.i(this, "turning speaker phone on");
-                mAudioManager.setSpeakerphoneOn(true);
-            }
+        if (mAudioState.route == CallAudioState.ROUTE_BLUETOOTH) {
+            turnOnSpeaker(false);
+            turnOnBluetooth(true);
+        } else if (mAudioState.route == CallAudioState.ROUTE_SPEAKER) {
+            turnOnBluetooth(false);
+            turnOnSpeaker(true);
         } else if (mAudioState.route == CallAudioState.ROUTE_EARPIECE ||
                 mAudioState.route == CallAudioState.ROUTE_WIRED_HEADSET) {
-            // Wired headset and earpiece work the same way
-            if (mAudioManager.isSpeakerphoneOn()) {
-                Log.i(this, "turning speaker phone off");
-                mAudioManager.setSpeakerphoneOn(false);
-            }
+            turnOnBluetooth(false);
+            turnOnSpeaker(false);
         }
 
         if (!oldAudioState.equals(mAudioState)) {
@@ -202,6 +236,27 @@
         }
     }
 
+    private void turnOnSpeaker(boolean on) {
+        // Wired headset and earpiece work the same way
+        if (mAudioManager.isSpeakerphoneOn() != on) {
+            Log.i(this, "turning speaker phone off");
+            mAudioManager.setSpeakerphoneOn(on);
+        }
+    }
+
+    private void turnOnBluetooth(boolean on) {
+        if (mBluetoothManager.isBluetoothAvailable()) {
+            boolean isAlreadyOn = mBluetoothManager.isBluetoothAudioConnected();
+            if (on != isAlreadyOn) {
+                if (on) {
+                    mBluetoothManager.connectBluetoothAudio();
+                } else {
+                    mBluetoothManager.disconnectBluetoothAudio();
+                }
+            }
+        }
+    }
+
     private void updateAudioStreamAndMode() {
         Log.v(this, "updateAudioStreamAndMode, mIsRinging: %b, mIsTonePlaying: %b", mIsRinging,
                 mIsTonePlaying);
@@ -286,18 +341,45 @@
             routeMask |= CallAudioState.ROUTE_EARPIECE;
         }
 
+        if (mBluetoothManager.isBluetoothAvailable()) {
+            routeMask |=  CallAudioState.ROUTE_BLUETOOTH;
+        }
+
         return routeMask;
     }
 
-    private CallAudioState getInitialAudioState() {
+    private CallAudioState getInitialAudioState(Call call) {
         int supportedRouteMask = calculateSupportedRoutes();
-        return new CallAudioState(false,
-                selectWiredOrEarpiece(CallAudioState.ROUTE_WIRED_OR_EARPIECE, supportedRouteMask),
-                supportedRouteMask);
+        int route = selectWiredOrEarpiece(
+                CallAudioState.ROUTE_WIRED_OR_EARPIECE, supportedRouteMask);
+
+        // We want the UI to indicate that "bluetooth is in use" in two slightly different cases:
+        // (a) The obvious case: if a bluetooth headset is currently in use for an ongoing call.
+        // (b) The not-so-obvious case: if an incoming call is ringing, and we expect that audio
+        //     *will* be routed to a bluetooth headset once the call is answered. In this case, just
+        //     check if the headset is available. Note this only applies when we are dealing with
+        //     the first call.
+        if (call != null && mBluetoothManager.isBluetoothAvailable()) {
+            switch(call.getState()) {
+                case ACTIVE:
+                case ON_HOLD:
+                    if (mBluetoothManager.isBluetoothAudioConnectedOrPending()) {
+                        route = CallAudioState.ROUTE_BLUETOOTH;
+                    }
+                    break;
+                case RINGING:
+                    route = CallAudioState.ROUTE_BLUETOOTH;
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        return new CallAudioState(false, route, supportedRouteMask);
     }
 
-    private void setInitialAudioState() {
-        CallAudioState audioState = getInitialAudioState();
+    private void setInitialAudioState(Call call) {
+        CallAudioState audioState = getInitialAudioState(call);
         setSystemAudioState(audioState.isMuted, audioState.route, audioState.supportedRouteMask);
     }
 
diff --git a/src/com/android/telecomm/CallsManager.java b/src/com/android/telecomm/CallsManager.java
index a4b2086..e242691 100644
--- a/src/com/android/telecomm/CallsManager.java
+++ b/src/com/android/telecomm/CallsManager.java
@@ -343,7 +343,7 @@
         if (!mCalls.contains(call)) {
             Log.w(this, "Unknown call (%s) asked to be removed from hold", call);
         } else {
-            Log.d(this, "Removing call from hold: (%s)", call);
+            Log.d(this, "unholding call: (%s)", call);
             call.unhold();
         }
     }
@@ -473,6 +473,25 @@
         }
     }
 
+    boolean hasActiveOrHoldingCall() {
+        for (Call call : mCalls) {
+            CallState state = call.getState();
+            if (state == CallState.ACTIVE || state == CallState.ON_HOLD) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    boolean hasRingingCall() {
+        for (Call call : mCalls) {
+            if (call.getState() == CallState.RINGING) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     /**
      * Adds the specified call to the main list of live calls.
      *
diff --git a/src/com/android/telecomm/InCallTonePlayer.java b/src/com/android/telecomm/InCallTonePlayer.java
index aa9655d..7f08587 100644
--- a/src/com/android/telecomm/InCallTonePlayer.java
+++ b/src/com/android/telecomm/InCallTonePlayer.java
@@ -178,9 +178,10 @@
                     throw new IllegalStateException("Bad toneId: " + mToneId);
             }
 
-            // TODO(santoscordon): Bluetooth should be set manually (STREAM_BLUETOOTH_SCO) for tone
-            // generator.
             int stream = AudioManager.STREAM_VOICE_CALL;
+            if (mCallAudioManager.isBluetoothAudioOn()) {
+                stream = AudioManager.STREAM_BLUETOOTH_SCO;
+            }
 
             // If the ToneGenerator creation fails, just continue without it. It is a local audio
             // signal, and is not as important.
diff --git a/src/com/android/telecomm/Ringer.java b/src/com/android/telecomm/Ringer.java
index 367a1de..40fb1aa 100644
--- a/src/com/android/telecomm/Ringer.java
+++ b/src/com/android/telecomm/Ringer.java
@@ -167,7 +167,12 @@
                 Log.v(this, "startRingingOrCallWaiting");
                 mCallAudioManager.setIsRinging(true);
 
-                mRingtonePlayer.play();
+                // Only play ringtone if a bluetooth device is not available. When a BT device
+                // is available, then we send it a signal to do its own ringtone and we dont need
+                // to play the ringtone on the device.
+                if (!mCallAudioManager.isBluetoothDeviceAvailable()) {
+                    mRingtonePlayer.play();
+                }
             } else {
                 Log.v(this, "startRingingOrCallWaiting, skipping because volume is 0");
             }