Introduced pause/resume functionality.

* Added pause/resume button beside the stop button to perform pause/resume action on the downloading zim file.
* Handling pause/resume via fetch library.
This commit is contained in:
MohitMali 2023-07-28 18:51:06 +05:30
parent 524c469d78
commit 0a2cff92b5
17 changed files with 146 additions and 128 deletions

View File

@ -113,20 +113,30 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions {
private val libraryAdapter: LibraryAdapter by lazy { private val libraryAdapter: LibraryAdapter by lazy {
LibraryAdapter( LibraryAdapter(
LibraryDelegate.BookDelegate(bookUtils, ::onBookItemClick, availableSpaceCalculator), LibraryDelegate.BookDelegate(bookUtils, ::onBookItemClick, availableSpaceCalculator),
LibraryDelegate.DownloadDelegate { LibraryDelegate.DownloadDelegate(
if (it.currentDownloadState == Status.FAILED) { {
if (isNotConnected) { if (it.currentDownloadState == Status.FAILED) {
noInternetSnackbar() if (isNotConnected) {
noInternetSnackbar()
} else {
downloader.retryDownload(it.downloadId)
}
} else { } else {
downloader.retryDownload(it.downloadId) dialogShower.show(
KiwixDialog.YesNoDialog.StopDownload,
{ downloader.cancelDownload(it.downloadId) }
)
}
},
{
context?.let { context ->
downloader.pauseResumeDownload(
it.downloadId,
it.downloadState.toReadableState(context) == "Paused"
)
} }
} else {
dialogShower.show(
KiwixDialog.YesNoDialog.StopDownload,
{ downloader.cancelDownload(it.downloadId) }
)
} }
}, ),
LibraryDelegate.DividerDelegate LibraryDelegate.DividerDelegate
) )
} }

View File

@ -51,14 +51,18 @@ sealed class LibraryDelegate<I : LibraryListItem, out VH : LibraryViewHolder<I>>
) )
} }
class DownloadDelegate(private val clickAction: (LibraryDownloadItem) -> Unit) : class DownloadDelegate(
private val clickAction: (LibraryDownloadItem) -> Unit,
private val pauseResumeClickAction: (LibraryDownloadItem) -> Unit
) :
LibraryDelegate<LibraryDownloadItem, DownloadViewHolder>() { LibraryDelegate<LibraryDownloadItem, DownloadViewHolder>() {
override val itemClass = LibraryDownloadItem::class.java override val itemClass = LibraryDownloadItem::class.java
override fun createViewHolder(parent: ViewGroup) = override fun createViewHolder(parent: ViewGroup) =
DownloadViewHolder( DownloadViewHolder(
parent.viewBinding(ItemDownloadBinding::inflate, false), parent.viewBinding(ItemDownloadBinding::inflate, false),
clickAction clickAction,
pauseResumeClickAction
) )
} }

View File

