diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java index b00d0f4f6..0cc8cdbe3 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java @@ -621,7 +621,7 @@ public class CallActivity extends AppCompatActivity { try { RelativeLayout relativeLayout = (RelativeLayout) getLayoutInflater().inflate(R.layout.surface_renderer, videosGrid, - false); + false); relativeLayout.setTag(session); SurfaceViewRenderer surfaceViewRenderer = relativeLayout.findViewById(R.id .surface_view); diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/MenuItem.java b/app/src/main/java/com/nextcloud/talk/adapters/items/MenuItem.java index 2687a8132..5bff2648e 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/MenuItem.java +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/MenuItem.java @@ -25,9 +25,6 @@ import android.view.View; import android.widget.TextView; import com.nextcloud.talk.R; -import com.nextcloud.talk.events.MenuItemClickEvent; - -import org.greenrobot.eventbus.EventBus; import java.util.List; @@ -37,7 +34,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter; import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; import eu.davidea.viewholders.FlexibleViewHolder; -public class MenuItem extends AbstractFlexibleItem { +public class MenuItem extends AbstractFlexibleItem { private String title; public MenuItem(String title) { @@ -70,8 +67,6 @@ public class MenuItem extends AbstractFlexibleItem @Override public void bindViewHolder(FlexibleAdapter adapter, MenuItem.MenuItemViewHolder holder, int position, List payloads) { holder.menuTitle.setText(title); - - holder.menuTitle.setOnClickListener(view -> EventBus.getDefault().post(new MenuItemClickEvent(title))); } static class MenuItemViewHolder extends FlexibleViewHolder { diff --git a/app/src/main/java/com/nextcloud/talk/api/NcApi.java b/app/src/main/java/com/nextcloud/talk/api/NcApi.java index e5f247422..79d4d5e5e 100644 --- a/app/src/main/java/com/nextcloud/talk/api/NcApi.java +++ b/app/src/main/java/com/nextcloud/talk/api/NcApi.java @@ -168,7 +168,7 @@ public interface NcApi { @FormUrlEncoded @POST Observable sendSignalingMessages(@Header("Authorization") String authorization, @Url String url, - @Field("messages") String messages); + @Field("messages") String messages); /* Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /signaling diff --git a/app/src/main/java/com/nextcloud/talk/api/models/json/rooms/Room.java b/app/src/main/java/com/nextcloud/talk/api/models/json/rooms/Room.java index 614b39548..c231f2130 100644 --- a/app/src/main/java/com/nextcloud/talk/api/models/json/rooms/Room.java +++ b/app/src/main/java/com/nextcloud/talk/api/models/json/rooms/Room.java @@ -63,13 +63,6 @@ public class Room { @JsonField(name = "sessionId") public String sessionId; - public enum RoomType { - DUMMY, - ROOM_TYPE_ONE_TO_ONE_CALL, - ROOM_GROUP_CALL, - ROOM_PUBLIC_CALL - } - public boolean isPublic() { return (RoomType.ROOM_PUBLIC_CALL.equals(type)); } @@ -87,4 +80,11 @@ public class Room { 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 + } + } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/CallsListController.java b/app/src/main/java/com/nextcloud/talk/controllers/CallsListController.java index 0a609b93c..a76004b50 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/CallsListController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/CallsListController.java @@ -114,26 +114,6 @@ public class CallsListController extends BaseController implements SearchView.On private SearchView searchView; 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() { super(); setHasOptionsMenu(true); @@ -158,7 +138,7 @@ public class CallsListController extends BaseController implements SearchView.On } } - adapter.addListener(onItemClickListener); + adapter.addListener(new OnItemClickListener()); prepareViews(); 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; + } + } + } diff --git a/app/src/main/java/com/nextcloud/talk/controllers/RoomMenuController.java b/app/src/main/java/com/nextcloud/talk/controllers/RoomMenuController.java index d1db24eb8..983542695 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/RoomMenuController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/RoomMenuController.java @@ -48,11 +48,9 @@ import eu.davidea.flexibleadapter.items.AbstractFlexibleItem; @AutoInjector(NextcloudTalkApplication.class) public class RoomMenuController extends BaseController { - private Room room; - @BindView(R.id.recycler_view) RecyclerView recyclerView; - + private Room room; private List menuItems; private FlexibleAdapter adapter; @@ -84,6 +82,7 @@ public class RoomMenuController extends BaseController { } recyclerView.setAdapter(adapter); + adapter.addListener(new OnItemClickListener()); recyclerView.addItemDecoration(new DividerItemDecoration( 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; + } + } } diff --git a/app/src/main/java/com/nextcloud/talk/events/MenuItemClickEvent.java b/app/src/main/java/com/nextcloud/talk/events/MenuItemClickEvent.java deleted file mode 100644 index 30fe63f9b..000000000 --- a/app/src/main/java/com/nextcloud/talk/events/MenuItemClickEvent.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Nextcloud Talk application - * - * @author Mario Danic - * Copyright (C) 2017 Mario Danic - * - * 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 . - */ - -package com.nextcloud.talk.events; - -public class MenuItemClickEvent { - private String menuTitle; - - public MenuItemClickEvent(String menuTitle) { - this.menuTitle = menuTitle; - } -} diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/MagicAudioManager.java b/app/src/main/java/com/nextcloud/talk/webrtc/MagicAudioManager.java index b878a861f..1fcc86c33 100644 --- a/app/src/main/java/com/nextcloud/talk/webrtc/MagicAudioManager.java +++ b/app/src/main/java/com/nextcloud/talk/webrtc/MagicAudioManager.java @@ -40,554 +40,571 @@ import java.util.Set; * MagicAudioManager manages all audio related parts of the AppRTC demo. */ public class MagicAudioManager { - private static final String TAG = "MagicAudioManager"; - private static final String SPEAKERPHONE_AUTO = "auto"; - private static final String SPEAKERPHONE_TRUE = "true"; - private static final String SPEAKERPHONE_FALSE = "false"; + private static final String TAG = "MagicAudioManager"; + private static final String SPEAKERPHONE_AUTO = "auto"; + private static final String SPEAKERPHONE_TRUE = "true"; + private static final String SPEAKERPHONE_FALSE = "false"; + 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 AudioManagerEvents audioManagerEvents; + private AudioManagerState amState; + private int savedAudioMode = AudioManager.MODE_INVALID; + private boolean savedIsSpeakerPhoneOn = false; + private boolean savedIsMicrophoneMute = false; + private boolean hasWiredHeadset = false; + // Default audio device; speaker phone for video calls or earpiece for audio + // only calls. + private AudioDevice defaultAudioDevice; + // Contains the currently selected audio device. + // 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 + // user to explicitly select a device (and overrid any predefined scheme). + // See |userSelectedAudioDevice| for details. + private AudioDevice selectedAudioDevice; + // Contains the user-selected audio device which overrides the predefined + // selection scheme. + // TODO(henrika): always set to AudioDevice.NONE today. Add support for + // explicit selection based on choice by userSelectedAudioDevice. + private AudioDevice userSelectedAudioDevice; + // 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 + // assist device switching (close to ear <=> use headset earpiece if + // available, far from ear <=> use speaker phone). + private MagicProximitySensor proximitySensor = null; + // Contains a list of available audio devices. A Set collection is used to + // avoid duplicate elements. + private Set audioDevices = new HashSet(); + // Broadcast receiver for wired headset intent broadcasts. + private BroadcastReceiver wiredHeadsetReceiver; + // Callback method for changes in audio focus. + private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener; - /** - * AudioDevice is the names of possible audio devices that we currently - * support. - */ - public enum AudioDevice { SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE } + private MagicAudioManager(Context context) { + Log.d(TAG, "ctor"); + ThreadUtils.checkIsOnMainThread(); + magicContext = context; + audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE)); + bluetoothManager = MagicBluetoothManager.create(context, this); + wiredHeadsetReceiver = new WiredHeadsetReceiver(); + amState = AudioManagerState.UNINITIALIZED; - /** 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 availableAudioDevices); - } - - private final Context magicContext; - private AudioManager audioManager; - - private AudioManagerEvents audioManagerEvents; - private AudioManagerState amState; - private int savedAudioMode = AudioManager.MODE_INVALID; - private boolean savedIsSpeakerPhoneOn = false; - private boolean savedIsMicrophoneMute = false; - private boolean hasWiredHeadset = false; - - // Default audio device; speaker phone for video calls or earpiece for audio - // only calls. - private AudioDevice defaultAudioDevice; - - // Contains the currently selected audio device. - // 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 - // user to explicitly select a device (and overrid any predefined scheme). - // See |userSelectedAudioDevice| for details. - private AudioDevice selectedAudioDevice; - - // Contains the user-selected audio device which overrides the predefined - // selection scheme. - // TODO(henrika): always set to AudioDevice.NONE today. Add support for - // explicit selection based on choice by 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 - // relative to the view screen of a device and can therefore be used to - // assist device switching (close to ear <=> use headset earpiece if - // available, far from ear <=> use speaker phone). - 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 - // avoid duplicate elements. - private Set audioDevices = new HashSet(); - - // Broadcast receiver for wired headset intent broadcasts. - private BroadcastReceiver wiredHeadsetReceiver; - - // Callback method for changes in audio focus. - 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) { - Log.d(TAG, "ctor"); - ThreadUtils.checkIsOnMainThread(); - magicContext = context; - audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE)); - bluetoothManager = MagicBluetoothManager.create(context, this); - wiredHeadsetReceiver = new WiredHeadsetReceiver(); - amState = AudioManagerState.UNINITIALIZED; - - useSpeakerphone = SPEAKERPHONE_AUTO; - if (useSpeakerphone.equals(SPEAKERPHONE_FALSE)) { - defaultAudioDevice = AudioDevice.EARPIECE; - } else { - defaultAudioDevice = AudioDevice.SPEAKER_PHONE; - } - - // Create and initialize the proximity sensor. - // Tablet devices (e.g. Nexus 7) does not support proximity sensors. - // Note that, the sensor will not be active until start() has been called. - proximitySensor = MagicProximitySensor.create(context, new Runnable() { - // This method will be called each time a state change is detected. - // Example: user holds his hand over the device (closer than ~5 cm), - // or removes his hand from the device. - public void run() { - onProximitySensorChangedState(); - } - }); - - Log.d(TAG, "defaultAudioDevice: " + defaultAudioDevice); - } - - public void start(AudioManagerEvents audioManagerEvents) { - Log.d(TAG, "start"); - ThreadUtils.checkIsOnMainThread(); - if (amState == AudioManagerState.RUNNING) { - Log.e(TAG, "AudioManager is already active"); - return; - } - // TODO(henrika): perhaps call new method called preInitAudio() here if UNINITIALIZED. - - Log.d(TAG, "AudioManager starts..."); - this.audioManagerEvents = audioManagerEvents; - amState = AudioManagerState.RUNNING; - - // Store current audio state so we can restore it when stop() is called. - savedAudioMode = audioManager.getMode(); - savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn(); - savedIsMicrophoneMute = audioManager.isMicrophoneMute(); - hasWiredHeadset = hasWiredHeadset(); - - // Create an AudioManager.OnAudioFocusChangeListener instance. - audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() { - // Called on the listener to notify if the audio focus for this listener has been changed. - // The |focusChange| value indicates whether the focus was gained, whether the focus was lost, - // and whether that loss is transient, or whether the new focus holder will hold it for an - // unknown amount of time. - // TODO(henrika): possibly extend support of handling audio-focus changes. Only contains - // logging for now. - @Override - public void onAudioFocusChange(int focusChange) { - String typeOfChange = "AUDIOFOCUS_NOT_DEFINED"; - switch (focusChange) { - case AudioManager.AUDIOFOCUS_GAIN: - typeOfChange = "AUDIOFOCUS_GAIN"; - break; - case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT: - typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT"; - break; - case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE: - typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE"; - break; - case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK: - typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK"; - break; - case AudioManager.AUDIOFOCUS_LOSS: - typeOfChange = "AUDIOFOCUS_LOSS"; - break; - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: - typeOfChange = "AUDIOFOCUS_LOSS_TRANSIENT"; - break; - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: - typeOfChange = "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK"; - break; - default: - typeOfChange = "AUDIOFOCUS_INVALID"; - break; + useSpeakerphone = SPEAKERPHONE_AUTO; + if (useSpeakerphone.equals(SPEAKERPHONE_FALSE)) { + defaultAudioDevice = AudioDevice.EARPIECE; + } else { + defaultAudioDevice = AudioDevice.SPEAKER_PHONE; } - Log.d(TAG, "onAudioFocusChange: " + typeOfChange); - } - }; - // Request audio playout focus (without ducking) and install listener for changes in focus. - int result = audioManager.requestAudioFocus(audioFocusChangeListener, - AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); - if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { - Log.d(TAG, "Audio focus request granted for VOICE_CALL streams"); - } else { - Log.e(TAG, "Audio focus request failed"); + // Create and initialize the proximity sensor. + // Tablet devices (e.g. Nexus 7) does not support proximity sensors. + // Note that, the sensor will not be active until start() has been called. + proximitySensor = MagicProximitySensor.create(context, new Runnable() { + // This method will be called each time a state change is detected. + // Example: user holds his hand over the device (closer than ~5 cm), + // or removes his hand from the device. + public void run() { + onProximitySensorChangedState(); + } + }); + + Log.d(TAG, "defaultAudioDevice: " + defaultAudioDevice); } - // Start by setting MODE_IN_COMMUNICATION as default audio mode. It is - // required to be in this mode when playout and/or recording starts for - // best possible VoIP performance. - audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); - - // Always disable microphone mute during a WebRTC call. - setMicrophoneMute(false); - - // Set initial device states. - userSelectedAudioDevice = AudioDevice.NONE; - selectedAudioDevice = AudioDevice.NONE; - audioDevices.clear(); - - // Initialize and start Bluetooth if a BT device is available or initiate - // detection of new (enabled) BT devices. - bluetoothManager.start(); - - // Do initial selection of audio device. This setting can later be changed - // either by adding/removing a BT or wired headset or by covering/uncovering - // the proximity sensor. - updateAudioDeviceState(); - - // Register receiver for broadcast intents related to adding/removing a - // wired headset. - registerReceiver(wiredHeadsetReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG)); - Log.d(TAG, "AudioManager started"); - } - - public void stop() { - Log.d(TAG, "stop"); - ThreadUtils.checkIsOnMainThread(); - if (amState != AudioManagerState.RUNNING) { - Log.e(TAG, "Trying to stop AudioManager in incorrect state: " + amState); - return; - } - amState = AudioManagerState.UNINITIALIZED; - - unregisterReceiver(wiredHeadsetReceiver); - - bluetoothManager.stop(); - - // Restore previously stored audio states. - setSpeakerphoneOn(savedIsSpeakerPhoneOn); - setMicrophoneMute(savedIsMicrophoneMute); - audioManager.setMode(savedAudioMode); - - // Abandon audio focus. Gives the previous focus owner, if any, focus. - audioManager.abandonAudioFocus(audioFocusChangeListener); - audioFocusChangeListener = null; - Log.d(TAG, "Abandoned audio focus for VOICE_CALL streams"); - - if (proximitySensor != null) { - proximitySensor.stop(); - proximitySensor = null; + /** + * Construction. + */ + public static MagicAudioManager create(Context context) { + return new MagicAudioManager(context); } - audioManagerEvents = null; - Log.d(TAG, "AudioManager stopped"); - } + /** + * 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; + } - /** Changes selection of the currently active audio device. */ - private void setAudioDeviceInternal(AudioDevice device) { - Log.d(TAG, "setAudioDeviceInternal(device=" + device + ")"); + // 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); + } + } + } - if (audioDevices.contains(device)) { + public void start(AudioManagerEvents audioManagerEvents) { + Log.d(TAG, "start"); + ThreadUtils.checkIsOnMainThread(); + if (amState == AudioManagerState.RUNNING) { + Log.e(TAG, "AudioManager is already active"); + return; + } + // TODO(henrika): perhaps call new method called preInitAudio() here if UNINITIALIZED. - switch (device) { + Log.d(TAG, "AudioManager starts..."); + this.audioManagerEvents = audioManagerEvents; + amState = AudioManagerState.RUNNING; + + // Store current audio state so we can restore it when stop() is called. + savedAudioMode = audioManager.getMode(); + savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn(); + savedIsMicrophoneMute = audioManager.isMicrophoneMute(); + hasWiredHeadset = hasWiredHeadset(); + + // Create an AudioManager.OnAudioFocusChangeListener instance. + audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() { + // Called on the listener to notify if the audio focus for this listener has been changed. + // The |focusChange| value indicates whether the focus was gained, whether the focus was lost, + // and whether that loss is transient, or whether the new focus holder will hold it for an + // unknown amount of time. + // TODO(henrika): possibly extend support of handling audio-focus changes. Only contains + // logging for now. + @Override + public void onAudioFocusChange(int focusChange) { + String typeOfChange = "AUDIOFOCUS_NOT_DEFINED"; + switch (focusChange) { + case AudioManager.AUDIOFOCUS_GAIN: + typeOfChange = "AUDIOFOCUS_GAIN"; + break; + case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT: + typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT"; + break; + case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE: + typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE"; + break; + case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK: + typeOfChange = "AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK"; + break; + case AudioManager.AUDIOFOCUS_LOSS: + typeOfChange = "AUDIOFOCUS_LOSS"; + break; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: + typeOfChange = "AUDIOFOCUS_LOSS_TRANSIENT"; + break; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: + typeOfChange = "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK"; + break; + default: + typeOfChange = "AUDIOFOCUS_INVALID"; + break; + } + Log.d(TAG, "onAudioFocusChange: " + typeOfChange); + } + }; + + // Request audio playout focus (without ducking) and install listener for changes in focus. + int result = audioManager.requestAudioFocus(audioFocusChangeListener, + AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); + if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + Log.d(TAG, "Audio focus request granted for VOICE_CALL streams"); + } else { + Log.e(TAG, "Audio focus request failed"); + } + + // Start by setting MODE_IN_COMMUNICATION as default audio mode. It is + // required to be in this mode when playout and/or recording starts for + // best possible VoIP performance. + audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); + + // Always disable microphone mute during a WebRTC call. + setMicrophoneMute(false); + + // Set initial device states. + userSelectedAudioDevice = AudioDevice.NONE; + selectedAudioDevice = AudioDevice.NONE; + audioDevices.clear(); + + // Initialize and start Bluetooth if a BT device is available or initiate + // detection of new (enabled) BT devices. + bluetoothManager.start(); + + // Do initial selection of audio device. This setting can later be changed + // either by adding/removing a BT or wired headset or by covering/uncovering + // the proximity sensor. + updateAudioDeviceState(); + + // Register receiver for broadcast intents related to adding/removing a + // wired headset. + registerReceiver(wiredHeadsetReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG)); + Log.d(TAG, "AudioManager started"); + } + + public void stop() { + Log.d(TAG, "stop"); + ThreadUtils.checkIsOnMainThread(); + if (amState != AudioManagerState.RUNNING) { + Log.e(TAG, "Trying to stop AudioManager in incorrect state: " + amState); + return; + } + amState = AudioManagerState.UNINITIALIZED; + + unregisterReceiver(wiredHeadsetReceiver); + + bluetoothManager.stop(); + + // Restore previously stored audio states. + setSpeakerphoneOn(savedIsSpeakerPhoneOn); + setMicrophoneMute(savedIsMicrophoneMute); + audioManager.setMode(savedAudioMode); + + // Abandon audio focus. Gives the previous focus owner, if any, focus. + audioManager.abandonAudioFocus(audioFocusChangeListener); + audioFocusChangeListener = null; + Log.d(TAG, "Abandoned audio focus for VOICE_CALL streams"); + + if (proximitySensor != null) { + proximitySensor.stop(); + proximitySensor = null; + } + + audioManagerEvents = null; + Log.d(TAG, "AudioManager stopped"); + } + + ; + + /** + * Changes selection of the currently active audio device. + */ + private void setAudioDeviceInternal(AudioDevice device) { + Log.d(TAG, "setAudioDeviceInternal(device=" + device + ")"); + + if (audioDevices.contains(device)) { + + switch (device) { + case SPEAKER_PHONE: + setSpeakerphoneOn(true); + break; + case EARPIECE: + setSpeakerphoneOn(false); + break; + case WIRED_HEADSET: + setSpeakerphoneOn(false); + break; + case BLUETOOTH: + setSpeakerphoneOn(false); + break; + default: + Log.e(TAG, "Invalid audio device selection"); + break; + } + selectedAudioDevice = device; + } + } + + /** + * Changes default audio device. + * TODO(henrika): add usage of this method in the AppRTCMobile client. + */ + public void setDefaultAudioDevice(AudioDevice defaultDevice) { + ThreadUtils.checkIsOnMainThread(); + switch (defaultDevice) { case SPEAKER_PHONE: - setSpeakerphoneOn(true); + defaultAudioDevice = defaultDevice; break; case EARPIECE: - setSpeakerphoneOn(false); - break; - case WIRED_HEADSET: - setSpeakerphoneOn(false); - break; - case BLUETOOTH: - setSpeakerphoneOn(false); + if (hasEarpiece()) { + defaultAudioDevice = defaultDevice; + } else { + defaultAudioDevice = AudioDevice.SPEAKER_PHONE; + } break; default: - Log.e(TAG, "Invalid audio device selection"); + Log.e(TAG, "Invalid default audio device selection"); break; } - selectedAudioDevice = device; + Log.d(TAG, "setDefaultAudioDevice(device=" + defaultAudioDevice + ")"); + updateAudioDeviceState(); } - } - /** - * Changes default audio device. - * TODO(henrika): add usage of this method in the AppRTCMobile client. - */ - public void setDefaultAudioDevice(AudioDevice defaultDevice) { - ThreadUtils.checkIsOnMainThread(); - switch (defaultDevice) { - case SPEAKER_PHONE: - defaultAudioDevice = defaultDevice; - break; - case EARPIECE: - if (hasEarpiece()) { - defaultAudioDevice = defaultDevice; + /** + * Changes selection of the currently active audio device. + */ + public void selectAudioDevice(AudioDevice device) { + ThreadUtils.checkIsOnMainThread(); + if (!audioDevices.contains(device)) { + Log.e(TAG, "Can not select " + device + " from available " + audioDevices); + } + userSelectedAudioDevice = device; + updateAudioDeviceState(); + } + + /** + * Returns current set of available/selectable audio devices. + */ + public Set getAudioDevices() { + ThreadUtils.checkIsOnMainThread(); + return Collections.unmodifiableSet(new HashSet(audioDevices)); + } + + /** + * Returns the currently selected audio device. + */ + public AudioDevice getSelectedAudioDevice() { + ThreadUtils.checkIsOnMainThread(); + return selectedAudioDevice; + } + + /** + * Helper method for receiver registration. + */ + private void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { + magicContext.registerReceiver(receiver, filter); + } + + /** + * Helper method for unregistration of an existing receiver. + */ + private void unregisterReceiver(BroadcastReceiver receiver) { + magicContext.unregisterReceiver(receiver); + } + + /** + * Sets the speaker phone mode. + */ + private void setSpeakerphoneOn(boolean on) { + boolean wasOn = audioManager.isSpeakerphoneOn(); + if (wasOn == on) { + return; + } + audioManager.setSpeakerphoneOn(on); + } + + /** + * Sets the microphone mute state. + */ + private void setMicrophoneMute(boolean on) { + boolean wasMuted = audioManager.isMicrophoneMute(); + if (wasMuted == on) { + return; + } + audioManager.setMicrophoneMute(on); + } + + /** + * Gets the current earpiece state. + */ + private boolean hasEarpiece() { + return magicContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY); + } + + /** + * Checks whether a wired headset is connected or not. + * This is not a valid indication that audio playback is actually over + * the wired headset as audio routing depends on other conditions. We + * only use it as an early indicator (during initialization) of an attached + * wired headset. + */ + @Deprecated + private boolean hasWiredHeadset() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return audioManager.isWiredHeadsetOn(); } else { - defaultAudioDevice = AudioDevice.SPEAKER_PHONE; + final AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL); + for (AudioDeviceInfo device : devices) { + final int type = device.getType(); + if (type == AudioDeviceInfo.TYPE_WIRED_HEADSET) { + Log.d(TAG, "hasWiredHeadset: found wired headset"); + return true; + } else if (type == AudioDeviceInfo.TYPE_USB_DEVICE) { + Log.d(TAG, "hasWiredHeadset: found USB audio device"); + return true; + } + } + return false; } - break; - default: - Log.e(TAG, "Invalid default audio device selection"); - break; } - Log.d(TAG, "setDefaultAudioDevice(device=" + defaultAudioDevice + ")"); - updateAudioDeviceState(); - } - /** Changes selection of the currently active audio device. */ - public void selectAudioDevice(AudioDevice device) { - ThreadUtils.checkIsOnMainThread(); - if (!audioDevices.contains(device)) { - Log.e(TAG, "Can not select " + device + " from available " + audioDevices); - } - userSelectedAudioDevice = device; - updateAudioDeviceState(); - } + /** + * Updates list of possible audio devices and make new device selection. + * TODO(henrika): add unit test to verify all state transitions. + */ + public void updateAudioDeviceState() { + ThreadUtils.checkIsOnMainThread(); + Log.d(TAG, "--- updateAudioDeviceState: " + + "wired headset=" + hasWiredHeadset + ", " + + "BT state=" + bluetoothManager.getState()); + Log.d(TAG, "Device status: " + + "available=" + audioDevices + ", " + + "selected=" + selectedAudioDevice + ", " + + "user selected=" + userSelectedAudioDevice); - /** Returns current set of available/selectable audio devices. */ - public Set getAudioDevices() { - ThreadUtils.checkIsOnMainThread(); - return Collections.unmodifiableSet(new HashSet(audioDevices)); - } - - /** Returns the currently selected audio device. */ - public AudioDevice getSelectedAudioDevice() { - ThreadUtils.checkIsOnMainThread(); - return selectedAudioDevice; - } - - /** Helper method for receiver registration. */ - private void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { - magicContext.registerReceiver(receiver, filter); - } - - /** Helper method for unregistration of an existing receiver. */ - private void unregisterReceiver(BroadcastReceiver receiver) { - magicContext.unregisterReceiver(receiver); - } - - /** Sets the speaker phone mode. */ - private void setSpeakerphoneOn(boolean on) { - boolean wasOn = audioManager.isSpeakerphoneOn(); - if (wasOn == on) { - return; - } - audioManager.setSpeakerphoneOn(on); - } - - /** Sets the microphone mute state. */ - private void setMicrophoneMute(boolean on) { - boolean wasMuted = audioManager.isMicrophoneMute(); - if (wasMuted == on) { - return; - } - audioManager.setMicrophoneMute(on); - } - - /** Gets the current earpiece state. */ - private boolean hasEarpiece() { - return magicContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY); - } - - /** - * Checks whether a wired headset is connected or not. - * This is not a valid indication that audio playback is actually over - * the wired headset as audio routing depends on other conditions. We - * only use it as an early indicator (during initialization) of an attached - * wired headset. - */ - @Deprecated - private boolean hasWiredHeadset() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - return audioManager.isWiredHeadsetOn(); - } else { - final AudioDeviceInfo[] devices = audioManager.getDevices(AudioManager.GET_DEVICES_ALL); - for (AudioDeviceInfo device : devices) { - final int type = device.getType(); - if (type == AudioDeviceInfo.TYPE_WIRED_HEADSET) { - Log.d(TAG, "hasWiredHeadset: found wired headset"); - return true; - } else if (type == AudioDeviceInfo.TYPE_USB_DEVICE) { - Log.d(TAG, "hasWiredHeadset: found USB audio device"); - return true; + // Check if any Bluetooth headset is connected. The internal BT state will + // change accordingly. + // TODO(henrika): perhaps wrap required state into BT manager. + if (bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_AVAILABLE + || bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_UNAVAILABLE + || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_DISCONNECTING) { + bluetoothManager.updateDevice(); } - } - return false; - } - } - /** - * Updates list of possible audio devices and make new device selection. - * TODO(henrika): add unit test to verify all state transitions. - */ - public void updateAudioDeviceState() { - ThreadUtils.checkIsOnMainThread(); - Log.d(TAG, "--- updateAudioDeviceState: " - + "wired headset=" + hasWiredHeadset + ", " - + "BT state=" + bluetoothManager.getState()); - Log.d(TAG, "Device status: " - + "available=" + audioDevices + ", " - + "selected=" + selectedAudioDevice + ", " - + "user selected=" + userSelectedAudioDevice); + // Update the set of available audio devices. + Set newAudioDevices = new HashSet<>(); - // Check if any Bluetooth headset is connected. The internal BT state will - // change accordingly. - // TODO(henrika): perhaps wrap required state into BT manager. - if (bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_AVAILABLE - || bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_UNAVAILABLE - || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_DISCONNECTING) { - bluetoothManager.updateDevice(); + if (bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED + || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTING + || bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_AVAILABLE) { + newAudioDevices.add(AudioDevice.BLUETOOTH); + } + + if (hasWiredHeadset) { + // If a wired headset is connected, then it is the only possible option. + newAudioDevices.add(AudioDevice.WIRED_HEADSET); + } else { + // No wired headset, hence the audio-device list can contain speaker + // phone (on a tablet), or speaker phone and earpiece (on mobile phone). + newAudioDevices.add(AudioDevice.SPEAKER_PHONE); + if (hasEarpiece()) { + newAudioDevices.add(AudioDevice.EARPIECE); + } + } + // Store state which is set to true if the device list has changed. + boolean audioDeviceSetUpdated = !audioDevices.equals(newAudioDevices); + // Update the existing audio device set. + audioDevices = newAudioDevices; + // Correct user selected audio devices if needed. + if (bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_UNAVAILABLE + && userSelectedAudioDevice == AudioDevice.BLUETOOTH) { + // If BT is not available, it can't be the user selection. + userSelectedAudioDevice = AudioDevice.NONE; + } + if (hasWiredHeadset && userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE) { + // If user selected speaker phone, but then plugged wired headset then make + // wired headset as user selected device. + userSelectedAudioDevice = AudioDevice.WIRED_HEADSET; + } + if (!hasWiredHeadset && userSelectedAudioDevice == AudioDevice.WIRED_HEADSET) { + // If user selected wired headset, but then unplugged wired headset then make + // speaker phone as user selected device. + userSelectedAudioDevice = AudioDevice.SPEAKER_PHONE; + } + + // Need to start Bluetooth if it is available and user either selected it explicitly or + // user did not select any output device. + boolean needBluetoothAudioStart = + bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_AVAILABLE + && (userSelectedAudioDevice == AudioDevice.NONE + || userSelectedAudioDevice == AudioDevice.BLUETOOTH); + + // Need to stop Bluetooth audio if user selected different device and + // Bluetooth SCO connection is established or in the process. + boolean needBluetoothAudioStop = + (bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED + || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTING) + && (userSelectedAudioDevice != AudioDevice.NONE + && userSelectedAudioDevice != AudioDevice.BLUETOOTH); + + if (bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_AVAILABLE + || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTING + || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED) { + Log.d(TAG, "Need BT audio: start=" + needBluetoothAudioStart + ", " + + "stop=" + needBluetoothAudioStop + ", " + + "BT state=" + bluetoothManager.getState()); + } + + // Start or stop Bluetooth SCO connection given states set earlier. + if (needBluetoothAudioStop) { + bluetoothManager.stopScoAudio(); + bluetoothManager.updateDevice(); + } + + if (needBluetoothAudioStart && !needBluetoothAudioStop) { + // Attempt to start Bluetooth SCO audio (takes a few second to start). + if (!bluetoothManager.startScoAudio()) { + // Remove BLUETOOTH from list of available devices since SCO failed. + audioDevices.remove(AudioDevice.BLUETOOTH); + audioDeviceSetUpdated = true; + } + } + + // Update selected audio device. + AudioDevice newAudioDevice = selectedAudioDevice; + + if (bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED) { + // If a Bluetooth is connected, then it should be used as output audio + // device. Note that it is not sufficient that a headset is available; + // an active SCO channel must also be up and running. + newAudioDevice = AudioDevice.BLUETOOTH; + } else if (hasWiredHeadset) { + // If a wired headset is connected, but Bluetooth is not, then wired headset is used as + // audio device. + newAudioDevice = AudioDevice.WIRED_HEADSET; + } else { + // No wired headset and no Bluetooth, hence the audio-device list can contain speaker + // phone (on a tablet), or speaker phone and earpiece (on mobile phone). + // |defaultAudioDevice| contains either AudioDevice.SPEAKER_PHONE or AudioDevice.EARPIECE + // depending on the user's selection. + newAudioDevice = defaultAudioDevice; + } + // Switch to new device but only if there has been any changes. + if (newAudioDevice != selectedAudioDevice || audioDeviceSetUpdated) { + // Do the required device switch. + setAudioDeviceInternal(newAudioDevice); + Log.d(TAG, "New device status: " + + "available=" + audioDevices + ", " + + "selected=" + newAudioDevice); + if (audioManagerEvents != null) { + // Notify a listening client that audio device has been changed. + audioManagerEvents.onAudioDeviceChanged(selectedAudioDevice, audioDevices); + } + } + Log.d(TAG, "--- updateAudioDeviceState done"); } - // Update the set of available audio devices. - Set newAudioDevices = new HashSet<>(); - - if (bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED - || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTING - || bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_AVAILABLE) { - newAudioDevices.add(AudioDevice.BLUETOOTH); + /** + * AudioDevice is the names of possible audio devices that we currently + * support. + */ + public enum AudioDevice { + SPEAKER_PHONE, WIRED_HEADSET, EARPIECE, BLUETOOTH, NONE } - if (hasWiredHeadset) { - // If a wired headset is connected, then it is the only possible option. - newAudioDevices.add(AudioDevice.WIRED_HEADSET); - } else { - // No wired headset, hence the audio-device list can contain speaker - // phone (on a tablet), or speaker phone and earpiece (on mobile phone). - newAudioDevices.add(AudioDevice.SPEAKER_PHONE); - if (hasEarpiece()) { - newAudioDevices.add(AudioDevice.EARPIECE); - } - } - // Store state which is set to true if the device list has changed. - boolean audioDeviceSetUpdated = !audioDevices.equals(newAudioDevices); - // Update the existing audio device set. - audioDevices = newAudioDevices; - // Correct user selected audio devices if needed. - if (bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_UNAVAILABLE - && userSelectedAudioDevice == AudioDevice.BLUETOOTH) { - // If BT is not available, it can't be the user selection. - userSelectedAudioDevice = AudioDevice.NONE; - } - if (hasWiredHeadset && userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE) { - // If user selected speaker phone, but then plugged wired headset then make - // wired headset as user selected device. - userSelectedAudioDevice = AudioDevice.WIRED_HEADSET; - } - if (!hasWiredHeadset && userSelectedAudioDevice == AudioDevice.WIRED_HEADSET) { - // If user selected wired headset, but then unplugged wired headset then make - // speaker phone as user selected device. - userSelectedAudioDevice = AudioDevice.SPEAKER_PHONE; + /** + * AudioManager state. + */ + public enum AudioManagerState { + UNINITIALIZED, + PREINITIALIZED, + RUNNING, } - // Need to start Bluetooth if it is available and user either selected it explicitly or - // user did not select any output device. - boolean needBluetoothAudioStart = - bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_AVAILABLE - && (userSelectedAudioDevice == AudioDevice.NONE - || userSelectedAudioDevice == AudioDevice.BLUETOOTH); - - // Need to stop Bluetooth audio if user selected different device and - // Bluetooth SCO connection is established or in the process. - boolean needBluetoothAudioStop = - (bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED - || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTING) - && (userSelectedAudioDevice != AudioDevice.NONE - && userSelectedAudioDevice != AudioDevice.BLUETOOTH); - - if (bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_AVAILABLE - || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTING - || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED) { - Log.d(TAG, "Need BT audio: start=" + needBluetoothAudioStart + ", " - + "stop=" + needBluetoothAudioStop + ", " - + "BT state=" + bluetoothManager.getState()); + /** + * 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 availableAudioDevices); } - // Start or stop Bluetooth SCO connection given states set earlier. - if (needBluetoothAudioStop) { - bluetoothManager.stopScoAudio(); - bluetoothManager.updateDevice(); - } + /* 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; - if (needBluetoothAudioStart && !needBluetoothAudioStop) { - // Attempt to start Bluetooth SCO audio (takes a few second to start). - if (!bluetoothManager.startScoAudio()) { - // Remove BLUETOOTH from list of available devices since SCO failed. - audioDevices.remove(AudioDevice.BLUETOOTH); - audioDeviceSetUpdated = true; - } + @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(); + } } - - // Update selected audio device. - AudioDevice newAudioDevice = selectedAudioDevice; - - if (bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED) { - // If a Bluetooth is connected, then it should be used as output audio - // device. Note that it is not sufficient that a headset is available; - // an active SCO channel must also be up and running. - newAudioDevice = AudioDevice.BLUETOOTH; - } else if (hasWiredHeadset) { - // If a wired headset is connected, but Bluetooth is not, then wired headset is used as - // audio device. - newAudioDevice = AudioDevice.WIRED_HEADSET; - } else { - // No wired headset and no Bluetooth, hence the audio-device list can contain speaker - // phone (on a tablet), or speaker phone and earpiece (on mobile phone). - // |defaultAudioDevice| contains either AudioDevice.SPEAKER_PHONE or AudioDevice.EARPIECE - // depending on the user's selection. - newAudioDevice = defaultAudioDevice; - } - // Switch to new device but only if there has been any changes. - if (newAudioDevice != selectedAudioDevice || audioDeviceSetUpdated) { - // Do the required device switch. - setAudioDeviceInternal(newAudioDevice); - Log.d(TAG, "New device status: " - + "available=" + audioDevices + ", " - + "selected=" + newAudioDevice); - if (audioManagerEvents != null) { - // Notify a listening client that audio device has been changed. - audioManagerEvents.onAudioDeviceChanged(selectedAudioDevice, audioDevices); - } - } - Log.d(TAG, "--- updateAudioDeviceState done"); - } } diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/MagicBluetoothManager.java b/app/src/main/java/com/nextcloud/talk/webrtc/MagicBluetoothManager.java index ba2a808c2..5d78efe9d 100644 --- a/app/src/main/java/com/nextcloud/talk/webrtc/MagicBluetoothManager.java +++ b/app/src/main/java/com/nextcloud/talk/webrtc/MagicBluetoothManager.java @@ -53,491 +53,510 @@ import java.util.List; import java.util.Set; public class MagicBluetoothManager { - private static final String TAG = "MagicBluetoothManager"; + private static final String TAG = "MagicBluetoothManager"; - // Timeout interval for starting or stopping audio to a Bluetooth SCO device. - private static final int BLUETOOTH_SCO_TIMEOUT_MS = 4000; - // Maximum number of SCO connection attempts. - private static final int MAX_SCO_CONNECTION_ATTEMPTS = 2; - - // Bluetooth connection state. - public enum State { - // Bluetooth is not available; no adapter or Bluetooth is off. - UNINITIALIZED, - // Bluetooth error happened when trying to start Bluetooth. - ERROR, - // Bluetooth proxy object for the Headset profile exists, but no connected headset devices, - // SCO is not started or disconnected. - HEADSET_UNAVAILABLE, - // Bluetooth proxy object for the Headset profile connected, connected Bluetooth headset - // present, but SCO is not started or disconnected. - HEADSET_AVAILABLE, - // Bluetooth audio SCO connection with remote device is closing. - SCO_DISCONNECTING, - // Bluetooth audio SCO connection with remote device is initiated. - SCO_CONNECTING, - // Bluetooth audio SCO connection with remote device is established. - 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 - * connected to or disconnected from the service. - */ - private class BluetoothServiceListener implements BluetoothProfile.ServiceListener { - @Override - // Called to notify the client when the proxy object has been connected to the service. - // Once we have the profile proxy object, we can use it to monitor the state of the - // connection and perform other operations that are relevant to the headset profile. - public void onServiceConnected(int profile, BluetoothProfile proxy) { - if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) { - return; - } - Log.d(TAG, "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState); - // Android only supports one connected Bluetooth Headset at a time. - bluetoothHeadset = (BluetoothHeadset) proxy; - updateAudioDeviceState(); - Log.d(TAG, "onServiceConnected done: BT state=" + bluetoothState); - } - - @Override - /** Notifies the client when the proxy object has been disconnected from the service. */ - public void onServiceDisconnected(int profile) { - if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) { - return; - } - Log.d(TAG, "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState); - stopScoAudio(); - bluetoothHeadset = null; - bluetoothDevice = null; - bluetoothState = State.HEADSET_UNAVAILABLE; - updateAudioDeviceState(); - Log.d(TAG, "onServiceDisconnected done: BT state=" + bluetoothState); - } - } - - // Intent broadcast receiver which handles changes in Bluetooth device availability. - // Detects headset changes and Bluetooth SCO state changes. - private class BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - if (bluetoothState == State.UNINITIALIZED) { - return; - } - final String action = intent.getAction(); - // Change in connection state of the Headset profile. Note that the - // change does not tell us anything about whether we're streaming - // audio to BT over SCO. Typically received when user turns on a BT - // headset while audio is active using another audio device. - if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) { - final int state = - intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED); - Log.d(TAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " - + "a=ACTION_CONNECTION_STATE_CHANGED, " - + "s=" + stateToString(state) + ", " - + "sb=" + isInitialStickyBroadcast() + ", " - + "BT state: " + bluetoothState); - if (state == BluetoothHeadset.STATE_CONNECTED) { - scoConnectionAttempts = 0; - updateAudioDeviceState(); - } else if (state == BluetoothHeadset.STATE_CONNECTING) { - // No action needed. - } else if (state == BluetoothHeadset.STATE_DISCONNECTING) { - // No action needed. - } else if (state == BluetoothHeadset.STATE_DISCONNECTED) { - // Bluetooth is probably powered off during the call. - stopScoAudio(); - updateAudioDeviceState(); + // Timeout interval for starting or stopping audio to a Bluetooth SCO device. + private static final int BLUETOOTH_SCO_TIMEOUT_MS = 4000; + // Maximum number of SCO connection attempts. + 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(); } - // Change in the audio (SCO) connection state of the Headset profile. - // Typically received after call to startScoAudio() has finalized. - } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) { - final int state = intent.getIntExtra( - BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_DISCONNECTED); - Log.d(TAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " - + "a=ACTION_AUDIO_STATE_CHANGED, " - + "s=" + stateToString(state) + ", " - + "sb=" + isInitialStickyBroadcast() + ", " - + "BT state: " + bluetoothState); - if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) { - cancelTimer(); - if (bluetoothState == State.SCO_CONNECTING) { - Log.d(TAG, "+++ Bluetooth audio SCO is now connected"); + }; + + 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 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 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 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; - updateAudioDeviceState(); - } else { - Log.w(TAG, "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED"); - } - } else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) { - Log.d(TAG, "+++ Bluetooth audio SCO is now connecting..."); - } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { - Log.d(TAG, "+++ Bluetooth audio SCO is now disconnected"); - if (isInitialStickyBroadcast()) { - Log.d(TAG, "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast."); - return; - } - updateAudioDeviceState(); + } else { + // Give up and "cancel" our request by calling stopBluetoothSco(). + Log.w(TAG, "BT failed to connect after timeout"); + stopScoAudio(); } - } - Log.d(TAG, "onReceive done: BT state=" + bluetoothState); + updateAudioDeviceState(); + Log.d(TAG, "bluetoothTimeout 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; + /** + * Checks whether audio uses Bluetooth SCO. + */ + private boolean isScoOn() { + return audioManager.isBluetoothScoOn(); } - 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; + /** + * 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"; + } } - // Stop BT SCO connection with remote device if needed. - stopScoAudio(); - // Close down remaining BT resources. - if (bluetoothState == State.UNINITIALIZED) { - return; + + // Bluetooth connection state. + public enum State { + // Bluetooth is not available; no adapter or Bluetooth is off. + UNINITIALIZED, + // Bluetooth error happened when trying to start Bluetooth. + ERROR, + // Bluetooth proxy object for the Headset profile exists, but no connected headset devices, + // SCO is not started or disconnected. + HEADSET_UNAVAILABLE, + // Bluetooth proxy object for the Headset profile connected, connected Bluetooth headset + // present, but SCO is not started or disconnected. + HEADSET_AVAILABLE, + // Bluetooth audio SCO connection with remote device is closing. + SCO_DISCONNECTING, + // Bluetooth audio SCO connection with remote device is initiated. + SCO_CONNECTING, + // Bluetooth audio SCO connection with remote device is established. + SCO_CONNECTED } - unregisterReceiver(bluetoothHeadsetReceiver); - cancelTimer(); - if (bluetoothHeadset != null) { - bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothHeadset); - bluetoothHeadset = null; + + /** + * Implementation of an interface that notifies BluetoothProfile IPC clients when they have been + * connected to or disconnected from the service. + */ + private class BluetoothServiceListener implements BluetoothProfile.ServiceListener { + @Override + // Called to notify the client when the proxy object has been connected to the service. + // Once we have the profile proxy object, we can use it to monitor the state of the + // connection and perform other operations that are relevant to the headset profile. + public void onServiceConnected(int profile, BluetoothProfile proxy) { + if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) { + return; + } + Log.d(TAG, "BluetoothServiceListener.onServiceConnected: BT state=" + bluetoothState); + // Android only supports one connected Bluetooth Headset at a time. + bluetoothHeadset = (BluetoothHeadset) proxy; + updateAudioDeviceState(); + Log.d(TAG, "onServiceConnected done: BT state=" + bluetoothState); + } + + @Override + /** Notifies the client when the proxy object has been disconnected from the service. */ + public void onServiceDisconnected(int profile) { + if (profile != BluetoothProfile.HEADSET || bluetoothState == State.UNINITIALIZED) { + return; + } + Log.d(TAG, "BluetoothServiceListener.onServiceDisconnected: BT state=" + bluetoothState); + stopScoAudio(); + bluetoothHeadset = null; + bluetoothDevice = null; + bluetoothState = State.HEADSET_UNAVAILABLE; + updateAudioDeviceState(); + Log.d(TAG, "onServiceDisconnected done: BT state=" + bluetoothState); + } } - 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; + // Intent broadcast receiver which handles changes in Bluetooth device availability. + // Detects headset changes and Bluetooth SCO state changes. + private class BluetoothHeadsetBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + if (bluetoothState == State.UNINITIALIZED) { + return; + } + final String action = intent.getAction(); + // Change in connection state of the Headset profile. Note that the + // change does not tell us anything about whether we're streaming + // audio to BT over SCO. Typically received when user turns on a BT + // headset while audio is active using another audio device. + if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) { + final int state = + intent.getIntExtra(BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_DISCONNECTED); + Log.d(TAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " + + "a=ACTION_CONNECTION_STATE_CHANGED, " + + "s=" + stateToString(state) + ", " + + "sb=" + isInitialStickyBroadcast() + ", " + + "BT state: " + bluetoothState); + if (state == BluetoothHeadset.STATE_CONNECTED) { + scoConnectionAttempts = 0; + updateAudioDeviceState(); + } else if (state == BluetoothHeadset.STATE_CONNECTING) { + // No action needed. + } else if (state == BluetoothHeadset.STATE_DISCONNECTING) { + // No action needed. + } else if (state == BluetoothHeadset.STATE_DISCONNECTED) { + // Bluetooth is probably powered off during the call. + stopScoAudio(); + updateAudioDeviceState(); + } + // Change in the audio (SCO) connection state of the Headset profile. + // Typically received after call to startScoAudio() has finalized. + } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) { + final int state = intent.getIntExtra( + BluetoothHeadset.EXTRA_STATE, BluetoothHeadset.STATE_AUDIO_DISCONNECTED); + Log.d(TAG, "BluetoothHeadsetBroadcastReceiver.onReceive: " + + "a=ACTION_AUDIO_STATE_CHANGED, " + + "s=" + stateToString(state) + ", " + + "sb=" + isInitialStickyBroadcast() + ", " + + "BT state: " + bluetoothState); + if (state == BluetoothHeadset.STATE_AUDIO_CONNECTED) { + cancelTimer(); + if (bluetoothState == State.SCO_CONNECTING) { + Log.d(TAG, "+++ Bluetooth audio SCO is now connected"); + bluetoothState = State.SCO_CONNECTED; + scoConnectionAttempts = 0; + updateAudioDeviceState(); + } else { + Log.w(TAG, "Unexpected state BluetoothHeadset.STATE_AUDIO_CONNECTED"); + } + } else if (state == BluetoothHeadset.STATE_AUDIO_CONNECTING) { + Log.d(TAG, "+++ Bluetooth audio SCO is now connecting..."); + } else if (state == BluetoothHeadset.STATE_AUDIO_DISCONNECTED) { + Log.d(TAG, "+++ Bluetooth audio SCO is now disconnected"); + if (isInitialStickyBroadcast()) { + Log.d(TAG, "Ignore STATE_AUDIO_DISCONNECTED initial sticky broadcast."); + return; + } + updateAudioDeviceState(); + } + } + Log.d(TAG, "onReceive done: BT state=" + bluetoothState); + } } - 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 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 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 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"; - } - } } diff --git a/app/src/main/java/com/nextcloud/talk/webrtc/MagicProximitySensor.java b/app/src/main/java/com/nextcloud/talk/webrtc/MagicProximitySensor.java index 9fb1746cf..523a051a8 100644 --- a/app/src/main/java/com/nextcloud/talk/webrtc/MagicProximitySensor.java +++ b/app/src/main/java/com/nextcloud/talk/webrtc/MagicProximitySensor.java @@ -51,129 +51,137 @@ import org.webrtc.ThreadUtils; * Anything less than the threshold value and the sensor returns "NEAR". */ public class MagicProximitySensor implements SensorEventListener { - private static final String TAG = "MagicProximitySensor"; + private static final String TAG = "MagicProximitySensor"; - // This class should be created, started and stopped on one thread - // (e.g. the main thread). We use |nonThreadSafe| to ensure that this is - // the case. Only active when |DEBUG| is set to true. - private final ThreadUtils.ThreadChecker threadChecker = new ThreadUtils.ThreadChecker(); + // This class should be created, started and stopped on one thread + // (e.g. the main thread). We use |nonThreadSafe| to ensure that this is + // the case. Only active when |DEBUG| is set to true. + private final ThreadUtils.ThreadChecker threadChecker = new ThreadUtils.ThreadChecker(); - private final Runnable onSensorStateListener; - private final SensorManager sensorManager; - private Sensor proximitySensor = null; - private boolean lastStateReportIsNear = false; + private final Runnable onSensorStateListener; + private final SensorManager sensorManager; + private Sensor proximitySensor = null; + private boolean lastStateReportIsNear = false; - /** Construction */ - static MagicProximitySensor create(Context context, Runnable sensorStateListener) { - return new MagicProximitySensor(context, sensorStateListener); - } - - private MagicProximitySensor(Context context, Runnable sensorStateListener) { - onSensorStateListener = sensorStateListener; - sensorManager = ((SensorManager) context.getSystemService(Context.SENSOR_SERVICE)); - } - - /** - * Activate the proximity sensor. Also do initialization if called for the - * first time. - */ - public boolean start() { - threadChecker.checkIsOnValidThread(); - if (!initDefaultSensor()) { - // Proximity sensor is not supported on this device. - return false; + private MagicProximitySensor(Context context, Runnable sensorStateListener) { + onSensorStateListener = sensorStateListener; + sensorManager = ((SensorManager) context.getSystemService(Context.SENSOR_SERVICE)); } - sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL); - return true; - } - /** Deactivate the proximity sensor. */ - public void stop() { - threadChecker.checkIsOnValidThread(); - if (proximitySensor == null) { - return; + /** + * Construction + */ + static MagicProximitySensor create(Context context, Runnable sensorStateListener) { + return new MagicProximitySensor(context, sensorStateListener); } - sensorManager.unregisterListener(this, proximitySensor); - } - /** Getter for last reported state. Set to true if "near" is reported. */ - public boolean sensorReportsNearState() { - threadChecker.checkIsOnValidThread(); - return lastStateReportIsNear; - } + /** + * Activate the proximity sensor. Also do initialization if called for the + * first time. + */ + public boolean start() { + threadChecker.checkIsOnValidThread(); + if (!initDefaultSensor()) { + // Proximity sensor is not supported on this device. + return false; + } + sensorManager.registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL); + return true; + } - @Override - public final void onAccuracyChanged(Sensor sensor, int accuracy) { - threadChecker.checkIsOnValidThread(); - if (sensor.getType() == Sensor.TYPE_PROXIMITY) { - if (accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) { - Log.e(TAG, "The values returned by this sensor cannot be trusted"); + /** + * Deactivate the proximity sensor. + */ + public void stop() { + threadChecker.checkIsOnValidThread(); + if (proximitySensor == null) { + return; + } + sensorManager.unregisterListener(this, proximitySensor); + } + + /** + * Getter for last reported state. Set to true if "near" is reported. + */ + public boolean sensorReportsNearState() { + threadChecker.checkIsOnValidThread(); + return lastStateReportIsNear; + } + + @Override + public final void onAccuracyChanged(Sensor sensor, int accuracy) { + threadChecker.checkIsOnValidThread(); + if (sensor.getType() == Sensor.TYPE_PROXIMITY) { + if (accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE) { + Log.e(TAG, "The values returned by this sensor cannot be trusted"); + } } } - } - @Override - public final void onSensorChanged(SensorEvent event) { - threadChecker.checkIsOnValidThread(); - if (event.sensor.getType() == Sensor.TYPE_PROXIMITY) { - // As a best practice; do as little as possible within this method and - // avoid blocking. - float distanceInCentimeters = event.values[0]; - if (distanceInCentimeters < proximitySensor.getMaximumRange()) { - Log.d(TAG, "Proximity sensor => NEAR state"); - lastStateReportIsNear = true; - } else { - Log.d(TAG, "Proximity sensor => FAR state"); - lastStateReportIsNear = false; - } + @Override + public final void onSensorChanged(SensorEvent event) { + threadChecker.checkIsOnValidThread(); + if (event.sensor.getType() == Sensor.TYPE_PROXIMITY) { + // As a best practice; do as little as possible within this method and + // avoid blocking. + float distanceInCentimeters = event.values[0]; + if (distanceInCentimeters < proximitySensor.getMaximumRange()) { + Log.d(TAG, "Proximity sensor => NEAR state"); + lastStateReportIsNear = true; + } else { + Log.d(TAG, "Proximity sensor => FAR state"); + lastStateReportIsNear = false; + } - // Report about new state to listening client. Client can then call - // sensorReportsNearState() to query the current state (NEAR or FAR). - if (onSensorStateListener != null) { - onSensorStateListener.run(); + // Report about new state to listening client. Client can then call + // sensorReportsNearState() to query the current state (NEAR or FAR). + if (onSensorStateListener != null) { + onSensorStateListener.run(); + } } } - } - /** - * Get default proximity sensor if it exists. Tablet devices (e.g. Nexus 7) - * does not support this type of sensor and false will be returned in such - * cases. - */ - private boolean initDefaultSensor() { - if (proximitySensor != null) { - return true; + /** + * Get default proximity sensor if it exists. Tablet devices (e.g. Nexus 7) + * does not support this type of sensor and false will be returned in such + * cases. + */ + private boolean initDefaultSensor() { + if (proximitySensor != null) { + return true; + } + proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY); + if (proximitySensor == null) { + return false; + } + logProximitySensorInfo(); + return true; } - proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY); - if (proximitySensor == null) { - return false; - } - logProximitySensorInfo(); - return true; - } - /** Helper method for logging information about the proximity sensor. */ - private void logProximitySensorInfo() { - if (proximitySensor == null) { - return; + /** + * Helper method for logging information about the proximity sensor. + */ + private void logProximitySensorInfo() { + if (proximitySensor == null) { + return; + } + StringBuilder info = new StringBuilder("Proximity sensor: "); + info.append("name=").append(proximitySensor.getName()); + info.append(", vendor: ").append(proximitySensor.getVendor()); + info.append(", power: ").append(proximitySensor.getPower()); + info.append(", resolution: ").append(proximitySensor.getResolution()); + info.append(", max range: ").append(proximitySensor.getMaximumRange()); + info.append(", min delay: ").append(proximitySensor.getMinDelay()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { + // Added in API level 20. + info.append(", type: ").append(proximitySensor.getStringType()); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // Added in API level 21. + info.append(", max delay: ").append(proximitySensor.getMaxDelay()); + info.append(", reporting mode: ").append(proximitySensor.getReportingMode()); + info.append(", isWakeUpSensor: ").append(proximitySensor.isWakeUpSensor()); + } + Log.d(TAG, info.toString()); } - StringBuilder info = new StringBuilder("Proximity sensor: "); - info.append("name=").append(proximitySensor.getName()); - info.append(", vendor: ").append(proximitySensor.getVendor()); - info.append(", power: ").append(proximitySensor.getPower()); - info.append(", resolution: ").append(proximitySensor.getResolution()); - info.append(", max range: ").append(proximitySensor.getMaximumRange()); - info.append(", min delay: ").append(proximitySensor.getMinDelay()); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { - // Added in API level 20. - info.append(", type: ").append(proximitySensor.getStringType()); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - // Added in API level 21. - info.append(", max delay: ").append(proximitySensor.getMaxDelay()); - info.append(", reporting mode: ").append(proximitySensor.getReportingMode()); - info.append(", isWakeUpSensor: ").append(proximitySensor.isWakeUpSensor()); - } - Log.d(TAG, info.toString()); - } } diff --git a/app/src/main/res/layout/rv_item_menu.xml b/app/src/main/res/layout/rv_item_menu.xml index 5d9b6f486..f75fe6f9e 100644 --- a/app/src/main/res/layout/rv_item_menu.xml +++ b/app/src/main/res/layout/rv_item_menu.xml @@ -21,13 +21,13 @@ + android:layout_margin="@dimen/margin_between_elements"/> \ No newline at end of file