mirror of
https://github.com/nextcloud/talk-android
synced 2025-07-11 14:54:09 +01:00
Some work on the menu
Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
parent
1dde5029f4
commit
6f641899f1
@ -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 {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
Loading…
Reference in New Issue
Block a user