Any way to monitor discovered BLE peripherals without connecting? - android

Is there any way of being notified if a discovered BLE peripheral moves out of range or otherwise drops out of sight? I'm using rxBleClient.scanBleDevices() to build a list of devices in the area that are advertising, but before shipping this list to the main application I'd like to be sure that all the devices are still reachable. What's the best way of doing this?

The vanilla Android Scan API allows for scanning BLE devices with callback types of:
/**
* A result callback is only triggered for the first advertisement packet received that matches
* the filter criteria.
*/
public static final int CALLBACK_TYPE_FIRST_MATCH = 2;
/**
* Receive a callback when advertisements are no longer received from a device that has been
* previously reported by a first match callback.
*/
public static final int CALLBACK_TYPE_MATCH_LOST = 4;
The same API is available via RxBleClient.scanBleDevices(ScanSettings, ScanFilter...)
The CALLBACK_TYPE_FIRST_MATCH and CALLBACK_TYPE_MATCH_LOST are flags that can be put into ScanSettings.
The timeout after which the CALLBACK_TYPE_MATCH_LOST is triggered is somewhere around 10 seconds. This may be an indication that a particular device is no longer in range/available.

You can create a Transformer that will collect scanned devices and emit a list, that is kept up to date, depending on how long ago the device was recently seen.

Robert, that may not be exactly what you expect but treat it as an example. My Transformer is emitting a list of items whenever it has been changed, either because an update from the scanner or the eviction happened (checked every second).
class RollingPairableDeviceReducer(
private val systemTime: SystemTime,
private val evictionTimeSeconds: Long,
private val pairableDeviceFactory: PairableDeviceFactory
) : Observable.Transformer<ScannedDevice, List<PairableDevice>> {
override fun call(source: Observable<ScannedDevice>): Observable<List<PairableDevice>> {
val accumulator: MutableSet<PairableDevice> = Collections.synchronizedSet(mutableSetOf())
return source
.map { createPairableDevice(it) }
.map { pairableDevice ->
val added = updateOrAddDevice(accumulator, pairableDevice)
val removed = removeOldDevices(accumulator)
added || removed
}
.mergeWith(checkEvictionEverySecond(accumulator))
.filter { addedOrRemoved -> addedOrRemoved == true }
.map { accumulator.toList() }
}
private fun createPairableDevice(scannedDevice: ScannedDevice)
= pairableDeviceFactory.create(scannedDevice)
private fun updateOrAddDevice(accumulator: MutableSet<PairableDevice>, emittedItem: PairableDevice): Boolean {
val existingPairableDevice = accumulator.find { it.deviceIdentifier.hardwareId == emittedItem.deviceIdentifier.hardwareId }
return if (existingPairableDevice != null) {
accumulator.remove(existingPairableDevice)
existingPairableDevice.updateWith(emittedItem)
accumulator.add(existingPairableDevice)
false
} else {
accumulator.add(emittedItem)
true
}
}
private fun checkEvictionEverySecond(collector: MutableSet<PairableDevice>): Observable<Boolean>
= Observable.interval(1, TimeUnit.SECONDS)
.map { removeOldDevices(collector) }
private fun removeOldDevices(accumulator: MutableSet<PairableDevice>): Boolean {
val currentTimeInMillis = systemTime.currentTimeInMillis()
val evictionTimeMillis = TimeUnit.SECONDS.toMillis(evictionTimeSeconds)
return accumulator.removeAll { (currentTimeInMillis - it.lastSeenTime) >= evictionTimeMillis }
}
}

Related

How to throttle Rx stream conditionally based on model field?

