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");
}