How to detect if phone is connected to Android Auto - android

I am making an app that should change its behavior when the phone is connected to an Android Auto. It does not have any Auto capabilities and will not be marketed/submitted as an Android Auto app.
Is there a way to detect if the phone is connected to an Android Auto? I know it is possible for Auto media apps to detect their connection status via a BroadcastReceiver registered for com.google.android.gms.car.media.STATUS action. The documentation is not 100% clear, so will this reliably work also for all other non-Auto apps?

Edit: with Android 12, the solution doesn't work anymore and instead, it's better to use CarConnection API documented here
I know this is an old thread but since it comes first in Google, here is the answer from another question
public static boolean isCarUiMode(Context c) {
UiModeManager uiModeManager = (UiModeManager) c.getSystemService(Context.UI_MODE_SERVICE);
if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) {
LogHelper.d(TAG, "Running in Car mode");
return true;
} else {
LogHelper.d(TAG, "Running on a non-Car mode");
return false;
}
}

Configuration.UI_MODE_TYPE_CAR is not working on Anroid 12. You can use CarConnection API in the androidx.car.app:app library. But that is too heavy to import entire library only for car connections if you don't need other features.
So I write a piece of code base on the CarConnection to detect Android Auto connection, as below:
class AutoConnectionDetector(val context: Context) {
companion object {
const val TAG = "AutoConnectionDetector"
// columnName for provider to query on connection status
const val CAR_CONNECTION_STATE = "CarConnectionState"
// auto app on your phone will send broadcast with this action when connection state changes
const val ACTION_CAR_CONNECTION_UPDATED = "androidx.car.app.connection.action.CAR_CONNECTION_UPDATED"
// phone is not connected to car
const val CONNECTION_TYPE_NOT_CONNECTED = 0
// phone is connected to Automotive OS
const val CONNECTION_TYPE_NATIVE = 1
// phone is connected to Android Auto
const val CONNECTION_TYPE_PROJECTION = 2
private const val QUERY_TOKEN = 42
private const val CAR_CONNECTION_AUTHORITY = "androidx.car.app.connection"
private val PROJECTION_HOST_URI = Uri.Builder().scheme("content").authority(CAR_CONNECTION_AUTHORITY).build()
}
private val carConnectionReceiver = CarConnectionBroadcastReceiver()
private val carConnectionQueryHandler = CarConnectionQueryHandler(context.contentResolver)
fun registerCarConnectionReceiver() {
context.registerReceiver(carConnectionReceiver, IntentFilter(ACTION_CAR_CONNECTION_UPDATED))
queryForState()
}
fun unRegisterCarConnectionReceiver() {
context.unregisterReceiver(carConnectionReceiver)
}
private fun queryForState() {
carConnectionQueryHandler.startQuery(
QUERY_TOKEN,
null,
PROJECTION_HOST_URI,
arrayOf(CAR_CONNECTION_STATE),
null,
null,
null
)
}
inner class CarConnectionBroadcastReceiver : BroadcastReceiver() {
// query for connection state every time the receiver receives the broadcast
override fun onReceive(context: Context?, intent: Intent?) {
queryForState()
}
}
internal class CarConnectionQueryHandler(resolver: ContentResolver?) : AsyncQueryHandler(resolver) {
// notify new queryed connection status when query complete
override fun onQueryComplete(token: Int, cookie: Any?, response: Cursor?) {
if (response == null) {
Log.w(TAG, "Null response from content provider when checking connection to the car, treating as disconnected")
notifyCarDisconnected()
return
}
val carConnectionTypeColumn = response.getColumnIndex(CAR_CONNECTION_STATE)
if (carConnectionTypeColumn < 0) {
Log.w(TAG, "Connection to car response is missing the connection type, treating as disconnected")
notifyCarDisconnected()
return
}
if (!response.moveToNext()) {
Log.w(TAG, "Connection to car response is empty, treating as disconnected")
notifyCarDisconnected()
return
}
val connectionState = response.getInt(carConnectionTypeColumn)
if (connectionState == CONNECTION_TYPE_NOT_CONNECTED) {
Log.i(TAG, "Android Auto disconnected")
notifyCarDisconnected()
} else {
Log.i(TAG, "Android Auto connected")
notifyCarConnected()
}
}
}
}
This solution works on android 6~12. If you need to detect car connection status on android 5, use the Configuration.UI_MODE_TYPE_CAR solution.

Recomendation G.Zxuan work perfect, but we must add in dependecies "androidx.car.app:app-projected:1.1.0" in build.gradle

See https://developer.android.com/training/cars/apps#car-connection
val Context.isAndroidAutoConnected: LiveData<Boolean>
get() = CarConnection(this).type
.map { it == CarConnection.CONNECTION_TYPE_PROJECTION }
app/build.gradle:
dependencies {
implementation 'androidx.car.app:app:1.1.0'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.5.1"
}

