Download file using WorkManager
Asked Answered
P

2

6

I am working/trying with download a file with pause and resume functionality using WorkManager with MVVM.

Here I am seeking pause/resume and download percentage progress update with use of WorkManager. So I am sharing my classes here.

MainActivity.kt

class MainActivity : AppCompatActivity() {

    lateinit var downloadViewModel : DownloaderViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //setContentView(R.layout.activity_main)
        val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this,R.layout.activity_main)
        downloadViewModel = ViewModelProviders.of(this).get(DownloaderViewModel::class.java)
        binding.viewmodel = downloadViewModel
        download_button.setOnClickListener({
                startDownload()
        })

        cancel_button.setOnClickListener({
            downloadViewModel.cancelDownloadWork(Constants.TAG_OUTPUT)
            WorkUtils.deleteFile(WorkUtils.getIsbn(Constants.TAG_OUTPUT))
        })

        downloadViewModel.mSavedWorkStatus.observe(this, Observer {

            it?.let {

            }
            if(it?.size!! > 0){
                val workStatus = it.first()
                val workState =  workStatus?.state
                downloadViewModel.updateDownloadWorkState(workState.toString())
                WorkUtils.makeStatusNotification(workState.toString(),this.applicationContext)
            }
        })

        pause_button.setOnClickListener({
            downloadViewModel.cancelDownloadWork(Constants.TAG_OUTPUT)
            val pausedAt = WorkUtils.getFileSize(WorkUtils.getIsbn(DOWNLOAD_URL))
            Log.d("paused at","paused at $pausedAt")
        })

        resume_button.setOnClickListener({
            val resumeFrom = WorkUtils.getFileSize(WorkUtils.getIsbn(DOWNLOAD_URL))
            Log.d("paused at","resumed at ${resumeFrom+1}")
            startDownload()
        })

    }

    fun startDownload() {
        downloadViewModel.downloadUrl =  DOWNLOAD_URL
        downloadViewModel.makeDownloadRequest()
    }

    fun isFileExists(url : String) : Boolean {

        return File(WorkUtils.getAbsolutePath(WorkUtils.getIsbn(url))).exists()


    }
}

Constants.kt

class Constants {
    companion object {

        val DOWNLOAD_URL = "download_url"
        // Notification Channel constants

        // Name of Notification Channel for verbose notifications of background work
        val VERBOSE_NOTIFICATION_CHANNEL_NAME: CharSequence = "Verbose WorkManager Notifications"
        var VERBOSE_NOTIFICATION_CHANNEL_DESCRIPTION = "Shows notifications whenever work starts"
        val NOTIFICATION_TITLE: CharSequence = "WorkRequest Starting"
        val CHANNEL_ID = "VERBOSE_NOTIFICATION"
        val NOTIFICATION_ID = 1

        // The name of the image manipulation work
        internal val IMAGE_MANIPULATION_WORK_NAME = "image_manipulation_work"

        // Other keys
        val OUTPUT_PATH = "blur_filter_outputs"
        val KEY_DOWNLOAD_URL = "KEY_DOWNLOAD_URL"
        internal val TAG_OUTPUT = "OUTPUT"

        val DELAY_TIME_MILLIS: Long = 3000

        // Ensures this class is never instantiated
        private fun Constants() {}
    }
}

DownloaderViewModel.kt

To make download request and notify the UI using binding. Cancelling the download(work) using WorkManager instance.

class DownloaderViewModel : ObservableViewModel() {
    var  mWorkManager : WorkManager = WorkManager.getInstance()

    var   mSavedWorkStatus: LiveData<List<WorkStatus>>
    @Bindable
    var currentDownloadState : MutableLiveData<String> = MutableLiveData()
    init {
        mSavedWorkStatus = mWorkManager.getStatusesByTagLiveData(Constants.TAG_OUTPUT)
    }

    lateinit var  downloadUrl: String
    /**
     * Creates the input data bundle which includes the Uri to operate on
     * @return Data which contains the Image Uri as a String
     */
    private fun createInputData(): Data {
        val builder = Data.Builder()
        if (downloadUrl != null) {
            builder.putString(KEY_DOWNLOAD_URL, downloadUrl)
        }
        return builder.build()
    }

    fun makeDownloadRequest() {

        // Create charging constraint
        val constraints = Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED)
                .setRequiresCharging(true)
                .build()

