share() operator not working for Observable in Rxjava - android

I have a scenario where we i have an emmiter which constantly emits data like this
fun subscribeForEvents(): Flowable<InkChannel> {
return Flowable.create<InkChannel>({
if (inkDevice.availableDeviceServices.contains(DeviceServiceType.EVENT_DEVICE_SERVICE)) {
(inkDevice.getDeviceService(DeviceServiceType.EVENT_DEVICE_SERVICE) as EventDeviceService).subscribe(object : EventCallback {
override fun onUserActionExpected(p0: UserAction?) {
it.onNext(InkChannel.UserActionEvent(p0))
}
override fun onEvent(p0: InkDeviceEvent?, p1: Any?) {
it.onNext(InkChannel.InkEvents<Any>(p0, p1))
}
override fun onUserActionCompleted(p0: UserAction?, p1: Boolean) {
}
}
)
}
}, BackpressureStrategy.BUFFER).share()
}
now i have a service which i start on application launch and listen to it
inkDeviceBus.subscribeForEvents()
.filter { it -> (it as InkChannel.InkEvents<*>).event == InkDeviceEvent.STATUS_CHANGED }
.map { it -> it as InkChannel.InkEvents<*> }
.map { it -> it.value.toString() }
.filter { value -> value == "CONNECTED" || value == "DISCONNECTED" }
.map { it -> it == "CONNECTED" }
.subscribeBy { b ->
if (b) stopSelf()
}
I have another activity MainActivity which is called upon launch where i observe the same event.
Now the issue is only the listener in the service gets the events and the activity is not receiving any events.
Now when i remove the listener form the service then activity starts receiving events. I have used the operator share for sharing the observable but it doesn't seem to work

.share() only affects the instance it is called on. Since each call to subscribeForEvents creates a new instance the .share() does not change the behavior.
You need to call subscribeForEvents once and then use the returned value when you want to receive the events. As long as the same object is used it will share the underlying listener.

Related

#Synchronized is not working as expected in KMM

