In the view, I have a class that has a Send Photo button.
Clicking the button calls up a function in another class in which:
there is a function to send a picture
there is a listener that tracks the progress of uploading the photo
After pressing the Send Photo button, I would like to wait until there is Success and go back to the previous screen.
I would like to call the onBackPressed() function (located in the view) right after the listener in the second class returns COMPLETED
Is it possible to somehow pass a function call as a parameter to another function? Are delegates on functions available? How to do it?
View:
if (Utils.isProfileImageUriInitialized()) {
spacesFileRepository.uploadExampleFile("${response.getString("profileId")}.jpeg")
onBackPressed()
}
Loader:
fun uploadExampleFile(fileName: String){
//Starts the upload of our file
val filePermission = CannedAccessControlList.PublicRead
var listener = transferUtility.upload(
spacename,
fileName,
File(URI(Utils.profileImageUri.toString())),
filePermission
)
//Listens to the file upload progress, or any errors that might occur
listener.setTransferListener(object : TransferListener {
override fun onError(id: Int, ex: Exception?) {
Log.e("S3 Upload", ex.toString())
}
override fun onProgressChanged(id: Int, bytesCurrent: Long, bytesTotal: Long) {
Log.i("S3 Upload", "Progress ${((bytesCurrent / bytesTotal) * 100)}")
}
override fun onStateChanged(id: Int, state: TransferState?) {
if (state == TransferState.COMPLETED) {
Log.i("S3 Upload", "Completed")
}
}
})
}
You can provide functions as a parameter to functions (High-Order-Function) https://kotlinlang.org/docs/lambdas.html.
But since you are using the Repository-Pattern, it would be a better option to use Coroutines-Flow https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/
So the uploadExampleFile-function returns a flow and emits a trigger when its done. From the outside you do a collect and execute onBackPressed().
Related
I want to send an text to firebase database when the user has finished typing (I use AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED). I don't want it to send text every time the user types a letter because that would result in A LOT of text data, however I don't want them to have to hit the enter button either.
Is there a way so I can detect when the user has finished typing and then send the text data?
Using Kotlin here! Thanks
I don't want to use button
#Inject lateinit var interactor: InteractorAccessibilityData
override fun onAccessibilityEvent(event: AccessibilityEvent) {
when (event.eventType) {
AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED -> {
val data = event.text.toString()
if (data != "[]") {
interactor.setDataKey("${getDateTime()} |(TEXT)| $data")
Log.i(TAG, "${getDateTime()} |(TEXT)| $data")
}
}
}
}
In InteractorAccessibilityData.kt->
override fun setDataKey(data: String) {
if (firebase.getUser()!=null) firebase.getDatabaseReference(KEYS).child(DATA).push().child(TEXT).setValue(data)
}
Using a Timer would be the best way.
Another way would be checking the length of the text.
say after an initial length of 10 sends data to firebase and after that every +2 or +3 length.
Introduce a variable outside this function which counts the timer.
// declare this outside onAccessibilityEvent function
private val timer = object : CountDownTimer(50000, 1000) {
override fun onTick(millisUntilFinished: Long) {
//use it if you want to show the timer
}
override fun onFinish() {
//send data to firebase, like
//if (firebase.getUser()!=null) firebase.getDatabaseReference(KEYS).child(DATA).push().child(TEXT).setValue(data)
}
}
//function to start or cancel the timer
private fun x(p: Int = 0) {
when (p) {
0 -> timer.start()
1 -> timer.cancel()
}
}
This method doesn't look efficient but this is how I would do it if I have no other options.
The Best you can do is send text after every word (by using spaces)
Like This
#Inject lateinit var interactor: InteractorAccessibilityData
override fun onAccessibilityEvent(event: AccessibilityEvent) {
when (event.eventType) {
AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED -> {
val data = event.text.toString()
if (data.contain( )) {
interactor.setDataKey("${getDateTime()} |(TEXT)| $data")
Log.i(TAG, "${getDateTime()} |(TEXT)| $data")
}
}
}
}
New at development of android apps. I am using kotlin and I am triying to retrieve a list from room database at my viewmodel and make a toast at my fragment when I push a button (codes below). If I push the button once, I get an empty string, but if I push twice, I get the list. How can I do to retrive the list with just one push? Probably I am missing something from coroutines.
Viewmodel code:
var Listado = ""
fun listaTotal(): String {
uiScope.launch {
getTodaListaCompra().forEach{
Log.i("Listado Compra",Listado )
Listado = Listado + " " + it
Log.i("Data",data.value)
Log.i("Pueba",it)
}
}
return Listado
}
Fragment call:
Toast.makeText(application, tabListaCompraViewModel.listaTotal(), Toast.LENGTH_SHORT)
.show()
Thanks in advance
I would suggest using LivaData to observe data:
class MyViewModel : ViewModel() {
val listado: LiveData<String> = MutableLiveData<String>()
fun listaTotal() = viewModelScope.launch {
var localListado = ""
getTodaListaCompra().forEach{
localListado = "$localListado $it"
}
(listado as MutableLiveData).value = localListado
}
// function marked as suspend to suspend a coroutine without blocking the Main Thread
private suspend fun getTodaListaCompra(): List<String> {
delay(1000) // simulate request delay
return listOf("one", "two", "three")
}
}
In your activity or fragment you can use next methods to instantiate ViewModel class and observe data:
private fun initViewModel() {
val viewModel = ViewModelProvider(
this,
viewModelFactory { MyViewModel() }
)[MyViewModel::class.java]
viewModel.listado.observe(this, androidx.lifecycle.Observer { data: String ->
Toast.makeText(application, data, Toast.LENGTH_SHORT).show()
})
viewModel.listaTotal()
}
inline fun <VM : ViewModel> viewModelFactory(crossinline f: () -> VM) = object : ViewModelProvider.Factory {
#Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(aClass: Class<T>):T = f() as T
}
Also you may need to import next libraries:
api 'androidx.lifecycle:lifecycle-viewmodel-ktx:$LIFECYCLE_VERSION'
You are defining an empty string at start. When listaTotal() gets called the first time, a coroutine gets launched in background to calculate the value of 'listado'. However the return of listaTotal is not waiting for the background coroutine to finish. That's why 'listado' is still empty.
Between your first and second click on the Button, the first coroutine finishes and 'listado' now is not empty anymore, so when you click the button the second time, the coroutine gets launched again but 'listado', again gets returned before that coroutine finishes, so it returns the result of the first button click.
Because you can only make a Toast on the main UI thread, you need to tell it to wait for the coroutine to finish to get the returned value. You can do it using runBlocking, like this:
fun listaTotal(): String = runBlocking {
getTodaListaCompra().forEach{
Log.i("Listado Compra",listado )
listado += " " + it
Log.i("Data",data.value)
Log.i("Pueba",it)
}
listado
}
Update:
To clarify, this approach blocks the main UI thread until the result is returned.
You should therefore consider using LiveData (see Sergeys answer) or Flows for fetching data. The purpose of this answer is mainly to explain the behavior of your code and coroutines in general.
What I'm trying to do is to use the Navigation controller inside a LiveData observer, so when the user clicks an item from the list it notifies the ViewModel, then the ViewModel updates the data and when this happens the fragment observes this and navigates to the next.
My problem is that for some reason the observer gets called twice and the second time I get an exception saying that the destination is unknown to this NavController.
My Fragment onCLick:
override fun onClick(view: View?) {
viewModel.productSelected.observe(viewLifecycleOwner, Observer<ProductModel> {
try {
this.navigationController.navigate(R.id.action_product_list_to_product_detail)
} catch (e: IllegalArgumentException) { }
})
val itemPosition = view?.let { recyclerView.getChildLayoutPosition(it) }
viewModel.onProductSelected(listWithHeaders[itemPosition!!].id)
}
And in my ViewModel:
fun onProductSelected(productId: String) {
productSelected.value = getProductById(productId)
}
It's called twice because first you subscribe and so you get a default value back, then you change a value in your productSelected LiveData and so your observer gets notified again.
Thereof, start observing after onProductSelected is called as below:
override fun onClick(view: View?) {
val itemPosition = view?.let { recyclerView.getChildLayoutPosition(it) }
viewModel.onProductSelected(listWithHeaders[itemPosition!!].id)
viewModel.productSelected.observe(viewLifecycleOwner, Observer<ProductModel> {
try {
this.navigationController.navigate(R.id.action_product_list_to_product_detail)
} catch (e: IllegalArgumentException) { }
})
}
Once again, beware that once you start observing your LiveData it will get notified each time productSelected is changed. Is it what you want? If not, then you should remove the observer once it's used once.
Catching the exception may work, but it can also make you miss several other issues. It might be better to check the current layout with the destination to validate if the user is already there. Another alternative that I prefer is to check with the previous destination, something like:
fun Fragment.currentDestination() = findNavController().currentDestination
fun Fragment.previousDestination() = findNavController().previousBackStackEntry?.destination
fun NavDestination.getDestinationIdFromAction(#IdRes actionId: Int) = getAction(actionId)?.destinationId
private fun Fragment.isAlreadyAtDestination(#IdRes actionId: Int): Boolean {
val previousDestinationId = previousDestination()?.getDestinationIdFromAction(actionId)
val currentDestinationId = currentDestination()?.id
return previousDestinationId == currentDestinationId
}
fun Fragment.navigate(directions: NavDirections) {
if (!isAlreadyAtDestination(directions.actionId)) {
findNavController().navigate(directions)
}
}
Basically, here we validate that we are not already at the destination. This can be done by comparing the previous action destination with the current destination. Let me know if the code helps!
I need to fetch some images from the gallery, process them (resize, compress...) and save them to a certain path. However, i need to queue the calls because older devices won't be able to process multiple images at the same time.
I am using Glide, this is the code used for processing one image:
fun processImage(context: Context, sourcePath: String, destinationPath: String, quality: Int, width: Int, height: Int, deleteOriginal: Boolean, callback: ((success: Boolean) -> Unit)) {
val sourceFile = File(sourcePath)
val destinationFile = File(destinationPath)
GlideApp.with(context)
.asBitmap()
.load(sourceFile)
.into(object : SimpleTarget<Bitmap>(width, height) {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
try {
destinationFile.writeBytes(ImageUtilities.imageToByteArray(resource, quality, Bitmap.CompressFormat.JPEG, false))
if (deleteOriginal) {
val originalFile = File(sourcePath)
originalFile.delete()
}
callback.invoke(true)
} catch (ex: Exception) {
callback.invoke(false)
}
}
})
}
Now i am queuing the calls manually by calling processNextImage which calls itself recursively until all the images are processed:
private fun processImages(sourceImagePaths: List<String>) {
processNextImage(sourceImagePaths, 0)
}
private fun processNextImage(sourceImagePaths: List<String>, index: Int) {
val imagePath = sourceImagePaths[index]
val destination = FileUtilities.generateImagePath()
processImage(this, imagePath, destination, 90, 1000, 1000, false) {
processedImagePaths.add(destination)
if (index + 1 < sourceImagePaths.count())
processImage(sourceImagePaths, index + 1)
else
success()
}
}
However I don't think this is the best way to do it and I tried to look into Kotlin coroutines but all I found were examples when the queued code is already blocking, which doesn't fit my case because Glide already handles the resizing asynchronously and returns the result in a callback onResourceReady
Any ideas for a clean way to do this?
As described in the official documentation, there is a simple pattern to follow if you want to turn a callback-based API into one based on suspendable functions. I'll paraphrase that description here.
Your key tool is the function from the standard library called suspendCoroutine(). Assume that you have someLongComputation function with a callback that receives a Result object:
fun someLongComputation(params: Params, callback: (Result) -> Unit)
You can convert it into a suspending function with the following straightforward code:
suspend fun someLongComputation(params: Params): Result =
suspendCoroutine { cont ->
someLongComputation(params) { cont.resume(it) }
}
Note how the type of the object passed to the original callback became simply the return value of the suspendable function.
With this you can see the magic of coroutines happen right in front of you: even though it looks exactly like a blocking call, it isn't. The coroutine will get suspended behind the scenes and resume when the return value is ready — and how it will resume is totally under your control.
I was able to solve the issue using suspendCoroutine as suggested in Marko's comment, here is my code:
private fun processImages(sourceImagePaths: List<String>) {
async(UI) {
sourceImagePaths.forEach { path ->
processNextImage(path)?.let {
processedImagePaths.add(it)
}
}
if (processedImagePaths.isEmpty()) finishWithFailure() else finishWithSuccess()
}
}
private suspend fun processNextImage(sourceImagePath: String): String? = suspendCoroutine { cont ->
val destination = FileUtilities.generateImagePath()
processImage(this, sourceImagePath, destination, 90, 1000, 1000, false) { success ->
if (success)
cont.resume(destination)
else
cont.resume(null)
}
}
The method processImages iterates over the list of paths, and calls processNextImage for each path. Since processNextImage contains a suspendCoroutine, it will block the thread until cont.resume is called, which guarantees that the next image will not be processed before the current one is done.
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
}
}
...
}
}
}
}