#G.Zxuan 's Kotlin solution works great. I've transferred it into Java and added a listener interface. Tested on Android 12
public class AutoConnectionDetector {
private final Context context;
private static String TAG = "AutoConnectionDetector";
private final CarConnectionBroadcastReceiver carConnectionReceiver = new CarConnectionBroadcastReceiver();
private final CarConnectionQueryHandler carConnectionQueryHandler;
// columnName for provider to query on connection status
private static final String CAR_CONNECTION_STATE = "CarConnectionState";
// auto app on your phone will send broadcast with this action when connection state changes
private final String ACTION_CAR_CONNECTION_UPDATED = "androidx.car.app.connection.action.CAR_CONNECTION_UPDATED";
// phone is not connected to car
private static final int CONNECTION_TYPE_NOT_CONNECTED = 0;
// phone is connected to Automotive OS
private final int CONNECTION_TYPE_NATIVE = 1;
// phone is connected to Android Auto
private final int CONNECTION_TYPE_PROJECTION = 2;
private final int QUERY_TOKEN = 42;
private final String CAR_CONNECTION_AUTHORITY = "androidx.car.app.connection";
private final Uri PROJECTION_HOST_URI = new Uri.Builder().scheme("content").authority(CAR_CONNECTION_AUTHORITY).build();
public interface OnCarConnectionStateListener {
void onCarConnected();
void onCarDisconnected();
}
private static OnCarConnectionStateListener listener;
public void setListener(OnCarConnectionStateListener listener) {
AutoConnectionDetector.listener = listener;
}
public AutoConnectionDetector(Context context) {
this.context = context;
carConnectionQueryHandler = new CarConnectionQueryHandler(context.getContentResolver());
}
public void registerCarConnectionReceiver() {
context.registerReceiver(carConnectionReceiver, new IntentFilter(ACTION_CAR_CONNECTION_UPDATED));
queryForState();
Log.i(TAG, "registerCarConnectionReceiver: ");
}
public void unRegisterCarConnectionReceiver() {
context.unregisterReceiver(carConnectionReceiver);
Log.i(TAG, "unRegisterCarConnectionReceiver: ");
}
private void queryForState() {
String[] projection = {CAR_CONNECTION_STATE};
carConnectionQueryHandler.startQuery(
QUERY_TOKEN,
null,
PROJECTION_HOST_URI,
projection,
null,
null,
null
);
}
private static void notifyCarConnected() {
listener.onCarConnected();
}
private static void notifyCarDisconnected() {
listener.onCarDisconnected();
}
class CarConnectionBroadcastReceiver extends BroadcastReceiver {
// query for connection state every time the receiver receives the broadcast
#Override
public void onReceive(Context context, Intent intent) {
queryForState();
}
}
private static class CarConnectionQueryHandler extends AsyncQueryHandler {
public CarConnectionQueryHandler(ContentResolver contentResolver) {
super(contentResolver);
}
#Override
protected void onQueryComplete(int token, Object cookie, Cursor response) {
if (response == null) {
Log.w(TAG, "Null response from content provider when checking connection to the car, treating as disconnected");
notifyCarDisconnected();
return;
}
int carConnectionTypeColumn = response.getColumnIndex(CAR_CONNECTION_STATE);
if (carConnectionTypeColumn < 0) {
Log.w(TAG, "Connection to car response is missing the connection type, treating as disconnected");
notifyCarDisconnected();
return;
}
if (!response.moveToNext()) {
Log.w(TAG, "Connection to car response is empty, treating as disconnected");
notifyCarDisconnected();
return;
}
int connectionState = response.getInt(carConnectionTypeColumn);
if (connectionState == CONNECTION_TYPE_NOT_CONNECTED) {
Log.i(TAG, "Android Auto disconnected");
notifyCarDisconnected();
} else {
Log.i(TAG, "Android Auto connected");
Log.i(TAG, "onQueryComplete: " + connectionState);
notifyCarConnected();
}
}
}
}

Related

AudioManager setCommunicationDevice is not working on Android 12

