[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.
Related
I'm working with Bluetooth LE devices and I was thinking about my current approach and best practices. Currently I have an activity which handles the connection and the GattCallbacks and I decided to restructure the code to get an better overview and maintainability cause its quite messy actually.
I found the BleManager from NordicSemiconductor https://github.com/NordicSemiconductor/Android-BLE-Library/
It's an abstraction of the basic steps for connecting with a BLE device and it handles the GattCallbacks + providing an appropriate interface to use it from a service or a ViewModel.
I'd like to use the ViewModel approach but I'm not so familiar with MVC, MVP, MVVM patterns and there are some questions that I still can't reply
This class is extending the BleManager (BlinkyManager.java)
It shows how to make use of the BleManager so I adopted the class and called it ECountBleManager.
EDIT:
The last 6 days I did reaearches especially facing the MVVM pattern and the Architecture Components. Unfortunately there are still a lot of questions that I can't reply myself. But I really want to get better so I made a draft of my current concept. I hope you can help me answering my questions and improving my project.
I'm especially interested in best practices.
Here is my draft:
And here are my class implementations:
ECountActivity.java
public class ECountActivity extends AppCompatActivity {
private ECountViewModel viewModel;
#Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.detail_view);
// hide magnifier icon
GifImageView customLoader = findViewById(R.id.progressBar);
customLoader.setVisibility(View.GONE);
// Get additional data from previous activity
final BluetoothDevice device = getIntent().getParcelableExtra("device");
initViewModel();
viewModel.connect(device);
}
private void initViewModel() {
viewModel = ViewModelProviders.of(this).get(ECountViewModel.class);
subscribeDataStreams(viewModel);
}
private void subscribeDataStreams(ECountViewModel viewModel) {
viewModel.isDeviceReady().observe(this, deviceReady -> openOptionsFragment());
viewModel.isConnected().observe(this, status -> {
// Todo: ...
});
}
private void openOptionsFragment() {
// load options fragment
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.replace(R.id.contentFragment, new OptionsFragment());
ft.commitNow();
}
}
OtaFragment.java
public class OtaFragment extends Fragment implements FolderChooserDialog.FolderCallback,
FileChooserDialog.FileCallback {
private Button partialOtaButton;
private Button fullOtaButton;
private Button submitButton;
private SeekBar mtuSeekBar;
private EditText mtuInput;
private LinearLayout stacklayout;
private Button browseAppButton;
private TextView folderPathText;
private TextView appFileNameText;
private MaterialDialog otaPrepareDialog;
private MaterialDialog otaProgressDialog;
private ECountViewModel viewModel;
private OtaViewModel otaViewModel;
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initViewModel();
}
#Override
public View onCreateView(#NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// inflate the layout for this fragment
View view = inflater.inflate(R.layout.ota_fragment, container, false);
initViews(view);
return view;
}
#Override
public void onFolderSelection(#NonNull FolderChooserDialog dialog, #NonNull File folder) {
final String otaFolderPath = folder.toString();
otaViewModel.setOtaFolderPath(otaFolderPath);
folderPathText.setText(otaFolderPath.substring(otaFolderPath.lastIndexOf("/")));
// enable app browse
browseAppButton.setClickable(true);
browseAppButton.setEnabled(true);
}
#Override
public void onFolderChooserDismissed(#NonNull FolderChooserDialog dialog) {}
#Override
public void onFileSelection(#NonNull FileChooserDialog dialog, #NonNull File file) {
final String otaAppFilePath = file.toString();
otaViewModel.setOtaAppFilePath(otaAppFilePath);
appFileNameText.setText(otaAppFilePath.substring(otaAppFilePath.lastIndexOf("/")));
// enable submitButton button
submitButton.setClickable(true);
submitButton.setEnabled(true);
}
#Override
public void onFileChooserDismissed(#NonNull FileChooserDialog dialog) {}
private void subscribeDataStreams(ECountViewModel viewModel) {
viewModel.isOtaMode().observe(this, otaMode -> {
otaPrepareDialog.dismiss();
initOtaProgressDialog();
otaProgressDialog.show();
// Todo: how can i get mtu?
viewModel.requestMtu(512);
});
}
private void initViewModel() {
viewModel = ViewModelProviders.of(getActivity()).get(ECountViewModel.class);
otaViewModel = ViewModelProviders.of(getActivity()).get(OtaViewModel.class);
subscribeDataStreams(viewModel);
}
private void initViews(View view) {
// get resources
final Button browseFolderButton = view.findViewById(R.id.browseFolder);
final Button cancelButton = view.findViewById(R.id.ota_cancel);
final SeekBar prioritySeekBar = view.findViewById(R.id.connection_seekBar);
partialOtaButton = view.findViewById(R.id.radio_ota);
fullOtaButton = view.findViewById(R.id.radio_ota_full);
browseAppButton = view.findViewById(R.id.browseApp);
folderPathText = view.findViewById(R.id.folderPathText);
appFileNameText = view.findViewById(R.id.appFileNameText);
stacklayout = view.findViewById(R.id.stacklayout);
submitButton = view.findViewById(R.id.ota_proceed);
mtuSeekBar = view.findViewById(R.id.mtu_seekBar);
mtuInput = view.findViewById(R.id.mtu_value);
// set initial states
mtuSeekBar.setMax(512-23);
mtuSeekBar.setProgress(244);
prioritySeekBar.setMax(2);
prioritySeekBar.setProgress(1);
browseAppButton.setClickable(false);
browseAppButton.setEnabled(false);
submitButton.setClickable(false);
submitButton.setEnabled(false);
mtuInput.setOnEditorActionListener((v, actionId, event) -> {
final Editable mtuText = mtuInput.getText();
if (mtuText != null) {
int mtu = Integer.valueOf(mtuText.toString());
if (mtu < 23) mtu = 23;
if (mtu > 512) mtu = 512;
mtuSeekBar.setProgress(mtu);
viewModel.setMtu(mtu);
}
return false;
});
mtuSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
#Override
public void onStartTrackingTouch(SeekBar seekBar) {}
#Override
public void onStopTrackingTouch(SeekBar seekBar) {}
#Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
mtuInput.setText(String.valueOf(progress));
viewModel.setMtu(progress);
}
});
prioritySeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener(){
#Override
public void onStartTrackingTouch(SeekBar seekBar) {}
#Override
public void onStopTrackingTouch(SeekBar seekBar) {}
#Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
viewModel.setPriority(progress);
}
});
browseFolderButton.setOnClickListener(v -> new FolderChooserDialog.Builder(getActivity())
.chooseButton(R.string.positiveTextChoose)
.tag("#folder")
.show(getChildFragmentManager()));
browseAppButton.setOnClickListener(v -> new FileChooserDialog.Builder(getActivity())
.initialPath(otaViewModel.getOtaFolderPath())
.extensionsFilter(".ebl")
.tag("#app")
.show(getChildFragmentManager()));
cancelButton.setOnClickListener(v -> Log.i("ota", "cancelButton"));
submitButton.setOnClickListener(v -> {
// disable OTA submitButton button
submitButton.setClickable(false);
submitButton.setEnabled(false);
// init OTA process
viewModel.initOtaMode();
// show OTA preparing dialog
otaPrepareDialog.show();
});
fullOtaButton.setOnClickListener(v -> {
stacklayout.setVisibility(View.VISIBLE);
partialOtaButton.setBackgroundColor(getResources().getColor(R.color.colorPrimary));
fullOtaButton.setBackgroundColor(getResources().getColor(R.color.colorPrimaryDark));
});
partialOtaButton.setOnClickListener(v -> {
stacklayout.setVisibility(View.GONE);
partialOtaButton.setBackgroundColor(getResources().getColor(R.color.colorPrimaryDark));
fullOtaButton.setBackgroundColor(getResources().getColor(R.color.colorPrimary));
});
otaPrepareDialog = new MaterialDialog.Builder(getActivity())
.title(R.string.otaDialogHeaderText)
.content(R.string.waiting)
.progress(true, 0)
.progressIndeterminateStyle(true)
.build();
otaProgressDialog = new MaterialDialog.Builder(getActivity())
.title("test")
.customView(R.layout.ota_progress2, false)
.build();
}
private void initOtaProgressDialog() {
// Todo: ...
}
}
ECountViewModel.java
public class ECountViewModel extends AndroidViewModel implements ECountBleManagerCallbacks {
private final ECountBleManager eCountBleManager;
// Connection states Connecting, Connected, Disconnecting, Disconnected etc.
private final MutableLiveData<String> connectionState = new MutableLiveData<>();
// Flag to determine if the device is connected
private final MutableLiveData<Boolean> isConnected = new MutableLiveData<>();
// Flag to determine if the device is ready
private final MutableLiveData<Void> onDeviceReady = new MutableLiveData<>();
// Flag to determine if the device is in OTA mode
private final MutableLiveData<Void> onOtaMode = new MutableLiveData<>();
public LiveData<Void> isDeviceReady() {
return onDeviceReady;
}
public LiveData<Void> isOtaMode() {
return onOtaMode;
}
public LiveData<String> getConnectionState() {
return connectionState;
}
public LiveData<Boolean> isConnected() {
return isConnected;
}
public ECountViewModel(#NonNull final Application application) {
super(application);
// Initialize the manager
eCountBleManager = new ECountBleManager(getApplication());
eCountBleManager.setGattCallbacks(this);
}
/**
* Connect to peripheral
*/
public void connect(final BluetoothDevice device) {
eCountBleManager.connect(device);
}
/**
* Disconnect from peripheral
*/
private void disconnect() {
eCountBleManager.disconnect();
}
#Override
protected void onCleared() {
super.onCleared();
if (eCountBleManager.isConnected()) {
disconnect();
}
}
#Override
public void onDeviceConnecting(BluetoothDevice device) {
}
#Override
public void onDeviceConnected(BluetoothDevice device) {
isConnected.postValue(true);
}
#Override
public void onDeviceDisconnecting(BluetoothDevice device) {
isConnected.postValue(false);
}
#Override
public void onDeviceDisconnected(BluetoothDevice device) {
isConnected.postValue(false);
}
#Override
public void onLinklossOccur(BluetoothDevice device) {
isConnected.postValue(false);
}
#Override
public void onServicesDiscovered(BluetoothDevice device, boolean optionalServicesFound) {
}
#Override
public void onDeviceReady(BluetoothDevice device) {
onDeviceReady.postValue(null);
}
#Override
public void onOptionalServiceSupported(BluetoothDevice device) {
onOtaMode.postValue(null);
}
#Override
public void onBondingRequired(BluetoothDevice device) {
}
#Override
public void onBonded(BluetoothDevice device) {
}
#Override
public void onError(BluetoothDevice device, String message, int errorCode) {
}
#Override
public void onDeviceNotSupported(BluetoothDevice device) {
disconnect();
}
// delegate call from options fragment to ECountBleManager
public String getDeviceId() {
return BinaryUtils.byteArrayToHexString(eCountBleManager.getDeviceId());
}
// delegate call from ota fragment to ECountBleManager
public void setMtu(final int value) {
eCountBleManager.setMtu(value);
}
public void setPriority(final int value) {
eCountBleManager.setPriority(value);
}
ECountBleManager.java
public class ECountBleManager extends BleManager<BleManagerCallbacks> {
private static final String TAG = ECountBleManager.class.getSimpleName();
private final Handler handler;
private BluetoothGattCharacteristic authCharacteristic;
private BluetoothGattCharacteristic deviceIdCharacteristic;
private BluetoothGattCharacteristic deviceVersionCharacteristic;
private BluetoothGattCharacteristic configIdCharacteristic;
private BluetoothGattCharacteristic configTransmissionIntervalCharacteristic;
private BluetoothGattCharacteristic configKeepAliveIntervalCharacteristic;
private BluetoothGattCharacteristic configRadioModeCharacteristic;
private BluetoothGattCharacteristic configGpsCharacteristic;
private BluetoothGattCharacteristic configRadarCharacteristic;
private BluetoothGattCharacteristic configOperationModeCharacteristic;
private BluetoothGattCharacteristic configLoRaAppEuiCharacteristic;
private BluetoothGattCharacteristic configLoRaAppKeyCharacteristic;
private BluetoothGattCharacteristic configLoRaDeviceEuiCharacteristic;
private BluetoothGattCharacteristic operationCmdCharacteristic;
private BluetoothGattCharacteristic nemeusStatusCharacteristic;
private BluetoothGattCharacteristic gmrStatusCharacteristic;
private BluetoothGattCharacteristic radarStatusCharacteristic;
private BluetoothGattCharacteristic otaControlCharacteristic;
private BluetoothGattCharacteristic otaDataCharacteristic;
private byte[] configTransmissionInterval;
private byte[] configKeepAliveInterval;
private byte[] configRadioMode;
private byte[] configOperationMode;
private byte[] configId;
private byte[] deviceId;
private byte[] deviceVersion;
private byte[] configGps;
private byte[] configRadar;
private byte[] configLoRaAppEui;
private byte[] configLoRaAppKey;
private byte[] configLoRaDeviceEui;
private byte[] operationCmd;
private byte[] nemeusStatus;
private byte[] gmrStatus;
private byte[] radarStatus;
// OTA flags
private boolean isOtaProcessing = false;
private boolean isReconnectRequired = false;
private MutableLiveData<Boolean> isOtaMode = new MutableLiveData<>();
// OTA variables
private int mtu = 512;
private int priority = BluetoothGatt.CONNECTION_PRIORITY_HIGH;
private byte[] otaAppFileStream;
////////////////////////////
public ECountBleManager(Context context) {
super(context);
handler = new Handler();
}
#Override
protected BleManagerGattCallback getGattCallback() {
return gattCallback;
}
#Override
protected boolean shouldAutoConnect() {
return true;
}
/**
* BluetoothGatt callbacks for connection/disconnection, service discovery, receiving indication, etc
*/
private final BleManagerGattCallback gattCallback = new BleManagerGattCallback() {
#Override
protected void onDeviceReady() {
super.onDeviceReady();
}
#Override
protected void onOptionalServiceSupported() {
super.onOptionalServiceSupported();
isOtaMode.postValue(true);
}
#Override
protected boolean isOptionalServiceSupported(BluetoothGatt gatt) {
final BluetoothGattService otaService = gatt.getService(DC_UUID.otaService);
otaDataCharacteristic = otaService.getCharacteristic(DC_UUID.otaData);
return otaDataCharacteristic != null;
}
#Override
protected boolean isRequiredServiceSupported(BluetoothGatt gatt) {
final BluetoothGattService dcService = gatt.getService(DC_UUID.dcService);
final BluetoothGattService otaService = gatt.getService(DC_UUID.otaService);
if (dcService == null || otaService == null) return false;
authCharacteristic = dcService.getCharacteristic(DC_UUID.authentication);
deviceIdCharacteristic = dcService.getCharacteristic(DC_UUID.deviceId);
deviceVersionCharacteristic = dcService.getCharacteristic(DC_UUID.deviceVersion);
configIdCharacteristic = dcService.getCharacteristic(DC_UUID.configId);
configTransmissionIntervalCharacteristic = dcService.getCharacteristic(DC_UUID.configTransmissionInterval);
configKeepAliveIntervalCharacteristic = dcService.getCharacteristic(DC_UUID.configKeepAliveInterval);
configRadioModeCharacteristic = dcService.getCharacteristic(DC_UUID.configRadioMode);
configGpsCharacteristic = dcService.getCharacteristic(DC_UUID.configGps);
configRadarCharacteristic = dcService.getCharacteristic(DC_UUID.configRadar);
configOperationModeCharacteristic = dcService.getCharacteristic(DC_UUID.configOperationMode);
configLoRaAppEuiCharacteristic = dcService.getCharacteristic(DC_UUID.configLoRaAppEui);
configLoRaAppKeyCharacteristic = dcService.getCharacteristic(DC_UUID.configLoRaAppKey);
configLoRaDeviceEuiCharacteristic = dcService.getCharacteristic(DC_UUID.configLoRaDeviceEui);
operationCmdCharacteristic = dcService.getCharacteristic(DC_UUID.operationCmd);
nemeusStatusCharacteristic = dcService.getCharacteristic(DC_UUID.nemeusStatus);
gmrStatusCharacteristic = dcService.getCharacteristic(DC_UUID.gmrStatus);
radarStatusCharacteristic = dcService.getCharacteristic(DC_UUID.radarStatus);
otaControlCharacteristic = otaService.getCharacteristic(DC_UUID.otaControl);
return authCharacteristic != null &&
deviceIdCharacteristic != null &&
deviceVersionCharacteristic != null&&
configIdCharacteristic != null &&
configTransmissionIntervalCharacteristic != null &&
configKeepAliveIntervalCharacteristic != null &&
configRadioModeCharacteristic != null &&
configGpsCharacteristic != null &&
configRadarCharacteristic != null &&
configOperationModeCharacteristic != null &&
configLoRaAppEuiCharacteristic != null &&
configLoRaAppKeyCharacteristic != null &&
configLoRaDeviceEuiCharacteristic != null &&
operationCmdCharacteristic != null &&
nemeusStatusCharacteristic != null &&
gmrStatusCharacteristic != null &&
radarStatusCharacteristic != null &&
otaControlCharacteristic != null;
}
#Override
protected Deque<Request> initGatt(BluetoothGatt gatt) {
final LinkedList<Request> requests = new LinkedList<>();
requests.push(Request.readRequest(deviceIdCharacteristic));
requests.push(Request.readRequest(deviceVersionCharacteristic));
requests.push(Request.readRequest(configIdCharacteristic));
requests.push(Request.readRequest(configTransmissionIntervalCharacteristic));
requests.push(Request.readRequest(configKeepAliveIntervalCharacteristic));
requests.push(Request.readRequest(configRadioModeCharacteristic));
requests.push(Request.readRequest(configGpsCharacteristic));
requests.push(Request.readRequest(configRadarCharacteristic));
requests.push(Request.readRequest(configOperationModeCharacteristic));
requests.push(Request.readRequest(configLoRaAppEuiCharacteristic));
requests.push(Request.readRequest(configLoRaAppKeyCharacteristic));
requests.push(Request.readRequest(operationCmdCharacteristic));
requests.push(Request.readRequest(configLoRaDeviceEuiCharacteristic));
requests.push(Request.readRequest(nemeusStatusCharacteristic));
requests.push(Request.readRequest(gmrStatusCharacteristic));
requests.push(Request.readRequest(radarStatusCharacteristic));
// write authentication key to characteristic
requests.push(Request.writeRequest(authCharacteristic));
// perform server authentication
requests.push(Request.readRequest(authCharacteristic));
return requests;
}
#Override
protected void onDeviceDisconnected() {
authCharacteristic = null;
deviceIdCharacteristic = null;
deviceVersionCharacteristic = null;
configIdCharacteristic = null;
configTransmissionIntervalCharacteristic = null;
configKeepAliveIntervalCharacteristic = null;
configRadioModeCharacteristic = null;
configGpsCharacteristic = null;
configRadarCharacteristic = null;
configOperationModeCharacteristic = null;
configLoRaAppEuiCharacteristic = null;
configLoRaAppKeyCharacteristic = null;
configLoRaDeviceEuiCharacteristic = null;
nemeusStatusCharacteristic = null;
gmrStatusCharacteristic = null;
radarStatusCharacteristic = null;
otaDataCharacteristic = null;
}
#Override
protected void onMtuChanged(int mtu) {
super.onMtuChanged(mtu);
ECountBleManager.this.mtu = mtu;
}
#Override
protected void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicRead(gatt, characteristic);
if (characteristic.getUuid().equals(DC_UUID.authentication)) {
byte encryptedData[];
try {
encryptedData = BinaryUtils.encryptByteArray(characteristic.getValue());
} catch (Exception e) {
e.printStackTrace();
return;
}
characteristic.setValue(encryptedData);
} else if (characteristic.getUuid().equals(DC_UUID.deviceId)) {
deviceId = characteristic.getValue();
} else if (characteristic.getUuid().equals(DC_UUID.deviceVersion)) {
deviceVersion = characteristic.getValue();
} else if (characteristic.getUuid().equals(DC_UUID.configId)) {
configId = characteristic.getValue();
} else if (characteristic.getUuid().equals(DC_UUID.configTransmissionInterval)) {
configTransmissionInterval = characteristic.getValue();
} else if (characteristic.getUuid().equals(DC_UUID.configKeepAliveInterval)) {
configKeepAliveInterval = characteristic.getValue();
} else if (characteristic.getUuid().equals(DC_UUID.configRadioMode)) {
configRadioMode = characteristic.getValue();
} else if (characteristic.getUuid().equals(DC_UUID.configGps)) {
configGps = characteristic.getValue();
} else if (characteristic.getUuid().equals(DC_UUID.configRadar)) {
configRadar = characteristic.getValue();
} else if (characteristic.getUuid().equals(DC_UUID.configOperationMode)) {
configOperationMode = characteristic.getValue();
} else if (characteristic.getUuid().equals(DC_UUID.configLoRaAppEui)) {
configLoRaAppEui = characteristic.getValue();
} else if (characteristic.getUuid().equals(DC_UUID.configLoRaAppKey)) {
configLoRaAppKey = characteristic.getValue();
} else if (characteristic.getUuid().equals(DC_UUID.configLoRaDeviceEui)) {
configLoRaDeviceEui = characteristic.getValue();
} else if (characteristic.getUuid().equals(DC_UUID.nemeusStatus)) {
nemeusStatus = characteristic.getValue();
} else if (characteristic.getUuid().equals(DC_UUID.gmrStatus)) {
gmrStatus = characteristic.getValue();
} else if (characteristic.getUuid().equals(DC_UUID.radarStatus)) {
radarStatus = characteristic.getValue();
}
}
#Override
protected void onCharacteristicWrite(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
super.onCharacteristicWrite(gatt, characteristic);
if (characteristic.getUuid().equals(DC_UUID.otaControl)) {
final byte[] otaControl = characteristic.getValue();
if (otaControl.length == 1) {
// OTA client initiates the update process
if (otaControl[0] == (byte) 0x00) {
// set OTA process flag
isOtaProcessing = true;
// check whether device is in OTA mode
if (isOtaMode.getValue()) {
// request MTU size
requestMtu(mtu);
// start update process,but ensure MTU size has been requested
handler.postDelayed(() -> uploadOta(), 2000);
} else {
// reconnect to establish OTA mode
isReconnectRequired = true;
// enforces device to reconnect
gatt.disconnect();
}
}
// OTA client finishes the update process
if (otaControl[0] == (byte) 0x03) {
if (isOtaProcessing) { // if device is in OTA mode and update process was successful
isOtaProcessing = false;
disconnect();
} else { // if device is in OTA mode, but update process was not established
// enforces device to reconnect
gatt.disconnect();
}
}
}
}
}
#Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
}
};
public byte[] getDeviceId() {
return deviceId;
}
public void setDeviceId(final byte[] value) {
writeCharacteristic(deviceIdCharacteristic,
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT,
value);
}
// Todo: implement other getters and setters
// Here I have to get the otaAppFilePath which I discovered in OtaFragment
public void uploadOta(final String otaAppFilePath) {
if (otaDataCharacteristic != null) {
otaDataCharacteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
byte[] ebl = null;
try {
FileInputStream fileInputStream = new FileInputStream(otaAppFilePath);
int size = fileInputStream.available();
byte[] temp = new byte[size];
fileInputStream.read(temp);
fileInputStream.close();
ebl = temp;
} catch (Exception e) {
Logger.e(TAG, "Couldn't open file " + e);
}
otaAppFileStream = ebl;
pack = 0;
// start update process in another thread
Thread otaUploadThread = new Thread(() -> otaWriteDataReliable());
otaUploadThread.start();
}
}
private void writeCharacteristic(final BluetoothGattCharacteristic c,
final int writeType,
final byte[] value) {
if (c == null)
return;
c.setWriteType(writeType);
c.setValue(value);
writeCharacteristic(c); // will call the underlying API of BleManager
}
}
The code covers the basic use cases but I'm still not sure how to link the particular components with each other.
While reading about MVVM I noticed that there is always more than one possible solution/approach. I discovered the following questions:
Is the ECountBleManager the right place to store the variables that I got by calling characteristics.getValue() and when yes, should I place the variables that I discover in OtaFragment in it too (that would mean, that I have to forward the values e.g. of mtu to the ECountBleManager)? Consider that I have to access the variables that I discover in OtaFragment and maybe other Fragments.
Where do I store the variables from OtaFragment? In ECountVieModel or in ECountBleManager or do I create an OtaViewModel (but how could I access the ECountBleManager instance that I already created in ECountViewModel within the OtaViewModel?)
How can I access mtu, priority and otaAppFile which were discovered in OtaFragment within the ECountBleManager?
Do I have one ViewModel for every activity and fragment? But how to solve the problem with the ECountBleManager instance, see question 2?
How does the ECountBleManager fit in the MVVM pattern? I would guess it is part of the Model?! But which part? Repository, Interactor, Controller, Mediator?
The code is not less so I'm sorry but you see I'm really try harding and I want to get better. I hope someone can help me with my questions and to improve my code. Thanks in advance!
My 5 cents about BLE and an architecture:
you don't need to mix up the app architecture and BLE layer.
just think about BLE as a data-source/-repository (based on Rx, Coroutines)
split your functionality into several parts: BLE-scanner, BLE-connector, BLE-command-executor. Cover these parts by using the BLE-Fasade.
BLE layer is generally the async stuff (and it's too complicated to make them is 100% testable)
BLE layer in a deep look is the thread-safe handling of bytes. Don't handle it on the Main thread.
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)));
}
});
}
}
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 modified ActivityTestRule to retry executing failed test cases. I need to restart the activity and re-run the execution whenever there is a test failure. I have tried calling mActivity.finish() but current activity is not finishing and not restarting. But the tests are restarted. I have followed this method from here
public class MyActivityTestRule<T extends Activity> extends ActivityTestRule<T> {
private final Class<T> mActivityClass;
private T mActivity;
private static final String TAG = "ActivityInstrumentationRule";
private boolean mInitialTouchMode = false;
private Instrumentation mInstrumentation;
public MyActivityTestRule(Class<T> activityClass, boolean initialTouchMode, boolean launchActivity) {
super(activityClass, initialTouchMode, launchActivity);
mActivityClass = activityClass;
mInitialTouchMode = initialTouchMode;
mInstrumentation = InstrumentationRegistry.getInstrumentation();
}
#Override
public Statement apply(Statement base, Description description) {
final String testClassName = description.getClassName();
final String testMethodName = description.getMethodName();
final Context context = InstrumentationRegistry.getTargetContext();
/* android.support.test.espresso.Espresso.setFailureHandler(new FailureHandler() {
#Override public void handle(Throwable throwable, Matcher<View> matcher) {
if(AutomationTestCase.mEnableScreenshotOnFailure)
SpoonScreenshotOnFailure.perform("espresso_assertion_failed", testClassName, testMethodName);
new DefaultFailureHandler(context).handle(throwable, matcher);
}
});*/
// return super.apply(base, description);
return statement(base, description);
}
private Statement statement(final Statement base, final Description description) {
return new Statement() {
#Override
public void evaluate() throws Throwable {
Throwable caughtThrowable = null;
//retry logic
for (int i = 0; i < 3; i++) {
try {
launchAppActivity(getActivityIntent());
base.evaluate();
return;
} catch (Throwable t) {
caughtThrowable = t;
System.err.println(description.getDisplayName() + ": run " + (i+1) + " failed");
finishActivity();
} finally {
finishActivity();
}
}
System.err.println(description.getDisplayName() + ": giving up after " + 3 + " failures");
throw caughtThrowable;
}
};
}
void finishActivity() {
if (mActivity != null) {
mActivity.finish();
mActivity = null;
}
}
public T launchAppActivity(#Nullable Intent startIntent) {
// set initial touch mode
mInstrumentation.setInTouchMode(mInitialTouchMode);
final String targetPackage = mInstrumentation.getTargetContext().getPackageName();
// inject custom intent, if provided
if (null == startIntent) {
startIntent = getActivityIntent();
if (null == startIntent) {
Log.w(TAG, "getActivityIntent() returned null using default: " +
"Intent(Intent.ACTION_MAIN)");
startIntent = new Intent(Intent.ACTION_MAIN);
}
}
startIntent.setClassName(targetPackage, mActivityClass.getName());
startIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Log.d(TAG, String.format("Launching activity %s",
mActivityClass.getName()));
beforeActivityLaunched();
// The following cast is correct because the activity we're creating is of the same type as
// the one passed in
mActivity = mActivityClass.cast(mInstrumentation.startActivitySync(startIntent));
mInstrumentation.waitForIdleSync();
afterActivityLaunched();
return mActivity;
}}
If your test ended up in a different activity than what the ActivityTestRule was initialized as, you'll have to close all the activities in the backstack. This example may help:
https://gist.github.com/patrickhammond/19e584b90d7aae20f8f4
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