Some work on the menu

Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
Mario Danic 2017-11-28 03:42:48 +01:00
parent 1dde5029f4
commit 6f641899f1
11 changed files with 1182 additions and 1162 deletions

View File

@ -25,9 +25,6 @@ import android.view.View;
import android.widget.TextView; import android.widget.TextView;
import com.nextcloud.talk.R; import com.nextcloud.talk.R;
import com.nextcloud.talk.events.MenuItemClickEvent;
import org.greenrobot.eventbus.EventBus;
import java.util.List; import java.util.List;
@ -70,8 +67,6 @@ public class MenuItem extends AbstractFlexibleItem<MenuItem.MenuItemViewHolder>
@Override @Override
public void bindViewHolder(FlexibleAdapter adapter, MenuItem.MenuItemViewHolder holder, int position, List payloads) { public void bindViewHolder(FlexibleAdapter adapter, MenuItem.MenuItemViewHolder holder, int position, List payloads) {
holder.menuTitle.setText(title); holder.menuTitle.setText(title);
holder.menuTitle.setOnClickListener(view -> EventBus.getDefault().post(new MenuItemClickEvent(title)));
} }
static class MenuItemViewHolder extends FlexibleViewHolder { static class MenuItemViewHolder extends FlexibleViewHolder {

View File

@ -63,13 +63,6 @@ public class Room {
@JsonField(name = "sessionId") @JsonField(name = "sessionId")
public String sessionId; public String sessionId;
public enum RoomType {
DUMMY,
ROOM_TYPE_ONE_TO_ONE_CALL,
ROOM_GROUP_CALL,
ROOM_PUBLIC_CALL
}
public boolean isPublic() { public boolean isPublic() {
return (RoomType.ROOM_PUBLIC_CALL.equals(type)); return (RoomType.ROOM_PUBLIC_CALL.equals(type));
} }
@ -87,4 +80,11 @@ public class Room {
return (canModerate() && ((participants != null && participants.size() > 2) || numberOfGuests > 0)); return (canModerate() && ((participants != null && participants.size() > 2) || numberOfGuests > 0));
} }
public enum RoomType {
DUMMY,
ROOM_TYPE_ONE_TO_ONE_CALL,
ROOM_GROUP_CALL,
ROOM_PUBLIC_CALL
}
} }

View File

@ -114,26 +114,6 @@ public class CallsListController extends BaseController implements SearchView.On
private SearchView searchView; private SearchView searchView;
private String searchQuery; private String searchQuery;
private FlexibleAdapter.OnItemClickListener onItemClickListener =
new FlexibleAdapter.OnItemClickListener() {
@Override
public boolean onItemClick(int position) {
if (callItems.size() > position) {
overridePushHandler(new NoOpControllerChangeHandler());
overridePopHandler(new NoOpControllerChangeHandler());
CallItem callItem = callItems.get(position);
Intent callIntent = new Intent(getActivity(), CallActivity.class);
BundleBuilder bundleBuilder = new BundleBuilder(new Bundle());
bundleBuilder.putString("roomToken", callItem.getModel().getToken());
bundleBuilder.putParcelable("userEntity", Parcels.wrap(userEntity));
callIntent.putExtras(bundleBuilder.build());
startActivity(callIntent);
}
return true;
}
};
public CallsListController() { public CallsListController() {
super(); super();
setHasOptionsMenu(true); setHasOptionsMenu(true);
@ -158,7 +138,7 @@ public class CallsListController extends BaseController implements SearchView.On
} }
} }
adapter.addListener(onItemClickListener); adapter.addListener(new OnItemClickListener());
prepareViews(); prepareViews();
if (userEntity == null) { if (userEntity == null) {
@ -404,4 +384,24 @@ public class CallsListController extends BaseController implements SearchView.On
} }
private class OnItemClickListener implements FlexibleAdapter.OnItemClickListener {
@Override
public boolean onItemClick(int position) {
if (callItems.size() > position) {
overridePushHandler(new NoOpControllerChangeHandler());
overridePopHandler(new NoOpControllerChangeHandler());
CallItem callItem = callItems.get(position);
Intent callIntent = new Intent(getActivity(), CallActivity.class);
BundleBuilder bundleBuilder = new BundleBuilder(new Bundle());
bundleBuilder.putString("roomToken", callItem.getModel().getToken());
bundleBuilder.putParcelable("userEntity", Parcels.wrap(userEntity));
callIntent.putExtras(bundleBuilder.build());
startActivity(callIntent);
}
return true;
}
}
} }

View File

@ -48,11 +48,9 @@ import eu.davidea.flexibleadapter.items.AbstractFlexibleItem;
@AutoInjector(NextcloudTalkApplication.class) @AutoInjector(NextcloudTalkApplication.class)
public class RoomMenuController extends BaseController { public class RoomMenuController extends BaseController {
private Room room;
@BindView(R.id.recycler_view) @BindView(R.id.recycler_view)
RecyclerView recyclerView; RecyclerView recyclerView;
private Room room;
private List<AbstractFlexibleItem> menuItems; private List<AbstractFlexibleItem> menuItems;
private FlexibleAdapter<AbstractFlexibleItem> adapter; private FlexibleAdapter<AbstractFlexibleItem> adapter;
@ -84,6 +82,7 @@ public class RoomMenuController extends BaseController {
} }
recyclerView.setAdapter(adapter); recyclerView.setAdapter(adapter);
adapter.addListener(new OnItemClickListener());
recyclerView.addItemDecoration(new DividerItemDecoration( recyclerView.addItemDecoration(new DividerItemDecoration(
recyclerView.getContext(), recyclerView.getContext(),
@ -118,4 +117,15 @@ public class RoomMenuController extends BaseController {
} }
} }
private class OnItemClickListener implements FlexibleAdapter.OnItemClickListener {
@Override
public boolean onItemClick(int position) {
if (menuItems.size() > position) {
MenuItem menuItem = (MenuItem) menuItems.get(position);
}
return true;
}
}
} }

View File

@ -1,29 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.events;
public class MenuItemClickEvent {
private String menuTitle;
public MenuItemClickEvent(String menuTitle) {
this.menuTitle = menuTitle;
}
}

View File