I'm developing a mini native (Java) SDK to turn ON/OFF speaker for a react-native app for calls , because React-Native Incall Manager doesn't work properly on Android 12.
Module is working good on Android 11, but since setSpeakerOn method from AudioManager is deprecated for newest SDK, on Android 12 there are some issues. I took the code snippet from official docs and adapted it. Even if boolean
result = audioManager.setCommunicationDevice(device) returns TRUE , it doesn't change the speaker state.
Permission
android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
AudioModule.java
public class AudioModule extends ReactContextBaseJavaModule {
private AudioManager audioManager;
private static final String TAG = "AudioModule";
private static ReactApplicationContext reactContext;
AudioModule(ReactApplicationContext context) {
super(context);
reactContext = context;
audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE));
}
#Override
public String getName() {
return "AudioModule";
}
#ReactMethod
public void setSpeakerState(Boolean speakerState) {
try {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
if (speakerState) {
setCommunicationDevice(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
} else {
audioManager.clearCommunicationDevice(); //turn off speaker
}
} else {
audioManager.setMode(speakerState ? AudioManager.MODE_IN_COMMUNICATION : AudioManager.MODE_NORMAL);
audioManager.setSpeakerphoneOn(speakerState);
}
Toast.makeText(reactContext, "speakerStatus " + audioManager.isSpeakerphoneOn(), Toast.LENGTH_LONG).show();
} catch (Exception e) {
Log.d(TAG, "AudioModule setSpeaker", e);
}
}
#RequiresApi(api = Build.VERSION_CODES.S)
public void setCommunicationDevice(Integer targetDeviceType) {
List<AudioDeviceInfo> audioDevices = audioManager.getAvailableCommunicationDevices();
for (AudioDeviceInfo device : audioDevices) {
if (device.getType() == targetDeviceType) {
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION );
boolean result = audioManager.setCommunicationDevice(device);
break;
}
}
}
Note: Native Module for React-Native is working properly on Android 11 OS and is showing Toast on Android 12 and 13 (tested on physical devices)
UPDATED:
Tried also with IncallService interface:
import android.telecom.InCallService;
public class CallService extends InCallService {
private static CallService sInstance;
CallService() {
sInstance = this;
}
public static CallService getInstance() {
return sInstance;
}
}
Method called
#ReactMethod
public void toggleSpeaker() {
boolean isSpeakerOn = audioManager.isSpeakerphoneOn();
int earpiece = CallAudioState.ROUTE_WIRED_OR_EARPIECE;
int speaker = CallAudioState.ROUTE_SPEAKER;
if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.P) {
callService.getInstance().setAudioRoute(isSpeakerOn ? earpiece : speaker);
} else {
audioManager.setSpeakerphoneOn(!isSpeakerOn);
}
Toast.makeText(reactContext, "speakerStatus " + audioManager.isSpeakerphoneOn(), Toast.LENGTH_LONG).show();
}
Logcat returns the next message:
android.hardware.audio.service: Failed to fetch the lookup information
of the device
I had similar issue with android 12 and even with some android 11 devices. When I call am.setCommunicationDevice(earpieceDevice) it was returning true but the audio was still playing on loudspeaker. Then I force enabled NORMAL_MODE by calling am.setMode(AudioManager.MODE_NORMAL), and now it's working. Weird!
private fun setAudioDevice(){
am.mode = AudioManager.MODE_NORMAL
val speakerDevice: AudioDeviceInfo? = getAudioDevice(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER)
am.setCommunicationDevice(speakerDevice)
}
private fun getAudioDevice(type: Int): AudioDeviceInfo? {
val audioDevices = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
for (deviceInfo in audioDevices) {
if (type == deviceInfo.type) return deviceInfo
}
return null
}

Unable to host cloud anchors successfully and retrieve cloud anchor id from google cloud anchor api

I am trying to host a cloud anchor on google arcore cloud api. I have included the api key in my manifest. I can also see the request that the android device makes when I want to host an anchor and there is also a response being sent back from the api as can be seen in the image below.
However, the cloud anchor state never changes to SUCCESS and I am never receiving the cloud anchor id back from google even though I have set up an update listener to do so as can be seen below. From debugging the program I can see that the AppAnchorState is never being changed from NONE but I haven't managed to gather any other information.
I am using both sceneform and arcore 1.7.0. Below is the code I am using to try and host my anchor and get a cloud anchor id back from the api.
Any help would be greatly appreciated as I have been struggling with the problem for hours at this stage.
public class ArActivity extends AppCompatActivity {
private enum AppAnchorState {
NONE,
HOSTING,
HOSTED,
RESOLVING,
RESOLVED
}
private AppAnchorState appAnchorState = AppAnchorState.NONE;
private static final String TAG = ArActivity.class.getSimpleName();
private static final double MIN_OPENGL_VERSION = 3.0;
private static final String GLTF_ASSET = "https://github.com/KhronosGroup/glTF-Sample-Models/raw/master/2.0/Duck/glTF/Duck.gltf";
private ArFragment arFragment;
private ModelRenderable renderable;
private SnackbarHelper snackbarHelper;
private Anchor cloudAnchor;
#Override
#SuppressWarnings({"AndroidApiChecker", "FutureReturnValueIgnored"})
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!checkIsSupportedDeviceOrFinish(this)) {
return;
}
snackbarHelper = new SnackbarHelper();
setContentView(R.layout.activity_ux);
arFragment = (CustomArFragment) getSupportFragmentManager().findFragmentById(R.id.sceneform_fragment);
arFragment.getArSceneView().getScene().addOnUpdateListener(this::onUpdateFrame);
ModelRenderable.builder()
.setSource(this, RenderableSource.builder().setSource(
this,
Uri.parse(GLTF_ASSET),
RenderableSource.SourceType.GLTF2)
.build())
.setRegistryId(GLTF_ASSET)
.build()
.thenAccept(renderable -> this.renderable = renderable)
.exceptionally(
throwable -> {
Toast toast =
Toast.makeText(this, "Unable to load renderable " +
GLTF_ASSET, Toast.LENGTH_LONG);
toast.setGravity(Gravity.CENTER, 0, 0);
toast.show();
return null;
});
Button clearButton = findViewById(R.id.clear_button);
clearButton.setOnClickListener(view -> setCloudAnchor(null));
Button hostButton = findViewById(R.id.host_button);
hostButton.setOnClickListener(view -> hostModel());
arFragment.setOnTapArPlaneListener(
(HitResult hitResult, Plane plane, MotionEvent motionEvent) -> {
if (renderable == null) {
return;
}
// Create the Anchor.
Anchor anchor = hitResult.createAnchor();
setCloudAnchor(anchor);
AnchorNode anchorNode = new AnchorNode(cloudAnchor);
TransformableNode node = new TransformableNode(arFragment.getTransformationSystem());
node.setRenderable(renderable);
node.setParent(anchorNode);
arFragment.getArSceneView().getScene().addChild(anchorNode);
node.select();
});
}
private void hostModel() {
if (cloudAnchor != null) {
arFragment.getArSceneView().getSession().hostCloudAnchor(cloudAnchor);
appAnchorState = AppAnchorState.HOSTING;
snackbarHelper.showMessage(this, "Now hosting anchor...");
} else {
snackbarHelper.showMessage(this, "No anchor to host, Please create an anchor...");
}
}
private void setCloudAnchor (Anchor newAnchor){
if (cloudAnchor != null){
cloudAnchor.detach();
}
cloudAnchor = newAnchor;
appAnchorState = AppAnchorState.NONE;
snackbarHelper.hide(this);
}
private void onUpdateFrame(FrameTime frameTime){
checkUpdatedAnchor();
}
private synchronized void checkUpdatedAnchor(){
if (appAnchorState != AppAnchorState.HOSTING){
return;
}
Anchor.CloudAnchorState cloudState = cloudAnchor.getCloudAnchorState();
if (appAnchorState == AppAnchorState.HOSTING) {
if (cloudState.isError()) {
snackbarHelper.showMessageWithDismiss(this, "Error hosting anchor.. "
+ cloudState);
appAnchorState = AppAnchorState.NONE;
} else if (cloudState == Anchor.CloudAnchorState.SUCCESS) {
snackbarHelper.showMessageWithDismiss(this, "Anchor hosted with id "
+ cloudAnchor.getCloudAnchorId());
appAnchorState = AppAnchorState.HOSTED;
}
}
}
public static boolean checkIsSupportedDeviceOrFinish(final Activity activity) {
String openGlVersionString =
((ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE))
.getDeviceConfigurationInfo()
.getGlEsVersion();
if (Double.parseDouble(openGlVersionString) < MIN_OPENGL_VERSION) {
Log.e(TAG, "Sceneform requires OpenGL ES 3.0 later");
Toast.makeText(activity, "Sceneform requires OpenGL ES 3.0 or later", Toast.LENGTH_LONG)
.show();
activity.finish();
return false;
}
return true;
}
}
public class CustomArFragment extends ArFragment {
#Override
protected Config getSessionConfiguration(Session session) {
Config config = super.getSessionConfiguration(session);
config.setCloudAnchorMode(Config.CloudAnchorMode.ENABLED);
return config;
}
}
So i figured out what my problem was. I wasn't setting the cloud anchor up to listen for the change from the server. That is why I could see the activity on the google api but nothing was happening on the device.
All I had to do was change this piece of code:
private void hostModel() {
if (cloudAnchor != null) {
arFragment.getArSceneView().getSession().hostCloudAnchor(cloudAnchor);
appAnchorState = AppAnchorState.HOSTING;
snackbarHelper.showMessage(this, "Now hosting anchor...");
} else {
snackbarHelper.showMessage(this, "No anchor to host, Please create an anchor...");
}
}
and set cloudAnchor to listen for the changes:
private void hostModel() {
if (cloudAnchor != null) {
cloudAnchor = arFragment.getArSceneView().getSession().hostCloudAnchor(cloudAnchor);
appAnchorState = AppAnchorState.HOSTING;
snackbarHelper.showMessage(this, "Now hosting anchor...");
} else {
snackbarHelper.showMessage(this, "No anchor to host, Please create an anchor...");
}
}

