Server connection status with RxJava in Android - android

I have a websocket connection
object StompWrapper {
private var emitter: ObservableEmitter<Event>? = null
init {
val client = OkHttpClient.Builder().build()
val stomp = StompClient(client)
stomp.connect()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
when (it.type) {
Event.Type.OPENED -> {
Timber.d("Connect OPENED")
isConnected = true
emitter?.onNext(Event(Event.Type.OPENED))
}
Event.Type.CLOSED,
Event.Type.ERROR -> {
Timber.d("Connect ERROR")
isConnected = false
emitter?.onNext(Event(Event.Type.ERROR))
}
else -> {}
}
}, { e ->
Timber.e(e)
})
}
fun status(): Observable<Event> {
return Observable.create {
Timber.d("Connect CREATE status $isConnected")
emitter = it
if (isConnected) {
emitter?.onNext(Event(Event.Type.OPENED))
}
}
}
}
calling like this
disposable.add(StompWrapper.status().subscribe {
Timber.d("Connect: %s", it.type)
})
It turns out that connection hangs in a static class. It works if there is one subscriber, if you subscribe a couple of times, the last subscription will work and that's it. Please tell me how to make it so that I can find out the current status of the connection to the server anywhere in the application and do this as many times as I like?

Related

How to combine two serial flows which input depends on other output?

I have sophisticated scenario where a set of mutually dependent coroutine flows depends on each other and chained:
viewModelScope.launch {
repository.cacheAccount(person)
.flatMapConcat { it->
Log.d(App.TAG, "[2] create account call (server)")
repository.createAccount(person)
}
.flatMapConcat { it ->
if (it is Response.Data) {
repository.cacheAccount(it.data)
.collect { it ->
// no op, just execute the command
Log.d(App.TAG, "account has been cached")
}
}
flow {
emit(it)
}
}
.catch { e ->
Log.d(App.TAG, "[3] get an exception in catch block")
Log.e(App.TAG, "Got an exception during network call", e)
state.update { state ->
val errors = state.errors + getErrorMessage(PersonRepository.Response.Error.Exception(e))
state.copy(errors = errors, isLoading = false)
}
}
.collect { it ->
Log.d(App.TAG, "[4] collect the result")
updateStateProfile(it)
}
}
cache an account on the local disk
create an account on the backend
in positive scenario, cache the newly create account in the local disk
Now I have to add more calls to a new API endpoint and the scenario become even more sophisticated. This endpoint is a ethereum chain.
4a. In the positive scenario, put in the local disk (cache) initiated transaction cacheRepository.createChainTx()
4b. In the negative scenario, just emit further the response from the backend
4a.->5. Register user on the 2nd endpoint repository.registerUser()
The response from 2nd endpoint put in the cache by updating existing row. Even negative case except of exception should be cached to update status of tx.
viewModelScope.launch {
lateinit var newTx: ITransaction
cacheRepository.createChainTxAsFlow(RegisterUserTransaction(userWalletAddress = userWalletAddress))
.map { it ->
newTx= it
repository.registerUserOnSwapMarket(userWalletAddress)
}
.onEach { it -> preProcessResponse(it, newTx) }
.flowOn(backgroundDispatcher)
.collect { it -> processResponse(it) }
}
This a scenario which should be integrated into the 1st Flow chain.
The issue is I do not see how to do it clear in Flow chain. I can rewrite code without chaining, but it also bring variety if else statements.
How would you do this scenario in human readable way?
I'll ended up with this code for transition period:
viewModelScope.launch(backgroundDispatcher) {
try {
var cachedPersonProfile = repository.cacheAccount(person)
var createAccountResponse = repository.createAccount(person)
when(createAccountResponse) {
is Response.Data -> {
repository.cacheAccount(createAccountResponse.data)
val cachedTx = cacheRepository.createChainTx(RegisterUserTransaction(userWalletAddress = person.userWalletAddress))
val chainTx = walletRepository.registerUserOnSwapMarket(userWalletAddress = person.userWalletAddress)
when(chainTx) {
is ru.home.swap.core.network.Response.Data -> {
if (chainTx.data.isStatusOK()) {
cachedTx.status = TxStatus.TX_MINED
} else {
cachedTx.status = TxStatus.TX_REVERTED
}
}
is ru.home.swap.core.network.Response.Error.Message -> {
cachedTx.status = TxStatus.TX_EXCEPTION
}
is ru.home.swap.core.network.Response.Error.Exception -> {
cachedTx.status = TxStatus.TX_EXCEPTION
}
}
cacheRepository.createChainTx(cachedTx)
withContext(Dispatchers.Main) {
state.update { state ->
if (cachedTx.status == TxStatus.TX_MINED) {
state.copy(
isLoading = false,
profile = createAccountResponse.data,
status = StateFlagV2.PROFILE
)
} else {
val txError = "Failed register the profile on chain with status ${TxStatus.TX_MINED}"
state.copy(
isLoading = false,
errors = state.errors + txError
)
}
}
}
}
else -> { updateStateProfile(createAccountResponse) }
}
} catch (ex: Exception) {
withContext(Dispatchers.Main) {
state.update { state ->
val errors = state.errors + getErrorMessage(PersonRepository.Response.Error.Exception(ex))
state.copy(errors = errors, isLoading = false)
}
}
}
}
If you have a better alternative, please share it in the post as an answer.

