5

I created the following application to illustrate some doubts. My Example on the Github

In this example, I copy a file to another package.

My doubts are as follows:

  1. Performing the tasks in parallel, is it possible to return the values that were completed before the cancellation?

  2. Why in contentResolver.openInputStream (uri) the message "Inappropriate blocking method call" appears, while I am working with IO context?

  3. While I am reading the file entry to copy to output, I always check the job status so that when this task is canceled, it is stopped immediately, the output file that was created is deleted and returns the cancellation exception, is that correct?

  4. Can I delimit the amount of tasks that are performed?

My onCreate:

private val listUri = mutableListOf<Uri>()
private val job = Job()

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    
    //get files from 1 to 40
    val packageName = "android.resource://${packageName}/raw/"
    for (i in 1..40) {
        listUri.add(Uri.parse("${packageName}file$i"))
    }
}

My button action:

  //Button action
   fun onClickStartTask(view: View) {
        var listNewPath = emptyList<String>()
        CoroutineScope(Main + job).launch {
            try {
                //shows something in the UI - progressBar
                withContext(IO) {
                    listNewPath = listUri.map { uri ->
                        async {
                            //path to file temp
                            val pathFileTemp =
                                "${getExternalFilesDir("Temp").toString()}/${uri.lastPathSegment}"
                            val file = File(pathFileTemp)
                            val inputStream = contentResolver.openInputStream(uri)
                            inputStream?.use { input ->
                                FileOutputStream(file).use { output ->
                                    val buffer = ByteArray(1024)
                                    var read: Int = input.read(buffer)
                                    while (read != -1) {
                                        if (isActive) {
                                            output.write(buffer, 0, read)
                                            read = input.read(buffer)
                                        } else {
                                            input.close()
                                            output.close()
                                            file.deleteRecursively()
                                            throw CancellationException()
                                        }
                                    }
                                }
                            }
                            //If completed then it returns the new path.
                            return@async pathFileTemp
                        }
                    }.awaitAll()
                }
            } finally {
                //shows list complete in the UI
            }
        }
    }

My button to cancel job:

fun onClickCancelTask(view: View) {
    if (job.isActive) {
        job.cancelChildren()
        println("Cancel children")
    }
}

This would be the button action to perform the task.

I thank all the help.

Murillo Comino
  • 221
  • 3
  • 11
  • `I copy a file to another package` ? Copying yo a package? What would that be? – blackapps Jun 20 '20 at 15:33
  • @blackapps Copy the file that is in one location and saved in another location. Keeping the original file. – Murillo Comino Jun 20 '20 at 15:44
  • From which location exactly? To which location exactly? – blackapps Jun 20 '20 at 15:55
  • @blackapps You can see it better [here](https://github.com/onimur/example-copy-file-coroutines/blob/master/app/src/main/java/br/com/comino/examplecoroutinestask/MainActivity.kt), from raw folder to externalStorageDir. I only added to the question the part of the code where I have these doubts. As I said the sample application is to illustrate my doubts. – Murillo Comino Jun 20 '20 at 16:02
  • You say externalStorageDir but in your code is externalFilesDir. Please give exact locations and dont think that we will follow links or look into a sample application. Do it all here. Post reproducable code here. To begin with there is nothing with raw. – blackapps Jun 20 '20 at 16:06
  • @blackapps done. In the raw folder I have 40 files named "fileX" where X goes from 1 to 40. – Murillo Comino Jun 20 '20 at 16:17
  • DId you resolve the `Inappropriate blocking method call`? – Bitwise DEVS Mar 12 '22 at 22:42

2 Answers2

1

Answering 1. and 4.:

In order to delimit parallel tasks and let them finish independently (getting some values, while cancelling the rest) you would need to use a Channel and preferably a Flow. Simplified example:

fun processListWithSomeWorkers(list: List<Whatever>, concurrency: Int): Flow<Result> = channelFlow {
   val workToDistribute = Channel<Whatever>()
   launch { for(item in list) workToDistribute.send(item) } // one coroutine distributes work...

    repeat(concurrency) { // launch a specified number of worker coroutines
      launch { 
         for (task in workToDistribute) { // which process tasks in a loop
            val atomicResult = process(task)
            send(atomicResult) // and send results downstream to a Flow
         }
      }
   }
}

And then you can process results, one by one, as they are getting produced waiting for the whole flow to finish or e.g. just take some of them, when desired: resultFlow.take(20).onEach { ... }.collectIn(someScope) Because it is a Flow, it will start working only when somebody starts collecting (it is cold), which is usually a good thing.

Whole thing may be made a little shorter, as you will discover some more specific and experimental functions (as produce). It can be generalized as a Flow operator like this:

fun <T, R> Flow<T>.concurrentMap(concurrency: Int, transform: suspend (T) -> R): Flow<R> {
    require(concurrency > 1) { "No sense with concurrency < 2" }
    return channelFlow {
        val inputChannel = produceIn(this)
        repeat(concurrency) {
            launch {
                for (input in inputChannel) send(transform(input))
            }
        }
    }
}

and used: list.asFlow().concurrentMap(concurrency = 4) { <your mapping logic> }

The corotuines team are thinking about adding a family of parallel operators to the Flow streams, but they are not there yet AFAIK.

0

I think this is a better way

fun onClickStartTask(view: View) {
    var listNewPath = emptyList<String>()
    val copiedFiles = mutableListOf<File>()
    CoroutineScope(Dispatchers.Main + job).launch {
        try {
            //shows something in the UI - progressBar
            withContext(Dispatchers.IO) {
                listNewPath = listUri.map { uri ->
                    async {
                        //path to file temp
                        val pathFileTemp =
                                "${getExternalFilesDir("Temp").toString()}/${uri.lastPathSegment}"
                        val file = File(pathFileTemp)
                        val inputStream = contentResolver.openInputStream(uri)
                        inputStream?.use { input ->
                            file.outputStream().use { output ->
                                copiedFiles.add(file)
                                input.copyTo(output, 1024)
                            }
                        }

                        //If completed then it returns the new path.
                        return@async pathFileTemp
                    }
                }.awaitAll()
            }
        } finally {
            //shows list complete in the UI
        }
    }
    job.invokeOnCompletion {
        it?.takeIf { it is CancellationException }?.let {
            GlobalScope.launch(Dispatchers.IO) {
                copiedFiles.forEach { file ->
                    file.delete()
                }
            }
        }
    }
}
  • Sorry, but it doesn't feel right to me. Files being copied at the time of cancellation are not deleted. Only those that have been finalized are excluded. – Murillo Comino Jun 22 '20 at 16:06
  • I updated the code and it included the file coping at the time of cancellation too – Omid Faraji Jun 23 '20 at 09:36