@ -44,123 +44,45 @@ public class MagicAudioManager {
private static final String SPEAKERPHONE_AUTO = "auto"; private static final String SPEAKERPHONE_AUTO = "auto";
private static final String SPEAKERPHONE_TRUE = "true"; private static final String SPEAKERPHONE_TRUE = "true";
private static final String SPEAKERPHONE_FALSE = "false"; private static final String SPEAKERPHONE_FALSE = "false";
/**
* AudioDevice is the names of possible audio devices that we currently
* support.
*/
public enum AudioDevice { SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE }
/** AudioManager state. */
public enum AudioManagerState {
UNINITIALIZED,
PREINITIALIZED,
RUNNING,
}
/** Selected audio device change event. */
public static interface AudioManagerEvents {
// Callback fired once audio device is changed or list of available audio devices changed.
void onAudioDeviceChanged(
AudioDevice selectedAudioDevice, Set<AudioDevice> availableAudioDevices);
}
private final Context magicContext; private final Context magicContext;
// Contains speakerphone setting: auto, true or false
private final String useSpeakerphone;
// Handles all tasks related to Bluetooth headset devices.
private final MagicBluetoothManager bluetoothManager;
private AudioManager audioManager; private AudioManager audioManager;
private AudioManagerEvents audioManagerEvents; private AudioManagerEvents audioManagerEvents;
private AudioManagerState amState; private AudioManagerState amState;
private int savedAudioMode = AudioManager.MODE_INVALID; private int savedAudioMode = AudioManager.MODE_INVALID;
private boolean savedIsSpeakerPhoneOn = false; private boolean savedIsSpeakerPhoneOn = false;
private boolean savedIsMicrophoneMute = false; private boolean savedIsMicrophoneMute = false;
private boolean hasWiredHeadset = false; private boolean hasWiredHeadset = false;
// Default audio device; speaker phone for video calls or earpiece for audio // Default audio device; speaker phone for video calls or earpiece for audio
// only calls. // only calls.
private AudioDevice defaultAudioDevice; private AudioDevice defaultAudioDevice;
// Contains the currently selected audio device. // Contains the currently selected audio device.
// This device is changed automatically using a certain scheme where e.g. // This device is changed automatically using a certain scheme where e.g.
// a wired headset "wins" over speaker phone. It is also possible for a // a wired headset "wins" over speaker phone. It is also possible for a
// user to explicitly select a device (and overrid any predefined scheme). // user to explicitly select a device (and overrid any predefined scheme).
// See |userSelectedAudioDevice| for details. // See |userSelectedAudioDevice| for details.
private AudioDevice selectedAudioDevice; private AudioDevice selectedAudioDevice;
// Contains the user-selected audio device which overrides the predefined // Contains the user-selected audio device which overrides the predefined
// selection scheme. // selection scheme.
// TODO(henrika): always set to AudioDevice.NONE today. Add support for // TODO(henrika): always set to AudioDevice.NONE today. Add support for
// explicit selection based on choice by userSelectedAudioDevice. // explicit selection based on choice by userSelectedAudioDevice.
private AudioDevice userSelectedAudioDevice; private AudioDevice userSelectedAudioDevice;
// Contains speakerphone setting: auto, true or false
private final String useSpeakerphone;
// Proximity sensor object. It measures the proximity of an object in cm // Proximity sensor object. It measures the proximity of an object in cm
// relative to the view screen of a device and can therefore be used to // relative to the view screen of a device and can therefore be used to
// assist device switching (close to ear <=> use headset earpiece if // assist device switching (close to ear <=> use headset earpiece if
// available, far from ear <=> use speaker phone). // available, far from ear <=> use speaker phone).
private MagicProximitySensor proximitySensor = null; private MagicProximitySensor proximitySensor = null;
// Handles all tasks related to Bluetooth headset devices.
private final MagicBluetoothManager bluetoothManager;
// Contains a list of available audio devices. A Set collection is used to // Contains a list of available audio devices. A Set collection is used to
// avoid duplicate elements. // avoid duplicate elements.
private Set<AudioDevice> audioDevices = new HashSet<AudioDevice>(); private Set<AudioDevice> audioDevices = new HashSet<AudioDevice>();
// Broadcast receiver for wired headset intent broadcasts. // Broadcast receiver for wired headset intent broadcasts.
private BroadcastReceiver wiredHeadsetReceiver; private BroadcastReceiver wiredHeadsetReceiver;
// Callback method for changes in audio focus. // Callback method for changes in audio focus.
private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener; private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
/**
* This method is called when the proximity sensor reports a state change,
* e.g. from "NEAR to FAR" or from "FAR to NEAR".
*/
private void onProximitySensorChangedState() {
if (!useSpeakerphone.equals(SPEAKERPHONE_AUTO)) {
return;
}
// The proximity sensor should only be activated when there are exactly two
// available audio devices.
if (audioDevices.size() == 2 && audioDevices.contains(MagicAudioManager.AudioDevice.EARPIECE)
&& audioDevices.contains(MagicAudioManager.AudioDevice.SPEAKER_PHONE)) {
if (proximitySensor.sensorReportsNearState()) {
// Sensor reports that a "handset is being held up to a person's ear",
// or "something is covering the light sensor".
setAudioDeviceInternal(MagicAudioManager.AudioDevice.EARPIECE);
} else {
// Sensor reports that a "handset is removed from a person's ear", or
// "the light sensor is no longer covered".
setAudioDeviceInternal(MagicAudioManager.AudioDevice.SPEAKER_PHONE);
}
}
}
/* Receiver which handles changes in wired headset availability. */
private class WiredHeadsetReceiver extends BroadcastReceiver {
private static final int STATE_UNPLUGGED = 0;
private static final int STATE_PLUGGED = 1;
private static final int HAS_NO_MIC = 0;
private static final int HAS_MIC = 1;
@Override
public void onReceive(Context context, Intent intent) {
int state = intent.getIntExtra("state", STATE_UNPLUGGED);
int microphone = intent.getIntExtra("microphone", HAS_NO_MIC);
String name = intent.getStringExtra("name");
hasWiredHeadset = (state == STATE_PLUGGED);
updateAudioDeviceState();
}
};
/** Construction. */
public static MagicAudioManager create(Context context) {
return new MagicAudioManager(context);
}
private MagicAudioManager(Context context) { private MagicAudioManager(Context context) {
Log.d(TAG, "ctor"); Log.d(TAG, "ctor");
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
@ -192,6 +114,38 @@ public class MagicAudioManager {
Log.d(TAG, "defaultAudioDevice: " + defaultAudioDevice); Log.d(TAG, "defaultAudioDevice: " + defaultAudioDevice);
} }
/**
* Construction.
*/
public static MagicAudioManager create(Context context) {
return new MagicAudioManager(context);
}
/**
* This method is called when the proximity sensor reports a state change,
* e.g. from "NEAR to FAR" or from "FAR to NEAR".
*/
private void onProximitySensorChangedState() {
if (!useSpeakerphone.equals(SPEAKERPHONE_AUTO)) {
return;
}
// The proximity sensor should only be activated when there are exactly two
// available audio devices.
if (audioDevices.size() == 2 && audioDevices.contains(MagicAudioManager.AudioDevice.EARPIECE)
&& audioDevices.contains(MagicAudioManager.AudioDevice.SPEAKER_PHONE)) {
if (proximitySensor.sensorReportsNearState()) {
// Sensor reports that a "handset is being held up to a person's ear",
// or "something is covering the light sensor".
setAudioDeviceInternal(MagicAudioManager.AudioDevice.EARPIECE);
} else {
// Sensor reports that a "handset is removed from a person's ear", or
// "the light sensor is no longer covered".
setAudioDeviceInternal(MagicAudioManager.AudioDevice.SPEAKER_PHONE);
}
}
}
public void start(AudioManagerEvents audioManagerEvents) { public void start(AudioManagerEvents audioManagerEvents) {
Log.d(TAG, "start"); Log.d(TAG, "start");
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
@ -321,7 +275,11 @@ public class MagicAudioManager {
Log.d(TAG, "AudioManager stopped"); Log.d(TAG, "AudioManager stopped");
} }
/** Changes selection of the currently active audio device. */ ;
/**
* Changes selection of the currently active audio device.
*/
private void setAudioDeviceInternal(AudioDevice device) { private void setAudioDeviceInternal(AudioDevice device) {
Log.d(TAG, "setAudioDeviceInternal(device=" + device + ")"); Log.d(TAG, "setAudioDeviceInternal(device=" + device + ")");
@ -373,7 +331,9 @@ public class MagicAudioManager {
updateAudioDeviceState(); updateAudioDeviceState();
} }
/** Changes selection of the currently active audio device. */ /**
* Changes selection of the currently active audio device.
*/
public void selectAudioDevice(AudioDevice device) { public void selectAudioDevice(AudioDevice device) {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
if (!audioDevices.contains(device)) { if (!audioDevices.contains(device)) {
@ -383,29 +343,39 @@ public class MagicAudioManager {
updateAudioDeviceState(); updateAudioDeviceState();
} }
/** Returns current set of available/selectable audio devices. */ /**
* Returns current set of available/selectable audio devices.
*/
public Set<AudioDevice> getAudioDevices() { public Set<AudioDevice> getAudioDevices() {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
return Collections.unmodifiableSet(new HashSet<AudioDevice>(audioDevices)); return Collections.unmodifiableSet(new HashSet<AudioDevice>(audioDevices));
} }
/** Returns the currently selected audio device. */ /**
* Returns the currently selected audio device.
*/
public AudioDevice getSelectedAudioDevice() { public AudioDevice getSelectedAudioDevice() {
ThreadUtils.checkIsOnMainThread(); ThreadUtils.checkIsOnMainThread();
return selectedAudioDevice; return selectedAudioDevice;
} }
/** Helper method for receiver registration. */ /**
* Helper method for receiver registration.
*/
private void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { private void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
magicContext.registerReceiver(receiver, filter); magicContext.registerReceiver(receiver, filter);
} }
/** Helper method for unregistration of an existing receiver. */ /**
* Helper method for unregistration of an existing receiver.
*/
private void unregisterReceiver(BroadcastReceiver receiver) { private void unregisterReceiver(BroadcastReceiver receiver) {
magicContext.unregisterReceiver(receiver); magicContext.unregisterReceiver(receiver);
} }
/** Sets the speaker phone mode. */ /**
* Sets the speaker phone mode.
*/
private void setSpeakerphoneOn(boolean on) { private void setSpeakerphoneOn(boolean on) {
boolean wasOn = audioManager.isSpeakerphoneOn(); boolean wasOn = audioManager.isSpeakerphoneOn();
if (wasOn == on) { if (wasOn == on) {
@ -414,7 +384,9 @@ public class MagicAudioManager {
audioManager.setSpeakerphoneOn(on); audioManager.setSpeakerphoneOn(on);
} }
/** Sets the microphone mute state. */ /**
* Sets the microphone mute state.
*/
private void setMicrophoneMute(boolean on) { private void setMicrophoneMute(boolean on) {
boolean wasMuted = audioManager.isMicrophoneMute(); boolean wasMuted = audioManager.isMicrophoneMute();
if (wasMuted == on) { if (wasMuted == on) {
@ -423,7 +395,9 @@ public class MagicAudioManager {
audioManager.setMicrophoneMute(on); audioManager.setMicrophoneMute(on);
} }
/** Gets the current earpiece state. */ /**
* Gets the current earpiece state.
*/
private boolean hasEarpiece() { private boolean hasEarpiece() {
return magicContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY); return magicContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
} }
@ -590,4 +564,47 @@ public class MagicAudioManager {
} }
Log.d(TAG, "--- updateAudioDeviceState done"); Log.d(TAG, "--- updateAudioDeviceState done");
} }
/**
* AudioDevice is the names of possible audio devices that we currently
* support.
*/
public enum AudioDevice {
SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE
}
/**
* AudioManager state.
*/
public enum AudioManagerState {
UNINITIALIZED,
PREINITIALIZED,
RUNNING,
}
/**
* Selected audio device change event.
*/
public static interface AudioManagerEvents {
// Callback fired once audio device is changed or list of available audio devices changed.
void onAudioDeviceChanged(
AudioDevice selectedAudioDevice, Set<AudioDevice> availableAudioDevices);
}
/* Receiver which handles changes in wired headset availability. */
private class WiredHeadsetReceiver extends BroadcastReceiver {
private static final int STATE_UNPLUGGED = 0;
private static final int STATE_PLUGGED = 1;
private static final int HAS_NO_MIC = 0;
private static final int HAS_MIC = 1;
@Override
public void onReceive(Context context, Intent intent) {
int state = intent.getIntExtra("state", STATE_UNPLUGGED);
int microphone = intent.getIntExtra("microphone", HAS_NO_MIC);
String name = intent.getStringExtra("name");
hasWiredHeadset = (state == STATE_PLUGGED);
updateAudioDeviceState();
}
}
} }

View File

@ -59,6 +59,382 @@ public class MagicBluetoothManager {
private static final int BLUETOOTH_SCO_TIMEOUT_MS = 4000; private static final int BLUETOOTH_SCO_TIMEOUT_MS = 4000;
// Maximum number of SCO connection attempts. // Maximum number of SCO connection attempts.
private static final int MAX_SCO_CONNECTION_ATTEMPTS = 2; private static final int MAX_SCO_CONNECTION_ATTEMPTS = 2;
private final Context apprtcContext;
private final MagicAudioManager apprtcAudioManager;
private final AudioManager audioManager;
private final Handler handler;
private final BluetoothProfile.ServiceListener bluetoothServiceListener;
private final BroadcastReceiver bluetoothHeadsetReceiver;
int scoConnectionAttempts;
private State bluetoothState;
private BluetoothAdapter bluetoothAdapter;
private BluetoothHeadset bluetoothHeadset;
private BluetoothDevice bluetoothDevice;
// Runs when the Bluetooth timeout expires. We use that timeout after calling
// startScoAudio() or stopScoAudio() because we're not guaranteed to get a
// callback after those calls.
private final Runnable bluetoothTimeoutRunnable = new Runnable() {
@Override
public void run() {
bluetoothTimeout();
}
};
protected MagicBluetoothManager(Context context, MagicAudioManager audioManager) {
Log.d(TAG, "ctor");
ThreadUtils.checkIsOnMainThread();
apprtcContext = context;
apprtcAudioManager = audioManager;
this.audioManager = getAudioManager(context);
bluetoothState = State.UNINITIALIZED;
bluetoothServiceListener = new BluetoothServiceListener();
bluetoothHeadsetReceiver = new BluetoothHeadsetBroadcastReceiver();
handler = new Handler(Looper.getMainLooper());
}
/**
* Construction.
*/
static MagicBluetoothManager create(Context context, MagicAudioManager audioManager) {
return new MagicBluetoothManager(context, audioManager);
}
/**
* Returns the internal state.
*/
public State getState() {
ThreadUtils.checkIsOnMainThread();
return bluetoothState;
}
;
/**
* Activates components required to detect Bluetooth devices and to enable
* BT SCO (audio is routed via BT SCO) for the headset profile. The end
* state will be HEADSET_UNAVAILABLE but a state machine has started which
* will start a state change sequence where the final outcome depends on
* if/when the BT headset is enabled.
* Example of state change sequence when start() is called while BT device
* is connected and enabled:
* UNINITIALIZED --> HEADSET_UNAVAILABLE --> HEADSET_AVAILABLE -->
* SCO_CONNECTING --> SCO_CONNECTED <==> audio is now routed via BT SCO.
* Note that the MagicAudioManager is also involved in driving this state
* change.
*/
public void start() {
ThreadUtils.checkIsOnMainThread();
Log.d(TAG, "start");
if (!hasPermission(apprtcContext, android.Manifest.permission.BLUETOOTH)) {
Log.w(TAG, "Process (pid=" + Process.myPid() + ") lacks BLUETOOTH permission");
return;
}
if (bluetoothState != State.UNINITIALIZED) {
Log.w(TAG, "Invalid BT state");
return;
}
bluetoothHeadset = null;
bluetoothDevice = null;
scoConnectionAttempts = 0;
// Get a handle to the default local Bluetooth adapter.
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter == null) {
Log.w(TAG, "Device does not support Bluetooth");
return;
}
// Ensure that the device supports use of BT SCO audio for off call use cases.
if (!audioManager.isBluetoothScoAvailableOffCall()) {
Log.e(TAG, "Bluetooth SCO audio is not available off call");
return;
}
logBluetoothAdapterInfo(bluetoothAdapter);
// Establish a connection to the HEADSET profile (includes both Bluetooth Headset and
// Hands-Free) proxy object and install a listener.
if (!getBluetoothProfileProxy(
apprtcContext, bluetoothServiceListener, BluetoothProfile.HEADSET)) {
Log.e(TAG, "BluetoothAdapter.getProfileProxy(HEADSET) failed");
return;
}
// Register receivers for BluetoothHeadset change notifications.
IntentFilter bluetoothHeadsetFilter = new IntentFilter();
// Register receiver for change in connection state of the Headset profile.
bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
// Register receiver for change in audio connection state of the Headset profile.
bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
registerReceiver(bluetoothHeadsetReceiver, bluetoothHeadsetFilter);
Log.d(TAG, "HEADSET profile state: "
+ stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET)));
Log.d(TAG, "Bluetooth proxy for headset profile has started");
bluetoothState = State.HEADSET_UNAVAILABLE;
Log.d(TAG, "start done: BT state=" + bluetoothState);
}
/**
* Stops and closes all components related to Bluetooth audio.
*/
public void stop() {
ThreadUtils.checkIsOnMainThread();
Log.d(TAG, "stop: BT state=" + bluetoothState);
if (bluetoothAdapter == null) {
return;
}
// Stop BT SCO connection with remote device if needed.
stopScoAudio();
// Close down remaining BT resources.
if (bluetoothState == State.UNINITIALIZED) {
return;
}
unregisterReceiver(bluetoothHeadsetReceiver);
cancelTimer();
if (bluetoothHeadset != null) {
bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset);
bluetoothHeadset = null;
}
bluetoothAdapter = null;
bluetoothDevice = null;
bluetoothState = State.UNINITIALIZED;
Log.d(TAG, "stop done: BT state=" + bluetoothState);
}
/**
* Starts Bluetooth SCO connection with remote device.
* Note that the phone application always has the priority on the usage of the SCO connection
* for telephony. If this method is called while the phone is in call it will be ignored.
* Similarly, if a call is received or sent while an application is using the SCO connection,
* the connection will be lost for the application and NOT returned automatically when the call
* ends. Also note that: up to and including API version JELLY_BEAN_MR1, this method initiates a
* virtual voice call to the Bluetooth headset. After API version JELLY_BEAN_MR2 only a raw SCO
* audio connection is established.
* TODO(henrika): should we add support for virtual voice call to BT headset also for JBMR2 and
* higher. It might be required to initiates a virtual voice call since many devices do not
* accept SCO audio without a "call".
*/
public boolean startScoAudio() {
ThreadUtils.checkIsOnMainThread();
Log.d(TAG, "startSco: BT state=" + bluetoothState + ", "
+ "attempts: " + scoConnectionAttempts + ", "
+ "SCO is on: " + isScoOn());
if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) {
Log.e(TAG, "BT SCO connection fails - no more attempts");
return false;
}
if (bluetoothState != State.HEADSET_AVAILABLE) {
Log.e(TAG, "BT SCO connection fails - no headset available");
return false;
}
// Start BT SCO channel and wait for ACTION_AUDIO_STATE_CHANGED.
Log.d(TAG, "Starting Bluetooth SCO and waits for ACTION_AUDIO_STATE_CHANGED...");
// The SCO connection establishment can take several seconds, hence we cannot rely on the
// connection to be available when the method returns but instead register to receive the
// intent ACTION_SCO_AUDIO_STATE_UPDATED and wait for the state to be SCO_AUDIO_STATE_CONNECTED.
bluetoothState = State.SCO_CONNECTING;
audioManager.startBluetoothSco();
audioManager.setBluetoothScoOn(true);
scoConnectionAttempts++;
startTimer();
Log.d(TAG, "startScoAudio done: BT state=" + bluetoothState + ", "
+ "SCO is on: " + isScoOn());
return true;
}
/**
* Stops Bluetooth SCO connection with remote device.
*/
public void stopScoAudio() {
ThreadUtils.checkIsOnMainThread();
Log.d(TAG, "stopScoAudio: BT state=" + bluetoothState + ", "
+ "SCO is on: " + isScoOn());
if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) {
return;
}
cancelTimer();
audioManager.stopBluetoothSco();
audioManager.setBluetoothScoOn(false);
bluetoothState = State.SCO_DISCONNECTING;
Log.d(TAG, "stopScoAudio done: BT state=" + bluetoothState + ", "
+ "SCO is on: " + isScoOn());
}
/**
* Use the BluetoothHeadset proxy object (controls the Bluetooth Headset
* Service via IPC) to update the list of connected devices for the HEADSET
* profile. The internal state will change to HEADSET_UNAVAILABLE or to
* HEADSET_AVAILABLE and |bluetoothDevice| will be mapped to the connected
* device if available.
*/
public void updateDevice() {
if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
return;
}
Log.d(TAG, "updateDevice");
// Get connected devices for the headset profile. Returns the set of
// devices which are in state STATE_CONNECTED. The BluetoothDevice class
// is just a thin wrapper for a Bluetooth hardware address.
List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
if (devices.isEmpty()) {
bluetoothDevice = null;
bluetoothState = State.HEADSET_UNAVAILABLE;
Log.d(TAG, "No connected bluetooth headset");
} else {
// Always use first device in list. Android only supports one device.
bluetoothDevice = devices.get(0);
bluetoothState = State.HEADSET_AVAILABLE;
Log.d(TAG, "Connected bluetooth headset: "
+ "name=" + bluetoothDevice.getName() + ", "
+ "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice))
+ ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice));
}
Log.d(TAG, "updateDevice done: BT state=" + bluetoothState);
}
/**
* Stubs for test mocks.
*/
protected AudioManager getAudioManager(Context context) {
return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
}
protected void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
apprtcContext.registerReceiver(receiver, filter);
}
protected void unregisterReceiver(BroadcastReceiver receiver) {
apprtcContext.unregisterReceiver(receiver);
}
protected boolean getBluetoothProfileProxy(
Context context, BluetoothProfile.ServiceListener listener, int profile) {
return bluetoothAdapter.getProfileProxy(context, listener, profile);
}
protected boolean hasPermission(Context context, String permission) {
return apprtcContext.checkPermission(permission, Process.myPid(), Process.myUid())
== PackageManager.PERMISSION_GRANTED;
}
/**
* Logs the state of the local Bluetooth adapter.
*/
@SuppressLint("HardwareIds")
protected void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) {
Log.d(TAG, "BluetoothAdapter: "
+ "enabled=" + localAdapter.isEnabled() + ", "
+ "state=" + stateToString(localAdapter.getState()) + ", "
+ "name=" + localAdapter.getName() + ", "
+ "address=" + localAdapter.getAddress());
// Log the set of BluetoothDevice objects that are bonded (paired) to the local adapter.
Set<BluetoothDevice> pairedDevices = localAdapter.getBondedDevices();
if (!pairedDevices.isEmpty()) {
Log.d(TAG, "paired devices:");
for (BluetoothDevice device : pairedDevices) {
Log.d(TAG, " name=" + device.getName() + ", address=" + device.getAddress());
}
}
}
/**
* Ensures that the audio manager updates its list of available audio devices.
*/
private void updateAudioDeviceState() {
ThreadUtils.checkIsOnMainThread();
Log.d(TAG, "updateAudioDeviceState");
apprtcAudioManager.updateAudioDeviceState();
}
/**
* Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds.
*/
private void startTimer() {
ThreadUtils.checkIsOnMainThread();
Log.d(TAG, "startTimer");
handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS);
}
/**
* Cancels any outstanding timer tasks.
*/
private void cancelTimer() {
ThreadUtils.checkIsOnMainThread();
Log.d(TAG, "cancelTimer");
handler.removeCallbacks(bluetoothTimeoutRunnable);
}
/**
* Called when start of the BT SCO channel takes too long time. Usually
* happens when the BT device has been turned on during an ongoing call.
*/
private void bluetoothTimeout() {
ThreadUtils.checkIsOnMainThread();
if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
return;
}
Log.d(TAG, "bluetoothTimeout: BT state=" + bluetoothState + ", "
+ "attempts: " + scoConnectionAttempts + ", "
+ "SCO is on: " + isScoOn());
if (bluetoothState != State.SCO_CONNECTING) {
return;
}
// Bluetooth SCO should be connecting; check the latest result.
boolean scoConnected = false;
List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
if (devices.size() > 0) {
bluetoothDevice = devices.get(0);
if (bluetoothHeadset.isAudioConnected(bluetoothDevice)) {
Log.d(TAG, "SCO connected with " + bluetoothDevice.getName());
scoConnected = true;
} else {
Log.d(TAG, "SCO is not connected with " + bluetoothDevice.getName());
}
}
if (scoConnected) {
// We thought BT had timed out, but it's actually on; updating state.
bluetoothState = State.SCO_CONNECTED;
scoConnectionAttempts = 0;
} else {
// Give up and "cancel" our request by calling stopBluetoothSco().
Log.w(TAG, "BT failed to connect after timeout");
stopScoAudio();
}
updateAudioDeviceState();
Log.d(TAG, "bluetoothTimeout done: BT state=" + bluetoothState);
}
/**
* Checks whether audio uses Bluetooth SCO.
*/
private boolean isScoOn() {
return audioManager.isBluetoothScoOn();
}
/**
* Converts BluetoothAdapter states into local string representations.
*/
private String stateToString(int state) {
switch (state) {
case BluetoothAdapter.STATE_DISCONNECTED:
return "DISCONNECTED";
case BluetoothAdapter.STATE_CONNECTED:
return "CONNECTED";
case BluetoothAdapter.STATE_CONNECTING:
return "CONNECTING";
case BluetoothAdapter.STATE_DISCONNECTING:
return "DISCONNECTING";
case BluetoothAdapter.STATE_OFF:
return "OFF";
case BluetoothAdapter.STATE_ON:
return "ON";
case BluetoothAdapter.STATE_TURNING_OFF:
// Indicates the local Bluetooth adapter is turning off. Local clients should immediately
// attempt graceful disconnection of any remote links.
return "TURNING_OFF";
case BluetoothAdapter.STATE_TURNING_ON:
// Indicates the local Bluetooth adapter is turning on. However local clients should wait
// for STATE_ON before attempting to use the adapter.
return "TURNING_ON";
default:
return "INVALID";
}
}
// Bluetooth connection state. // Bluetooth connection state.
public enum State { public enum State {
@ -80,29 +456,6 @@ public class MagicBluetoothManager {
SCO_CONNECTED SCO_CONNECTED
} }
private final Context apprtcContext;
private final MagicAudioManager apprtcAudioManager;
private final AudioManager audioManager;
private final Handler handler;
int scoConnectionAttempts;
private State bluetoothState;
private final BluetoothProfile.ServiceListener bluetoothServiceListener;
private BluetoothAdapter bluetoothAdapter;
private BluetoothHeadset bluetoothHeadset;
private BluetoothDevice bluetoothDevice;
private final BroadcastReceiver bluetoothHeadsetReceiver;
// Runs when the Bluetooth timeout expires. We use that timeout after calling
// startScoAudio() or stopScoAudio() because we're not guaranteed to get a
// callback after those calls.
private final Runnable bluetoothTimeoutRunnable = new Runnable() {
@Override
public void run() {
bluetoothTimeout();
}
};
/** /**
* Implementation of an interface that notifies BluetoothProfile IPC clients when they have been * Implementation of an interface that notifies BluetoothProfile IPC clients when they have been
* connected to or disconnected from the service. * connected to or disconnected from the service.
@ -205,339 +558,5 @@ public class MagicBluetoothManager {
} }
Log.d(TAG, "onReceive done: BT state=" + bluetoothState); Log.d(TAG, "onReceive done: BT state=" + bluetoothState);
} }
};
/** Construction. */
static MagicBluetoothManager create(Context context, MagicAudioManager audioManager) {
return new MagicBluetoothManager(context, audioManager);
}
protected MagicBluetoothManager(Context context, MagicAudioManager audioManager) {
Log.d(TAG, "ctor");
ThreadUtils.checkIsOnMainThread();
apprtcContext = context;
apprtcAudioManager = audioManager;
this.audioManager = getAudioManager(context);
bluetoothState = State.UNINITIALIZED;
bluetoothServiceListener = new BluetoothServiceListener();
bluetoothHeadsetReceiver = new BluetoothHeadsetBroadcastReceiver();
handler = new Handler(Looper.getMainLooper());
}
/** Returns the internal state. */
public State getState() {
ThreadUtils.checkIsOnMainThread();
return bluetoothState;
}
/**
* Activates components required to detect Bluetooth devices and to enable
* BT SCO (audio is routed via BT SCO) for the headset profile. The end
* state will be HEADSET_UNAVAILABLE but a state machine has started which
* will start a state change sequence where the final outcome depends on
* if/when the BT headset is enabled.
* Example of state change sequence when start() is called while BT device
* is connected and enabled:
* UNINITIALIZED --> HEADSET_UNAVAILABLE --> HEADSET_AVAILABLE -->
* SCO_CONNECTING --> SCO_CONNECTED <==> audio is now routed via BT SCO.
* Note that the MagicAudioManager is also involved in driving this state
* change.
*/
public void start() {
ThreadUtils.checkIsOnMainThread();
Log.d(TAG, "start");
if (!hasPermission(apprtcContext, android.Manifest.permission.BLUETOOTH)) {
Log.w(TAG, "Process (pid=" + Process.myPid() + ") lacks BLUETOOTH permission");
return;
}
if (bluetoothState != State.UNINITIALIZED) {
Log.w(TAG, "Invalid BT state");
return;
}
bluetoothHeadset = null;
bluetoothDevice = null;
scoConnectionAttempts = 0;
// Get a handle to the default local Bluetooth adapter.
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter == null) {
Log.w(TAG, "Device does not support Bluetooth");
return;
}
// Ensure that the device supports use of BT SCO audio for off call use cases.
if (!audioManager.isBluetoothScoAvailableOffCall()) {
Log.e(TAG, "Bluetooth SCO audio is not available off call");
return;
}
logBluetoothAdapterInfo(bluetoothAdapter);
// Establish a connection to the HEADSET profile (includes both Bluetooth Headset and
// Hands-Free) proxy object and install a listener.
if (!getBluetoothProfileProxy(
apprtcContext, bluetoothServiceListener, BluetoothProfile.HEADSET)) {
Log.e(TAG, "BluetoothAdapter.getProfileProxy(HEADSET) failed");
return;
}
// Register receivers for BluetoothHeadset change notifications.
IntentFilter bluetoothHeadsetFilter = new IntentFilter();
// Register receiver for change in connection state of the Headset profile.
bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
// Register receiver for change in audio connection state of the Headset profile.
bluetoothHeadsetFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
registerReceiver(bluetoothHeadsetReceiver, bluetoothHeadsetFilter);
Log.d(TAG, "HEADSET profile state: "
+ stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET)));
Log.d(TAG, "Bluetooth proxy for headset profile has started");
bluetoothState = State.HEADSET_UNAVAILABLE;
Log.d(TAG, "start done: BT state=" + bluetoothState);
}
/** Stops and closes all components related to Bluetooth audio. */
public void stop() {
ThreadUtils.checkIsOnMainThread();
Log.d(TAG, "stop: BT state=" + bluetoothState);
if (bluetoothAdapter == null) {
return;
}
// Stop BT SCO connection with remote device if needed.
stopScoAudio();
// Close down remaining BT resources.
if (bluetoothState == State.UNINITIALIZED) {
return;
}
unregisterReceiver(bluetoothHeadsetReceiver);
cancelTimer();
if (bluetoothHeadset != null) {
bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset);
bluetoothHeadset = null;
}
bluetoothAdapter = null;
bluetoothDevice = null;
bluetoothState = State.UNINITIALIZED;
Log.d(TAG, "stop done: BT state=" + bluetoothState);
}
/**
* Starts Bluetooth SCO connection with remote device.
* Note that the phone application always has the priority on the usage of the SCO connection
* for telephony. If this method is called while the phone is in call it will be ignored.
* Similarly, if a call is received or sent while an application is using the SCO connection,
* the connection will be lost for the application and NOT returned automatically when the call
* ends. Also note that: up to and including API version JELLY_BEAN_MR1, this method initiates a
* virtual voice call to the Bluetooth headset. After API version JELLY_BEAN_MR2 only a raw SCO
* audio connection is established.
* TODO(henrika): should we add support for virtual voice call to BT headset also for JBMR2 and
* higher. It might be required to initiates a virtual voice call since many devices do not
* accept SCO audio without a "call".
*/
public boolean startScoAudio() {
ThreadUtils.checkIsOnMainThread();
Log.d(TAG, "startSco: BT state=" + bluetoothState + ", "
+ "attempts: " + scoConnectionAttempts + ", "
+ "SCO is on: " + isScoOn());
if (scoConnectionAttempts >= MAX_SCO_CONNECTION_ATTEMPTS) {
Log.e(TAG, "BT SCO connection fails - no more attempts");
return false;
}
if (bluetoothState != State.HEADSET_AVAILABLE) {
Log.e(TAG, "BT SCO connection fails - no headset available");
return false;
}
// Start BT SCO channel and wait for ACTION_AUDIO_STATE_CHANGED.
Log.d(TAG, "Starting Bluetooth SCO and waits for ACTION_AUDIO_STATE_CHANGED...");
// The SCO connection establishment can take several seconds, hence we cannot rely on the
// connection to be available when the method returns but instead register to receive the
// intent ACTION_SCO_AUDIO_STATE_UPDATED and wait for the state to be SCO_AUDIO_STATE_CONNECTED.
bluetoothState = State.SCO_CONNECTING;
audioManager.startBluetoothSco();
audioManager.setBluetoothScoOn(true);
scoConnectionAttempts++;
startTimer();
Log.d(TAG, "startScoAudio done: BT state=" + bluetoothState + ", "
+ "SCO is on: " + isScoOn());
return true;
}
/** Stops Bluetooth SCO connection with remote device. */
public void stopScoAudio() {
ThreadUtils.checkIsOnMainThread();
Log.d(TAG, "stopScoAudio: BT state=" + bluetoothState + ", "
+ "SCO is on: " + isScoOn());
if (bluetoothState != State.SCO_CONNECTING && bluetoothState != State.SCO_CONNECTED) {
return;
}
cancelTimer();
audioManager.stopBluetoothSco();
audioManager.setBluetoothScoOn(false);
bluetoothState = State.SCO_DISCONNECTING;
Log.d(TAG, "stopScoAudio done: BT state=" + bluetoothState + ", "
+ "SCO is on: " + isScoOn());
}
/**
* Use the BluetoothHeadset proxy object (controls the Bluetooth Headset
* Service via IPC) to update the list of connected devices for the HEADSET
* profile. The internal state will change to HEADSET_UNAVAILABLE or to
* HEADSET_AVAILABLE and |bluetoothDevice| will be mapped to the connected
* device if available.
*/
public void updateDevice() {
if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
return;
}
Log.d(TAG, "updateDevice");
// Get connected devices for the headset profile. Returns the set of
// devices which are in state STATE_CONNECTED. The BluetoothDevice class
// is just a thin wrapper for a Bluetooth hardware address.
List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
if (devices.isEmpty()) {
bluetoothDevice = null;
bluetoothState = State.HEADSET_UNAVAILABLE;
Log.d(TAG, "No connected bluetooth headset");
} else {
// Always use first device in list. Android only supports one device.
bluetoothDevice = devices.get(0);
bluetoothState = State.HEADSET_AVAILABLE;
Log.d(TAG, "Connected bluetooth headset: "
+ "name=" + bluetoothDevice.getName() + ", "
+ "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice))
+ ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice));
}
Log.d(TAG, "updateDevice done: BT state=" + bluetoothState);
}
/**
* Stubs for test mocks.
*/
protected AudioManager getAudioManager(Context context) {
return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
}
protected void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
apprtcContext.registerReceiver(receiver, filter);
}
protected void unregisterReceiver(BroadcastReceiver receiver) {
apprtcContext.unregisterReceiver(receiver);
}
protected boolean getBluetoothProfileProxy(
Context context, BluetoothProfile.ServiceListener listener, int profile) {
return bluetoothAdapter.getProfileProxy(context, listener, profile);
}
protected boolean hasPermission(Context context, String permission) {
return apprtcContext.checkPermission(permission, Process.myPid(), Process.myUid())
== PackageManager.PERMISSION_GRANTED;
}
/** Logs the state of the local Bluetooth adapter. */
@SuppressLint("HardwareIds")
protected void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) {
Log.d(TAG, "BluetoothAdapter: "
+ "enabled=" + localAdapter.isEnabled() + ", "
+ "state=" + stateToString(localAdapter.getState()) + ", "
+ "name=" + localAdapter.getName() + ", "
+ "address=" + localAdapter.getAddress());
// Log the set of BluetoothDevice objects that are bonded (paired) to the local adapter.
Set<BluetoothDevice> pairedDevices = localAdapter.getBondedDevices();
if (!pairedDevices.isEmpty()) {
Log.d(TAG, "paired devices:");
for (BluetoothDevice device : pairedDevices) {
Log.d(TAG, " name=" + device.getName() + ", address=" + device.getAddress());
}
}
}
/** Ensures that the audio manager updates its list of available audio devices. */
private void updateAudioDeviceState() {
ThreadUtils.checkIsOnMainThread();
Log.d(TAG, "updateAudioDeviceState");
apprtcAudioManager.updateAudioDeviceState();
}
/** Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds. */
private void startTimer() {
ThreadUtils.checkIsOnMainThread();
Log.d(TAG, "startTimer");
handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS);
}
/** Cancels any outstanding timer tasks. */
private void cancelTimer() {
ThreadUtils.checkIsOnMainThread();
Log.d(TAG, "cancelTimer");
handler.removeCallbacks(bluetoothTimeoutRunnable);
}
/**
* Called when start of the BT SCO channel takes too long time. Usually
* happens when the BT device has been turned on during an ongoing call.
*/
private void bluetoothTimeout() {
ThreadUtils.checkIsOnMainThread();
if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
return;
}
Log.d(TAG, "bluetoothTimeout: BT state=" + bluetoothState + ", "
+ "attempts: " + scoConnectionAttempts + ", "
+ "SCO is on: " + isScoOn());
if (bluetoothState != State.SCO_CONNECTING) {
return;
}
// Bluetooth SCO should be connecting; check the latest result.
boolean scoConnected = false;
List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
if (devices.size() > 0) {
bluetoothDevice = devices.get(0);
if (bluetoothHeadset.isAudioConnected(bluetoothDevice)) {
Log.d(TAG, "SCO connected with " + bluetoothDevice.getName());
scoConnected = true;
} else {
Log.d(TAG, "SCO is not connected with " + bluetoothDevice.getName());
}
}
if (scoConnected) {
// We thought BT had timed out, but it's actually on; updating state.
bluetoothState = State.SCO_CONNECTED;
scoConnectionAttempts = 0;
} else {
// Give up and "cancel" our request by calling stopBluetoothSco().
Log.w(TAG, "BT failed to connect after timeout");
stopScoAudio();
}
updateAudioDeviceState();
Log.d(TAG, "bluetoothTimeout done: BT state=" + bluetoothState);
}
/** Checks whether audio uses Bluetooth SCO. */
private boolean isScoOn() {
return audioManager.isBluetoothScoOn();
}
/** Converts BluetoothAdapter states into local string representations. */
private String stateToString(int state) {
switch (state) {
case BluetoothAdapter.STATE_DISCONNECTED:
return "DISCONNECTED";
case BluetoothAdapter.STATE_CONNECTED:
return "CONNECTED";
case BluetoothAdapter.STATE_CONNECTING:
return "CONNECTING";
case BluetoothAdapter.STATE_DISCONNECTING:
return "DISCONNECTING";
case BluetoothAdapter.STATE_OFF:
return "OFF";
case BluetoothAdapter.STATE_ON:
return "ON";
case BluetoothAdapter.STATE_TURNING_OFF:
// Indicates the local Bluetooth adapter is turning off. Local clients should immediately
// attempt graceful disconnection of any remote links.
return "TURNING_OFF";
case BluetoothAdapter.STATE_TURNING_ON:
// Indicates the local Bluetooth adapter is turning on. However local clients should wait
// for STATE_ON before attempting to use the adapter.
return "TURNING_ON";
default:
return "INVALID";
}
} }
} }