RxJava ConcatArrayDelayError and filters: returning an error only if both sources fail

I'm new to RxJava and after a few days of trying everything I could find online I see that I really need help with this one.
I fetch a member in my repository with local and remote sources. I added some operators to return my remote source in priority (via debounce), and to filter out errors so it would return only 1 of the 2 if either remote is not available or the database is empty.
It works fine as long as something is returned by one of my 2 sources, but the problem occurs if both sources returns errors: as I filter out the errors, it doesn't return anything, and my subscribe is never called.
Maybe there is a simple solution but I have not found it so far, could someone help?
Here is my fetchMember() in my Repository:
override fun fetchMember(): Observable<MemberModel?> {
return Observable.concatArrayDelayError(memberLocalSource.fetchMember(), memberRemoteSource.fetchMember())
.doOnNext { member ->
saveMember(member!!)
}
.materialize()
.filter { !it.isOnError }
.dematerialize { it -> it }
.debounce(400, TimeUnit.MILLISECONDS)
}
And here is my viewmodel:
fun fetchToken(username: String, password: String) {
val loginDisposable = authApiService.loginWithJWT(username, password)
.flatMap {
isAuthenticated = isTokenValid(username, password, it)
sharedPreferences.setHasValidCredentials(isAuthenticated)
memberRepository.fetchMember()
}
.subscribeOn(Schedulers.io())
.observeOn((AndroidSchedulers.mainThread()))
.doOnError { throwable ->
throwable.printStackTrace()
}
.subscribe(
{ member ->
memberLiveData.value = member
this.memberId = member!!.id.toString()
this.memberName = member.name.split(" ")[0]
if(isAuthenticated) {
authenticationState.value = AuthenticationState.AUTHENTICATED_VALID_MEMBER
} else {
authenticationState.value = AuthenticationState.UNAUTHENTICATED_VALID_MEMBER
}
},
{ error ->
if(isAuthenticated) {
authenticationState.value = AuthenticationState.AUTHENTICATED_INVALID_MEMBER
} else {
authenticationState.value = AuthenticationState.INVALID_AUTHENTICATION
}
})
disposable.add(loginDisposable)
}
private fun isTokenValid(username: String, password: String, authResponse: AuthModel): Boolean {
return if (authResponse.data != null) {
false
} else {
tokenInterceptor.token = authResponse.token
val tokenWithCredentials = AuthModel(authResponse.token, null, null, username, password)
tokenRepository.saveToken(tokenWithCredentials)
true
}
}
In the end I managed to make it work by adding:
.defaultIfEmpty(MemberModel(-1))
and checking against id == -1.

How i can handle network disconnect using RxJava 2

I have some problems, which bound with lose of network connection. How i can handle it in RxJava 2? Thank you very much.
i have this method:
Disposable disposable = api.setStatus(params)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.doOnSubscribe(listener::onPreExecute)
.doFinally(listener::onPostExecute)
.subscribe(serviceRequest -> handleResponse(listener, serviceRequest), listener::onError);
//////////////
#POST("set_status")
Single<OrderResponse> setStatus(#FieldMap Map<String, String> params);
New answer after question was update:
You can handle it in doOnError or your listener::onError:
Disposable disposable = api.setStatus(params)
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.doOnError(error->{
handleError(error)
})
.subscribe(serviceRequest -> handleResponse(listener, serviceRequest), listener::onError);
void handleError(Throwable error){
if (error instanceof IOException){
// handle network error
} else {
if(error instanceof SocketTimeoutException){
// handle timeout error
}
}
}
Old answer about subscribing to connection change:
For this purposes you need to catch the network connection change and dispatch it via BehaviorSubject.
Like this:
class NetworkManager(
private val context: Context
) {
private val state: BehaviorSubject<Boolean> = BehaviorSubject.create()
private val receiver = object : BroadcastReceiver() {
override fun onReceive(c: Context?, intent: Intent?) {
state.onNext(isConnected())
}
}
init {
val intentFilter = IntentFilter()
intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION)
context.registerReceiver(receiver, intentFilter)
state.onNext(isConnected())
}
fun subscribe(): Observable<Boolean> {
return state
}
fun isConnected(): Boolean {
val cm = context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val netInfo = cm.activeNetworkInfo
return netInfo != null && netInfo.isConnectedOrConnecting
}
}
Create this classs in your Application scope and it will be ok
if you return Single for network calls you can use the onErrorReturn method to catch error and return state to indicate network error.
Handle in onError method as below
void onError(Throwable throwable) {
if(throwable instanceof IOException) {
//Handle network error
}else if(throwable instanceof SocketTimeoutException) {
//Handle Request timeout
}else {
//Show some error like something went wrong
}
}