@ -24,6 +24,7 @@ import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.core.base.adapter.BaseViewHolder import org.kiwix.kiwixmobile.core.base.adapter.BaseViewHolder
import org.kiwix.kiwixmobile.core.downloader.model.Base64String import org.kiwix.kiwixmobile.core.downloader.model.Base64String
import org.kiwix.kiwixmobile.core.extensions.setBitmap import org.kiwix.kiwixmobile.core.extensions.setBitmap
import org.kiwix.kiwixmobile.core.extensions.setImageDrawableCompat
import org.kiwix.kiwixmobile.core.extensions.setTextAndVisibility import org.kiwix.kiwixmobile.core.extensions.setTextAndVisibility
import org.kiwix.kiwixmobile.core.extensions.toast import org.kiwix.kiwixmobile.core.extensions.toast
import org.kiwix.kiwixmobile.core.utils.BookUtils import org.kiwix.kiwixmobile.core.utils.BookUtils
@ -90,7 +91,8 @@ sealed class LibraryViewHolder<in T : LibraryListItem>(containerView: View) :
class DownloadViewHolder( class DownloadViewHolder(
private val itemDownloadBinding: ItemDownloadBinding, private val itemDownloadBinding: ItemDownloadBinding,
private val clickAction: (LibraryDownloadItem) -> Unit private val clickAction: (LibraryDownloadItem) -> Unit,
private val pauseResumeClickAction: (LibraryDownloadItem) -> Unit
) : ) :
LibraryViewHolder<LibraryDownloadItem>(itemDownloadBinding.root) { LibraryViewHolder<LibraryDownloadItem>(itemDownloadBinding.root) {
@ -100,8 +102,18 @@ sealed class LibraryViewHolder<in T : LibraryListItem>(containerView: View) :
itemDownloadBinding.libraryDownloadDescription.text = item.description itemDownloadBinding.libraryDownloadDescription.text = item.description
itemDownloadBinding.downloadProgress.progress = item.progress itemDownloadBinding.downloadProgress.progress = item.progress
itemDownloadBinding.stop.setOnClickListener { clickAction.invoke(item) } itemDownloadBinding.stop.setOnClickListener { clickAction.invoke(item) }
itemDownloadBinding.pauseResume.setOnClickListener {
pauseResumeClickAction.invoke(item)
}
itemDownloadBinding.downloadState.text = itemDownloadBinding.downloadState.text =
item.downloadState.toReadableState(containerView.context) item.downloadState.toReadableState(containerView.context).also {
val pauseResumeIconId = if (it == "Paused") {
R.drawable.ic_play_24dp
} else {
R.drawable.ic_pause_24dp
}
itemDownloadBinding.pauseResume.setImageDrawableCompat(pauseResumeIconId)
}
if (item.currentDownloadState == Status.FAILED) { if (item.currentDownloadState == Status.FAILED) {
clickAction.invoke(item) clickAction.invoke(item)
} }

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M8,5v14l11,-7z" />
</vector>

View File

@ -0,0 +1,27 @@
<!--
~ Kiwix Android
~ Copyright (c) 2023 Kiwix <android.kiwix.org>
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
~
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z" />
</vector>

View File

@ -0,0 +1,27 @@
<!--
~ Kiwix Android
~ Copyright (c) 2023 Kiwix <android.kiwix.org>
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
~
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M8,5v14l11,-7z" />
</vector>

View File

@ -87,6 +87,18 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="horizontal"> android:orientation="horizontal">
<ImageView
android:id="@+id/pauseResume"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="@dimen/stop_horizontal_margin"
android:layout_marginEnd="@dimen/stop_horizontal_margin"
android:layout_weight="0.5"
android:minWidth="@dimen/stop_min_width"
android:minHeight="@dimen/stop_min_height"
android:src="@drawable/ic_pause_24dp" />
<ImageView <ImageView
android:id="@+id/stop" android:id="@+id/stop"
android:layout_width="0dp" android:layout_width="0dp"

View File

@ -31,14 +31,12 @@ import org.kiwix.kiwixmobile.core.downloader.DownloadRequester
import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel
import org.kiwix.kiwixmobile.core.downloader.model.DownloadRequest import org.kiwix.kiwixmobile.core.downloader.model.DownloadRequest
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk
import javax.inject.Inject import javax.inject.Inject
class FetchDownloadDao @Inject constructor( class FetchDownloadDao @Inject constructor(
private val box: Box<FetchDownloadEntity>, private val box: Box<FetchDownloadEntity>,
private val newBookDao: NewBookDao, private val newBookDao: NewBookDao
private val sharedPreferenceUtil: SharedPreferenceUtil
) { ) {
fun downloads(): Flowable<List<DownloadModel>> = fun downloads(): Flowable<List<DownloadModel>> =
@ -54,8 +52,6 @@ class FetchDownloadDao @Inject constructor(
box.store.callInTx { box.store.callInTx {
box.remove(it) box.remove(it)
newBookDao.insert(it.map(::BookOnDisk)) newBookDao.insert(it.map(::BookOnDisk))
// remove the canceled id from shared preference if exist
it.map { sharedPreferenceUtil.removeCanceledDownload(it.downloadId) }
} }
} }
} }
@ -79,10 +75,7 @@ class FetchDownloadDao @Inject constructor(
box.put(FetchDownloadEntity(downloadId, book)) box.put(FetchDownloadEntity(downloadId, book))
} }
fun delete(download: Download, isDownloadCanceled: Boolean) { fun delete(download: Download) {
if (isDownloadCanceled) {
sharedPreferenceUtil.addCanceledDownloadIfNotExist(download)
}
box.query { box.query {
equal(FetchDownloadEntity_.downloadId, download.id) equal(FetchDownloadEntity_.downloadId, download.id)
}.remove() }.remove()

View File

@ -31,7 +31,6 @@ import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao
import org.kiwix.kiwixmobile.core.dao.NewNoteDao import org.kiwix.kiwixmobile.core.dao.NewNoteDao
import org.kiwix.kiwixmobile.core.dao.NewRecentSearchDao import org.kiwix.kiwixmobile.core.dao.NewRecentSearchDao
import org.kiwix.kiwixmobile.core.dao.entities.MyObjectBox import org.kiwix.kiwixmobile.core.dao.entities.MyObjectBox
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@ -71,8 +70,7 @@ open class DatabaseModule {
@Provides @Singleton fun providesFetchDownloadDao( @Provides @Singleton fun providesFetchDownloadDao(
boxStore: BoxStore, boxStore: BoxStore,
newBookDao: NewBookDao, newBookDao: NewBookDao
sharedPreferenceUtil: SharedPreferenceUtil
): FetchDownloadDao = ): FetchDownloadDao =
FetchDownloadDao(boxStore.boxFor(), newBookDao, sharedPreferenceUtil) FetchDownloadDao(boxStore.boxFor(), newBookDao)
} }

View File

@ -23,4 +23,5 @@ interface DownloadRequester {
fun enqueue(downloadRequest: DownloadRequest): Long fun enqueue(downloadRequest: DownloadRequest): Long
fun cancel(downloadId: Long) fun cancel(downloadId: Long)
fun retryDownload(downloadId: Long) fun retryDownload(downloadId: Long)
fun pauseResumeDownload(downloadId: Long, isPause: Boolean)
} }