I have a Ktor class where I need to do action on unauthorized exception(when token is expired), for this action I need to have synchronized action, otherwise it is not working correctly, the problem is that #Synchronized is not synchronize the action and is not waiting for action to finish for next one.
fun ktorFunction(){
HttpResponseValidator {
handleResponseException { exception ->
kermit.e { "In ${exception.message}" }
val clientException =
exception as? ClientRequestException ?: return#handleResponseException
val exceptionResponse = clientException.response
when (exceptionResponse.status) {
HttpStatusCode.Unauthorized -> {
test(){
kermit.v { "Error message" }
}
}
}
}
}
#Synchronized
fun test(messageTest: () -> Unit) {
CoroutineScope(Dispatchers.Default).launch {
delay(3000)
messageTest()
}
}
The idea is I want test function to not be called from other thread until it is finished, whatever the action is in it.
launch is a function that starts a coroutine asynchronously and immediately returns, so this is behaving as it should. If you want to synchronize coroutines, you should use Mutex.
I'll leave it alone in the example before, but IMO, it's a code smell to create a CoroutineScope if you're not going to manage its lifecycle.
private val testMutex = Mutex()
fun test(messageTest: () -> Unit) {
CoroutineScope(Dispatchers.Default).launch {
testMutex.withLock {
delay(3000)
messageTest()
}
}
}

Kotlin Coroutine Flow: Limit the number of collector

Is there a way to limit the number of collector in a function that returns a Flow using flow builder?
I have this public method in a ViewModel
fun fetchAssets(limit: String) {
viewModelScope.launch {
withContext(Dispatchers.IO){
getAssetsUseCase(AppConfigs.ASSET_PARAMS, limit).onEach {
when (it) {
is RequestStatus.Loading -> {
_assetState.tryEmit(AssetState.FetchLoading)
}
is RequestStatus.Success -> {
_assetState.tryEmit(AssetState.FetchSuccess(it.data.assetDataDomain))
}
is RequestStatus.Failed -> {
_assetState.tryEmit(AssetState.FetchFailed(it.message))
}
}
}.collect()
}
}
}
This method is called on ViewModel's init block, but can also be called manually on UI.
This flow emits value every 10 seconds.
Repository
override fun fetchAssets(
query: String,
limit: String
) = flow {
while (true) {
try {
interceptor.baseUrl = AppConfigs.ASSET_BASE_URL
emit(RequestStatus.Loading())
val domainModel = mapper.mapToDomainModel(service.getAssetItems(query, limit))
emit(RequestStatus.Success(domainModel))
} catch (e: HttpException) {
emit(RequestStatus.Failed(e))
} catch (e: IOException) {
emit(RequestStatus.Failed(e))
}
delay(10_000)
}
}
Unfortunately every time fetch() was invoke from UI, I noticed that it creates another collectors thus can ended up having tons of collector which is really bad and incorrect.
The idea is having a flow that emits value every 10 seconds but can also be invoke manually via UI for immediate data update without having multiple collectors.
You seem to misunderstand what does it mean to collect the flow or you misuse the collect operation. By collecting the flow we mean we observe it for changes. But you try to use collect() to introduce changes to the flow, which can't really work. It just starts another flow in the background.
You should collect the flow only once, so keep it inside init or wherever it is appropriate for your case. Then you need to update the logic of the flow to make it possible to trigger reloading on demand. There are many ways to do it and the solution will differ depending whether you need to reset the timer on manual update or not. For example, we can use the channel to notify the flow about the need to reload:
val reloadChannel = Channel<Unit>(Channel.CONFLATED)
fun fetchAssets(
query: String,
limit: String
) = flow {
while (true) {
try {
...
}
withTimeoutOrNull(10.seconds) { reloadChannel.receive() } // replace `delay()` with this
}
}
fun reload() {
reloadChannel.trySend(Unit)
}
Whenever you need to trigger the manual reload, do not start another flow or invoke another collect() operation, but instead just invoke reload(). Then the flow that is already being collected, will start reloading and will emit state changes.
This solution resets the timer on manual reload, which I believe is better for the user experience.
I ended up moving the timer on ViewModel as I can request on demand fetch while also not having multiple collectors that runs at the same time.
private var job: Job? = null
private val _assetState = defaultMutableSharedFlow<AssetState>()
fun getAssetState() = _assetState.asSharedFlow()
init {
job = viewModelScope.launch {
while(true) {
if (lifecycleState == LifeCycleState.ON_START || lifecycleState == LifeCycleState.ON_RESUME)
fetchAssets()
delay(10_000)
}
}
}
fun fetchAssets() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
getAssetsUseCase(
AppConfigs.ASSET_BASE_URL,
AppConfigs.ASSET_PARAMS,
AppConfigs.ASSET_SIZES[AppConfigs.ASSET_LIMIT_INDEX]
).onEach {
when(it){
is RequestStatus.Loading -> {
_assetState.tryEmit(AssetState.FetchLoading)
}
is RequestStatus.Success -> {
_assetState.tryEmit(AssetState.FetchSuccess(it.data.assetDataDomain))
}
is RequestStatus.Failed -> {
_assetState.tryEmit(AssetState.FetchFailed(it.message))
}
}
}.collect()
}
}
}
override fun onCleared() {
job?.cancel()
super.onCleared()
}
Please correct me if this one is a code smell.

Cancelling a callback inside a suspendCancellableCoroutine

