mirror of
https://github.com/nextcloud/talk-android
synced 2025-07-10 06:14:10 +01:00
Some work on the menu
Signed-off-by: Mario Danic <mario@lovelyhq.com>
This commit is contained in:
parent
1dde5029f4
commit
6f641899f1
@ -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);
|
||||
|
@ -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<MenuItem.MenuItemViewHolder> {
|
||||
public class MenuItem extends AbstractFlexibleItem<MenuItem.MenuItemViewHolder> {
|
||||
private String title;
|
||||
|
||||
public MenuItem(String title) {
|
||||
@ -70,8 +67,6 @@ public class MenuItem extends AbstractFlexibleItem<MenuItem.MenuItemViewHolder>
|
||||
@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 {
|
||||
|
@ -168,7 +168,7 @@ public interface NcApi {
|
||||
@FormUrlEncoded
|
||||
@POST
|
||||
Observable<SignalingOverall> sendSignalingMessages(@Header("Authorization") String authorization, @Url String url,
|
||||
@Field("messages") String messages);
|
||||
@Field("messages") String messages);
|
||||
|
||||
/*
|
||||
Server URL is: baseUrl + ocsApiVersion + spreedApiVersion + /signaling
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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<AbstractFlexibleItem> menuItems;
|
||||
private FlexibleAdapter<AbstractFlexibleItem> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Nextcloud Talk application
|
||||
*
|
||||
* @author Mario Danic
|
||||
* Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.events;
|
||||
|
||||
public class MenuItemClickEvent {
|
||||
private String menuTitle;
|
||||
|
||||
public MenuItemClickEvent(String menuTitle) {
|
||||
this.menuTitle = menuTitle;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
|
||||
if (devices.isEmpty()) {
|
||||
bluetoothDevice = null;
|
||||
bluetoothState = State.HEADSET_UNAVAILABLE;
|
||||
Log.d(TAG, "No connected bluetooth headset");
|
||||
} else {
|
||||
// Always use first device in list. Android only supports one device.
|
||||
bluetoothDevice = devices.get(0);
|
||||
bluetoothState = State.HEADSET_AVAILABLE;
|
||||
Log.d(TAG, "Connected bluetooth headset: "
|
||||
+ "name=" + bluetoothDevice.getName() + ", "
|
||||
+ "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice))
|
||||
+ ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice));
|
||||
}
|
||||
Log.d(TAG, "updateDevice done: BT state=" + bluetoothState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stubs for test mocks.
|
||||
*/
|
||||
protected AudioManager getAudioManager(Context context) {
|
||||
return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
}
|
||||
|
||||
protected void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
|
||||
apprtcContext.registerReceiver(receiver, filter);
|
||||
}
|
||||
|
||||
protected void unregisterReceiver(BroadcastReceiver receiver) {
|
||||
apprtcContext.unregisterReceiver(receiver);
|
||||
}
|
||||
|
||||
protected boolean getBluetoothProfileProxy(
|
||||
Context context, BluetoothProfile.ServiceListener listener, int profile) {
|
||||
return bluetoothAdapter.getProfileProxy(context, listener, profile);
|
||||
}
|
||||
|
||||
protected boolean hasPermission(Context context, String permission) {
|
||||
return apprtcContext.checkPermission(permission, Process.myPid(), Process.myUid())
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the state of the local Bluetooth adapter.
|
||||
*/
|
||||
@SuppressLint("HardwareIds")
|
||||
protected void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) {
|
||||
Log.d(TAG, "BluetoothAdapter: "
|
||||
+ "enabled=" + localAdapter.isEnabled() + ", "
|
||||
+ "state=" + stateToString(localAdapter.getState()) + ", "
|
||||
+ "name=" + localAdapter.getName() + ", "
|
||||
+ "address=" + localAdapter.getAddress());
|
||||
// Log the set of BluetoothDevice objects that are bonded (paired) to the local adapter.
|
||||
Set<BluetoothDevice> pairedDevices = localAdapter.getBondedDevices();
|
||||
if (!pairedDevices.isEmpty()) {
|
||||
Log.d(TAG, "paired devices:");
|
||||
for (BluetoothDevice device : pairedDevices) {
|
||||
Log.d(TAG, " name=" + device.getName() + ", address=" + device.getAddress());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the audio manager updates its list of available audio devices.
|
||||
*/
|
||||
private void updateAudioDeviceState() {
|
||||
ThreadUtils.checkIsOnMainThread();
|
||||
Log.d(TAG, "updateAudioDeviceState");
|
||||
apprtcAudioManager.updateAudioDeviceState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds.
|
||||
*/
|
||||
private void startTimer() {
|
||||
ThreadUtils.checkIsOnMainThread();
|
||||
Log.d(TAG, "startTimer");
|
||||
handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels any outstanding timer tasks.
|
||||
*/
|
||||
private void cancelTimer() {
|
||||
ThreadUtils.checkIsOnMainThread();
|
||||
Log.d(TAG, "cancelTimer");
|
||||
handler.removeCallbacks(bluetoothTimeoutRunnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when start of the BT SCO channel takes too long time. Usually
|
||||
* happens when the BT device has been turned on during an ongoing call.
|
||||
*/
|
||||
private void bluetoothTimeout() {
|
||||
ThreadUtils.checkIsOnMainThread();
|
||||
if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
|
||||
return;
|
||||
}
|
||||
Log.d(TAG, "bluetoothTimeout: BT state=" + bluetoothState + ", "
|
||||
+ "attempts: " + scoConnectionAttempts + ", "
|
||||
+ "SCO is on: " + isScoOn());
|
||||
if (bluetoothState != State.SCO_CONNECTING) {
|
||||
return;
|
||||
}
|
||||
// Bluetooth SCO should be connecting; check the latest result.
|
||||
boolean scoConnected = false;
|
||||
List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
|
||||
if (devices.size() > 0) {
|
||||
bluetoothDevice = devices.get(0);
|
||||
if (bluetoothHeadset.isAudioConnected(bluetoothDevice)) {
|
||||
Log.d(TAG, "SCO connected with " + bluetoothDevice.getName());
|
||||
scoConnected = true;
|
||||
} else {
|
||||
Log.d(TAG, "SCO is not connected with " + bluetoothDevice.getName());
|
||||
}
|
||||
}
|
||||
if (scoConnected) {
|
||||
// We thought BT had timed out, but it's actually on; updating state.
|
||||
bluetoothState = State.SCO_CONNECTED;
|
||||
scoConnectionAttempts = 0;
|
||||
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<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
|
||||
if (devices.isEmpty()) {
|
||||
bluetoothDevice = null;
|
||||
bluetoothState = State.HEADSET_UNAVAILABLE;
|
||||
Log.d(TAG, "No connected bluetooth headset");
|
||||
} else {
|
||||
// Always use first device in list. Android only supports one device.
|
||||
bluetoothDevice = devices.get(0);
|
||||
bluetoothState = State.HEADSET_AVAILABLE;
|
||||
Log.d(TAG, "Connected bluetooth headset: "
|
||||
+ "name=" + bluetoothDevice.getName() + ", "
|
||||
+ "state=" + stateToString(bluetoothHeadset.getConnectionState(bluetoothDevice))
|
||||
+ ", SCO audio=" + bluetoothHeadset.isAudioConnected(bluetoothDevice));
|
||||
}
|
||||
Log.d(TAG, "updateDevice done: BT state=" + bluetoothState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stubs for test mocks.
|
||||
*/
|
||||
protected AudioManager getAudioManager(Context context) {
|
||||
return (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
}
|
||||
|
||||
protected void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
|
||||
apprtcContext.registerReceiver(receiver, filter);
|
||||
}
|
||||
|
||||
protected void unregisterReceiver(BroadcastReceiver receiver) {
|
||||
apprtcContext.unregisterReceiver(receiver);
|
||||
}
|
||||
|
||||
protected boolean getBluetoothProfileProxy(
|
||||
Context context, BluetoothProfile.ServiceListener listener, int profile) {
|
||||
return bluetoothAdapter.getProfileProxy(context, listener, profile);
|
||||
}
|
||||
|
||||
protected boolean hasPermission(Context context, String permission) {
|
||||
return apprtcContext.checkPermission(permission, Process.myPid(), Process.myUid())
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
/** Logs the state of the local Bluetooth adapter. */
|
||||
@SuppressLint("HardwareIds")
|
||||
protected void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) {
|
||||
Log.d(TAG, "BluetoothAdapter: "
|
||||
+ "enabled=" + localAdapter.isEnabled() + ", "
|
||||
+ "state=" + stateToString(localAdapter.getState()) + ", "
|
||||
+ "name=" + localAdapter.getName() + ", "
|
||||
+ "address=" + localAdapter.getAddress());
|
||||
// Log the set of BluetoothDevice objects that are bonded (paired) to the local adapter.
|
||||
Set<BluetoothDevice> pairedDevices = localAdapter.getBondedDevices();
|
||||
if (!pairedDevices.isEmpty()) {
|
||||
Log.d(TAG, "paired devices:");
|
||||
for (BluetoothDevice device : pairedDevices) {
|
||||
Log.d(TAG, " name=" + device.getName() + ", address=" + device.getAddress());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Ensures that the audio manager updates its list of available audio devices. */
|
||||
private void updateAudioDeviceState() {
|
||||
ThreadUtils.checkIsOnMainThread();
|
||||
Log.d(TAG, "updateAudioDeviceState");
|
||||
apprtcAudioManager.updateAudioDeviceState();
|
||||
}
|
||||
|
||||
/** Starts timer which times out after BLUETOOTH_SCO_TIMEOUT_MS milliseconds. */
|
||||
private void startTimer() {
|
||||
ThreadUtils.checkIsOnMainThread();
|
||||
Log.d(TAG, "startTimer");
|
||||
handler.postDelayed(bluetoothTimeoutRunnable, BLUETOOTH_SCO_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
/** Cancels any outstanding timer tasks. */
|
||||
private void cancelTimer() {
|
||||
ThreadUtils.checkIsOnMainThread();
|
||||
Log.d(TAG, "cancelTimer");
|
||||
handler.removeCallbacks(bluetoothTimeoutRunnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when start of the BT SCO channel takes too long time. Usually
|
||||
* happens when the BT device has been turned on during an ongoing call.
|
||||
*/
|
||||
private void bluetoothTimeout() {
|
||||
ThreadUtils.checkIsOnMainThread();
|
||||
if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
|
||||
return;
|
||||
}
|
||||
Log.d(TAG, "bluetoothTimeout: BT state=" + bluetoothState + ", "
|
||||
+ "attempts: " + scoConnectionAttempts + ", "
|
||||
+ "SCO is on: " + isScoOn());
|
||||
if (bluetoothState != State.SCO_CONNECTING) {
|
||||
return;
|
||||
}
|
||||
// Bluetooth SCO should be connecting; check the latest result.
|
||||
boolean scoConnected = false;
|
||||
List<BluetoothDevice> devices = bluetoothHeadset.getConnectedDevices();
|
||||
if (devices.size() > 0) {
|
||||
bluetoothDevice = devices.get(0);
|
||||
if (bluetoothHeadset.isAudioConnected(bluetoothDevice)) {
|
||||
Log.d(TAG, "SCO connected with " + bluetoothDevice.getName());
|
||||
scoConnected = true;
|
||||
} else {
|
||||
Log.d(TAG, "SCO is not connected with " + bluetoothDevice.getName());
|
||||
}
|
||||
}
|
||||
if (scoConnected) {
|
||||
// We thought BT had timed out, but it's actually on; updating state.
|
||||
bluetoothState = State.SCO_CONNECTED;
|
||||
scoConnectionAttempts = 0;
|
||||
} else {
|
||||
// Give up and "cancel" our request by calling stopBluetoothSco().
|
||||
Log.w(TAG, "BT failed to connect after timeout");
|
||||
stopScoAudio();
|
||||
}
|
||||
updateAudioDeviceState();
|
||||
Log.d(TAG, "bluetoothTimeout done: BT state=" + bluetoothState);
|
||||
}
|
||||
|
||||
/** Checks whether audio uses Bluetooth SCO. */
|
||||
private boolean isScoOn() {
|
||||
return audioManager.isBluetoothScoOn();
|
||||
}
|
||||
|
||||
/** Converts BluetoothAdapter states into local string representations. */
|
||||
private String stateToString(int state) {
|
||||
switch (state) {
|
||||
case BluetoothAdapter.STATE_DISCONNECTED:
|
||||
return "DISCONNECTED";
|
||||
case BluetoothAdapter.STATE_CONNECTED:
|
||||
return "CONNECTED";
|
||||
case BluetoothAdapter.STATE_CONNECTING:
|
||||
return "CONNECTING";
|
||||
case BluetoothAdapter.STATE_DISCONNECTING:
|
||||
return "DISCONNECTING";
|
||||
case BluetoothAdapter.STATE_OFF:
|
||||
return "OFF";
|
||||
case BluetoothAdapter.STATE_ON:
|
||||
return "ON";
|
||||
case BluetoothAdapter.STATE_TURNING_OFF:
|
||||
// Indicates the local Bluetooth adapter is turning off. Local clients should immediately
|
||||
// attempt graceful disconnection of any remote links.
|
||||
return "TURNING_OFF";
|
||||
case BluetoothAdapter.STATE_TURNING_ON:
|
||||
// Indicates the local Bluetooth adapter is turning on. However local clients should wait
|
||||
// for STATE_ON before attempting to use the adapter.
|
||||
return "TURNING_ON";
|
||||
default:
|
||||
return "INVALID";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -21,13 +21,13 @@
|
||||
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/menu_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/activity_horizontal_margin"/>
|
||||
android:layout_margin="@dimen/margin_between_elements"/>
|
||||
|
||||
</RelativeLayout>
|
Loading…
Reference in New Issue
Block a user