Rxandroid ble Guidelines for handling re-connection

[Update]
I am able to reconnect to the ble device after disconnect but can't seem to read or write any characteristics. Logcat spews this out after reconnect, is this my app doing something wrong or is it because of the ble device?
08-09 15:05:45.109 9601-10364/com.project.app D/BluetoothGatt: setCharacteristicNotification() - uuid: cb67d1e1-cfb5-45f5-9123-3f07d9189f1b enable: false
08-09 15:05:45.111 9601-10352/com.project.app D/RxBle#ConnectionOperationQueue: STARTED DescriptorWriteOperation(54881118)
08-09 15:05:45.114 9601-10364/com.project.app D/RxBle#ConnectionOperationQueue: QUEUED DescriptorWriteOperation(191754430)
08-09 15:05:45.116 9601-9601/com.project.app D/BtleConnManager: RETRY 2/-1 :::: com.polidea.rxandroidble.exceptions.BleCannotSetCharacteristicNotificationException
08-09 15:06:15.117 9601-10352/com.project.app D/RxBle#ConnectionOperationQueue: FINISHED DescriptorWriteOperation(54881118)
08-09 15:06:15.118 9601-10365/com.project.app D/BluetoothGatt: setCharacteristicNotification() - uuid: cb67d0e1-cfb5-45f5-9123-3f07d9189f1b enable: false
08-09 15:06:15.120 9601-10352/com.project.app D/RxBle#ConnectionOperationQueue: STARTED DescriptorWriteOperation(88995281)
08-09 15:06:15.124 9601-10365/com.project.app D/RxBle#ConnectionOperationQueue: QUEUED DescriptorWriteOperation(108601267)
08-09 15:06:15.124 9601-10366/com.project.app D/BluetoothGatt: setCharacteristicNotification() - uuid: cb67d1e1-cfb5-45f5-9123-3f07d9189f1b enable: true
08-09 15:06:15.126 9601-9601/com.project.app D/BtleConnManager: RETRY 2/-1 :::: com.polidea.rxandroidble.exceptions.BleCannotSetCharacteristicNotificationException
08-09 15:06:15.131 9601-10366/com.project.app D/RxBle#ConnectionOperationQueue: QUEUED DescriptorWriteOperation(98838561)
08-09 15:06:45.126 9601-10352/com.project.app D/RxBle#ConnectionOperationQueue: FINISHED DescriptorWriteOperation(88995281)
[Update]
Using rxandroidble1 and rxjava1
Hi I am new to the concept of rxjava and ble connections but I've been put on a existing project with very little documentation and I'm having problems handling the re-connection after a connection is lost.
I've checked out the sample app of rxandroidble but it only handles connection and no the re-connection if it looses it. Or is the library supposed to handle it by it's own or am I missing something.
The general problem can be described as such :
I connect the phone-app to my ble device. Everything works as expected i get notifications from my subsriptions when temperature is changing on my ble device.
I loose connection either by turning of the ble chip on the device or turning off Bluetooth on my phone or walking out of range.
I turn on Bluetooth again either on my phone or the ble device.
I manage to reconnect but my subscriptions aren't re subscribed so i don't get any notifications back to my phone when temperature or other values are changing.
According to my employer this code should have worked fine in the past but I can't seem to get it to work after it looses the connection. So can any of you guys see any errors in the logic of the code. Or might there be a problem with the ble device? Or is this a common bug or problem with the RxBleConnectionSharingAdapter or what? I've tried everything but nothing seems to work.
Or am I missing something in like the onUnsibcribeMethod or something?
I guess the establish connection method is the most relevant part of the code. Iv'e tried re-subscribing via to a characteristic after reconnect via the test method but the app just crashes then.
This is my connection manager class :
private static final String TAG = "HELLOP";
private static RxBleClient rxBleClient;
private RxBleConnection rxBleConnection;
private static final int MAX_RETRIES = 10;
private static final int SHORT_RETRY_DELAY_MS = 1000;
private static final int LONG_RETRY_DELAY_MS = 30000;
private final Context mContext;
private final String mMacAddress;
private final String gatewayName;
private final RxBleDevice mBleDevice;
private PublishSubject<Void> mDisconnectTriggerSubject = PublishSubject.create();
private Observable<RxBleConnection> mConnectionObservable;
private final ProjectDeviceManager mProjectDeviceManager;
private BehaviorSubject<Boolean> connectionStatusSubject = BehaviorSubject.create();
private boolean isAutoSignIn = false;
private BondStateReceiver bondStateReceiver;
private boolean isBonded = false;
//gets the client
public static RxBleClient getRxBleClient(Context context) {
if (rxBleClient == null) {
// rxBleClient = /*MockedClient.getClient();*/RxBleClient.create(this);
// RxBleClient.setLogLevel(RxBleLog.DEBUG);
// super.onCreate();
rxBleClient = RxBleClient.create(context);
RxBleClient.setLogLevel(RxBleLog.DEBUG);
}
return rxBleClient;
}
public BtleConnectionManager(final Context context, final String macAddress, String name) {
mContext = context;
mMacAddress = macAddress;
gatewayName = name;
mBleDevice = getRxBleClient(context).getBleDevice(macAddress);
mProjectDeviceManager = new ProjectDeviceManager(this);
}
#Override
public final Context getContext() {
return mContext;
}
#Override
public final ProjectDeviceManager getProjectDeviceManager() {
return mProjectDeviceManager;
}
#Override
public final boolean isConnected() {
return mBleDevice.getConnectionState() == RxBleConnection.RxBleConnectionState.CONNECTED;
}
#Override
public String getConnectionName() {
if (gatewayName != null && !gatewayName.isEmpty()) {
return gatewayName;
} else {
return mMacAddress;
}
}
final RxBleDevice getBleDevice() {
return mBleDevice;
}
public final synchronized Observable<RxBleConnection> getConnection() {
if (mConnectionObservable == null || mBleDevice.getConnectionState() == RxBleConnection.RxBleConnectionState.DISCONNECTED
|| mBleDevice.getConnectionState() == RxBleConnection.RxBleConnectionState.DISCONNECTING) {
establishConnection();
}
return mConnectionObservable;
}
public void goBack() {
Intent intent = null;
try {
intent = new Intent(mContext,
Class.forName("com.Project.dcpapp.BluetoothActivity"));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
((Activity) mContext).startActivity(intent);
((Activity) mContext).finish();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public void setAutoSignIn(boolean value) {
this.isAutoSignIn = value;
}
public boolean getAutoSignIn() {
return this.isAutoSignIn;
}
#Override
public void pause() {
}
#Override
public void resume() {
}
#Override
public Observable<Boolean> observeConnectionStatus() {
return connectionStatusSubject;
}
#Override
public Calendar getLastConnectionTime() {
return mProjectDeviceManager.getLastUpdateTime();
}
public void disconnect() {
BluetoothDevice bluetoothDevice = getBleDevice().getBluetoothDevice();
Log.d("BtleConnManager", " disconnect " + bluetoothDevice.getBondState());
Handler handler = new Handler(Looper.getMainLooper());
handler.postDelayed(new Runnable() {
#Override
public void run() {
mDisconnectTriggerSubject.onNext(null);
mConnectionObservable = null;
}
}, 700);
}
public void removeBond() {
Method m = null;
BluetoothDevice bluetoothDevice = getBleDevice().getBluetoothDevice();
Log.d("BtleConnManager", " removeBond " + bluetoothDevice.getBondState());
if (bluetoothDevice.getBondState() == BluetoothDevice.BOND_BONDED) {
try {
m = bluetoothDevice.getClass().getMethod("removeBond", (Class[]) null);
m.invoke(bluetoothDevice, (Object[]) null);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
public void bond() {
BluetoothDevice bluetoothDevice = getBleDevice().getBluetoothDevice();
Log.d("BtleConnManager ", "bond state " + bluetoothDevice.getBondState());
if (bluetoothDevice.getBondState() == BluetoothDevice.BOND_NONE
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
bondStateReceiver = new BondStateReceiver();
final IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
getContext().registerReceiver(bondStateReceiver, filter);
bluetoothDevice.createBond();
}
}
public void setBonded(boolean value) {
this.isBonded = value;
}
public boolean isBonded() {
return this.isBonded;
}
private class BondStateReceiver extends BroadcastReceiver {
#Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (action.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) {
final int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR);
switch (state) {
case BluetoothDevice.BOND_BONDED:
setBonded(true);
Log.d("BtleConManager", "Bonded ");
break;
case BluetoothDevice.BOND_BONDING:
Log.d("BtleConManager", "Bonding ");
break;
case BluetoothDevice.BOND_NONE:
Log.d("BtleConManager", "unbonded ");
setBonded(false);
final int prevState = intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, BluetoothDevice.ERROR);
if (prevState == BluetoothDevice.BOND_BONDING) {
Toast.makeText(getContext(), R.string.error_bluetooth_bonding_failed, Toast.LENGTH_LONG).show();
}
break;
}
}
}
}
private void establishConnection() {
Log.d("BtleConnManager", " establishConnection");
mConnectionObservable = mBleDevice
.establishConnection(false)
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(rxBleConnection -> {
// Save connection to use if retry is done when already connected
this.rxBleConnection = rxBleConnection;
// Notify observers that connection is established
connectionStatusSubject.onNext(true);
})
.onErrorResumeNext(error -> {
// Return the saved connection if already connected
if (error instanceof BleAlreadyConnectedException && rxBleConnection != null) {
return Observable.just(rxBleConnection);
} else {
return Observable.error(error);
}
})
//.retryWhen(getRetryRule()) Do not retry connect here - retry when using getConnection instead (otherwise a double retry connection will be done)
.takeUntil(mDisconnectTriggerSubject)
.doOnError(throwable -> {
this.rxBleConnection = null;
if (!isConnected()) {
// Notify observers that connection has failed
connectionStatusSubject.onNext(false);
}
}).doOnUnsubscribe(() -> {
Log.d("BtleConnManager", "establishConnection Unsubscribe ");
connectionStatusSubject.onNext(false);
}).doOnCompleted(() -> Log.d("BtleConnManager", "establishConnection completed"))
.doOnSubscribe(() -> {
})
//.subscribeOn(AndroidSchedulers.mainThread())
//.compose(bindUntilEvent(PAUSE))
.compose(new ConnectionSharingAdapter());
}
public void test(){
mConnectionObservable
.flatMap(rxBleConnection -> rxBleConnection.setupNotification(UUID.fromString("cb67d1c1-cfb5-45f5-9123-3f07d9189f1b")))
.flatMap(notificationObservable -> notificationObservable)
.observeOn(AndroidSchedulers.mainThread())
.retryWhen(errors -> errors.flatMap(error -> {
if (error instanceof BleDisconnectedException) {
Log.d("Retry", "Retrying");
return Observable.just(null);
}
return Observable.error(error);
}))
.doOnError(throwable -> {
Log.d(TAG, "establishConnection: " + throwable.getMessage());
})
.subscribe(bytes -> {
Log.d(TAG, "establishConnection: characteristics changed" + new String(bytes));
// React on characteristic changes
});
}
public RetryWithDelay getRetryRule() {
return new RetryWithDelay(MAX_RETRIES, SHORT_RETRY_DELAY_MS);
}
public RetryWithDelay getInfiniteRetryRule() {
return new RetryWithDelay(RetryWithDelay.NO_MAX, LONG_RETRY_DELAY_MS);
}
public class RetryWithDelay implements
Func1<Observable<? extends Throwable>, Observable<?>> {
public static final int NO_MAX = -1;
private final int maxRetries;
private final int retryDelayMs;
private int retryCount;
public RetryWithDelay(final int maxRetries, final int retryDelayMs) {
this.maxRetries = maxRetries;
this.retryDelayMs = retryDelayMs;
this.retryCount = 0;
}
#Override
public Observable<?> call(Observable<? extends Throwable> attempts) {
return attempts
.flatMap(new Func1<Throwable, Observable<?>>() {
#Override
public Observable<?> call(Throwable throwable) {
++retryCount;
if (mConnectionObservable == null) {
// If manually disconnected return empty observable
return Observable.empty();
} else if (throwable instanceof BleAlreadyConnectedException) {
return Observable.error(throwable);
} else if (retryCount < maxRetries || maxRetries == NO_MAX) {
Log.d("BtleConnManager", " RETRY " + retryCount + "/" + maxRetries + " :::: " + throwable.getClass().getName());
// When this Observable calls onNext, the original
// Observable will be retried (i.e. re-subscribed).
return Observable.timer(retryDelayMs, TimeUnit.MILLISECONDS);
} else {
//Last try
Log.d("BtleConnManager", " LAST RETRY " + retryCount + "/" + maxRetries + " :::: " + throwable.getClass().getName());
return Observable.error(throwable);
}
}
});
}
}
In establishConnection you set the autoConnect parameter to false, which will prevent automatic reconnection. If you set it to true instead, it should automatically reconnect. See https://stackoverflow.com/a/40187086/556495 and http://polidea.github.io/RxAndroidBle/ under Auto connect.
Note that this will not work if Bluetooth is turned off/restarted on the phone/tablet. So you'll probably also need a Bluetooth state change broadcast listener to restart everything when that happens.

BleGattException when writing to a particular characteristic

Writing to a specific characteristic crashes the application and throws the following exception:
Caused by: BleGattException{status=8, bleGattOperation=BleGattOperation{description='CHARACTERISTIC_WRITE'}}
at com.polidea.rxandroidble.internal.connection.RxBleGattCallback.propagateStatusErrorIfGattErrorOccurred(RxBleGattCallback.java:245)
at com.polidea.rxandroidble.internal.connection.RxBleGattCallback.access$100(RxBleGattCallback.java:26)
at com.polidea.rxandroidble.internal.connection.RxBleGattCallback$1.onCharacteristicWrite(RxBleGattCallback.java:110)
at android.bluetooth.BluetoothGatt$1.onCharacteristicWrite(BluetoothGatt.java:407)
at android.bluetooth.IBluetoothGattCallback$Stub.onTransact(IBluetoothGattCallback.java:279)
A connection is established to the device, and other methods that both read and write seem to work fine.
Code being used:
mConnectoin.writeCharacteristic(UUID, bytes)
.observeOn(AndroidSchedulers.mainThread());
My first thought was that perhaps the characteristic does not have a write permission enabled,
but the following log statement for characteristic.getProperties() returns 8, indicating it does in fact have write permissions:
.getCharacteristic(CharacteristicUUID)
.subscribe(new Action1<BluetoothGattCharacteristic>() {
#Override
public void call(BluetoothGattCharacteristic characteristic) {
Log.d(TAG, "characteristic permissions: " + characteristic.getPermissions());
Log.d(TAG, "characteristic properties: " + characteristic.getProperties());
}
});
So what might the issue be?
BleGattException is emitted in BluetoothGattCallback class onCharacteristicWrite() callback method. (it is inside the RxBleGattCallback class).
status comes from the Android OS BLE stack. These are described for instance here: https://android.googlesource.com/platform/external/bluetooth/bluedroid/+/android-5.1.0_r1/stack/include/gatt_api.h
status=8 indicates #define GATT_INSUF_AUTHORIZATION 0x08 - so it seems that you're trying to write a BluetoothCharacteristic that needs an encrypted connection (a paired device).
Unfortunately as for the moment RxAndroidBle doesn't help with pairing the devices.
As #s_noopy has pointed out, there's no specific support for pairing the device inside the library, but there are options for creating your little helper that can assist you before connecting.
RxBluetooth library has support for doing bonds in a reactive way.
I extracted the particular pieces of code and I created a small gist to share (that only does the bonding process). Feel free to fork it or improve it (the code is based on RxBluetooth). Also if someone finds a better way or there's something wrong, just point it out!
The original gist:
Update
I modified the source code of the RxBluetooth (previously named RxBluetoothHelper) as there were a bug when doing unbonding. In Android when you bond you have to listen for the bonding process in a intent (but when you unbond is not necessary, as the system is only removing the stored keys). This updated version addresses this that the previous not. Also if for some reason when you call bond the method returns false, the observable will emit onError. The gist is also updated!
public class BluetoothCompat {
public static boolean createBondCompat(final BluetoothDevice device)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return device.createBond();
} else {
Method method = device.getClass().getMethod("createBond", (Class[]) null);
final Object invoke = method.invoke(device, (Object[]) null);
return (Boolean) invoke;
}
}
public static boolean removeBondCompat(final BluetoothDevice device) throws NoSuchMethodException, InvocationTargetException,
IllegalAccessException {
Method method = device.getClass().getMethod("removeBond", (Class[]) null);
final Object invoke = method.invoke(device, (Object[]) null);
return (Boolean) invoke;
}
private BluetoothCompat() {
throw new AssertionError("No instances!");
}
}
public class RxBluetooth {
private final Context context;
private final BluetoothAdapter adapter;
private static final Observable.Transformer BOND_STATUS_TRANSFORMER = statusObservable -> statusObservable.map(status -> {
switch (status) {
case BluetoothDevice.BOND_NONE:
default:
return BondStatus.NONE;
case BluetoothDevice.BOND_BONDING:
return BondStatus.BONDING;
case BluetoothDevice.BOND_BONDED:
return BondStatus.BONDED;
}
});
public enum BondStatus {
NONE,
BONDING,
BONDED
}
public RxBluetooth(Context context, BluetoothAdapter bluetoothAdapter) {
this.context = context.getApplicationContext();
this.adapter = bluetoothAdapter;
}
public Observable bondStatus(#NonNull final BluetoothDevice device) {
return Observable.defer(() -> Observable.just(device.getBondState()).compose(BOND_STATUS_TRANSFORMER));
}
public Observable bond(#NonNull final BluetoothDevice device) {
return Observable.create(subscriber -> {
bondStatus(device).subscribe(bondStatus -> {
switch (bondStatus) {
case NONE:
observeDeviceBonding(context, device).compose(BOND_STATUS_TRANSFORMER).subscribe(subscriber);
try {
final boolean bonding = BluetoothCompat.createBondCompat(device);
if (!bonding) {
subscriber.onError(new BluetoothBondingException("Can't initiate a bonding operation!"));
}
} catch (Exception e) {
subscriber.onError(new BluetoothIncompatibleBondingException(e));
}
break;
case BONDING:
subscriber.onError(new BluetoothBondingException("device is already in the process of bonding"));
break;
case BONDED:
subscriber.onNext(BondStatus.BONDED);
subscriber.onCompleted();
break;
}
});
});
}
public Observable removeBond(#NonNull final BluetoothDevice device) {
return Observable.defer(() -> {
for (BluetoothDevice bondedDevice : adapter.getBondedDevices()) {
if (bondedDevice.getAddress().equals(device.getAddress())) {
try {
final boolean removeBond = BluetoothCompat.removeBondCompat(device);
if (!removeBond) {
return Observable.error(new BluetoothBondingException("Can't delete the bonding for this device!"));
}
} catch (Exception e) {
return Observable.error(new BluetoothIncompatibleBondingException(e));
}
}
}
return Observable.just(BondStatus.NONE);
});
}
private static Observable observeDeviceBonding(#NonNull final Context context, #NonNull final BluetoothDevice device) {
return observeBroadcast(context, new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)).filter(pair -> {
BluetoothDevice bondingDevice = pair.getValue1().getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
return bondingDevice.equals(device);
})
.map(pair1 -> pair1.getValue1().getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1))
.skipWhile(state -> state != BluetoothDevice.BOND_BONDING)
.takeUntil(state -> state == BluetoothDevice.BOND_BONDED || state == BluetoothDevice.BOND_NONE);
}
private static Observable> observeBroadcast(final Context context, final IntentFilter filter) {
return Observable.create(new Observable.OnSubscribe>() {
#Override public void call(Subscriber> subscriber) {
Enforcer.onMainThread();
final BroadcastReceiver receiver = new BroadcastReceiver() {
#Override public void onReceive(Context context, Intent intent) {
subscriber.onNext(Pair.with(context, intent));
}
};
context.registerReceiver(receiver, filter);
subscriber.add(Subscriptions.create(() -> context.unregisterReceiver(receiver)));
}
});
}
}