I had two classes Error and Alert, and a bunch of Rx streams of Error type.
For simplification let's agreed upon two streams.
private val exampleErrorStream1 = PublishSubject.create<Error>()
private val exampleErrorStream2 = PublishSubject.create<Error>()
The whole point is to map error streams to alert streams with according names:
private val exampleAlertStream1 = PublishSubject.create<Alert>()
private val exampleAlertStream2 = PublishSubject.create<Alert>()
Also, I declared a map where:
key is pair of those streams
value is mapper function for each stream transformation
private val errorToAlerts = mutableMapOf<Pair<Subject<Error>, Subject<Alert>>, (Error) -> Alert>(
Pair(exampleErrorStream1, exampleAlertStream1) to { Alert(it.message, 1)},
Pair(exampleErrorStream2, exampleAlertStream2) to { Alert(it.message, 2)}
)
Finally, I run the method once on the app start for mapping those streams:
fun mapErrorsToAlerts() {
errorToAlerts.forEach { (streams, toAlert) ->
val (errorStream, alertStream) = streams
errorStream.map(toAlert).throttleByPriority()
.doOnNext {
Log.d("Alert","New alert: $it")
}
.subscribe(alertStream)
}
}
The only problem I had is throttling alerts depending on one of the Alert fields.
The throttleByPriority() is an extension function:
private fun Observable<Alert>.throttleByPriority(): Observable<Alert> {
return this.flatMap {
val time = if (it.priority == 1) 5L else 10L
throttleFirst(time, TimeUnit.SECONDS)
}
}
The throttling is not working how I imagined it.
I have 10 seconds window but the same events are emitted one by one despite that.
I assumed that the problem is with flatMap where all previous Observables are kept alive, but I'm not sure of that and I don't know how to achieve this differently.

Wrong If check statement

I wanted to make a method that determine if the application is started for the very first time, no matter the current version of the application. People suggest that we should use SharedPreferences as seen from this qustion. Below is the function that determine if application is started for the very first time.
companion object {
const val APP_LAUNCH_FIRST_TIME: Int = 0 // first start ever
const val APP_LAUNCH_FIRST_TIME_VERSION: Int = 1 // first start in this version (when app is updated)
const val APP_LAUNCH_NORMAL: Int = 2 // normal app start
/**
* Method that checks if the application is started for the very first time, or for the first time
* of the updated version, or just normal start.
*/
fun checkForFirstAppStart(context: Context): Int {
val sharedPreferencesVersionTag = "last_app_version"
val sharedPreferences = androidx.preference.PreferenceManager.getDefaultSharedPreferences(context)
var appStart = APP_LAUNCH_NORMAL
try {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
val lastVersionCode = sharedPreferences.getLong(sharedPreferencesVersionTag, -1L)
val currentVersionCode = PackageInfoCompat.getLongVersionCode(packageInfo)
appStart = when {
lastVersionCode == -1L -> APP_LAUNCH_FIRST_TIME
lastVersionCode < currentVersionCode -> APP_LAUNCH_FIRST_TIME_VERSION
lastVersionCode > currentVersionCode -> APP_LAUNCH_NORMAL
else -> APP_LAUNCH_NORMAL
}
// Update version in preferences
sharedPreferences.edit().putLong(sharedPreferencesVersionTag, currentVersionCode).commit()
} catch (e: PackageManager.NameNotFoundException) {
// Unable to determine current app version from package manager. Defensively assuming normal app start
}
return appStart
}
}
Now in my MainActivity I make the check in this way, but strangely enough I always end up inside the if statement, although appLaunch is different from MainActivityHelper.APP_LAUNCH_FIRST_TIME
val appLaunch = MainActivityHelper.checkForFirstAppStart(this)
if (appLaunch == MainActivityHelper.APP_LAUNCH_FIRST_TIME) {
val c = 299_792_458L
}
Here we see that appLaunch is 2
Here we see that MainActivityHelper.APP_LAUNCH_FIRST_TIME is 0
I am in the main thread I check using Thread.currentThread(), and when I add watches in the debugger (appLaunch == MainActivityHelper.APP_LAUNCH_FIRST_TIME) I get false.
So I suggest that there is some delay, and by the time the if check is made the result is changed?
There's nothing wrong with the code. I tested it and it works as intended. I get all three return values depending on the circumstances. I simplified the code a bit but the original code should nevertheless works.
enum class AppLaunch {
LAUNCH_FIRST_TIME, // first start ever
FIRST_TIME_VERSION, // first start in this version (when app is updated)
NORMAL // normal app start
}
/**
* Method that checks if the application is started for the very first time, or for the first time
* of the updated version, or just normal start.
*/
fun checkForFirstAppStart(context: Context): AppLaunch {
val sharedPreferencesVersionTag = "last_app_version"
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
return try {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
val lastVersionCode = sharedPreferences.getLong(sharedPreferencesVersionTag, -1L)
val currentVersionCode = PackageInfoCompat.getLongVersionCode(packageInfo)
// Update version in preferences
sharedPreferences.edit().putLong(sharedPreferencesVersionTag, currentVersionCode).commit()
when (lastVersionCode) {
-1L -> AppLaunch.LAUNCH_FIRST_TIME
in 0L until currentVersionCode -> AppLaunch.FIRST_TIME_VERSION
else -> AppLaunch.NORMAL
}
} catch (e: PackageManager.NameNotFoundException) {
// Unable to determine current app version from package manager. Defensively assuming normal app start
AppLaunch.NORMAL
}
}
I experimented a bit and the issue you see looks like a bug in Android Studio. If the code in the if statement is a NOP (no operation) then the debugger seems to stop there. If the code does have a side effect, the debugger doesn't stop.
Things like this can be infuriating but with Android, Android Studio and the tooling, bugs like this are pretty common (unfortunately).
if (appLaunch == APP_LAUNCH_FIRST_TIME) {
val c = 299_792_458L
}
translates to the following byte code:
L3 (the if statement)
LINENUMBER 32 L3
ILOAD 4
IFNE L4
L5
LINENUMBER 33 L5
LDC 299792458
LSTORE 2
Converting c to a var
var c = 1L
if (appLaunch == APP_LAUNCH_FIRST_TIME) {
c = 299_792_458L
}
results in identical byte code so it's certainly not a code problem but an issue with Android Studio.
Update
If you need fast writes with enums you can use something like this:
fun appLaunchById(id: Int, def: AppLaunch = AppLaunch.NORMAL) = AppLaunch.values().find { it.id == id } ?: def
enum class AppLaunch(val id: Int) {
LAUNCH_FIRST_TIME(0), // first start ever
FIRST_TIME_VERSION(1), // first start in this version (when app is updated)
NORMAL(2); // normal app start
}
^^^ writes an Int so fast and short. Reading is certainly not super fast though.
Update 2
Generic version of the enum solution:
inline fun <reified T : Enum<*>> enumById(hash: Int, def: T) = enumValues<T>()
.find { it.hashCode() == hash }
?: def
enum class AppLaunch {
LAUNCH_FIRST_TIME, // first start ever
FIRST_TIME_VERSION, // first start in this version (when app is updated)
NORMAL // normal app start
}
Usage:
val enum = enumById(value.hashCode(), AppLaunch.NORMAL)

