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)));
}
});
}
}
Related
[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.
Lets say I have a SDK in form of Android Library (aar) that offers some basic media processing (it has its own UI as a single activity). Currently, any client Android app, when invoking my SDK sends required data via Bundle.
Now, for various reasons some extra info for the data being sent may be required after my SDK is invoked so I would need a two-way communication with the caller app.
In short, from within the SDK I need to be able to check if the client app has implemented some interface so that SDK can use it to communicate with the client app (the client may choose not to provide the implementation in which case the SDK will fallback to internal, the default implementation..).
Anyway, the way I've done it initialy, is as following:
Within SDK I have exposed the data provider interface:
public interface ISDKDataProvider {
void getMeSomething(Params param, Callback callback);
SomeData getMeSomethingBlocking(Params param);
}
a Local binder interface that should return an instance of the implemented interface:
public interface LocalBinder {
ISDKDataProvider getService();
}
Then, on the client side, an application using the SDK, must provide a service that does the job and implements those interfaces:
public class SDKDataProviderService extends Service implements ISDKDataProvider {
private final IBinder mBinder = new MyBinder();
#Override
public IBinder onBind(Intent intent) {
return mBinder;
}
#Override
public void getMeSomething(Params param, Callback callback) {
// ... do something on another thread
// once done, invoke callback and return result to the SDK
}
#Override
public SomeData getMeSomethingBlocking(Params param);{
// do something..
// return SomeData
}
public class MyBinder extends Binder implements LocalBinder {
#Override
public ISDKDataProvider getService() {
return ISDKDataProvider.this;
}
}
}
Additionally, when invoking SDK, the clinet app passes the ComponentName via bundle options:
sdkInvokationOptions.put("DATA_PROVIDER_EXTRAS", new ComponentName(getPackageName(), SDKDataProviderService.class.getName()));
..from the SDK, I then check whether the service exists and whether we can bind to it:
final ComponentName componentName = // get passed componentname "DATA_PROVIDER_EXTRAS"
if (componentName != null) {
final Intent serviceIntent = new Intent(componentName.getClassName());
serviceIntent.setComponent(componentName);
bindService(serviceIntent, mConnection, Context.BIND_AUTO_CREATE);
}
where mConnection is:
private boolean mBound;
private ISDKDataProvider mService;
private ServiceConnection mConnection = new ServiceConnection() {
#Override
public void onServiceConnected(ComponentName name, IBinder service) {
final LocalBinder binder = (LocalBinder) service;
mService = binder.getService();
mBound = true;
}
#Override
public void onServiceDisconnected(ComponentName name) {
mBound = false;
}
};
This seem to work ok and it looks clean but my question is there a better way\practice to accomplish the same type of a communication?
Your API should be simple, for example a static class/singleton:
MyAPI.start()
MyAPI.stop()
MyAPI.sendMessgage(mgs,callback)
MyAPI.setCallback(callback)
About the service, I think you should decide who is in charge of it.
If its the user - leave him the implementation, just give the API.
If you always want your API to run as a service, implement it yourself and inside the singleton handle the messaging (you can do so with intents, for example).
I used this architecture for image processing service too :)
My API wrapping class looked like:
public class MyAPI {
public static final String TAG = "MyAPI";
public MyAPI() {
}
public static MyAPI.Result startMyAPI(ScanParams scanParams) {
try {
Log.d("MyAPI", "in startMyAPI");
if (scanParams.ctx == null || scanParams.appID == null || scanParams.api_key == null) {
Log.d("MyAPI", "missing parameters");
return MyAPI.Result.FAILED;
}
if (scanParams.userID == null) {
scanParams.userID = "no_user";
}
if (scanParams.minBatteryThreshold == null) {
scanParams.minBatteryThreshold = Consts.DEFAULT_BATTERY_THRESHOLD;
}
if (scanParams.minCpuThreshold == null) {
scanParams.minCpuThreshold = Consts.DEFAULT_CPU_THRESHOLD;
}
if (!DeviceUtils.checkBatteryLevel(scanParams.ctx, (float)scanParams.minBatteryThreshold)) {
ReportUtils.error("low battery");
return MyAPI.Result.FAILED;
}
if (MyAPIUtils.isRunning(scanParams.ctx)) {
return MyAPI.Result.FAILED;
}
Intent intent = new Intent(scanParams.ctx, MyAPIService.class);
ServiceParams serviceParams = new ServiceParams(scanParams.appID, scanParams.api_key, scanParams.userID, scanParams.minBatteryThreshold, scanParams.minCpuThreshold);
intent.putExtra("SERVICE_PARAMS", serviceParams);
scanParams.ctx.startService(intent);
} catch (Exception var3) {
var3.printStackTrace();
}
return MyAPI.Result.SUCCESS;
}
public static void getBestCampaignPrediction(Context ctx, String apiKey, String appID, String creativeID, AppInterface appInterface) {
try {
String deviceID = DeviceUtils.getDeviceID(ctx);
GetBestCampaignTask getBestCampaignTask = new GetBestCampaignTask(ctx, apiKey, deviceID, appID, creativeID, appInterface);
getBestCampaignTask.execute(new Void[0]);
} catch (Exception var7) {
var7.printStackTrace();
}
}
public static boolean sendAdEvent(Context ctx, String apiKey, Event event) {
boolean res = false;
try {
boolean isValid = Utils.getIsValid(ctx);
if (isValid) {
Long timeStamp = System.currentTimeMillis();
event.setTimeStamp(BigDecimal.valueOf(timeStamp));
event.setDeviceID(DeviceUtils.getDeviceID(ctx));
(new SendEventTask(ctx, apiKey, event)).execute(new Void[0]);
}
} catch (Exception var6) {
var6.printStackTrace();
}
return res;
}
public static enum PredictionLevel {
MAIN_CATEGORY,
SUB_CATEGORY,
ATTRIBUTE;
private PredictionLevel() {
}
}
public static enum Result {
SUCCESS,
FAILED,
LOW_BATTERY,
LOW_CPU,
NOT_AUTHENTICATED;
private Result() {
}
}
}
You can see that startMyAPI actually starts a service and getBestCampaignPrediction runs an async task that communicates with the service behind the scenes and returns its result to appInterface callback. This way the user get a very simple API
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();
}
}
}
}
I have strange issue, I am creating mediaprovider for chromecast using following code that works fine for first instance, list of devices is shown and once slected I use router.selectRoute(routeinfo);
but once I exit app this code unable to find Chromecast device, how ever when I remove app from running apps stack this code works fine again and show devices.
If no device is selected and app is exited using back press then also this code works fine
So what I am doing wrong here? what I think is resources are not cleared when my app exit in simple back pressed.
public class ChromecastRouteProviderService extends MediaRouteProviderService {
final String LOGTAG = "Chromecast";
private static final String CONTROL_CATEGORY = CastMediaControlIntent.categoryForCast(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID);
private static final MediaRouteSelector SELECTOR = new MediaRouteSelector.Builder().addControlCategory(CONTROL_CATEGORY)
.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK).build();
private IntentFilter controlFilter;
public ChromecastRouteProviderService() {
controlFilter = new IntentFilter();
}
public void onCreate() {
super.onCreate();
controlFilter.addCategory(IAppConstants.CATEGORY);
controlFilter.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
}
#Override
public MediaRouteProvider onCreateMediaRouteProvider() {
return new ChromecastRouteProvider(this);
}
class ChromecastRouteProvider extends MediaRouteProvider {
MediaRouter.Callback callback;
Hashtable routes;
public ChromecastRouteProvider(Context context) {
super(context);
routes = new Hashtable();
callback = new CastCallBack();
}
#Nullable
#Override
public RouteController onCreateRouteController(String routeId) {
MediaRouter.RouteInfo routeInfo = (MediaRouter.RouteInfo) routes.get(routeId);
if (routeInfo == null) {
return super.onCreateRouteController(routeId);
} else {
return new ChromecastRouteController(getContext(), routeInfo);
}
}
#Override
public void onDiscoveryRequestChanged(#Nullable MediaRouteDiscoveryRequest request) {
super.onDiscoveryRequestChanged(request);
if (request == null || !request.isActiveScan() || !request.isValid()) {
stopScan();
return;
}
if (!request.getSelector().hasControlCategory(IAppConstants.CATEGORY)) {
Log.i(LOGTAG, "Not scanning for non remote playback");
stopScan();
return;
} else {
Log.i(LOGTAG, "Scanning...");
mediarouter.addCallback(ChromecastRouteProviderService.SELECTOR, callback, MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
return;
}
}
void updateDescriptor() {
final MediaRouteProviderDescriptor.Builder descriptor = new MediaRouteProviderDescriptor.Builder();
for (Iterator iterator = routes.values().iterator(); iterator.hasNext(); ) {
MediaRouter.RouteInfo routeinfo = (MediaRouter.RouteInfo) iterator.next();
try {
Bundle bundle = new Bundle();
bundle.putBoolean("has_upsell", true);
descriptor.addRoute(new MediaRouteDescriptor.Builder(routeinfo.getId(), routeinfo.getName())
.addControlFilter(controlFilter).setPlaybackStream(3)
.setDescription(routeinfo.getDescription())
.setEnabled(true).setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
.setVolumeHandling(1).setVolumeMax(100).setVolume(100)
.setExtras(bundle).build());
} catch (Exception e) {
throw new Error("wtf");
}
}
getHandler().post(new Runnable() {
#Override
public void run() {
setDescriptor(descriptor.build());
}
});
}
void stopScan() {
Log.i(LOGTAG, "Stopping scan...");
try {
MediaRouter.getInstance(getContext()).removeCallback(callback);
return;
} catch (Exception exception) {
return;
}
}
class CastCallBack extends MediaRouter.Callback {
void check(MediaRouter mediarouter, MediaRouter.RouteInfo routeinfo) {
Log.i(LOGTAG, new StringBuilder().append("Checking route ").append
(routeinfo.getName()).toString());
CastDevice device = CastDevice.getFromBundle(routeinfo.getExtras());
if (routeinfo.matchesSelector(ChromecastRouteProviderService.SELECTOR)
&& device != null && device.isOnLocalNetwork()) {
routes.put(routeinfo.getId(), routeinfo);
updateDescriptor();
return;
} else {
return;
}
}
public void onRouteAdded(MediaRouter mediarouter, MediaRouter.RouteInfo routeinfo) {
super.onRouteAdded(mediarouter, routeinfo);
check(mediarouter, routeinfo);
}
public void onRouteChanged(MediaRouter mediarouter, MediaRouter.RouteInfo routeinfo) {
super.onRouteChanged(mediarouter, routeinfo);
check(mediarouter, routeinfo);
}
public void onRouteRemoved(MediaRouter mediarouter, MediaRouter.RouteInfo routeinfo) {
super.onRouteRemoved(mediarouter, routeinfo);
if (routeinfo.matchesSelector(ChromecastRouteProviderService.SELECTOR)) ;
}
}
}
}
Ok finally I found answer on my own,
Problem is when any provider is selected it's not added using onRouteAdded why? I really dont understand google logic
So the solution is to unselect the router when you want or better select default route when so that your route is released
MediaRouter.getInstance(this).getDefaultRoute().select();
But again 1 out of 10 times it will not work
Hope will help someone
My Android app should do the following:
The MainActivity launches another thread at the beginning called UdpListener which can receive UDP calls from a remote server. If it receives a packet with a content "UPDATE", the UdpListener should notify the MainActivity to do something.
(In the real app, the use case looks like this that my app listens on the remote server. If there is any new data available on the remote server, it notifies every client (app) by UDP, so the client knows that it can download the new data by using HTTP).
I tried to simulate this in an JUnit test. The test contains an inner class which mocks the MainActivity as well as it sends the UDP call to the UdpListener:
public class UdpListener extends Thread implements Subject {
private DatagramSocket serverSocket;
private DatagramPacket receivedPacket;
private boolean running = false;
private String sentence = "";
private Observer observer;
private static final String TAG = "UdpListener";
public UdpListener(Observer o) throws SocketException {
serverSocket = new DatagramSocket(9800);
setRunning(true);
observer = o;
}
#Override
public void run() {
setName(TAG);
while (isRunning()) {
byte[] receivedData = new byte[1024];
receivedPacket = new DatagramPacket(receivedData, receivedData.length);
try {
serverSocket.receive(receivedPacket);
}
catch (IOException e) {
Log.w(TAG, e.getMessage());
}
try {
sentence = new String(receivedPacket.getData(), 0, receivedPacket.getLength(), "UTF-8");
if ("UPDATE".equals(sentence)) {
notifyObserver();
}
}
catch (UnsupportedEncodingException e) {
Log.w(TAG, e.getMessage());
}
}
}
private boolean isRunning() {
return running;
}
public void setRunning(boolean running) {
this.running = running;
}
#Override
public void notifyObserver() {
observer.update();
}
}
This is the corresponding test:
#RunWith(RobolectricTestRunner.class)
public class UdpListenerTest {
private MainActivityMock mainActivityMock = new MainActivityMock();
#Before
public void setUp() throws Exception {
mainActivityMock.setUpdate(false);
}
#After
public void tearDown() throws Exception {
mainActivityMock.setUpdate(false);
}
#Test
public void canNotifyObserver() throws IOException, InterruptedException {
UdpListener udpListener = new UdpListener(mainActivityMock);
udpListener.setRunning(true);
udpListener.start();
InetAddress ipAddress = InetAddress.getByName("localhost");
DatagramSocket datagramSocket = new DatagramSocket();
DatagramPacket sendPacket = new DatagramPacket("UPDATE".getBytes(), "UPDATE".length(), ipAddress, 9800);
datagramSocket.send(sendPacket);
datagramSocket.close();
assertTrue(mainActivityMock.isUpdate());
udpListener.setRunning(false);
}
private class MainActivityMock implements Observer {
private boolean update = false;
#Override
public void update() {
update = true;
}
public boolean isUpdate() {
return update;
}
public void setUpdate(boolean update) {
this.update = update;
}
}
}
The good thing is that my concept works. But, this test doesn't. That means it only does when I stop with a breakpoint at this line datagramSocket.close(); and wait for about a second. Why this happens is clear. But how can I do that automatically? I thought about using wait() but I have to invoke notify() from the other thread for that. The same problem with CountDownLatch. I'm not sure how to solve that without changing UdpListener.
You could write a simple loop with a specified timeout.
try {
long timeout = 500; // ms
long lastTime = System.currentTimeMillis();
while(timeout > 0 && !mainActivityMock.isUpdate()) {
Thread.sleep(timeout);
timeout -= System.currentTimeMillis() - lastTime;
lastTime = System.currentTimeMillis();
}
} catch(InterruptedException e) {
} finally {
assertTrue(mainActivityMock.isUpdate());
}
By the way - you should declare your running attribute to volatile.
one solution would be to use a blocking queue with size 1 for storing your received results.
The request for isUpdate (which would take an element from the blocking queue) would block until the update package(or any other package) is put into the queue.
is case you want all your calls to be non-blocking you could use a Future for receiving your result. Future.get() would block ontil your result is received.