Signal strength in p2p connection with Wifi Direct?

I am taking a look at the WifiDirect demo for Android and wanted to know whether it was possible to get something as the RSSI or signal strength between two connected devices.
Sorry if the question makes no sense, I know that one can get RSSI from access points but what I want to know is if this concept of RSSI exists in p2p connections among devices in Wifi Direct.
Thank you for your time.
Note: This answer was true for the API level 14, I don't know if it still applies to the latest Android versions.
A non documented file named WifiP2pPeer exists in the Android code source. It contains some "interesting" lines.
We can see that a RSSI value is hard-coded (mRssi = 60; //TODO: fix), so the feature may not be implemented yet... (Like others in Android 14 regarding WifiP2p).
public class WifiP2pPeer extends Preference {
private static final int[] STATE_SECURED = {R.attr.state_encrypted};
public WifiP2pDevice device;
private int mRssi;
private ImageView mSignal;
private static final int SIGNAL_LEVELS = 4;
public WifiP2pPeer(Context context, WifiP2pDevice dev) {
super(context);
device = dev;
setWidgetLayoutResource(R.layout.preference_widget_wifi_signal);
mRssi = 60; //TODO: fix
}
#Override
protected void onBindView(View view) {
if (TextUtils.isEmpty(device.deviceName)) {
setTitle(device.deviceAddress);
} else {
setTitle(device.deviceName);
}
mSignal = (ImageView) view.findViewById(R.id.signal);
if (mRssi == Integer.MAX_VALUE) {
mSignal.setImageDrawable(null);
} else {
mSignal.setImageResource(R.drawable.wifi_signal);
mSignal.setImageState(STATE_SECURED, true);
}
refresh();
super.onBindView(view);
}
#Override
public int compareTo(Preference preference) {
if (!(preference instanceof WifiP2pPeer)) {
return 1;
}
WifiP2pPeer other = (WifiP2pPeer) preference;
// devices go in the order of the status
if (device.status != other.device.status) {
return device.status < other.device.status ? -1 : 1;
}
// Sort by name/address
if (device.deviceName != null) {
return device.deviceName.compareToIgnoreCase(other.device.deviceName);
}
return device.deviceAddress.compareToIgnoreCase(other.device.deviceAddress);
}
int getLevel() {
if (mRssi == Integer.MAX_VALUE) {
return -1;
}
return WifiManager.calculateSignalLevel(mRssi, SIGNAL_LEVELS);
}
private void refresh() {
if (mSignal == null) {
return;
}
Context context = getContext();
mSignal.setImageLevel(getLevel());
String[] statusArray = context.getResources().getStringArray(R.array.wifi_p2p_status);
setSummary(statusArray[device.status]);
}
}

Categories

Resources