Using CompanionDeviceManager to read the devices information

I am using BluetoothLeScanner to scan for BLE devices and get a list of objects representing the devices to show inside my app (not connecting to any of them).
I am interested in doing the same, but using the CompanionDeviceManager now. Its callback CompanionDeviceManager.Callback.onDeviceFound(chooserLauncher: IntentSender?) unfortunately does not return any human readable form of found devices... the closest it gets is the IntentSender.writeToParcel method, but I am not sure how to use it in this situation.
I am not constrained to use the CompanionDeviceManager but I wanted to follow the OS version specific guidelines, we are supposed to use CompanionDeviceManager for Bluetooth devices scanning starting from API 26, but it seems useless in my case... so is there any way to get devices data from that callback, or should I just ditch it and stay with BluetoothLeScanner for all OS versions?
Late answer but it might help someone else. You can create a bluetooth device picker in combination with ActivityResultContracts.StartIntentSenderForResult() in order to get the BluetoothDevice. From there you will have access to all the device info that you need. Recent changes added some Android 12 permissions like android.permission.BLUETOOTH_CONNECT. Your mileage may vary.
val context = LocalContext.current
// Get the device manager instance
val deviceManager: CompanionDeviceManager by lazy {
ContextCompat.getSystemService(
context,
CompanionDeviceManager::class.java
) as CompanionDeviceManager
}
// Create a filter of your choice. Here I just look for specific device names
val deviceFilter: BluetoothDeviceFilter by lazy {
BluetoothDeviceFilter.Builder()
.setNamePattern(Pattern.compile(supportedDevices))
.build()
}
// Create a pairing request with your filter from the last step
val pairingRequest: AssociationRequest = AssociationRequest.Builder()
.addDeviceFilter(deviceFilter)
.build()
// Create a picker for discovered bluetooth devices
val bluetoothDevicePicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartIntentSenderForResult(),
onResult = {
val device: BluetoothDevice? =
it.data?.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE)
try {
// Now that you have the desired device, do what you need to with it
device?.apply {
when {
name?.matches(Regex(firstDevicePattern)) == true -> {
Log.i(TAG, "${this.name} connected")
onFirstDeviceDiscovered(device)
}
name?.matches(Regex(secondDevicePattern)) == true -> {
Log.i(TAG, "${this.name} connected")
onSecondDeviceDiscovered(device)
}
}
}
} catch (e: SecurityException) {
e.printStackTrace()
//TODO: handle the security exception (this is possibly a bug)
// https://issuetracker.google.com/issues/198986283
}
}
)
// A utility function to centralize calling associate (optional)
val associateDevice: (AssociationRequest) -> Unit = { request ->
// Attempt to associate device(s)
deviceManager.associate(
request,
object : CompanionDeviceManager.Callback() {
override fun onDeviceFound(chooserLauncher: IntentSender) {
val sender = IntentSenderRequest.Builder(chooserLauncher)
.build()
bluetoothDevicePicker.launch(sender)
}
override fun onFailure(error: CharSequence?) {
//TODO: handle association failure
}
}, null
)
}