        // Add WorkRequest to download the epub to the filesystem
        val save = OneTimeWorkRequest.Builder(DownloadWorker::class.java)
                .setConstraints(constraints)
                .setInputData(createInputData())
                .addTag(TAG_OUTPUT)
                .build()
        mWorkManager.enqueue(save)
    }

    fun cancelDownloadWork(tag : String) {
        mWorkManager.cancelAllWorkByTag(tag)
    }

    fun getDownloadWorkState()  {
        getOutputStatus()
    }

    fun updateDownloadWorkState(state: String){
        currentDownloadState.value = state
        notifyPropertyChanged(BR.currentDownloadState)
    }


    internal fun getOutputStatus(): LiveData<List<WorkStatus>> {
        return mWorkManager.getStatusesByTagLiveData(TAG_OUTPUT)
    }
}

Worker class : DownloadWorker.kt This class used to start the download work . As the server supports range request, every OneTimeWorkRequest started with this work range to download the file.

class DownloadWorker(context : Context,workerParameters: WorkerParameters) : Worker(context,workerParameters) {
    override fun doWork(): Result {

        val applicationContext = applicationContext

        // Makes a notification when the work starts and slows down the work so that it's easier to
        // see each WorkRequest start, even on emulated devices

        lateinit var result : Result
        val inputData : String? = inputData.getString(KEY_DOWNLOAD_URL)
        inputData?.let {
             result =  downloadFile(inputData)

        }
        return result
    }

    fun downloadFile(url : String) : Result {
        var input: InputStream? = null
        var output: OutputStream? = null
        var connection: HttpURLConnection? = null
        lateinit var result : Result

        val isbnFileName = WorkUtils.getIsbn(url);
        val filePathToWrite = WorkUtils.getAbsolutePath(isbnFileName)

        //val path = "${Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).path}${File.separator}${"file.zip"}"
        Log.d("Download file path :","Download file path :"+filePathToWrite)
        val targetFile = File(filePathToWrite)
        if(!targetFile?.exists()){
            targetFile.createNewFile()
        }

        try {
            val urlConnection = URL(url)
            connection = urlConnection.openConnection() as HttpURLConnection
            val range = WorkUtils.getFileSize(isbnFileName)+1
            connection.setRequestProperty("Range","bytes=${range}-")
            Log.d("Range","Request header Range is $range")
            connection.connect()

            // expect HTTP 200 OK, so we don't mistakenly save error report
            // instead of the file
            if (connection.responseCode != HttpURLConnection.HTTP_OK) {
                Log.e("","Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
                result =  Result.FAILURE
            }

            // download the file

            input = connection.inputStream

            input?.let {
                output = FileOutputStream(targetFile, false)

                val data = ByteArray(1024 * 4)
                var count: Int

                do {
                    count = input.read(data)
                    if (count != 1) {
                        output!!.write(data, 0, count)

                    } else {
                        break
                    }

                } while (count != -1)
            }

            result = Result.SUCCESS

        } catch (e: Exception) {
            Log.e("Exception occured:",e.message)
            result =  Result.FAILURE
        } finally {
            try {
                output?.close()
                input?.close()
                connection?.disconnect()
            } catch (e: IOException) {
                Log.e("Exception occured:",e.message)
            }
        }
        return result
    }
}

Utilities used in this sample Utils.kt

class WorkUtils {

    companion object {
        private val TAG = WorkUtils::class.java.getSimpleName()

    fun makeStatusNotification(message: String, context: Context) {

        // Make a channel if necessary
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            // Create the NotificationChannel, but only on API 26+ because
            // the NotificationChannel class is new and not in the support library
            val name = Constants.VERBOSE_NOTIFICATION_CHANNEL_NAME
            val description = Constants.VERBOSE_NOTIFICATION_CHANNEL_DESCRIPTION
            val importance = NotificationManager.IMPORTANCE_HIGH
            val channel = NotificationChannel(Constants.CHANNEL_ID, name, importance)
            channel.description = description

            // Add the channel
            val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

            notificationManager?.createNotificationChannel(channel)
        }

        // Create the notification
        val builder = NotificationCompat.Builder(context, Constants.CHANNEL_ID)
                .setSmallIcon(R.mipmap.ic_launcher)
                .setContentTitle(Constants.NOTIFICATION_TITLE)
                .setContentText(message)
                .setPriority(NotificationCompat.PRIORITY_HIGH)
                .setVibrate(LongArray(0))

        // Show the notification
        NotificationManagerCompat.from(context).notify(Constants.NOTIFICATION_ID, builder.build())
    }