View File

@ -63,16 +63,18 @@ public class MagicProximitySensor implements SensorEventListener {
private Sensor proximitySensor = null; private Sensor proximitySensor = null;
private boolean lastStateReportIsNear = false; private boolean lastStateReportIsNear = false;
/** Construction */
static MagicProximitySensor create(Context context, Runnable sensorStateListener) {
return new MagicProximitySensor(context, sensorStateListener);
}
private MagicProximitySensor(Context context, Runnable sensorStateListener) { private MagicProximitySensor(Context context, Runnable sensorStateListener) {
onSensorStateListener = sensorStateListener; onSensorStateListener = sensorStateListener;
sensorManager = ((SensorManager) context.getSystemService(Context.SENSOR_SERVICE)); sensorManager = ((SensorManager) context.getSystemService(Context.SENSOR_SERVICE));
} }
/**
* Construction
*/
static MagicProximitySensor create(Context context, Runnable sensorStateListener) {
return new MagicProximitySensor(context, sensorStateListener);
}
/** /**
* Activate the proximity sensor. Also do initialization if called for the * Activate the proximity sensor. Also do initialization if called for the
* first time. * first time.
@ -87,7 +89,9 @@ public class MagicProximitySensor implements SensorEventListener {
return true; return true;
} }
/** Deactivate the proximity sensor. */ /**
* Deactivate the proximity sensor.
*/
public void stop() { public void stop() {
threadChecker.checkIsOnValidThread(); threadChecker.checkIsOnValidThread();
if (proximitySensor == null) { if (proximitySensor == null) {
@ -96,7 +100,9 @@ public class MagicProximitySensor implements SensorEventListener {
sensorManager.unregisterListener(this, proximitySensor); sensorManager.unregisterListener(this, proximitySensor);
} }
/** Getter for last reported state. Set to true if "near" is reported. */ /**
* Getter for last reported state. Set to true if "near" is reported.
*/
public boolean sensorReportsNearState() { public boolean sensorReportsNearState() {
threadChecker.checkIsOnValidThread(); threadChecker.checkIsOnValidThread();
return lastStateReportIsNear; return lastStateReportIsNear;
@ -152,7 +158,9 @@ public class MagicProximitySensor implements SensorEventListener {
return true; return true;
} }
/** Helper method for logging information about the proximity sensor. */ /**
* Helper method for logging information about the proximity sensor.
*/
private void logProximitySensorInfo() { private void logProximitySensorInfo() {
if (proximitySensor == null) { if (proximitySensor == null) {
return; return;

View File

@ -21,13 +21,13 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
android:id="@+id/menu_text" android:id="@+id/menu_text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="@dimen/activity_horizontal_margin"/> android:layout_margin="@dimen/margin_between_elements"/>
</RelativeLayout> </RelativeLayout>