How to complete a Kotlin Flow in Android Worker

I'm investigating the use of Kotlin Flow within my current Android application
My application retrieves its data from a remote server via Retrofit API calls.
Some of these API's return 50,000 data items in 500 item pages.
Each API response contains an HTTP Link header containing the Next pages complete URL.
These calls can take up to 2 seconds to complete.
In an attempt to reduce the elapsed time I have employed a Kotlin Flow to concurrently process each page
of data while also making the next page API call.
My flow is defined as follows:
private val persistenceThreadPool = Executors.newFixedThreadPool(3).asCoroutineDispatcher()
private val internalWorkWorkState = MutableStateFlow<Response<List<MyPage>>?>(null)
private val workWorkState = internalWorkWorkState.asStateFlow()
private val myJob: Job
init {
myJob = GlobalScope.launch(persistenceThreadPool) {
workWorkState.collect { page ->
if (page == null) {
} else managePage(page!!)
}
}
}
My Recursive function is defined as follows that fetches all pages:-
private suspend fun managePages(accessToken: String, response: Response<List<MyPage>>) {
when {
result != null -> return
response.isSuccessful -> internalWorkWorkState.emit(response)
else -> {
manageError(response.errorBody())
result = Result.failure()
return
}
}
response.headers().filter { it.first == HTTP_HEADER_LINK && it.second.contains(REL_NEXT) }.forEach {
val parts = it.second.split(OPEN_ANGLE, CLOSE_ANGLE)
if (parts.size >= 2) {
managePages(accessToken, service.myApiCall(accessToken, parts[1]))
}
}
}
private suspend fun managePage(response: Response<List<MyPage>>) {
val pages = response.body()
pages?.let {
persistResponse(it)
}
}
private suspend fun persistResponse(myPage: List<MyPage>) {
val myPageDOs = ArrayList<MyPageDO>()
myPage.forEach { page ->
myPageDOs.add(page.mapDO())
}
database.myPageDAO().insertAsync(myPageDOs)
}
My numerous issues are
This code does not insert all data items that I retrieve
How do complete the flow when all data items have been retrieved
How do I complete the GlobalScope job once all the data items have been retrieved and persisted
UPDATE
By making the following changes I have managed to insert all the data
private val persistenceThreadPool = Executors.newFixedThreadPool(3).asCoroutineDispatcher()
private val completed = CompletableDeferred<Int>()
private val channel = Channel<Response<List<MyPage>>?>(UNLIMITED)
private val channelFlow = channel.consumeAsFlow().flowOn(persistenceThreadPool)
private val frank: Job
init {
frank = GlobalScope.launch(persistenceThreadPool) {
channelFlow.collect { page ->
if (page == null) {
completed.complete(totalItems)
} else managePage(page!!)
}
}
}
...
...
...
channel.send(null)
completed.await()
return result ?: Result.success(outputData)
I do not like having to rely on a CompletableDeferred, is there a better approach than this to know when the Flow has completed everything?
You are looking for the flow builder and Flow.buffer():
suspend fun getData(): Flow<Data> = flow {
var pageData: List<Data>
var pageUrl: String? = "bla"
while (pageUrl != null) {
TODO("fetch pageData from pageUrl and change pageUrl to the next page")
emitAll(pageData)
}
}
.flowOn(Dispatchers.IO /* no need for a thread pool executor, IO does it automatically */)
.buffer(3)
You can use it just like a normal Flow, iterate, etc. If you want to know the total length of the output, you should calculate it on the consumer with a mutable closure variable. Note you shouldn't need to use GlobalScope anywhere (ideally ever).
There are a few ways to achieve the desired behaviour. I would suggest to use coroutineScope which is designed specifically for parallel decomposition. It also provides good cancellation and error handling behaviour out of the box. In conjunction with Channel.close behaviour it makes the implementation pretty simple. Conceptually the implementation may look like this:
suspend fun fetchAllPages() {
coroutineScope {
val channel = Channel<MyPage>(Channel.UNLIMITED)
launch(Dispatchers.IO){ loadData(channel) }
launch(Dispatchers.IO){ processData(channel) }
}
}
suspend fun loadData(sendChannel: SendChannel<MyPage>){
while(hasMoreData()){
sendChannel.send(loadPage())
}
sendChannel.close()
}
suspend fun processData(channel: ReceiveChannel<MyPage>){
for(page in channel){
// process page
}
}
It works in the following way:
coroutineScope suspends until all children are finished. So you don't need CompletableDeferred anymore.
loadData() loads pages in cycle and posts them into the channel. It closes the channel as soon as all pages have been loaded.
processData fetches items from the channel one by one and process them. The cycle will finish as soon as all the items have been processed (and the channel has been closed).
In this implementation the producer coroutine works independently, with no back-pressure, so it can take a lot of memory if the processing is slow. Limit the buffer capacity to have the producer coroutine suspend when the buffer is full.
It might be also a good idea to use channels fan-out behaviour to launch multiple processors to speed up the computation.