I have wrapped a callback in suspendCancellableCoroutine to convert it to a suspend function:
suspend fun TextToSpeech.speakAndWait(text: String) : Boolean {
val uniqueUtteranceId = getUniqueUtteranceId(text)
speak(text, TextToSpeech.QUEUE_FLUSH, null, uniqueUtteranceId)
return suspendCancellableCoroutine { continuation ->
this.setOnUtteranceProgressListener(object : JeLisUtteranceProgressListener() {
override fun onDone(utteranceId: String?) {
if(utteranceId == uniqueUtteranceId) {
Timber.d("word is read, resuming with the next word")
continuation.resume(true)
}
}
})
}
}
I'm calling this function with the lifecycleScope coroutine scope of the fragment and I was assuming that it was cancelled when fragment is destroyed. However, LeakCanary reported that my fragment was leaking because of this listener and I verified with logs that the callback was called even after the coroutine is cancelled.
So it seems that wrapping with suspendCancellableCoroutine instead of suspendCoroutine does not suffice to cancel the callback. I guess I should actively check whether the job is active, but how? I tried coroutineContext.ensureActive() and checking coroutineContext.isActive inside the callback but IDE gives an error saying that "suspension functions can be called only within coroutine body" What else can I do to ensure that it doesn't resume if the job is cancelled?
LeakCanary reported that my fragment was leaking because of this listener and I verified with logs that the callback was called even after the coroutine is cancelled.
Yes, the underlying async API is unaware of Kotlin coroutines and you have to work with it to explicitly propagate cancellation. Kotlin provides the invokeOnCancellation callback specifically for this purpose:
return suspendCancellableCoroutine { continuation ->
this.setOnUtteranceProgressListener(object : JeLisUtteranceProgressListener() {
/* continuation.resume() */
})
continuation.invokeOnCancellation {
this.setOnUtteranceProgressListener(null)
}
}
If you want to remove your JeLisUtteranceProgressListener regardless of result (success, cancellation or other errors) you can instead use a classic try/finally block:
suspend fun TextToSpeech.speakAndWait(text: String) : Boolean {
val uniqueUtteranceId = getUniqueUtteranceId(text)
speak(text, TextToSpeech.QUEUE_FLUSH, null, uniqueUtteranceId)
return try {
suspendCancellableCoroutine { continuation ->
this.setOnUtteranceProgressListener(object : JeLisUtteranceProgressListener() {
override fun onDone(utteranceId: String?) {
if(utteranceId == uniqueUtteranceId) {
Timber.d("word is read, resuming with the next word")
continuation.resume(true)
}
}
})
} finally {
this.setOnUtteranceProgressListener(null)
}
}
In addition to the accepted answer, I recognized that continuation object has an isActive property as well. So alternatively we can check whether coroutine is still active inside the callback before resuming:
return suspendCancellableCoroutine { continuation ->
this.setOnUtteranceProgressListener(object : JeLisUtteranceProgressListener()
{
override fun onDone(utteranceId: String?) {
if(utteranceId == uniqueUtteranceId) {
if (continuation.isActive) {
continuation.resume(true)
}
}
}
})
continuation.invokeOnCancellation {
this.setOnUtteranceProgressListener(null)
}
}

Use RxJava to emit a Single<List<Item>> from firestore given multiple ids

I'm building an app that uses GeoFirestore to make location based queries of items stored in a firestore database. I have my function that gets a list of all the ids that meet the location criteria:
private fun loadIds(location: Location, distance: Double): Single<List<String>> {
val query = geoFirestore.queryAtLocation(GeoPoint(location.latitude, location.longitude), distance)
val ids = ArrayList<String>()
return Single.create<List<String>> {
query.addGeoQueryEventListener(object : GeoQueryEventListener {
override fun onKeyEntered(uid: String, p1: GeoPoint?) {
ids.add(uid)
}
override fun onGeoQueryReady() {
it.onSuccess(ids)
}
override fun onKeyMoved(p0: String?, p1: GeoPoint?) {
}
override fun onKeyExited(p0: String?) {
}
override fun onGeoQueryError(p0: Exception?) {
}
})
}
}
but now I'm having problems trying to combine the results of items from each id into a Single<List<Item>> as a return value.
This is what I was doing before:
fun loadItems(ids: List<String>): Observable<Item> {
return Observable.create<Item> { emitter ->
for (id in ids) {
val reference = firestore.collection("items").document(id)
reference.get().addOnSuccessListener {
emitter.onNext(it.toObject(Item::class.java)!!)
if (ids.indexOf(id) == ids.size - 1) {
emitter.onComplete()
}
}
}
}
}
where ids was the result of loadIds(). This worked fine, but in my activity where I called this I had to append each item in the activity as it came through and then listen for onComplete, which would sometimes fire before all items were even loaded.
I'm trying to improve my code and further separate the database logic from my activity, so I want to be able to return a Single<List<Item>> so once I get that in my activity I can just take it and run. I've been trying to figure it out on my own, but I'm pretty new to RxJava and don't quite understand it too well. Here is my latest attempt:
fun loadItems(filter: Filter): Single<List<Item>> {
return Single.create<List<Item>> { emitter ->
val items= mutableListOf<Item>()
loadIds(filter.currentLocation, filter.distance).map {
for (uid in it) {
getItem(uid).map {item ->
items.add(item)
}.subscribe()
}
}.subscribe { _ ->
emitter.onSuccess(items)
}
}
}
private fun getItem(uid: String): Single<Item> {
return Single.create<Item> { emitter ->
firestore.collection("items").document(uid).get().addOnSuccessListener {
it.toObject(Item::class.java)?.let { item ->
emitter.onSuccess(item)
} ?: run {
emitter.onError(Throwable("Error finding item"))
}
}.addOnFailureListener {
emitter.onError(it)
}
}
}
but obviously onSuccess is called almost immediately so I'm not getting any results.
The getItem function looks fine, the problem lies in with the loadItems function.
You wrapped the Firebase callback mechanism nicely with Single in getItem function, but it is not necessary in the loadItems function. It's best to keep one chain in the function for readability reasons (IMO). That means, whenever you can, don't wrap the existing reactive object (Observable, Single, Flowable) into it's subscribe, but use flatMap (or any of it's versions).
fun loadItems(ids: List<String>): Single<List<Item>> {
return Observable.just(ids) // -> Observable<List<String>>
.flatMapIterable { it } // -> Observable<String>
.flatMapSingle { getItem(it) } // -> Observable<Item>
// Create a single from the observable
.collect<MutableList<Item>>(
Callable { ArrayList<Item>() },
BiConsumer { list, elem -> list.add(elem) }
) // -> Single<MutableList<Item>>
.map { it } // -> Single<List<Item>>
}
Hope it helps.