setupNotification returns "Error already connected" whereas no connection request is send

I'm making an Android application with some BLE interraction using the RxAndroidBLE API. I followd examples guidelines and samples from https://github.com/Polidea/RxAndroidBle
I establish a BLE connection with a specified device, later while connected I read and write characteristic with no problem, but when i try to setup notification for the battery level characteristic I get the following throwable error message : Already connected to device with MAC address XX:XX..."
I really don't understand the error in that context since I can read and write in characteristic with no problem.
I want to setup notification for this characteristic after an initial read of its value for specific purpose.
Here is a sample code that reproduce my problem :
private lateinit var device: RxBleDevice
private var connectionObservable: Observable<RxBleConnection>? = null
private var rxBleConnection: RxBleConnection? = null
private val connectionDisposable = CompositeDisposable()
private val connectionStateDisposable = CompositeDisposable()
private var notifyValueChangeSubscription = CompositeDisposable()
var enableBatteryNotificationRunnable: Runnable = Runnable {
enableBatteryNotification()
}
private var myHandler = Handler()
val DELAY_BEFORE_ENABLE_NOTIFICATION: Long = 100
private fun connect() {
connectionObservable = device.establishConnection(false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
connectionObservable?.let {
connectionDisposable.add(it.subscribe(
{ rxBleConnection ->
this.rxBleConnection = rxBleConnection
},
{ _ ->
Log.e("connect", "connexion error")
})
)
}
val state = device.observeConnectionStateChanges().subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
connectionStateDisposable.add(
state.subscribe(
{ connectionState ->
Log.i("connect", "connexion state :$connectionState")
if(connectionState == RxBleConnection.RxBleConnectionState.CONNECTED) {
myHandler.postDelayed(enableBatteryNotificationRunnable, DELAY_BEFORE_ENABLE_NOTIFICATION);
}
}
)
{ _ ->
Log.e("connection listener", "connexion state error")
}
)
}
private fun enableBatteryNotification () {
connectionObservable?.let {
var observableToReturn = it
.flatMap { it.setupNotification(UUID_BATTERY_LEVEL) }
.doOnNext {
Log.i("NOTIFICATION", "doOnNext")
}
.flatMap { it }
.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
notifyValueChangeSubscription.add(observableToReturn.subscribe({ bytes ->
var strBytes = String(bytes)
Log.i("NOTIFICATION", "value change: $strBytes")
},
{ throwable ->
Log.e("NOTIFICATION", "Error in notification process: " + throwable.message)
})
)
}
}
Thanks in advance for any help :)
setupNotification returns “Error already connected” whereas no connection request is send
Two connection requests are actually made — hence the error. From the RxBleDevice.establishConnection() Javadoc:
* Establishes connection with a given BLE device. {#link RxBleConnection} is a handle, used to process BLE operations with a connected
* device.
In your code there are two subscriptions to the establishConnection() Observable.
private lateinit var device: RxBleDevice
private var connectionObservable: Observable<RxBleConnection>? = null
private var rxBleConnection: RxBleConnection? = null
private val connectionDisposable = CompositeDisposable()
private val connectionStateDisposable = CompositeDisposable()
private var notifyValueChangeSubscription = CompositeDisposable()
var enableBatteryNotificationRunnable: Runnable = Runnable {
enableBatteryNotification()
}
private var myHandler = Handler()
val DELAY_BEFORE_ENABLE_NOTIFICATION: Long = 100
private fun connect() {
connectionObservable = device.establishConnection(false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
connectionObservable?.let {
connectionDisposable.add(it.subscribe( // << Here is the first subscription
{ rxBleConnection ->
this.rxBleConnection = rxBleConnection
},
{ _ ->
Log.e("connect", "connexion error")
})
)
}
val state = device.observeConnectionStateChanges().subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
connectionStateDisposable.add(
state.subscribe(
{ connectionState ->
Log.i("connect", "connexion state :$connectionState")
if(connectionState == RxBleConnection.RxBleConnectionState.CONNECTED) {
myHandler.postDelayed(enableBatteryNotificationRunnable, DELAY_BEFORE_ENABLE_NOTIFICATION);
}
}
)
{ _ ->
Log.e("connection listener", "connexion state error")
}
)
}
private fun enableBatteryNotification () {
connectionObservable?.let {
var observableToReturn = it
.flatMap { it.setupNotification(UUID_BATTERY_LEVEL) }
.doOnNext {
Log.i("NOTIFICATION", "doOnNext")
}
.flatMap { it }
.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
notifyValueChangeSubscription.add(observableToReturn.subscribe({ bytes -> // << Here is the second subscription
var strBytes = String(bytes)
Log.i("NOTIFICATION", "value change: $strBytes")
},
{ throwable ->
Log.e("NOTIFICATION", "Error in notification process: " + throwable.message)
})
)
}
}
This situation is a common source of confusion for people learning RxJava. There are three paths to fix your situation. From least to most amount of work:
Share the establishConnection Observable
It is possible to share a single RxBleConnection with RxReplayingShare. Change this:
connectionObservable = device.establishConnection(false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
To this:
connectionObservable = device.establishConnection(false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.compose(ReplayingShare.instance())
Use the rxBleConnection: RxBleConnection? property
Instead of:
connectionObservable?.let {
var observableToReturn = it
.flatMap { it.setupNotification(UUID_BATTERY_LEVEL) }
.doOnNext {
Log.i("NOTIFICATION", "doOnNext")
}
.flatMap { it }
.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
notifyValueChangeSubscription.add(observableToReturn.subscribe({ bytes -> // << Here is the second subscription
var strBytes = String(bytes)
Log.i("NOTIFICATION", "value change: $strBytes")
},
{ throwable ->
Log.e("NOTIFICATION", "Error in notification process: " + throwable.message)
})
)
}
Make it:
rxBleConnection?.let {
var observableToReturn = rxBleConnection.setupNotification(UUID_BATTERY_LEVEL)
.doOnNext {
Log.i("NOTIFICATION", "doOnNext")
}
.flatMap { it }
.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
notifyValueChangeSubscription.add(observableToReturn.subscribe({ bytes -> // << Here is the second subscription
var strBytes = String(bytes)
Log.i("NOTIFICATION", "value change: $strBytes")
},
{ throwable ->
Log.e("NOTIFICATION", "Error in notification process: " + throwable.message)
})
)
}
This is discouraged as you may end up with a RxBleConnection that is no longer valid as it may have been disconnected before calling enableBatteryNotification()
Change the flow of your code to use a single .subscribe()
This is a custom solution tailored to your exact use-case. Unfortunately with the information you have added is not enough to create a drop-in code replacement but it could look something like this:
device.establishConnection(false)
.flatMap { connection ->
Observable.merge(
connection.readCharacteristic(uuid0).map { ReadResult(uuid0, it) }.toObservable(),
connection.setupNotification(uuid1).flatMap { it }.map { NotifyResult(uuid1, it) }.delaySubscription(100, TimeUnit.MILLISECONDS)
)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ /* handle ReadResult/NotifyResult */ },
{ /* handle potential errors */ }
)
Where ReadResult and NotifyResult would be data class that take UUID and ByteArray

retryWhen on RxJava with Rx2Apollo

I'm using Rx2Apollo to make a graphql call:
private fun registerCardToken(token: String): io.reactivex.Observable<RegisterCardTokenMutation.RegisterCreditCard> {
val apolloCall = apolloClient().mutate(RegisterCardTokenMutation.builder().token(token).build())
return Rx2Apollo.from(apolloCall).map {
(it.data() as RegisterCardTokenMutation.Data).registerCreditCard()
}.doOnError({ error ->
//Log.e("registerCardToke", error.message)
})
}
This works well, but I want to handle specific error and retry this onces. I have tried to work around this using retryWhen and retry , but not able to write any executable code yet.
The retry persons a token refresh before performing the actual retry. Here's the token refresh sample:
private fun refreshBearerToken(callback: OnCompleteListener<GetTokenResult>) {
FirebaseAuth.getInstance().currentUser?.getIdToken(true)?.addOnCompleteListener(callback)
}
First, you have to turn refreshBearerToken into an Observable
val refreshTokenSource = Observable.create({ emitter ->
FirebaseAuth.
getInstance().
currentUser?.
getIdToken(true)?.
addOnCompleteListener({ task ->
if (task.isSuccessful()) {
emitter.onNext(task.getResult())
emitter.onComplete()
} else {
emitter.onError(task.getException())
}
})
})
Second, use some external reference holding the current token and conditionally use it before calling registerCardToken:
val currentToken = AtomicReference<String>()
val registerCardTokenObservable = Observable.defer({
val token = currentToken.get()
if (token == null) {
return refreshTokenSource
.doOnNext({ currentToken.set(it) })
.flatMap({ registerCardToken(it) })
}
return registerCardToken(token)
})
.retry({ error ->
if ((error is IOException) || (error.getMessage().contains("network")) {
currentToken.set(null)
return true
}
return false
})

Categories

Resources