How to return a default value when an Observer subscribes to my Observable

I'm still a rookie in RxJava and trying to implement something using it.
I have notifications counter in my Android application and I want to fetch the count from an API every five minutes, and I want to share this count to multiple activities. So I want to store the count in a pipeline so that each activity listens to that pipeline can get the last emitted value.
I started with an Object class and added two BehaviorSubjects one for providing the default value and the second to emit true value when the 5 minutes finish "to tell the activity that's listening if there's any" that you should fetch new count.
But the problem is that when I listen to the BehaviorSubject it emits all the data in the pipeline! How can I get only that last one
object RxBus {
#JvmStatic
var count: Int = 0
#JvmStatic
private var fetchData: Boolean = true
#JvmStatic
private var behaviorSubject: BehaviorSubject<Any> = BehaviorSubject.createDefault(CountEvent(count))
#JvmStatic
private var fetchSubject: BehaviorSubject<Any> = BehaviorSubject.createDefault(FetchEvent(fetchData))
private val FIVE_MINUTES: Long = 1000 * 60 * 5
init {
fixedRateTimer("countTimer", false, 0L, FIVE_MINUTES) {
fetchSubject.onNext(FetchEvent(true))
}
}
fun publish(event: Any) {
if (event is CountEvent) {
Log.d(TAG, "eventCount = ${event.count}")
count = event.count
fetchData = false
}
}
fun listenToLoadEvent(): Observable<FetchEvent> {
return fetchSubject.ofType(FetchEvent::class.java)
}
fun listen(): Observable<CountEvent> {
return behaviorSubject.ofType(CountEvent::class.java)
}
Please note that I want to listen to both Subjects in onResume of each activity.. Is there any way to do that in a better way? And if not can you help me to just get the last emitted item only.
Thanks in Advance
class CountEvent(val value: Int)
object CounterManager {
val counterDataSource = BehaviorProcessor.create<CountEvent>()
val refreshNotifier = interval(5, TimeUnit.MINUTES).publish()
.refCount()
fun publish(event: Any) {
if (event is CountEvent) {
counterDataSource.onNext(event)
}
}
}

Categories

Resources