        /**
         * Method for sleeping for a fixed about of time to emulate slower work
         */
        fun sleep() {
            try {
                Thread.sleep(Constants.DELAY_TIME_MILLIS, 0)
            } catch (e: InterruptedException) {
                Log.d(TAG, e.message)
            }

        }

        fun getIsbn(url: String) : String {
            return url.substring(url.lastIndexOf("/")+1)
        }

        fun getAbsolutePath(isbnFileName: String): String {
            return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toString()+"/"+isbnFileName+".zip";
        }

        fun getFileSize(isbn:String) : Long {

            val file: File = File(getAbsolutePath(isbn))
            if(!file?.exists())
            {
                return 0
            }else{
                return file?.length()
            }
        }

        fun deleteFile(isbn: String) {
            val file = File(getAbsolutePath(isbn))
            if(file.exists() && file.isFile) {
                file.delete()
            }
        }

    }
}

For the UI part with databinding

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    >
    <data>

        <import type="com.pkonf.downloadebook.viewmodels.DownloaderViewModel" />

        <variable
            name="viewmodel"
            type="com.pkonf.downloadebook.viewmodels.DownloaderViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

        <TextView
            android:id="@+id/download_status"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="8dp"
            android:text="@{viewmodel.currentDownloadState}"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <LinearLayout
            android:id="@+id/linearLayout"
            android:layout_width="368dp"
            android:layout_height="50dp"
            android:layout_marginStart="8dp"
            android:layout_marginEnd="8dp"
            android:layout_marginBottom="8dp"
            android:orientation="horizontal"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent">

            <Button
                android:id="@+id/pause_button"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="pause_button"
                tools:text="Pause" />

            <Button
                android:id="@+id/resume_button"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="resume_button"
                tools:text="Resume" />
        </LinearLayout>

        <LinearLayout
            android:id="@+id/linearLayout2"
            android:layout_width="377dp"
            android:layout_height="157dp"
            android:layout_marginStart="8dp"
            android:layout_marginTop="28dp"
            android:layout_marginEnd="8dp"
            android:layout_marginBottom="8dp"
            android:gravity="center_horizontal|center_vertical"
            android:orientation="vertical"
            app:layout_constraintBottom_toTopOf="@+id/linearLayout"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="1.0"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/download_status"
            app:layout_constraintVertical_bias="0.95">

            <Button
                android:id="@+id/download_button"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginStart="8dp"
                android:layout_marginTop="8dp"
                android:layout_marginEnd="-1dp"
                android:text="Download"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="@id/linearLayout2"
                app:layout_constraintTop_toTopOf="@+id/linearLayout2" />

            <Button
                android:id="@+id/cancel_button"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginStart="8dp"
                android:layout_marginTop="8dp"
                android:layout_marginEnd="8dp"
                android:layout_marginBottom="8dp"
                android:text="Cancel"
                app:layout_constraintBottom_toBottomOf="@+id/linearLayout2"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/download_button" />


        </LinearLayout>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

I have tried with - starting the download - pausing the download (here pausing means cancelling the current OneTimeWorkRequest) and resuming by creating new OneTimeWorkRequest and start/resume with downloaded file length+1 as bytes range in request header. But the cancel is not working in my case. Don't know what I am doing wrong. And my questions are, 1. Can we do pause and resume download using WorkManager? 2. Can we update progress bar using WorkManager? 3. Ref : link is this is the only way to update the progress bar?

Pyrrho answered 9/11, 2018 at 9:20 Comment(0)
R
0

Try an object lock like this:

private Object lock = new Object();    
new Thread() {
        @Override
        public void run() {
            synchronized (lock) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            callApi();
        }
    }.start();

The task will hang forever until user click resume button:

 synchronized (lock) {
                    lock.notify(); // Will wake up lock.wait()
                }
Ranique answered 23/11, 2018 at 4:49 Comment(1)
for this case, you might wanna to check this answer instead: #50403513Meson
R
0

You can do this, override the onStopped() method of DownloadWorker class and then execute, connection.disconnect()

Something like this:

class DownloadWorker(context : Context,workerParameters: WorkerParameters) : Worker(context,workerParameters) {
    private val connection: HttpUrlConnection? = null

    override fun doWork(): Result {
        connection = ...

        ...
    }

    override fun onStopped() {
        super.onStopped()
        connection.disconnect()
    }
}
Remediable answered 2/6, 2020 at 5:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.