View File

@ -23,4 +23,5 @@ interface Downloader {
fun download(book: LibraryNetworkEntity.Book) fun download(book: LibraryNetworkEntity.Book)
fun cancelDownload(downloadId: Long) fun cancelDownload(downloadId: Long)
fun retryDownload(downloadId: Long) fun retryDownload(downloadId: Long)
fun pauseResumeDownload(downloadId: Long, isPause: Boolean)
} }

View File

@ -57,4 +57,8 @@ class DownloaderImpl @Inject constructor(
override fun retryDownload(downloadId: Long) { override fun retryDownload(downloadId: Long) {
downloadRequester.retryDownload(downloadId) downloadRequester.retryDownload(downloadId)
} }
override fun pauseResumeDownload(downloadId: Long, isPause: Boolean) {
downloadRequester.pauseResumeDownload(downloadId, isPause)
}
} }

View File

@ -37,7 +37,7 @@ class FetchDownloadMonitor @Inject constructor(fetch: Fetch, fetchDownloadDao: F
override fun onAdded(download: Download) {} override fun onAdded(download: Download) {}
override fun onCancelled(download: Download) { override fun onCancelled(download: Download) {
delete(download, true) delete(download)
} }
override fun onCompleted(download: Download) { override fun onCompleted(download: Download) {
@ -100,8 +100,8 @@ class FetchDownloadMonitor @Inject constructor(fetch: Fetch, fetchDownloadDao: F
updater.onNext { fetchDownloadDao.update(download) } updater.onNext { fetchDownloadDao.update(download) }
} }
private fun delete(download: Download, isDownloadCanceled: Boolean = false) { private fun delete(download: Download) {
updater.onNext { fetchDownloadDao.delete(download, isDownloadCanceled) } updater.onNext { fetchDownloadDao.delete(download) }
} }
} }

View File

@ -33,23 +33,25 @@ class FetchDownloadRequester @Inject constructor(
) : DownloadRequester { ) : DownloadRequester {
override fun enqueue(downloadRequest: DownloadRequest): Long { override fun enqueue(downloadRequest: DownloadRequest): Long {
val canceledDownloadId = sharedPreferenceUtil.getDownloadIdIfExist("${downloadRequest.uri}")
if (canceledDownloadId != 0L) {
fetch.retry(canceledDownloadId.toInt())
return canceledDownloadId
}
val request = downloadRequest.toFetchRequest(sharedPreferenceUtil) val request = downloadRequest.toFetchRequest(sharedPreferenceUtil)
fetch.enqueue(request) fetch.enqueue(request)
return request.id.toLong() return request.id.toLong()
} }
override fun cancel(downloadId: Long) { override fun cancel(downloadId: Long) {
fetch.cancel(downloadId.toInt()) fetch.delete(downloadId.toInt())
} }
override fun retryDownload(downloadId: Long) { override fun retryDownload(downloadId: Long) {
fetch.retry(downloadId.toInt()) fetch.retry(downloadId.toInt())
} }
override fun pauseResumeDownload(downloadId: Long, isPause: Boolean) {
if (isPause)
fetch.resume(downloadId.toInt())
else
fetch.pause(downloadId.toInt())
}
} }
private fun DownloadRequest.toFetchRequest(sharedPreferenceUtil: SharedPreferenceUtil) = private fun DownloadRequest.toFetchRequest(sharedPreferenceUtil: SharedPreferenceUtil) =

View File

