使用WorkManager下载文件

6

我正在使用WorkManager和MVVM尝试下载一个具有暂停和恢复功能的文件。在这里,我希望使用WorkManager实现暂停/恢复和下载百分比进度更新。因此,我在这里分享我的类。

以下是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

使用绑定方式发起下载请求并通知UI。使用WorkManager实例取消下载(工作)。

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 类:DownloadWorker.kt

该类用于启动下载工作。由于服务器支持范围请求,每个 OneTimeWorkRequest 都使用此工作范围来下载文件。

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
    }
}

此示例中使用的实用程序

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()
            }
        }

    }
}

对于使用数据绑定的UI部分

<?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>

我尝试过以下方法:开始下载-暂停下载(这里的暂停是指取消当前的OneTimeWorkRequest),然后通过创建新的OneTimeWorkRequest并在请求头中使用已下载文件长度+1作为字节范围来恢复/开始下载。但是在我的情况下,取消操作无效。不知道我做错了什么。 我的问题是: 1. 我们可以使用WorkManager进行下载的暂停和恢复吗? 2. 我们可以使用WorkManager更新进度条吗? 3. 参考:链接,这是更新进度条的唯一方法吗?

2个回答

0

你可以这样做,重写 DownloadWorker 类的 onStopped() 方法,然后执行 connection.disconnect()

就像这样:

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()
    }
}

0
尝试使用对象锁,像这样:

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

任务将一直挂起,直到用户点击恢复按钮:

 synchronized (lock) {
                    lock.notify(); // Will wake up lock.wait()
                }

对于这种情况,你可能想要查看这个答案:https://dev59.com/RFUL5IYBdhLWcg3waHVw - mochadwi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接