Refreshing MediaBrowserService subcription content

TL;DR: I have successfully created and coupled (via a subscription) an activity to a media browser service. This media browser service can continue running and play music in the background. I'd like to be able to refresh the content at some stage, either when the app comes to the foreground again or during a SwipeRefreshLayout event.
I have the following functionality I'd like to implement:
Start a MediaBrowserServiceCompat service.
From an activity, connect to and subscribe to the media browser service.
Allow the service to continue running and playing music while the app is closed.
At a later stage, or on a SwipeRefreshLayout event, reconnect and subscribe to the service to get fresh content.
The issue I am receiving is that within a MediaBrowserService (after a subscription has been created) you can only call sendResult() once from the onLoadChildren() method, so the next time you try to subscribe to the media browser service using the same root, you get the following exception when sendResult() is called for the second time:
E/UncaughtException: java.lang.IllegalStateException: sendResult() called when either sendResult() or sendError() had already been called for: MEDIA_ID_ROOT
at android.support.v4.media.MediaBrowserServiceCompat$Result.sendResult(MediaBrowserServiceCompat.java:602)
at com.roostermornings.android.service.MediaService.loadChildrenImpl(MediaService.kt:422)
at com.roostermornings.android.service.MediaService.access$loadChildrenImpl(MediaService.kt:50)
at com.roostermornings.android.service.MediaService$onLoadChildren$1$onSyncFinished$playerEventListener$1.onPlayerStateChanged(MediaService.kt:376)
at com.google.android.exoplayer2.ExoPlayerImpl.handleEvent(ExoPlayerImpl.java:422)
at com.google.android.exoplayer2.ExoPlayerImpl$1.handleMessage(ExoPlayerImpl.java:103)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:150)
at android.app.ActivityThread.main(ActivityThread.java:5665)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:822)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:712)
I call the following methods to connect to and disconnect from the media browser (again, everything runs smoothly on first connection, but on the second connection I'm not sure how to refresh the content via a subscription):
override fun onStart() {
super.onStart()
mMediaBrowser = MediaBrowserCompat(this, ComponentName(this, MediaService::class.java), connectionCallback, null)
if (!mMediaBrowser.isConnected)
mMediaBrowser.connect()
}
override fun onPause() {
super.onPause()
//Unsubscribe and unregister MediaControllerCompat callbacks
MediaControllerCompat.getMediaController(this#DiscoverFragmentActivity)?.unregisterCallback(mediaControllerCallback)
if (mMediaBrowser.isConnected) {
mMediaBrowser.unsubscribe(mMediaBrowser.root, subscriptionCallback)
mMediaBrowser.disconnect()
}
}
I unsubscribe and disconnect in onPause() instead of onDestroy() so that the subscription is recreated even if the activity is kept on the back-stack.
Actual method used for swipe refresh, in activity and service respectively:
Activity
if (mMediaBrowser.isConnected)
mMediaController?.sendCommand(MediaService.Companion.CustomCommand.REFRESH.toString(), null, null)
Service
inner class MediaPlaybackPreparer : MediaSessionConnector.PlaybackPreparer {
...
override fun onCommand(command: String?, extras: Bundle?, cb: ResultReceiver?) {
when(command) {
// Refresh media browser content and send result to subscribers
CustomCommand.REFRESH.toString() -> {
notifyChildrenChanged(MEDIA_ID_ROOT)
}
}
}}
Other research:
I have referred to the Google Samples code on Github, as well as...
https://github.com/googlesamples/android-MediaBrowserService
https://github.com/moondroid/UniversalMusicPlayer
Neither of the above repos seem to handle the issue of refreshing content after the media browser service has been created and the activity has subscribed at least once - I'd like to avoid restarting the service so that the music can continue playing in the background.
Possible related issues:
MediaBrowser.subscribe doesn't work after I get back to activity 1 from activity 2 (6.0.1 Android) --no effect on current issue
Calling you music service implementations notifyChildrenChanged(String parentId) will trigger the onLoadChildren and inside there, you can send a different result with result.sendResult().
What I did was that I added a BroadcastReceiver to my music service and inside it, I just called the notifyChildrenChanged(String parentId). And inside my Activity, I sent a broadcast when I changed the music list.
Optional (not Recommended) Quick fix
MusicService ->
companion object {
var musicServiceInstance:MusicService?=null
}
override fun onCreate() {
super.onCreate()
musicServiceInstance=this
}
//api call
fun fetchSongs(params:Int){
serviceScope.launch {
firebaseMusicSource.fetchMediaData(params)
//Edit Data or Change Data
notifyChildrenChanged(MEDIA_ROOT_ID)
}
}
ViewModel ->
fun fetchSongs(){
MusicService.musicServiceInstance?.let{
it.fetchSongs(params)
}
}
Optional (Recommended)
MusicPlaybackPreparer
class MusicPlaybackPreparer (
private val firebaseMusicSource: FirebaseMusicSource,
private val serviceScope: CoroutineScope,
private val exoPlayer: SimpleExoPlayer,
private val playerPrepared: (MediaMetadataCompat?) -> Unit
) : MediaSessionConnector.PlaybackPreparer {
override fun onCommand(player: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?
): Boolean {
when(command){
//edit data or fetch more data from api
"Add Songs"->{
serviceScope.launch {
firebaseMusicSource.fetchMediaData()
}
}
}
return false
}
override fun getSupportedPrepareActions(): Long {
return PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID or
PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID
}
override fun onPrepare(playWhenReady: Boolean) = Unit
override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
firebaseMusicSource.whenReady {
val itemToPlay = firebaseMusicSource.songs.find { mediaId == it.description.mediaId }
playerPrepared(itemToPlay)
}
}
override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) = Unit
override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) = Unit
}
MusicServiceConnection
fun sendCommand(command: String, parameters: Bundle?) =
sendCommand(command, parameters) { _, _ -> }
private fun sendCommand(
command: String,
parameters: Bundle?,
resultCallback: ((Int, Bundle?) -> Unit)
) = if (mediaBrowser.isConnected) {
mediaController.sendCommand(command, parameters, object : ResultReceiver(Handler()) {
override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
resultCallback(resultCode, resultData)
}
})
true
} else {
false
}
ViewModel
fun fetchSongs(){
val args = Bundle()
args.putInt("nRecNo", 2)
musicServiceConnection.sendCommand("Add Songs", args )
}
MusicService ->
override fun onLoadChildren(
parentId: String,
result: Result<MutableList<MediaBrowserCompat.MediaItem>>
) {
when(parentId) {
MEDIA_ROOT_ID -> {
val resultsSent = firebaseMusicSource.whenReady { isInitialized ->
if(isInitialized) {
try {
result.sendResult(firebaseMusicSource.asMediaItems())
if(!isPlayerInitialized && firebaseMusicSource.songs.isNotEmpty()) {
preparePlayer(firebaseMusicSource.songs, firebaseMusicSource.songs[0], true)
isPlayerInitialized = true
}
}
catch (exception: Exception){
// not recommend to notify here , instead notify when you
// change existing list in MusicPlaybackPreparer onCommand()
notifyChildrenChanged(MEDIA_ROOT_ID)
}
} else {
result.sendResult(null)
}
}
if(!resultsSent) {
result.detach()
}
}
}
}
My issue was unrelated to the MediaBrowserServiceCompat class. The issue was coming about because I was calling result.detach() in order to implement some asynchronous data fetching, and the listener I was using had both the parentId and result variables from the onLoadChildren method passed in and assigned final val rather than var.
I still don't fully understand why this occurs, whether it's an underlying result of using a Player.EventListener within another asynchronous network call listener, but the solution was to create and assign a variable (and perhaps someone else can explain this phenomenon):
// Create variable
var currentResult: Result<List<MediaBrowserCompat.MediaItem>>? = null
override fun onLoadChildren(parentId: String, result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>) {
// Use result.detach to allow calling result.sendResult from another thread
result.detach()
// Assign returned result to temporary variable
currentResult = result
currentParentId = parentId
// Create listener for network call
ChannelManager.onFlagChannelManagerDataListener = object : ChannelManager.Companion.OnFlagChannelManagerDataListener {
override fun onSyncFinished() {
// Create a listener to determine when player is prepared
val playerEventListener = object : Player.EventListener {
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
when(playbackState) {
Player.STATE_READY -> {
if(mPlayerPreparing) {
// Prepare content to send to subscribed content
loadChildrenImpl(currentParentId, currentResult as MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>)
mPlayerPreparing = false
}
}
...
}
}
}
}

Categories

Resources