@ -1,21 +0,0 @@
/*
* Kiwix Android
* Copyright (c) 2023 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.core.downloader.model
data class CanceledDownloadModel(val downloadId: Long, val url: String)

View File

@ -24,15 +24,11 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat.getExternalFilesDirs import androidx.core.content.ContextCompat.getExternalFilesDirs
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.tonyodev.fetch2.Download
import io.reactivex.Flowable import io.reactivex.Flowable
import io.reactivex.processors.PublishProcessor import io.reactivex.processors.PublishProcessor
import org.json.JSONArray
import org.json.JSONObject
import org.kiwix.kiwixmobile.core.NightModeConfig import org.kiwix.kiwixmobile.core.NightModeConfig
import org.kiwix.kiwixmobile.core.NightModeConfig.Mode.Companion.from import org.kiwix.kiwixmobile.core.NightModeConfig.Mode.Companion.from
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.downloader.model.CanceledDownloadModel
import org.kiwix.kiwixmobile.core.extensions.isFileExist import org.kiwix.kiwixmobile.core.extensions.isFileExist
import java.io.File import java.io.File
import java.util.Locale import java.util.Locale
@ -96,11 +92,9 @@ class SharedPreferenceUtil @Inject constructor(val context: Context) {
putPrefStorage(it) putPrefStorage(it)
putStoragePosition(0) putStoragePosition(0)
} }
!File(storage).isFileExist() -> getPublicDirectoryPath(defaultStorage()).also { !File(storage).isFileExist() -> getPublicDirectoryPath(defaultStorage()).also {
putStoragePosition(0) putStoragePosition(0)
} }
else -> storage else -> storage
} }
} }
@ -224,67 +218,6 @@ class SharedPreferenceUtil @Inject constructor(val context: Context) {
fun isPlayStoreBuildWithAndroid11OrAbove(): Boolean = fun isPlayStoreBuildWithAndroid11OrAbove(): Boolean =
isPlayStoreBuild && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R isPlayStoreBuild && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
fun addCanceledDownloadIfNotExist(download: Download) {
if (getDownloadIdIfExist(download.url) == 0L) {
val jsonArray = JSONArray()
for (item in getCanceledDownloadItems()) {
jsonArray.put(JSONObject().put(DOWNLOAD_ID, item.downloadId).put(DOWNLOAD_URL, item.url))
}
jsonArray.put(JSONObject().put(DOWNLOAD_ID, download.id).put(DOWNLOAD_URL, download.url))
saveCanceledDownloads(jsonArray)
}
}
private fun saveCanceledDownloads(canceledJsonArray: JSONArray) {
sharedPreferences.edit {
putString(DOWNLOAD_LIST, "$canceledJsonArray")
}
}
private fun getCanceledDownloadItems(): MutableList<CanceledDownloadModel> {
val savedCanceledDownloads = sharedPreferences.getString(DOWNLOAD_LIST, "")
val canceledDownloadList = mutableListOf<CanceledDownloadModel>()
if (!savedCanceledDownloads.isNullOrEmpty()) {
val jsonArray = JSONArray(savedCanceledDownloads)
(0 until jsonArray.length())
.asSequence()
.map(jsonArray::getJSONObject)
.mapTo(canceledDownloadList) {
CanceledDownloadModel(
it.getInt(DOWNLOAD_ID).toLong(),
it.getString(DOWNLOAD_URL)
)
}
}
return canceledDownloadList
}
fun removeCanceledDownload(downloadId: Long) {
val canceledList = getCanceledDownloadItems().apply {
asSequence()
.filter { it.downloadId == downloadId }
.forEach(this::remove)
}
// Save the updated list back to SharedPreferences
val jsonArray = JSONArray()
for (item in canceledList) {
jsonArray.put(JSONObject().put(DOWNLOAD_ID, item.downloadId).put(DOWNLOAD_URL, item.url))
}
saveCanceledDownloads(jsonArray)
}
fun getDownloadIdIfExist(url: String): Long {
return getCanceledDownloadItems()
.firstOrNull {
it.url.substringAfterLast("/") == url.substringAfterLast("/")
}
?.downloadId
?: 0L
}
companion object { companion object {
// Prefs // Prefs
const val PREF_LANG = "pref_language_chooser" const val PREF_LANG = "pref_language_chooser"
@ -311,8 +244,5 @@ class SharedPreferenceUtil @Inject constructor(val context: Context) {
private const val DEFAULT_ZOOM = 100 private const val DEFAULT_ZOOM = 100
const val PREF_MANAGE_EXTERNAL_FILES = "pref_manage_external_files" const val PREF_MANAGE_EXTERNAL_FILES = "pref_manage_external_files"
const val IS_PLAY_STORE_BUILD = "is_play_store_build" const val IS_PLAY_STORE_BUILD = "is_play_store_build"
const val DOWNLOAD_ID = "download_id"
const val DOWNLOAD_URL = "download_url"
const val DOWNLOAD_LIST = "download_list"
} }
} }