mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-09-07 22:31:17 -04:00
Merge pull request #4130 from kiwix/Issue#4106
Fixed: Downloads were not automatically starting showing progress when reopening the app(If downloads paused due to any network error).
This commit is contained in:
commit
4b8fe6df69
@ -12,6 +12,9 @@
|
|||||||
android:name="android.permission.NEARBY_WIFI_DEVICES"
|
android:name="android.permission.NEARBY_WIFI_DEVICES"
|
||||||
android:usesPermissionFlags="neverForLocation"
|
android:usesPermissionFlags="neverForLocation"
|
||||||
tools:targetApi="s" />
|
tools:targetApi="s" />
|
||||||
|
<!-- Device with versions >= Pie need this permission -->
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".KiwixApp"
|
android:name=".KiwixApp"
|
||||||
|
@ -148,6 +148,10 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
context?.let { context ->
|
context?.let { context ->
|
||||||
|
if (isNotConnected) {
|
||||||
|
noInternetSnackbar()
|
||||||
|
return@let
|
||||||
|
}
|
||||||
downloader.pauseResumeDownload(
|
downloader.pauseResumeDownload(
|
||||||
it.downloadId,
|
it.downloadId,
|
||||||
it.downloadState.toReadableState(context).contains(getString(string.paused_state))
|
it.downloadState.toReadableState(context).contains(getString(string.paused_state))
|
||||||
|
@ -13,12 +13,8 @@
|
|||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission
|
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
|
||||||
android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
|
|
||||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
|
|
||||||
<!-- Device with versions >= Pie need this permission -->
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.TTS_SERVICE" />
|
<action android:name="android.intent.action.TTS_SERVICE" />
|
||||||
@ -52,8 +48,8 @@
|
|||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:dataExtractionRules = "@xml/data_extraction_rules"
|
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
@ -80,9 +76,9 @@
|
|||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.fileprovider"
|
android:authorities="${applicationId}.fileprovider"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true"
|
||||||
tools:node="merge"
|
tools:node="merge"
|
||||||
tools:overrideLibrary="com.squareup.picasso.picasso"
|
tools:overrideLibrary="com.squareup.picasso.picasso">
|
||||||
android:grantUriPermissions="true">
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/provider_paths" />
|
android:resource="@xml/provider_paths" />
|
||||||
@ -92,8 +88,5 @@
|
|||||||
android:name=".error.DiagnosticReportActivity"
|
android:name=".error.DiagnosticReportActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
<service android:name=".read_aloud.ReadAloudService" />
|
<service android:name=".read_aloud.ReadAloudService" />
|
||||||
<service
|
|
||||||
android:name=".downloader.downloadManager.DownloadMonitorService"
|
|
||||||
android:foregroundServiceType="dataSync" />
|
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -113,7 +113,7 @@ abstract class DownloadRoomDao {
|
|||||||
sharedPreferenceUtil: SharedPreferenceUtil
|
sharedPreferenceUtil: SharedPreferenceUtil
|
||||||
) {
|
) {
|
||||||
if (doesNotAlreadyExist(book)) {
|
if (doesNotAlreadyExist(book)) {
|
||||||
val downloadRequest = DownloadRequest(url)
|
val downloadRequest = DownloadRequest(url, book.title)
|
||||||
saveDownload(
|
saveDownload(
|
||||||
DownloadRoomEntity(
|
DownloadRoomEntity(
|
||||||
url,
|
url,
|
||||||
|
@ -54,7 +54,8 @@ data class DownloadRoomEntity(
|
|||||||
val size: String,
|
val size: String,
|
||||||
val name: String?,
|
val name: String?,
|
||||||
val favIcon: String,
|
val favIcon: String,
|
||||||
val tags: String? = null
|
val tags: String? = null,
|
||||||
|
var pausedByUser: Boolean = false
|
||||||
) {
|
) {
|
||||||
constructor(downloadUrl: String, downloadId: Long, book: Book, file: String?) : this(
|
constructor(downloadUrl: String, downloadId: Long, book: Book, file: String?) : this(
|
||||||
file = file,
|
file = file,
|
||||||
@ -99,7 +100,8 @@ data class DownloadRoomEntity(
|
|||||||
totalSizeOfDownload = download.totalSizeOfDownload,
|
totalSizeOfDownload = download.totalSizeOfDownload,
|
||||||
status = download.state,
|
status = download.state,
|
||||||
error = download.error,
|
error = download.error,
|
||||||
progress = download.progress
|
progress = download.progress,
|
||||||
|
pausedByUser = download.pausedByUser
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ import org.kiwix.kiwixmobile.core.dao.entities.ZimSourceRoomConverter
|
|||||||
NotesRoomEntity::class,
|
NotesRoomEntity::class,
|
||||||
DownloadRoomEntity::class
|
DownloadRoomEntity::class
|
||||||
],
|
],
|
||||||
version = 5,
|
version = 6,
|
||||||
exportSchema = false
|
exportSchema = false
|
||||||
)
|
)
|
||||||
@TypeConverters(HistoryRoomDaoCoverts::class, ZimSourceRoomConverter::class)
|
@TypeConverters(HistoryRoomDaoCoverts::class, ZimSourceRoomConverter::class)
|
||||||
@ -62,7 +62,13 @@ abstract class KiwixRoomDatabase : RoomDatabase() {
|
|||||||
?: Room.databaseBuilder(context, KiwixRoomDatabase::class.java, "KiwixRoom.db")
|
?: Room.databaseBuilder(context, KiwixRoomDatabase::class.java, "KiwixRoom.db")
|
||||||
// We have already database name called kiwix.db in order to avoid complexity we named
|
// We have already database name called kiwix.db in order to avoid complexity we named
|
||||||
// as kiwixRoom.db
|
// as kiwixRoom.db
|
||||||
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5)
|
.addMigrations(
|
||||||
|
MIGRATION_1_2,
|
||||||
|
MIGRATION_2_3,
|
||||||
|
MIGRATION_3_4,
|
||||||
|
MIGRATION_4_5,
|
||||||
|
MIGRATION_5_6
|
||||||
|
)
|
||||||
.build().also { db = it }
|
.build().also { db = it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -202,6 +208,15 @@ abstract class KiwixRoomDatabase : RoomDatabase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
private val MIGRATION_5_6 = object : Migration(5, 6) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
database.execSQL(
|
||||||
|
"ALTER TABLE DownloadRoomEntity ADD COLUMN pausedByUser INTEGER NOT NULL DEFAULT 0"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun destroyInstance() {
|
fun destroyInstance() {
|
||||||
db = null
|
db = null
|
||||||
}
|
}
|
||||||
|
@ -23,14 +23,12 @@ import dagger.BindsInstance
|
|||||||
import dagger.Subcomponent
|
import dagger.Subcomponent
|
||||||
import org.kiwix.kiwixmobile.core.di.CoreServiceScope
|
import org.kiwix.kiwixmobile.core.di.CoreServiceScope
|
||||||
import org.kiwix.kiwixmobile.core.di.modules.CoreServiceModule
|
import org.kiwix.kiwixmobile.core.di.modules.CoreServiceModule
|
||||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadMonitorService
|
|
||||||
import org.kiwix.kiwixmobile.core.read_aloud.ReadAloudService
|
import org.kiwix.kiwixmobile.core.read_aloud.ReadAloudService
|
||||||
|
|
||||||
@Subcomponent(modules = [CoreServiceModule::class])
|
@Subcomponent(modules = [CoreServiceModule::class])
|
||||||
@CoreServiceScope
|
@CoreServiceScope
|
||||||
interface CoreServiceComponent {
|
interface CoreServiceComponent {
|
||||||
fun inject(readAloudService: ReadAloudService)
|
fun inject(readAloudService: ReadAloudService)
|
||||||
fun inject(downloadMonitorService: DownloadMonitorService)
|
|
||||||
|
|
||||||
@Subcomponent.Builder
|
@Subcomponent.Builder
|
||||||
interface Builder {
|
interface Builder {
|
||||||
|
@ -18,8 +18,6 @@
|
|||||||
package org.kiwix.kiwixmobile.core.di.modules
|
package org.kiwix.kiwixmobile.core.di.modules
|
||||||
|
|
||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.content.Context
|
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao
|
import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao
|
||||||
@ -30,7 +28,6 @@ import org.kiwix.kiwixmobile.core.downloader.DownloaderImpl
|
|||||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadManagerBroadcastReceiver
|
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadManagerBroadcastReceiver
|
||||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadManagerMonitor
|
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadManagerMonitor
|
||||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadManagerRequester
|
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadManagerRequester
|
||||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager
|
|
||||||
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
|
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@ -69,11 +66,4 @@ object DownloaderModule {
|
|||||||
fun providesDownloadManagerBroadcastReceiver(
|
fun providesDownloadManagerBroadcastReceiver(
|
||||||
callback: DownloadManagerBroadcastReceiver.Callback
|
callback: DownloadManagerBroadcastReceiver.Callback
|
||||||
): DownloadManagerBroadcastReceiver = DownloadManagerBroadcastReceiver(callback)
|
): DownloadManagerBroadcastReceiver = DownloadManagerBroadcastReceiver(callback)
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun providesDownloadNotificationManager(
|
|
||||||
context: Context,
|
|
||||||
notificationManager: NotificationManager
|
|
||||||
): DownloadNotificationManager = DownloadNotificationManager(context, notificationManager)
|
|
||||||
}
|
}
|
||||||
|
@ -18,37 +18,89 @@
|
|||||||
|
|
||||||
package org.kiwix.kiwixmobile.core.downloader.downloadManager
|
package org.kiwix.kiwixmobile.core.downloader.downloadManager
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.net.Uri
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.disposables.Disposable
|
import io.reactivex.disposables.Disposable
|
||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import io.reactivex.subjects.PublishSubject
|
||||||
import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao
|
import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao
|
||||||
import org.kiwix.kiwixmobile.core.dao.entities.DownloadRoomEntity
|
import org.kiwix.kiwixmobile.core.dao.entities.DownloadRoomEntity
|
||||||
import org.kiwix.kiwixmobile.core.downloader.DownloadMonitor
|
import org.kiwix.kiwixmobile.core.downloader.DownloadMonitor
|
||||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager.Companion.ACTION_CANCEL
|
import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel
|
||||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager.Companion.ACTION_PAUSE
|
import org.kiwix.kiwixmobile.core.utils.NetworkUtils
|
||||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager.Companion.ACTION_QUERY_DOWNLOAD_STATUS
|
|
||||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager.Companion.ACTION_RESUME
|
|
||||||
import org.kiwix.kiwixmobile.core.extensions.isServiceRunning
|
|
||||||
import org.kiwix.kiwixmobile.core.utils.files.Log
|
import org.kiwix.kiwixmobile.core.utils.files.Log
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
const val ZERO = 0
|
||||||
|
const val HUNDERED = 100
|
||||||
|
const val THOUSAND = 1000
|
||||||
|
const val DEFAULT_INT_VALUE = -1
|
||||||
|
|
||||||
|
/*
|
||||||
|
These below values of android.provider.Downloads.Impl class,
|
||||||
|
there is no direct way to access them so we defining the values
|
||||||
|
from https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/provider/Downloads.java
|
||||||
|
*/
|
||||||
|
const val CONTROL_PAUSE = 1
|
||||||
|
const val CONTROL_RUN = 0
|
||||||
|
const val STATUS_RUNNING = 192
|
||||||
|
const val STATUS_PAUSED_BY_APP = 193
|
||||||
|
const val COLUMN_CONTROL = "control"
|
||||||
|
val downloadBaseUri: Uri = Uri.parse("content://downloads/my_downloads")
|
||||||
|
const val DOWNLOAD_NOTIFICATION_TITLE = "OPEN_ZIM_FILE"
|
||||||
|
|
||||||
class DownloadManagerMonitor @Inject constructor(
|
class DownloadManagerMonitor @Inject constructor(
|
||||||
|
private var downloadManager: DownloadManager,
|
||||||
val downloadRoomDao: DownloadRoomDao,
|
val downloadRoomDao: DownloadRoomDao,
|
||||||
private val context: Context
|
private val context: Context
|
||||||
) : DownloadMonitor, DownloadManagerBroadcastReceiver.Callback {
|
) : DownloadMonitor, DownloadManagerBroadcastReceiver.Callback {
|
||||||
private val lock = Any()
|
private val lock = Any()
|
||||||
private var monitoringDisposable: Disposable? = null
|
private var monitoringDisposable: Disposable? = null
|
||||||
|
private val downloadInfoMap = mutableMapOf<Long, DownloadInfo>()
|
||||||
|
private val updater = PublishSubject.create<() -> Unit>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
setupUpdater()
|
||||||
startMonitoringDownloads()
|
startMonitoringDownloads()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun downloadCompleteOrCancelled(intent: Intent) {
|
||||||
|
synchronized(lock) {
|
||||||
|
intent.extras?.let {
|
||||||
|
val downloadId = it.getLong(DownloadManager.EXTRA_DOWNLOAD_ID, DEFAULT_INT_VALUE.toLong())
|
||||||
|
if (downloadId != DEFAULT_INT_VALUE.toLong()) {
|
||||||
|
queryDownloadStatus(downloadId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("CheckResult")
|
||||||
|
private fun setupUpdater() {
|
||||||
|
updater.subscribeOn(Schedulers.io()).observeOn(Schedulers.io()).subscribe(
|
||||||
|
{
|
||||||
|
synchronized(lock) { it.invoke() }
|
||||||
|
},
|
||||||
|
Throwable::printStackTrace
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts monitoring ongoing downloads using a periodic observable.
|
||||||
|
* This method sets up an observable that runs every 5 seconds to check the status of downloads.
|
||||||
|
* It only starts the monitoring process if it's not already running and disposes of the observable
|
||||||
|
* when there are no ongoing downloads to avoid unnecessary resource usage.
|
||||||
|
*/
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
fun startMonitoringDownloads() {
|
fun startMonitoringDownloads() {
|
||||||
|
// Check if monitoring is already active. If it is, do nothing.
|
||||||
if (monitoringDisposable?.isDisposed == false) return
|
if (monitoringDisposable?.isDisposed == false) return
|
||||||
monitoringDisposable = Observable.interval(ZERO.toLong(), 5, TimeUnit.SECONDS)
|
monitoringDisposable = Observable.interval(ZERO.toLong(), 5, TimeUnit.SECONDS)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
@ -57,19 +109,9 @@ class DownloadManagerMonitor @Inject constructor(
|
|||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
synchronized(lock) {
|
synchronized(lock) {
|
||||||
// Observe downloads when the application is in the foreground.
|
val downloadingList = downloadRoomDao.downloads().blockingFirst()
|
||||||
// This is especially useful when downloads are resumed but the
|
if (downloadingList.isNotEmpty()) {
|
||||||
// Download Manager takes some time to update the download status.
|
checkDownloads(downloadingList)
|
||||||
// In such cases, the foreground service may stop prematurely due to
|
|
||||||
// a lack of active downloads during this update delay.
|
|
||||||
if (downloadRoomDao.downloads().blockingFirst().isNotEmpty()) {
|
|
||||||
// Check if there are active downloads and the service is not running.
|
|
||||||
// If so, start the DownloadMonitorService to properly track download progress.
|
|
||||||
if (shouldStartService()) {
|
|
||||||
startService()
|
|
||||||
} else {
|
|
||||||
// Do nothing; it is for fixing the error when "if" is used as an expression.
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
monitoringDisposable?.dispose()
|
monitoringDisposable?.dispose()
|
||||||
}
|
}
|
||||||
@ -85,61 +127,490 @@ class DownloadManagerMonitor @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@SuppressLint("Range")
|
||||||
* Determines if the DownloadMonitorService should be started.
|
private fun checkDownloads(downloadingList: List<DownloadModel>) {
|
||||||
* Checks if there are active downloads and if the service is not already running.
|
|
||||||
*/
|
|
||||||
private fun shouldStartService(): Boolean =
|
|
||||||
getActiveDownloads().isNotEmpty() &&
|
|
||||||
!context.isServiceRunning(DownloadMonitorService::class.java)
|
|
||||||
|
|
||||||
private fun getActiveDownloads(): List<DownloadRoomEntity> =
|
|
||||||
downloadRoomDao.downloadRoomEntity().blockingFirst().filter {
|
|
||||||
it.status != Status.PAUSED && it.status != Status.CANCELLED
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun downloadCompleteOrCancelled(intent: Intent) {
|
|
||||||
synchronized(lock) {
|
synchronized(lock) {
|
||||||
intent.extras?.let {
|
downloadingList.forEach {
|
||||||
val downloadId = it.getLong(DownloadManager.EXTRA_DOWNLOAD_ID, -1L)
|
queryDownloadStatus(it.downloadId)
|
||||||
if (downloadId != -1L) {
|
}
|
||||||
context.startService(
|
}
|
||||||
getDownloadMonitorIntent(
|
}
|
||||||
ACTION_QUERY_DOWNLOAD_STATUS,
|
|
||||||
downloadId.toInt()
|
@SuppressLint("Range")
|
||||||
)
|
fun queryDownloadStatus(downloadId: Long) {
|
||||||
)
|
synchronized(lock) {
|
||||||
|
updater.onNext {
|
||||||
|
downloadManager.query(DownloadManager.Query().setFilterById(downloadId)).use { cursor ->
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
handleDownloadStatus(cursor, downloadId)
|
||||||
|
} else {
|
||||||
|
handleCancelledDownload(downloadId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startService() {
|
@SuppressLint("Range")
|
||||||
context.startService(Intent(context, DownloadMonitorService::class.java))
|
private fun handleDownloadStatus(cursor: Cursor, downloadId: Long) {
|
||||||
|
val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
||||||
|
val reason = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_REASON))
|
||||||
|
val bytesDownloaded =
|
||||||
|
cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
|
||||||
|
val totalBytes = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
|
||||||
|
val progress = calculateProgress(bytesDownloaded, totalBytes)
|
||||||
|
|
||||||
|
val etaInMilliSeconds = calculateETA(downloadId, bytesDownloaded, totalBytes)
|
||||||
|
|
||||||
|
when (status) {
|
||||||
|
DownloadManager.STATUS_FAILED -> handleFailedDownload(
|
||||||
|
downloadId,
|
||||||
|
reason,
|
||||||
|
progress,
|
||||||
|
etaInMilliSeconds,
|
||||||
|
bytesDownloaded,
|
||||||
|
totalBytes
|
||||||
|
)
|
||||||
|
|
||||||
|
DownloadManager.STATUS_PAUSED -> handlePausedDownload(
|
||||||
|
downloadId,
|
||||||
|
progress,
|
||||||
|
bytesDownloaded,
|
||||||
|
totalBytes,
|
||||||
|
reason
|
||||||
|
)
|
||||||
|
|
||||||
|
DownloadManager.STATUS_PENDING -> handlePendingDownload(downloadId)
|
||||||
|
DownloadManager.STATUS_RUNNING -> handleRunningDownload(
|
||||||
|
downloadId,
|
||||||
|
progress,
|
||||||
|
etaInMilliSeconds,
|
||||||
|
bytesDownloaded,
|
||||||
|
totalBytes
|
||||||
|
)
|
||||||
|
|
||||||
|
DownloadManager.STATUS_SUCCESSFUL -> handleSuccessfulDownload(
|
||||||
|
downloadId,
|
||||||
|
progress,
|
||||||
|
etaInMilliSeconds
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleCancelledDownload(downloadId: Long) {
|
||||||
|
updater.onNext {
|
||||||
|
updateDownloadStatus(downloadId, Status.CANCELLED, Error.CANCELLED)
|
||||||
|
downloadRoomDao.delete(downloadId)
|
||||||
|
downloadInfoMap.remove(downloadId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
private fun handleFailedDownload(
|
||||||
|
downloadId: Long,
|
||||||
|
reason: Int,
|
||||||
|
progress: Int,
|
||||||
|
etaInMilliSeconds: Long,
|
||||||
|
bytesDownloaded: Long,
|
||||||
|
totalBytes: Long
|
||||||
|
) {
|
||||||
|
val error = mapDownloadError(reason)
|
||||||
|
updateDownloadStatus(
|
||||||
|
downloadId,
|
||||||
|
Status.FAILED,
|
||||||
|
error,
|
||||||
|
progress,
|
||||||
|
etaInMilliSeconds,
|
||||||
|
bytesDownloaded,
|
||||||
|
totalBytes
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePausedDownload(
|
||||||
|
downloadId: Long,
|
||||||
|
progress: Int,
|
||||||
|
bytesDownloaded: Long,
|
||||||
|
totalSizeOfDownload: Long,
|
||||||
|
reason: Int
|
||||||
|
) {
|
||||||
|
val pauseReason = mapDownloadPauseReason(reason)
|
||||||
|
updateDownloadStatus(
|
||||||
|
downloadId = downloadId,
|
||||||
|
status = Status.PAUSED,
|
||||||
|
error = pauseReason,
|
||||||
|
progress = progress,
|
||||||
|
bytesDownloaded = bytesDownloaded,
|
||||||
|
totalSizeOfDownload = totalSizeOfDownload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePendingDownload(downloadId: Long) {
|
||||||
|
updateDownloadStatus(
|
||||||
|
downloadId,
|
||||||
|
Status.QUEUED,
|
||||||
|
Error.NONE
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleRunningDownload(
|
||||||
|
downloadId: Long,
|
||||||
|
progress: Int,
|
||||||
|
etaInMilliSeconds: Long,
|
||||||
|
bytesDownloaded: Long,
|
||||||
|
totalSizeOfDownload: Long
|
||||||
|
) {
|
||||||
|
updateDownloadStatus(
|
||||||
|
downloadId,
|
||||||
|
Status.DOWNLOADING,
|
||||||
|
Error.NONE,
|
||||||
|
progress,
|
||||||
|
etaInMilliSeconds,
|
||||||
|
bytesDownloaded,
|
||||||
|
totalSizeOfDownload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSuccessfulDownload(
|
||||||
|
downloadId: Long,
|
||||||
|
progress: Int,
|
||||||
|
etaInMilliSeconds: Long
|
||||||
|
) {
|
||||||
|
updateDownloadStatus(
|
||||||
|
downloadId,
|
||||||
|
Status.COMPLETED,
|
||||||
|
Error.NONE,
|
||||||
|
progress,
|
||||||
|
etaInMilliSeconds
|
||||||
|
)
|
||||||
|
downloadInfoMap.remove(downloadId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun calculateProgress(bytesDownloaded: Long, totalBytes: Long): Int =
|
||||||
|
if (totalBytes > ZERO) {
|
||||||
|
(bytesDownloaded / totalBytes.toDouble()).times(HUNDERED).toInt()
|
||||||
|
} else {
|
||||||
|
ZERO
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun calculateETA(downloadedFileId: Long, bytesDownloaded: Long, totalBytes: Long): Long {
|
||||||
|
val currentTime = System.currentTimeMillis()
|
||||||
|
val downloadInfo = downloadInfoMap.getOrPut(downloadedFileId) {
|
||||||
|
DownloadInfo(startTime = currentTime, initialBytesDownloaded = bytesDownloaded)
|
||||||
|
}
|
||||||
|
|
||||||
|
val elapsedTime = currentTime - downloadInfo.startTime
|
||||||
|
val downloadSpeed = if (elapsedTime > ZERO) {
|
||||||
|
(bytesDownloaded - downloadInfo.initialBytesDownloaded) / (elapsedTime / THOUSAND.toFloat())
|
||||||
|
} else {
|
||||||
|
ZERO.toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (downloadSpeed > ZERO) {
|
||||||
|
((totalBytes - bytesDownloaded) / downloadSpeed).toLong() * THOUSAND
|
||||||
|
} else {
|
||||||
|
ZERO.toLong()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapDownloadError(reason: Int): Error {
|
||||||
|
return when (reason) {
|
||||||
|
DownloadManager.ERROR_CANNOT_RESUME -> Error.ERROR_CANNOT_RESUME
|
||||||
|
DownloadManager.ERROR_DEVICE_NOT_FOUND -> Error.ERROR_DEVICE_NOT_FOUND
|
||||||
|
DownloadManager.ERROR_FILE_ALREADY_EXISTS -> Error.ERROR_FILE_ALREADY_EXISTS
|
||||||
|
DownloadManager.ERROR_FILE_ERROR -> Error.ERROR_FILE_ERROR
|
||||||
|
DownloadManager.ERROR_HTTP_DATA_ERROR -> Error.ERROR_HTTP_DATA_ERROR
|
||||||
|
DownloadManager.ERROR_INSUFFICIENT_SPACE -> Error.ERROR_INSUFFICIENT_SPACE
|
||||||
|
DownloadManager.ERROR_TOO_MANY_REDIRECTS -> Error.ERROR_TOO_MANY_REDIRECTS
|
||||||
|
DownloadManager.ERROR_UNHANDLED_HTTP_CODE -> Error.ERROR_UNHANDLED_HTTP_CODE
|
||||||
|
DownloadManager.ERROR_UNKNOWN -> Error.UNKNOWN
|
||||||
|
else -> Error.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapDownloadPauseReason(reason: Int): Error {
|
||||||
|
return when (reason) {
|
||||||
|
DownloadManager.PAUSED_QUEUED_FOR_WIFI -> Error.QUEUED_FOR_WIFI
|
||||||
|
DownloadManager.PAUSED_WAITING_TO_RETRY -> Error.WAITING_TO_RETRY
|
||||||
|
DownloadManager.PAUSED_WAITING_FOR_NETWORK -> Error.WAITING_FOR_NETWORK
|
||||||
|
DownloadManager.PAUSED_UNKNOWN -> Error.PAUSED_UNKNOWN
|
||||||
|
else -> Error.PAUSED_UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
private fun updateDownloadStatus(
|
||||||
|
downloadId: Long,
|
||||||
|
status: Status,
|
||||||
|
error: Error,
|
||||||
|
progress: Int = DEFAULT_INT_VALUE,
|
||||||
|
etaInMilliSeconds: Long = DEFAULT_INT_VALUE.toLong(),
|
||||||
|
bytesDownloaded: Long = DEFAULT_INT_VALUE.toLong(),
|
||||||
|
totalSizeOfDownload: Long = DEFAULT_INT_VALUE.toLong(),
|
||||||
|
pausedByUser: Boolean? = null
|
||||||
|
) {
|
||||||
|
synchronized(lock) {
|
||||||
|
updater.onNext {
|
||||||
|
Log.e(
|
||||||
|
"DOWNLOAD_MONITOR",
|
||||||
|
"updateDownloadStatus: " +
|
||||||
|
"\n Status = $status" +
|
||||||
|
"\n Error = $error" +
|
||||||
|
"\n Progress = $progress" +
|
||||||
|
"\n DownloadId = $downloadId"
|
||||||
|
)
|
||||||
|
downloadRoomDao.getEntityForDownloadId(downloadId)?.let { downloadEntity ->
|
||||||
|
if (shouldUpdateDownloadStatus(downloadEntity)) {
|
||||||
|
val downloadModel = DownloadModel(downloadEntity).apply {
|
||||||
|
pausedByUser?.let {
|
||||||
|
this.pausedByUser = it
|
||||||
|
downloadEntity.pausedByUser = it
|
||||||
|
}
|
||||||
|
if (shouldUpdateDownloadStatus(status, error, downloadEntity)) {
|
||||||
|
state = status
|
||||||
|
}
|
||||||
|
this.error = error
|
||||||
|
if (progress > ZERO) {
|
||||||
|
this.progress = progress
|
||||||
|
}
|
||||||
|
this.etaInMilliSeconds = etaInMilliSeconds
|
||||||
|
if (bytesDownloaded != DEFAULT_INT_VALUE.toLong()) {
|
||||||
|
this.bytesDownloaded = bytesDownloaded
|
||||||
|
}
|
||||||
|
if (totalSizeOfDownload != DEFAULT_INT_VALUE.toLong()) {
|
||||||
|
this.totalSizeOfDownload = totalSizeOfDownload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
downloadRoomDao.update(downloadModel)
|
||||||
|
return@let
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether the download status should be updated based on the current status and error.
|
||||||
|
*
|
||||||
|
* This method evaluates the current download status and error conditions, ensuring proper handling
|
||||||
|
* for paused downloads, queued downloads, and network-related retries. It coordinates with the
|
||||||
|
* Download Manager to resume downloads when necessary and prevents premature status updates.
|
||||||
|
*
|
||||||
|
* @param status The current status of the download.
|
||||||
|
* @param error The current error state of the download.
|
||||||
|
* @param downloadRoomEntity The download entity containing the current status and download ID.
|
||||||
|
* @return `true` if the status should be updated, `false` otherwise.
|
||||||
|
*/
|
||||||
|
private fun shouldUpdateDownloadStatus(
|
||||||
|
status: Status,
|
||||||
|
error: Error,
|
||||||
|
downloadRoomEntity: DownloadRoomEntity
|
||||||
|
): Boolean {
|
||||||
|
synchronized(lock) {
|
||||||
|
return@shouldUpdateDownloadStatus when {
|
||||||
|
// Check if the download is paused and was previously queued.
|
||||||
|
isPausedAndQueued(status, downloadRoomEntity) ->
|
||||||
|
handlePausedAndQueuedDownload(error, downloadRoomEntity)
|
||||||
|
|
||||||
|
// Check if the download is paused and retryable due to network availability.
|
||||||
|
isPausedAndRetryable(
|
||||||
|
status,
|
||||||
|
error,
|
||||||
|
downloadRoomEntity.pausedByUser
|
||||||
|
) -> {
|
||||||
|
handleRetryablePausedDownload(downloadRoomEntity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default case: update the status.
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the download is paused and was previously queued.
|
||||||
|
*
|
||||||
|
* Specifically, it evaluates whether the current status is "Paused" while the previous status
|
||||||
|
* was "Queued", indicating that the user might have initiated a resume action.
|
||||||
|
*
|
||||||
|
* @param status The current status of the download.
|
||||||
|
* @param downloadRoomEntity The download entity to evaluate.
|
||||||
|
* @return `true` if the download is paused and queued, `false` otherwise.
|
||||||
|
*/
|
||||||
|
private fun isPausedAndQueued(status: Status, downloadRoomEntity: DownloadRoomEntity): Boolean =
|
||||||
|
status == Status.PAUSED && downloadRoomEntity.status == Status.QUEUED
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the download is paused and retryable based on the error and network conditions.
|
||||||
|
*
|
||||||
|
* This evaluates whether the download can be resumed, considering its paused state,
|
||||||
|
* error condition (e.g., waiting for retry), and the availability of a network connection.
|
||||||
|
*
|
||||||
|
* @param status The current status of the download.
|
||||||
|
* @param error The current error state of the download.
|
||||||
|
* @param pausedByUser To identify if the download paused by user or downloadManager.
|
||||||
|
* @return `true` if the download is paused and retryable, `false` otherwise.
|
||||||
|
*/
|
||||||
|
private fun isPausedAndRetryable(status: Status, error: Error, pausedByUser: Boolean): Boolean {
|
||||||
|
return status == Status.PAUSED &&
|
||||||
|
(error == Error.WAITING_TO_RETRY || error == Error.PAUSED_UNKNOWN) &&
|
||||||
|
NetworkUtils.isNetworkAvailable(context) &&
|
||||||
|
!pausedByUser
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the case where a paused download was previously queued.
|
||||||
|
*
|
||||||
|
* This ensures that the download manager is instructed to resume the download and prevents
|
||||||
|
* the status from being prematurely updated to "Paused". Instead, the user will see the "Pending"
|
||||||
|
* state, indicating that the download is in the process of resuming.
|
||||||
|
*
|
||||||
|
* @param error The current error state of the download.
|
||||||
|
* @param downloadRoomEntity The download entity to evaluate.
|
||||||
|
* @return `true` if the status should be updated, `false` otherwise.
|
||||||
|
*/
|
||||||
|
private fun handlePausedAndQueuedDownload(
|
||||||
|
error: Error,
|
||||||
|
downloadRoomEntity: DownloadRoomEntity
|
||||||
|
): Boolean {
|
||||||
|
return when (error) {
|
||||||
|
// When the pause reason is unknown or waiting to retry, and the user
|
||||||
|
// resumes the download, attempt to resume the download if it was not resumed
|
||||||
|
// due to some reason.
|
||||||
|
Error.PAUSED_UNKNOWN,
|
||||||
|
Error.WAITING_TO_RETRY -> {
|
||||||
|
resumeDownload(downloadRoomEntity.downloadId, shouldUpdateStatus = false)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
// For any other error state, update the status to reflect the current state
|
||||||
|
// and provide feedback to the user.
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the case where a paused download is retryable due to network availability.
|
||||||
|
*
|
||||||
|
* If the download manager is waiting to retry due to a network error caused by fluctuations,
|
||||||
|
* this method resumes the download and ensures the status reflects the resumption process.
|
||||||
|
*
|
||||||
|
* @param downloadRoomEntity The download entity to evaluate.
|
||||||
|
* @return `true` to update the status and attempt to resume the download.
|
||||||
|
*/
|
||||||
|
private fun handleRetryablePausedDownload(downloadRoomEntity: DownloadRoomEntity): Boolean {
|
||||||
|
resumeDownload(downloadRoomEntity.downloadId, shouldUpdateStatus = false)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pauseDownload(downloadId: Long) {
|
fun pauseDownload(downloadId: Long) {
|
||||||
context.startService(getDownloadMonitorIntent(ACTION_PAUSE, downloadId.toInt()))
|
synchronized(lock) {
|
||||||
startMonitoringDownloads()
|
updater.onNext {
|
||||||
|
if (pauseResumeDownloadInDownloadManagerContentResolver(
|
||||||
|
downloadId,
|
||||||
|
CONTROL_PAUSE,
|
||||||
|
STATUS_PAUSED_BY_APP
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// pass true when user paused the download to not retry the download automatically.
|
||||||
|
updateDownloadStatus(downloadId, Status.PAUSED, Error.NONE, pausedByUser = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resumeDownload(downloadId: Long) {
|
fun resumeDownload(
|
||||||
context.startService(getDownloadMonitorIntent(ACTION_RESUME, downloadId.toInt()))
|
downloadId: Long,
|
||||||
startMonitoringDownloads()
|
shouldUpdateStatus: Boolean = true
|
||||||
|
) {
|
||||||
|
synchronized(lock) {
|
||||||
|
updater.onNext {
|
||||||
|
if (pauseResumeDownloadInDownloadManagerContentResolver(
|
||||||
|
downloadId,
|
||||||
|
CONTROL_RUN,
|
||||||
|
STATUS_RUNNING
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (shouldUpdateStatus) {
|
||||||
|
// pass false when user resumed the download to proceed with further checks.
|
||||||
|
updateDownloadStatus(downloadId, Status.QUEUED, Error.NONE, pausedByUser = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancelDownload(downloadId: Long) {
|
fun cancelDownload(downloadId: Long) {
|
||||||
context.startService(getDownloadMonitorIntent(ACTION_CANCEL, downloadId.toInt()))
|
synchronized(lock) {
|
||||||
startMonitoringDownloads()
|
updater.onNext {
|
||||||
|
// Remove the download from DownloadManager on IO thread.
|
||||||
|
downloadManager.remove(downloadId)
|
||||||
|
handleCancelledDownload(downloadId)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDownloadMonitorIntent(action: String, downloadId: Int): Intent =
|
@SuppressLint("Range")
|
||||||
Intent(context, DownloadMonitorService::class.java).apply {
|
private fun pauseResumeDownloadInDownloadManagerContentResolver(
|
||||||
putExtra(DownloadNotificationManager.NOTIFICATION_ACTION, action)
|
downloadId: Long,
|
||||||
putExtra(DownloadNotificationManager.EXTRA_DOWNLOAD_ID, downloadId)
|
control: Int,
|
||||||
|
status: Int
|
||||||
|
): Boolean {
|
||||||
|
return try {
|
||||||
|
// Update the status to paused/resumed in the database
|
||||||
|
val contentValues = ContentValues().apply {
|
||||||
|
put(COLUMN_CONTROL, control)
|
||||||
|
put(DownloadManager.COLUMN_STATUS, status)
|
||||||
|
}
|
||||||
|
context.contentResolver
|
||||||
|
.update(
|
||||||
|
downloadBaseUri,
|
||||||
|
contentValues,
|
||||||
|
getWhereClauseForIds(longArrayOf(downloadId)),
|
||||||
|
getWhereArgsForIds(longArrayOf(downloadId))
|
||||||
|
)
|
||||||
|
true
|
||||||
|
} catch (ignore: Exception) {
|
||||||
|
Log.e("DOWNLOAD_MONITOR", "Couldn't pause/resume the download. Original exception = $ignore")
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getWhereArgsForIds(ids: LongArray): Array<String?> {
|
||||||
|
val whereArgs = arrayOfNulls<String>(ids.size)
|
||||||
|
return getWhereArgsForIds(ids, whereArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getWhereArgsForIds(ids: LongArray, args: Array<String?>): Array<String?> {
|
||||||
|
assert(args.size >= ids.size)
|
||||||
|
for (i in ids.indices) {
|
||||||
|
args[i] = "${ids[i]}"
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getWhereClauseForIds(ids: LongArray): String {
|
||||||
|
val whereClause = StringBuilder()
|
||||||
|
whereClause.append("(")
|
||||||
|
for (i in ids.indices) {
|
||||||
|
if (i > 0) {
|
||||||
|
whereClause.append("OR ")
|
||||||
|
}
|
||||||
|
whereClause.append("_id")
|
||||||
|
whereClause.append(" = ? ")
|
||||||
|
}
|
||||||
|
whereClause.append(")")
|
||||||
|
return "$whereClause"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shouldUpdateDownloadStatus(downloadRoomEntity: DownloadRoomEntity) =
|
||||||
|
downloadRoomEntity.status != Status.COMPLETED
|
||||||
|
|
||||||
override fun init() {
|
override fun init() {
|
||||||
// empty method to so class does not get reported unused
|
// empty method to so class does not get reported unused
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class DownloadInfo(
|
||||||
|
var startTime: Long,
|
||||||
|
var initialBytesDownloaded: Long
|
||||||
|
)
|
||||||
|
@ -20,7 +20,7 @@ package org.kiwix.kiwixmobile.core.downloader.downloadManager
|
|||||||
|
|
||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
import android.app.DownloadManager.Request
|
import android.app.DownloadManager.Request
|
||||||
import android.app.DownloadManager.Request.VISIBILITY_HIDDEN
|
import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
@ -60,7 +60,7 @@ class DownloadManagerRequester @Inject constructor(
|
|||||||
.downloadRoomDao
|
.downloadRoomDao
|
||||||
.getEntityForDownloadId(downloadId)?.let { downloadRoomEntity ->
|
.getEntityForDownloadId(downloadId)?.let { downloadRoomEntity ->
|
||||||
downloadRoomEntity.url?.let {
|
downloadRoomEntity.url?.let {
|
||||||
val downloadRequest = DownloadRequest(urlString = it)
|
val downloadRequest = DownloadRequest(urlString = it, downloadRoomEntity.title)
|
||||||
val newDownloadEntity =
|
val newDownloadEntity =
|
||||||
downloadRoomEntity.copy(downloadId = enqueue(downloadRequest), id = 0)
|
downloadRoomEntity.copy(downloadId = enqueue(downloadRequest), id = 0)
|
||||||
// cancel the previous download and its data from database and fileSystem.
|
// cancel the previous download and its data from database and fileSystem.
|
||||||
@ -97,6 +97,7 @@ fun DownloadRequest.toDownloadManagerRequest(
|
|||||||
return if (urlString.isAuthenticationUrl) {
|
return if (urlString.isAuthenticationUrl) {
|
||||||
// return the request with "Authorization" header if the url is a Authentication url.
|
// return the request with "Authorization" header if the url is a Authentication url.
|
||||||
DownloadManager.Request(urlString.removeAuthenticationFromUrl.toUri()).apply {
|
DownloadManager.Request(urlString.removeAuthenticationFromUrl.toUri()).apply {
|
||||||
|
setTitle(bookTitle)
|
||||||
setDestinationUri(Uri.fromFile(getDestinationFile(sharedPreferenceUtil)))
|
setDestinationUri(Uri.fromFile(getDestinationFile(sharedPreferenceUtil)))
|
||||||
setAllowedNetworkTypes(
|
setAllowedNetworkTypes(
|
||||||
if (sharedPreferenceUtil.prefWifiOnly)
|
if (sharedPreferenceUtil.prefWifiOnly)
|
||||||
@ -105,7 +106,7 @@ fun DownloadRequest.toDownloadManagerRequest(
|
|||||||
Request.NETWORK_MOBILE or Request.NETWORK_WIFI
|
Request.NETWORK_MOBILE or Request.NETWORK_WIFI
|
||||||
)
|
)
|
||||||
setAllowedOverMetered(!sharedPreferenceUtil.prefWifiOnly)
|
setAllowedOverMetered(!sharedPreferenceUtil.prefWifiOnly)
|
||||||
setNotificationVisibility(VISIBILITY_HIDDEN) // hide the default notification.
|
setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||||
val userNameAndPassword = System.getenv(urlString.secretKey) ?: ""
|
val userNameAndPassword = System.getenv(urlString.secretKey) ?: ""
|
||||||
val userName = userNameAndPassword.substringBefore(":", "")
|
val userName = userNameAndPassword.substringBefore(":", "")
|
||||||
val password = userNameAndPassword.substringAfter(":", "")
|
val password = userNameAndPassword.substringAfter(":", "")
|
||||||
@ -115,6 +116,7 @@ fun DownloadRequest.toDownloadManagerRequest(
|
|||||||
} else {
|
} else {
|
||||||
// return the request for normal urls.
|
// return the request for normal urls.
|
||||||
DownloadManager.Request(uri).apply {
|
DownloadManager.Request(uri).apply {
|
||||||
|
setTitle(bookTitle)
|
||||||
setDestinationUri(Uri.fromFile(getDestinationFile(sharedPreferenceUtil)))
|
setDestinationUri(Uri.fromFile(getDestinationFile(sharedPreferenceUtil)))
|
||||||
setAllowedNetworkTypes(
|
setAllowedNetworkTypes(
|
||||||
if (sharedPreferenceUtil.prefWifiOnly)
|
if (sharedPreferenceUtil.prefWifiOnly)
|
||||||
@ -123,7 +125,7 @@ fun DownloadRequest.toDownloadManagerRequest(
|
|||||||
Request.NETWORK_MOBILE or Request.NETWORK_WIFI
|
Request.NETWORK_MOBILE or Request.NETWORK_WIFI
|
||||||
)
|
)
|
||||||
setAllowedOverMetered(!sharedPreferenceUtil.prefWifiOnly)
|
setAllowedOverMetered(!sharedPreferenceUtil.prefWifiOnly)
|
||||||
setNotificationVisibility(VISIBILITY_HIDDEN) // hide the default notification.
|
setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,629 +0,0 @@
|
|||||||
/*
|
|
||||||
* Kiwix Android
|
|
||||||
* Copyright (c) 2024 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.downloadManager
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.DownloadManager
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.ContentUris
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.content.Intent
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.IBinder
|
|
||||||
import io.reactivex.Observable
|
|
||||||
import io.reactivex.disposables.Disposable
|
|
||||||
import io.reactivex.schedulers.Schedulers
|
|
||||||
import io.reactivex.subjects.PublishSubject
|
|
||||||
import org.kiwix.kiwixmobile.core.CoreApp
|
|
||||||
import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao
|
|
||||||
import org.kiwix.kiwixmobile.core.dao.entities.DownloadRoomEntity
|
|
||||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager.Companion.ACTION_CANCEL
|
|
||||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager.Companion.ACTION_PAUSE
|
|
||||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager.Companion.ACTION_QUERY_DOWNLOAD_STATUS
|
|
||||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager.Companion.ACTION_RESUME
|
|
||||||
import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel
|
|
||||||
import org.kiwix.kiwixmobile.core.downloader.model.DownloadState
|
|
||||||
import org.kiwix.kiwixmobile.core.utils.files.Log
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
const val ZERO = 0
|
|
||||||
const val HUNDERED = 100
|
|
||||||
const val THOUSAND = 1000
|
|
||||||
const val DEFAULT_INT_VALUE = -1
|
|
||||||
|
|
||||||
/*
|
|
||||||
These below values of android.provider.Downloads.Impl class,
|
|
||||||
there is no direct way to access them so we defining the values
|
|
||||||
from https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/provider/Downloads.java
|
|
||||||
*/
|
|
||||||
const val CONTROL_PAUSE = 1
|
|
||||||
const val CONTROL_RUN = 0
|
|
||||||
const val STATUS_RUNNING = 192
|
|
||||||
const val STATUS_PAUSED_BY_APP = 193
|
|
||||||
const val COLUMN_CONTROL = "control"
|
|
||||||
val downloadBaseUri: Uri = Uri.parse("content://downloads/my_downloads")
|
|
||||||
|
|
||||||
class DownloadMonitorService : Service() {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var downloadManager: DownloadManager
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var downloadRoomDao: DownloadRoomDao
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var downloadNotificationManager: DownloadNotificationManager
|
|
||||||
private val lock = Any()
|
|
||||||
private var monitoringDisposable: Disposable? = null
|
|
||||||
private val downloadInfoMap = mutableMapOf<Long, DownloadInfo>()
|
|
||||||
private val updater = PublishSubject.create<() -> Unit>()
|
|
||||||
private var foreGroundServiceInformation: Pair<Boolean, Int> = true to DEFAULT_INT_VALUE
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? = null
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
CoreApp.coreComponent
|
|
||||||
.coreServiceComponent()
|
|
||||||
.service(this)
|
|
||||||
.build()
|
|
||||||
.inject(this)
|
|
||||||
super.onCreate()
|
|
||||||
setupUpdater()
|
|
||||||
startMonitoringDownloads()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
val downloadId =
|
|
||||||
intent?.getIntExtra(DownloadNotificationManager.EXTRA_DOWNLOAD_ID, DEFAULT_INT_VALUE)
|
|
||||||
?: DEFAULT_INT_VALUE
|
|
||||||
val notificationAction = intent?.getStringExtra(DownloadNotificationManager.NOTIFICATION_ACTION)
|
|
||||||
if (downloadId != DEFAULT_INT_VALUE) {
|
|
||||||
when (notificationAction) {
|
|
||||||
ACTION_PAUSE -> pauseDownload(downloadId.toLong())
|
|
||||||
ACTION_RESUME -> resumeDownload(downloadId.toLong())
|
|
||||||
ACTION_CANCEL -> cancelDownload(downloadId.toLong())
|
|
||||||
ACTION_QUERY_DOWNLOAD_STATUS -> {
|
|
||||||
updater.onNext {
|
|
||||||
queryDownloadStatus(downloadId.toLong())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return START_NOT_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("CheckResult")
|
|
||||||
private fun setupUpdater() {
|
|
||||||
updater.subscribeOn(Schedulers.io()).observeOn(Schedulers.io()).subscribe(
|
|
||||||
{
|
|
||||||
synchronized(lock) { it.invoke() }
|
|
||||||
},
|
|
||||||
Throwable::printStackTrace
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts monitoring ongoing downloads using a periodic observable.
|
|
||||||
* This method sets up an observable that runs every 5 seconds to check the status of downloads.
|
|
||||||
* It only starts the monitoring process if it's not already running and disposes of the observable
|
|
||||||
* when there are no ongoing downloads to avoid unnecessary resource usage.
|
|
||||||
*/
|
|
||||||
@Suppress("MagicNumber")
|
|
||||||
private fun startMonitoringDownloads() {
|
|
||||||
// Check if monitoring is already active. If it is, do nothing.
|
|
||||||
if (monitoringDisposable?.isDisposed == false) return
|
|
||||||
monitoringDisposable = Observable.interval(ZERO.toLong(), 5, TimeUnit.SECONDS)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(Schedulers.io())
|
|
||||||
.subscribe(
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
synchronized(lock) {
|
|
||||||
if (downloadRoomDao.downloads().blockingFirst().isNotEmpty()) {
|
|
||||||
checkDownloads()
|
|
||||||
} else {
|
|
||||||
stopForegroundServiceForDownloads()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (ignore: Exception) {
|
|
||||||
Log.e(
|
|
||||||
"DOWNLOAD_MONITOR",
|
|
||||||
"Couldn't get the downloads update. Original exception = $ignore"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Throwable::printStackTrace
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("Range")
|
|
||||||
private fun checkDownloads() {
|
|
||||||
synchronized(lock) {
|
|
||||||
val query = DownloadManager.Query().setFilterByStatus(
|
|
||||||
DownloadManager.STATUS_RUNNING or
|
|
||||||
DownloadManager.STATUS_PAUSED or
|
|
||||||
DownloadManager.STATUS_PENDING or
|
|
||||||
DownloadManager.STATUS_SUCCESSFUL
|
|
||||||
)
|
|
||||||
downloadManager.query(query).use { cursor ->
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
do {
|
|
||||||
val downloadId = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_ID))
|
|
||||||
queryDownloadStatus(downloadId)
|
|
||||||
} while (cursor.moveToNext())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("Range")
|
|
||||||
fun queryDownloadStatus(downloadId: Long) {
|
|
||||||
synchronized(lock) {
|
|
||||||
downloadManager.query(DownloadManager.Query().setFilterById(downloadId)).use { cursor ->
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
handleDownloadStatus(cursor, downloadId)
|
|
||||||
} else {
|
|
||||||
handleCancelledDownload(downloadId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("Range")
|
|
||||||
private fun handleDownloadStatus(cursor: Cursor, downloadId: Long) {
|
|
||||||
val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
|
||||||
val reason = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_REASON))
|
|
||||||
val bytesDownloaded =
|
|
||||||
cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
|
|
||||||
val totalBytes = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
|
|
||||||
val progress = calculateProgress(bytesDownloaded, totalBytes)
|
|
||||||
|
|
||||||
val etaInMilliSeconds = calculateETA(downloadId, bytesDownloaded, totalBytes)
|
|
||||||
|
|
||||||
when (status) {
|
|
||||||
DownloadManager.STATUS_FAILED -> handleFailedDownload(
|
|
||||||
downloadId,
|
|
||||||
reason,
|
|
||||||
progress,
|
|
||||||
etaInMilliSeconds,
|
|
||||||
bytesDownloaded,
|
|
||||||
totalBytes
|
|
||||||
)
|
|
||||||
|
|
||||||
DownloadManager.STATUS_PAUSED -> handlePausedDownload(
|
|
||||||
downloadId,
|
|
||||||
progress,
|
|
||||||
bytesDownloaded,
|
|
||||||
totalBytes,
|
|
||||||
reason
|
|
||||||
)
|
|
||||||
|
|
||||||
DownloadManager.STATUS_PENDING -> handlePendingDownload(downloadId)
|
|
||||||
DownloadManager.STATUS_RUNNING -> handleRunningDownload(
|
|
||||||
downloadId,
|
|
||||||
progress,
|
|
||||||
etaInMilliSeconds,
|
|
||||||
bytesDownloaded,
|
|
||||||
totalBytes
|
|
||||||
)
|
|
||||||
|
|
||||||
DownloadManager.STATUS_SUCCESSFUL -> handleSuccessfulDownload(
|
|
||||||
downloadId,
|
|
||||||
progress,
|
|
||||||
etaInMilliSeconds
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleCancelledDownload(downloadId: Long) {
|
|
||||||
updater.onNext {
|
|
||||||
updateDownloadStatus(downloadId, Status.CANCELLED, Error.CANCELLED)
|
|
||||||
downloadRoomDao.delete(downloadId)
|
|
||||||
downloadInfoMap.remove(downloadId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
|
||||||
private fun handleFailedDownload(
|
|
||||||
downloadId: Long,
|
|
||||||
reason: Int,
|
|
||||||
progress: Int,
|
|
||||||
etaInMilliSeconds: Long,
|
|
||||||
bytesDownloaded: Long,
|
|
||||||
totalBytes: Long
|
|
||||||
) {
|
|
||||||
val error = mapDownloadError(reason)
|
|
||||||
updateDownloadStatus(
|
|
||||||
downloadId,
|
|
||||||
Status.FAILED,
|
|
||||||
error,
|
|
||||||
progress,
|
|
||||||
etaInMilliSeconds,
|
|
||||||
bytesDownloaded,
|
|
||||||
totalBytes
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handlePausedDownload(
|
|
||||||
downloadId: Long,
|
|
||||||
progress: Int,
|
|
||||||
bytesDownloaded: Long,
|
|
||||||
totalSizeOfDownload: Long,
|
|
||||||
reason: Int
|
|
||||||
) {
|
|
||||||
val pauseReason = mapDownloadPauseReason(reason)
|
|
||||||
updateDownloadStatus(
|
|
||||||
downloadId = downloadId,
|
|
||||||
status = Status.PAUSED,
|
|
||||||
error = pauseReason,
|
|
||||||
progress = progress,
|
|
||||||
bytesDownloaded = bytesDownloaded,
|
|
||||||
totalSizeOfDownload = totalSizeOfDownload
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handlePendingDownload(downloadId: Long) {
|
|
||||||
updateDownloadStatus(
|
|
||||||
downloadId,
|
|
||||||
Status.QUEUED,
|
|
||||||
Error.NONE
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleRunningDownload(
|
|
||||||
downloadId: Long,
|
|
||||||
progress: Int,
|
|
||||||
etaInMilliSeconds: Long,
|
|
||||||
bytesDownloaded: Long,
|
|
||||||
totalSizeOfDownload: Long
|
|
||||||
) {
|
|
||||||
updateDownloadStatus(
|
|
||||||
downloadId,
|
|
||||||
Status.DOWNLOADING,
|
|
||||||
Error.NONE,
|
|
||||||
progress,
|
|
||||||
etaInMilliSeconds,
|
|
||||||
bytesDownloaded,
|
|
||||||
totalSizeOfDownload
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleSuccessfulDownload(
|
|
||||||
downloadId: Long,
|
|
||||||
progress: Int,
|
|
||||||
etaInMilliSeconds: Long
|
|
||||||
) {
|
|
||||||
updateDownloadStatus(
|
|
||||||
downloadId,
|
|
||||||
Status.COMPLETED,
|
|
||||||
Error.NONE,
|
|
||||||
progress,
|
|
||||||
etaInMilliSeconds
|
|
||||||
)
|
|
||||||
downloadInfoMap.remove(downloadId)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun calculateProgress(bytesDownloaded: Long, totalBytes: Long): Int =
|
|
||||||
if (totalBytes > ZERO) {
|
|
||||||
(bytesDownloaded / totalBytes.toDouble()).times(HUNDERED).toInt()
|
|
||||||
} else {
|
|
||||||
ZERO
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun calculateETA(downloadedFileId: Long, bytesDownloaded: Long, totalBytes: Long): Long {
|
|
||||||
val currentTime = System.currentTimeMillis()
|
|
||||||
val downloadInfo = downloadInfoMap.getOrPut(downloadedFileId) {
|
|
||||||
DownloadInfo(startTime = currentTime, initialBytesDownloaded = bytesDownloaded)
|
|
||||||
}
|
|
||||||
|
|
||||||
val elapsedTime = currentTime - downloadInfo.startTime
|
|
||||||
val downloadSpeed = if (elapsedTime > ZERO) {
|
|
||||||
(bytesDownloaded - downloadInfo.initialBytesDownloaded) / (elapsedTime / THOUSAND.toFloat())
|
|
||||||
} else {
|
|
||||||
ZERO.toFloat()
|
|
||||||
}
|
|
||||||
|
|
||||||
return if (downloadSpeed > ZERO) {
|
|
||||||
((totalBytes - bytesDownloaded) / downloadSpeed).toLong() * THOUSAND
|
|
||||||
} else {
|
|
||||||
ZERO.toLong()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mapDownloadError(reason: Int): Error {
|
|
||||||
return when (reason) {
|
|
||||||
DownloadManager.ERROR_CANNOT_RESUME -> Error.ERROR_CANNOT_RESUME
|
|
||||||
DownloadManager.ERROR_DEVICE_NOT_FOUND -> Error.ERROR_DEVICE_NOT_FOUND
|
|
||||||
DownloadManager.ERROR_FILE_ALREADY_EXISTS -> Error.ERROR_FILE_ALREADY_EXISTS
|
|
||||||
DownloadManager.ERROR_FILE_ERROR -> Error.ERROR_FILE_ERROR
|
|
||||||
DownloadManager.ERROR_HTTP_DATA_ERROR -> Error.ERROR_HTTP_DATA_ERROR
|
|
||||||
DownloadManager.ERROR_INSUFFICIENT_SPACE -> Error.ERROR_INSUFFICIENT_SPACE
|
|
||||||
DownloadManager.ERROR_TOO_MANY_REDIRECTS -> Error.ERROR_TOO_MANY_REDIRECTS
|
|
||||||
DownloadManager.ERROR_UNHANDLED_HTTP_CODE -> Error.ERROR_UNHANDLED_HTTP_CODE
|
|
||||||
DownloadManager.ERROR_UNKNOWN -> Error.UNKNOWN
|
|
||||||
else -> Error.UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mapDownloadPauseReason(reason: Int): Error {
|
|
||||||
return when (reason) {
|
|
||||||
DownloadManager.PAUSED_QUEUED_FOR_WIFI -> Error.QUEUED_FOR_WIFI
|
|
||||||
DownloadManager.PAUSED_WAITING_TO_RETRY -> Error.WAITING_TO_RETRY
|
|
||||||
DownloadManager.PAUSED_WAITING_FOR_NETWORK -> Error.WAITING_FOR_NETWORK
|
|
||||||
DownloadManager.PAUSED_UNKNOWN -> Error.PAUSED_UNKNOWN
|
|
||||||
else -> Error.PAUSED_UNKNOWN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
|
||||||
private fun updateDownloadStatus(
|
|
||||||
downloadId: Long,
|
|
||||||
status: Status,
|
|
||||||
error: Error,
|
|
||||||
progress: Int = DEFAULT_INT_VALUE,
|
|
||||||
etaInMilliSeconds: Long = DEFAULT_INT_VALUE.toLong(),
|
|
||||||
bytesDownloaded: Long = DEFAULT_INT_VALUE.toLong(),
|
|
||||||
totalSizeOfDownload: Long = DEFAULT_INT_VALUE.toLong()
|
|
||||||
) {
|
|
||||||
synchronized(lock) {
|
|
||||||
updater.onNext {
|
|
||||||
downloadRoomDao.getEntityForDownloadId(downloadId)?.let { downloadEntity ->
|
|
||||||
if (shouldUpdateDownloadStatus(downloadEntity)) {
|
|
||||||
val downloadModel = DownloadModel(downloadEntity).apply {
|
|
||||||
if (shouldUpdateDownloadStatus(status, error, downloadEntity)) {
|
|
||||||
state = status
|
|
||||||
}
|
|
||||||
this.error = error
|
|
||||||
if (progress > ZERO) {
|
|
||||||
this.progress = progress
|
|
||||||
}
|
|
||||||
this.etaInMilliSeconds = etaInMilliSeconds
|
|
||||||
if (bytesDownloaded != DEFAULT_INT_VALUE.toLong()) {
|
|
||||||
this.bytesDownloaded = bytesDownloaded
|
|
||||||
}
|
|
||||||
if (totalSizeOfDownload != DEFAULT_INT_VALUE.toLong()) {
|
|
||||||
this.totalSizeOfDownload = totalSizeOfDownload
|
|
||||||
}
|
|
||||||
}
|
|
||||||
downloadRoomDao.update(downloadModel)
|
|
||||||
updateNotification(downloadModel, downloadEntity.title, downloadEntity.description)
|
|
||||||
return@let
|
|
||||||
}
|
|
||||||
cancelNotificationAndAssignNewNotificationToForegroundService(downloadId)
|
|
||||||
} ?: run {
|
|
||||||
// already downloaded/cancelled so cancel the notification if any running, and
|
|
||||||
// assign new notification to foreground service.
|
|
||||||
cancelNotificationAndAssignNewNotificationToForegroundService(downloadId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines whether the download status should be updated based on the current status and error.
|
|
||||||
*
|
|
||||||
* This method checks the current download status and error, and decides whether to update the status
|
|
||||||
* of the download entity. Specifically, it handles the case where a download is paused but has been
|
|
||||||
* queued for resumption. In such cases, it ensures that the download manager is instructed to resume
|
|
||||||
* the download, and prevents the status from being prematurely updated to "Paused".
|
|
||||||
*
|
|
||||||
* @param status The current status of the download.
|
|
||||||
* @param error The current error state of the download.
|
|
||||||
* @param downloadRoomEntity The download entity containing the current status and download ID.
|
|
||||||
* @return `true` if the status should be updated, `false` otherwise.
|
|
||||||
*/
|
|
||||||
private fun shouldUpdateDownloadStatus(
|
|
||||||
status: Status,
|
|
||||||
error: Error,
|
|
||||||
downloadRoomEntity: DownloadRoomEntity
|
|
||||||
): Boolean {
|
|
||||||
synchronized(lock) {
|
|
||||||
return@shouldUpdateDownloadStatus if (
|
|
||||||
status == Status.PAUSED &&
|
|
||||||
downloadRoomEntity.status == Status.QUEUED
|
|
||||||
) {
|
|
||||||
// Check if the user has resumed the download.
|
|
||||||
// Do not update the download status immediately since the download manager
|
|
||||||
// takes some time to actually resume the download. During this time,
|
|
||||||
// it will still return the paused state.
|
|
||||||
// By not updating the status right away, we ensure that the user
|
|
||||||
// sees the "Pending" state, indicating that the download is in the process
|
|
||||||
// of resuming.
|
|
||||||
when (error) {
|
|
||||||
// When the pause reason is unknown or waiting to retry, and the user
|
|
||||||
// resumes the download, attempt to resume the download if it was not resumed
|
|
||||||
// due to some reason.
|
|
||||||
Error.PAUSED_UNKNOWN,
|
|
||||||
Error.WAITING_TO_RETRY -> {
|
|
||||||
resumeDownload(downloadRoomEntity.downloadId)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return true to update the status of the download if there is any other status,
|
|
||||||
// e.g., WAITING_FOR_WIFI, WAITING_FOR_NETWORK, or any other pause reason
|
|
||||||
// to inform the user.
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cancelNotificationAndAssignNewNotificationToForegroundService(downloadId: Long) {
|
|
||||||
downloadNotificationManager.cancelNotification(downloadId.toInt())
|
|
||||||
updateForegroundNotificationOrStopService()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateForegroundNotificationOrStopService() {
|
|
||||||
val activeDownloads = getActiveDownloads()
|
|
||||||
if (activeDownloads.isNotEmpty()) {
|
|
||||||
// Promote the first active download to foreground
|
|
||||||
val downloadRoomEntity = activeDownloads.first()
|
|
||||||
foreGroundServiceInformation =
|
|
||||||
foreGroundServiceInformation.first to downloadRoomEntity.downloadId.toInt()
|
|
||||||
val downloadNotificationModel =
|
|
||||||
getDownloadNotificationModel(
|
|
||||||
DownloadModel(downloadRoomEntity),
|
|
||||||
downloadRoomEntity.title,
|
|
||||||
downloadRoomEntity.description
|
|
||||||
)
|
|
||||||
val notification = downloadNotificationManager.createNotification(downloadNotificationModel)
|
|
||||||
startForeground(foreGroundServiceInformation.second, notification)
|
|
||||||
} else {
|
|
||||||
// Stop the service if no active downloads remain
|
|
||||||
stopForegroundServiceForDownloads()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getActiveDownloads(): List<DownloadRoomEntity> =
|
|
||||||
downloadRoomDao.downloadRoomEntity().blockingFirst().filter {
|
|
||||||
it.status != Status.PAUSED && it.status != Status.CANCELLED
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateNotification(
|
|
||||||
downloadModel: DownloadModel,
|
|
||||||
title: String,
|
|
||||||
description: String?
|
|
||||||
) {
|
|
||||||
val downloadNotificationModel = getDownloadNotificationModel(downloadModel, title, description)
|
|
||||||
val notification = downloadNotificationManager.createNotification(downloadNotificationModel)
|
|
||||||
if (foreGroundServiceInformation.first) {
|
|
||||||
startForeground(downloadModel.downloadId.toInt(), notification)
|
|
||||||
foreGroundServiceInformation = false to downloadModel.downloadId.toInt()
|
|
||||||
} else {
|
|
||||||
downloadNotificationManager.updateNotification(
|
|
||||||
downloadNotificationModel,
|
|
||||||
object : AssignNewForegroundServiceNotification {
|
|
||||||
override fun assignNewForegroundServiceNotification(downloadId: Long) {
|
|
||||||
cancelNotificationAndAssignNewNotificationToForegroundService(downloadId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getDownloadNotificationModel(
|
|
||||||
downloadModel: DownloadModel,
|
|
||||||
title: String,
|
|
||||||
description: String?
|
|
||||||
): DownloadNotificationModel =
|
|
||||||
DownloadNotificationModel(
|
|
||||||
downloadId = downloadModel.downloadId.toInt(),
|
|
||||||
status = downloadModel.state,
|
|
||||||
progress = downloadModel.progress,
|
|
||||||
etaInMilliSeconds = downloadModel.etaInMilliSeconds,
|
|
||||||
title = title,
|
|
||||||
description = description,
|
|
||||||
filePath = downloadModel.file,
|
|
||||||
error = DownloadState.from(
|
|
||||||
downloadModel.state,
|
|
||||||
downloadModel.error,
|
|
||||||
downloadModel.book.url
|
|
||||||
).toReadableState(this).toString()
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun pauseDownload(downloadId: Long) {
|
|
||||||
synchronized(lock) {
|
|
||||||
updater.onNext {
|
|
||||||
if (pauseResumeDownloadInDownloadManagerContentResolver(
|
|
||||||
downloadId,
|
|
||||||
CONTROL_PAUSE,
|
|
||||||
STATUS_PAUSED_BY_APP
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
updateDownloadStatus(downloadId, Status.PAUSED, Error.NONE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun resumeDownload(downloadId: Long) {
|
|
||||||
synchronized(lock) {
|
|
||||||
updater.onNext {
|
|
||||||
if (pauseResumeDownloadInDownloadManagerContentResolver(
|
|
||||||
downloadId,
|
|
||||||
CONTROL_RUN,
|
|
||||||
STATUS_RUNNING
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
updateDownloadStatus(downloadId, Status.QUEUED, Error.NONE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cancelDownload(downloadId: Long) {
|
|
||||||
synchronized(lock) {
|
|
||||||
updater.onNext {
|
|
||||||
// Remove the download from DownloadManager on IO thread.
|
|
||||||
downloadManager.remove(downloadId)
|
|
||||||
handleCancelledDownload(downloadId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("Range")
|
|
||||||
private fun pauseResumeDownloadInDownloadManagerContentResolver(
|
|
||||||
downloadId: Long,
|
|
||||||
control: Int,
|
|
||||||
status: Int
|
|
||||||
): Boolean {
|
|
||||||
return try {
|
|
||||||
// Update the status to paused/resumed in the database
|
|
||||||
val contentValues = ContentValues().apply {
|
|
||||||
put(COLUMN_CONTROL, control)
|
|
||||||
put(DownloadManager.COLUMN_STATUS, status)
|
|
||||||
}
|
|
||||||
val uri = ContentUris.withAppendedId(downloadBaseUri, downloadId)
|
|
||||||
contentResolver
|
|
||||||
.update(uri, contentValues, null, null)
|
|
||||||
true
|
|
||||||
} catch (ignore: Exception) {
|
|
||||||
Log.e("DOWNLOAD_MONITOR", "Couldn't pause/resume the download. Original exception = $ignore")
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun shouldUpdateDownloadStatus(downloadRoomEntity: DownloadRoomEntity) =
|
|
||||||
downloadRoomEntity.status != Status.COMPLETED
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
monitoringDisposable?.dispose()
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun stopForegroundServiceForDownloads() {
|
|
||||||
foreGroundServiceInformation = true to DEFAULT_INT_VALUE
|
|
||||||
monitoringDisposable?.dispose()
|
|
||||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
|
||||||
stopSelf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class DownloadInfo(
|
|
||||||
var startTime: Long,
|
|
||||||
var initialBytesDownloaded: Long
|
|
||||||
)
|
|
||||||
|
|
||||||
interface AssignNewForegroundServiceNotification {
|
|
||||||
fun assignNewForegroundServiceNotification(downloadId: Long)
|
|
||||||
}
|
|
@ -1,259 +0,0 @@
|
|||||||
/*
|
|
||||||
* Kiwix Android
|
|
||||||
* Copyright (c) 2024 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.downloadManager
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Notification
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import org.kiwix.kiwixmobile.core.Intents
|
|
||||||
import org.kiwix.kiwixmobile.core.R
|
|
||||||
import org.kiwix.kiwixmobile.core.downloader.model.Seconds
|
|
||||||
import org.kiwix.kiwixmobile.core.main.CoreMainActivity
|
|
||||||
import org.kiwix.kiwixmobile.core.utils.DEFAULT_NOTIFICATION_TIMEOUT_AFTER
|
|
||||||
import org.kiwix.kiwixmobile.core.utils.DEFAULT_NOTIFICATION_TIMEOUT_AFTER_RESET
|
|
||||||
import org.kiwix.kiwixmobile.core.utils.DOWNLOAD_NOTIFICATION_CHANNEL_ID
|
|
||||||
import java.util.Locale
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
const val DOWNLOAD_NOTIFICATION_TITLE = "OPEN_ZIM_FILE"
|
|
||||||
|
|
||||||
class DownloadNotificationManager @Inject constructor(
|
|
||||||
private val context: Context,
|
|
||||||
private val notificationManager: NotificationManager
|
|
||||||
) {
|
|
||||||
private val downloadNotificationsBuilderMap = mutableMapOf<Int, NotificationCompat.Builder>()
|
|
||||||
private fun createNotificationChannel() {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
if (notificationManager.getNotificationChannel(DOWNLOAD_NOTIFICATION_CHANNEL_ID) == null) {
|
|
||||||
notificationManager.createNotificationChannel(createChannel(context))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateNotification(
|
|
||||||
downloadNotificationModel: DownloadNotificationModel,
|
|
||||||
assignNewForegroundServiceNotification: AssignNewForegroundServiceNotification
|
|
||||||
) {
|
|
||||||
synchronized(downloadNotificationsBuilderMap) {
|
|
||||||
if (shouldUpdateNotification(downloadNotificationModel)) {
|
|
||||||
notificationManager.notify(
|
|
||||||
downloadNotificationModel.downloadId,
|
|
||||||
createNotification(downloadNotificationModel)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// the download is cancelled/paused so remove the notification.
|
|
||||||
assignNewForegroundServiceNotification.assignNewForegroundServiceNotification(
|
|
||||||
downloadNotificationModel.downloadId.toLong()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun createNotification(downloadNotificationModel: DownloadNotificationModel): Notification {
|
|
||||||
synchronized(downloadNotificationsBuilderMap) {
|
|
||||||
createNotificationChannel()
|
|
||||||
val notificationBuilder = getNotificationBuilder(downloadNotificationModel.downloadId)
|
|
||||||
val smallIcon = if (downloadNotificationModel.progress != HUNDERED) {
|
|
||||||
android.R.drawable.stat_sys_download
|
|
||||||
} else {
|
|
||||||
android.R.drawable.stat_sys_download_done
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationBuilder.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
|
||||||
.setSmallIcon(smallIcon)
|
|
||||||
.setContentTitle(downloadNotificationModel.title)
|
|
||||||
.setContentText(getSubtitleText(context, downloadNotificationModel))
|
|
||||||
.setOngoing(downloadNotificationModel.isOnGoingNotification)
|
|
||||||
.setGroupSummary(false)
|
|
||||||
if (downloadNotificationModel.isFailed || downloadNotificationModel.isCompleted) {
|
|
||||||
notificationBuilder.setProgress(ZERO, ZERO, false)
|
|
||||||
} else {
|
|
||||||
notificationBuilder.setProgress(HUNDERED, downloadNotificationModel.progress, false)
|
|
||||||
}
|
|
||||||
when {
|
|
||||||
downloadNotificationModel.isDownloading ->
|
|
||||||
notificationBuilder.setTimeoutAfter(DEFAULT_NOTIFICATION_TIMEOUT_AFTER)
|
|
||||||
.addAction(
|
|
||||||
R.drawable.ic_baseline_stop,
|
|
||||||
context.getString(R.string.cancel),
|
|
||||||
getActionPendingIntent(ACTION_CANCEL, downloadNotificationModel.downloadId)
|
|
||||||
).addAction(
|
|
||||||
R.drawable.ic_baseline_pause,
|
|
||||||
getPauseOrResumeTitle(true),
|
|
||||||
getActionPendingIntent(ACTION_PAUSE, downloadNotificationModel.downloadId)
|
|
||||||
)
|
|
||||||
|
|
||||||
downloadNotificationModel.isPaused ->
|
|
||||||
notificationBuilder.setTimeoutAfter(DEFAULT_NOTIFICATION_TIMEOUT_AFTER)
|
|
||||||
.addAction(
|
|
||||||
R.drawable.ic_baseline_stop,
|
|
||||||
context.getString(R.string.cancel),
|
|
||||||
getActionPendingIntent(ACTION_CANCEL, downloadNotificationModel.downloadId)
|
|
||||||
).addAction(
|
|
||||||
R.drawable.ic_baseline_play,
|
|
||||||
getPauseOrResumeTitle(false),
|
|
||||||
getActionPendingIntent(ACTION_RESUME, downloadNotificationModel.downloadId)
|
|
||||||
)
|
|
||||||
|
|
||||||
downloadNotificationModel.isQueued ->
|
|
||||||
notificationBuilder.setTimeoutAfter(DEFAULT_NOTIFICATION_TIMEOUT_AFTER)
|
|
||||||
|
|
||||||
else -> notificationBuilder.setTimeoutAfter(DEFAULT_NOTIFICATION_TIMEOUT_AFTER_RESET)
|
|
||||||
}
|
|
||||||
notificationCustomisation(downloadNotificationModel, notificationBuilder, context)
|
|
||||||
return@createNotification notificationBuilder.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getPauseOrResumeTitle(isPause: Boolean): String {
|
|
||||||
val pauseOrResumeTitle = if (isPause) {
|
|
||||||
context.getString(R.string.tts_pause)
|
|
||||||
} else {
|
|
||||||
context.getString(R.string.tts_resume)
|
|
||||||
}
|
|
||||||
return pauseOrResumeTitle.replaceFirstChar {
|
|
||||||
if (it.isLowerCase()) {
|
|
||||||
it.titlecase(Locale.ROOT)
|
|
||||||
} else {
|
|
||||||
"$it"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun shouldUpdateNotification(
|
|
||||||
downloadNotificationModel: DownloadNotificationModel
|
|
||||||
): Boolean = !downloadNotificationModel.isCancelled && !downloadNotificationModel.isPaused
|
|
||||||
|
|
||||||
@SuppressLint("UnspecifiedImmutableFlag")
|
|
||||||
private fun notificationCustomisation(
|
|
||||||
downloadNotificationModel: DownloadNotificationModel,
|
|
||||||
notificationBuilder: NotificationCompat.Builder,
|
|
||||||
context: Context
|
|
||||||
) {
|
|
||||||
if (downloadNotificationModel.isCompleted) {
|
|
||||||
val internal = Intents.internal(CoreMainActivity::class.java).apply {
|
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
putExtra(DOWNLOAD_NOTIFICATION_TITLE, downloadNotificationModel.filePath)
|
|
||||||
}
|
|
||||||
val pendingIntent =
|
|
||||||
PendingIntent.getActivity(
|
|
||||||
context,
|
|
||||||
ZERO,
|
|
||||||
internal,
|
|
||||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
)
|
|
||||||
notificationBuilder.setContentIntent(pendingIntent)
|
|
||||||
notificationBuilder.setAutoCancel(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
private fun getNotificationBuilder(notificationId: Int): NotificationCompat.Builder {
|
|
||||||
synchronized(downloadNotificationsBuilderMap) {
|
|
||||||
val notificationBuilder = downloadNotificationsBuilderMap[notificationId]
|
|
||||||
?: NotificationCompat.Builder(context, DOWNLOAD_NOTIFICATION_CHANNEL_ID)
|
|
||||||
downloadNotificationsBuilderMap[notificationId] = notificationBuilder
|
|
||||||
notificationBuilder
|
|
||||||
.setGroup("$notificationId")
|
|
||||||
.setStyle(null)
|
|
||||||
.setProgress(ZERO, ZERO, false)
|
|
||||||
.setContentTitle(null)
|
|
||||||
.setContentText(null)
|
|
||||||
.setContentIntent(null)
|
|
||||||
.setGroupSummary(false)
|
|
||||||
.setTimeoutAfter(DEFAULT_NOTIFICATION_TIMEOUT_AFTER_RESET)
|
|
||||||
.setOngoing(false)
|
|
||||||
.setOnlyAlertOnce(true)
|
|
||||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
|
||||||
.mActions.clear()
|
|
||||||
return@getNotificationBuilder notificationBuilder
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getActionPendingIntent(action: String, downloadId: Int): PendingIntent {
|
|
||||||
val pendingIntent =
|
|
||||||
Intent(context, DownloadMonitorService::class.java).apply {
|
|
||||||
putExtra(NOTIFICATION_ACTION, action)
|
|
||||||
putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
|
||||||
}
|
|
||||||
val requestCode = downloadId + action.hashCode()
|
|
||||||
return PendingIntent.getService(
|
|
||||||
context,
|
|
||||||
requestCode,
|
|
||||||
pendingIntent,
|
|
||||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
private fun createChannel(context: Context) =
|
|
||||||
NotificationChannel(
|
|
||||||
DOWNLOAD_NOTIFICATION_CHANNEL_ID,
|
|
||||||
context.getString(R.string.download_notification_channel_name),
|
|
||||||
NotificationManager.IMPORTANCE_HIGH
|
|
||||||
).apply {
|
|
||||||
setSound(null, null)
|
|
||||||
enableVibration(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSubtitleText(
|
|
||||||
context: Context,
|
|
||||||
downloadNotificationModel: DownloadNotificationModel
|
|
||||||
): String {
|
|
||||||
return when {
|
|
||||||
downloadNotificationModel.isCompleted -> context.getString(R.string.complete)
|
|
||||||
downloadNotificationModel.isFailed -> context.getString(
|
|
||||||
R.string.failed_state,
|
|
||||||
downloadNotificationModel.error
|
|
||||||
)
|
|
||||||
|
|
||||||
downloadNotificationModel.isPaused -> context.getString(R.string.paused_state)
|
|
||||||
downloadNotificationModel.isQueued -> context.getString(R.string.pending_state)
|
|
||||||
downloadNotificationModel.etaInMilliSeconds <= ZERO ->
|
|
||||||
context.getString(R.string.running_state)
|
|
||||||
|
|
||||||
else -> Seconds(
|
|
||||||
downloadNotificationModel.etaInMilliSeconds / THOUSAND.toLong()
|
|
||||||
).toHumanReadableTime()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancelNotification(notificationId: Int) {
|
|
||||||
synchronized(downloadNotificationsBuilderMap) {
|
|
||||||
notificationManager.cancel(notificationId)
|
|
||||||
downloadNotificationsBuilderMap.remove(notificationId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val NOTIFICATION_ACTION = "notification_action"
|
|
||||||
const val ACTION_PAUSE = "action_pause"
|
|
||||||
const val ACTION_RESUME = "action_resume"
|
|
||||||
const val ACTION_CANCEL = "action_cancel"
|
|
||||||
const val ACTION_QUERY_DOWNLOAD_STATUS = "action_query_download_status"
|
|
||||||
const val EXTRA_DOWNLOAD_ID = "extra_download_id"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
/*
|
|
||||||
* Kiwix Android
|
|
||||||
* Copyright (c) 2024 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.downloadManager
|
|
||||||
|
|
||||||
data class DownloadNotificationModel(
|
|
||||||
val downloadId: Int,
|
|
||||||
val status: Status = Status.NONE,
|
|
||||||
val progress: Int,
|
|
||||||
val etaInMilliSeconds: Long,
|
|
||||||
val title: String,
|
|
||||||
val description: String?,
|
|
||||||
val filePath: String?,
|
|
||||||
val error: String
|
|
||||||
) {
|
|
||||||
val isPaused get() = status == Status.PAUSED
|
|
||||||
val isCompleted get() = status == Status.COMPLETED
|
|
||||||
val isFailed get() = status == Status.FAILED
|
|
||||||
val isQueued get() = status == Status.QUEUED
|
|
||||||
val isDownloading get() = status == Status.DOWNLOADING
|
|
||||||
val isCancelled get() = status == Status.CANCELLED
|
|
||||||
val isOnGoingNotification: Boolean
|
|
||||||
get() {
|
|
||||||
return when (status) {
|
|
||||||
Status.QUEUED,
|
|
||||||
Status.DOWNLOADING -> true
|
|
||||||
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -33,7 +33,8 @@ data class DownloadModel(
|
|||||||
var state: Status,
|
var state: Status,
|
||||||
var error: Error,
|
var error: Error,
|
||||||
var progress: Int,
|
var progress: Int,
|
||||||
val book: Book
|
val book: Book,
|
||||||
|
var pausedByUser: Boolean
|
||||||
) {
|
) {
|
||||||
val bytesRemaining: Long by lazy { totalSizeOfDownload - bytesDownloaded }
|
val bytesRemaining: Long by lazy { totalSizeOfDownload - bytesDownloaded }
|
||||||
val fileNameFromUrl: String by lazy { StorageUtils.getFileNameFromUrl(book.url) }
|
val fileNameFromUrl: String by lazy { StorageUtils.getFileNameFromUrl(book.url) }
|
||||||
@ -48,6 +49,7 @@ data class DownloadModel(
|
|||||||
downloadEntity.status,
|
downloadEntity.status,
|
||||||
downloadEntity.error,
|
downloadEntity.error,
|
||||||
downloadEntity.progress,
|
downloadEntity.progress,
|
||||||
downloadEntity.toBook()
|
downloadEntity.toBook(),
|
||||||
|
downloadEntity.pausedByUser
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
|
|||||||
import org.kiwix.kiwixmobile.core.utils.StorageUtils
|
import org.kiwix.kiwixmobile.core.utils.StorageUtils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
data class DownloadRequest(val urlString: String) {
|
data class DownloadRequest(val urlString: String, val bookTitle: String) {
|
||||||
|
|
||||||
val uri: Uri get() = Uri.parse(urlString)
|
val uri: Uri get() = Uri.parse(urlString)
|
||||||
|
|
||||||
|
@ -192,11 +192,12 @@ class SearchFragment : BaseFragment() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UnnecessarySafeCall")
|
||||||
private fun setupToolbar(view: View) {
|
private fun setupToolbar(view: View) {
|
||||||
view.post {
|
view.post {
|
||||||
with(requireActivity() as CoreMainActivity) {
|
with(activity as? CoreMainActivity) {
|
||||||
setSupportActionBar(view.findViewById(R.id.toolbar))
|
this?.setSupportActionBar(view.findViewById(R.id.toolbar))
|
||||||
supportActionBar?.apply {
|
this?.supportActionBar?.apply {
|
||||||
setHomeButtonEnabled(true)
|
setHomeButtonEnabled(true)
|
||||||
title = getString(R.string.menu_search_in_text)
|
title = getString(R.string.menu_search_in_text)
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ fun downloadModel(
|
|||||||
book: Book = book()
|
book: Book = book()
|
||||||
) = DownloadModel(
|
) = DownloadModel(
|
||||||
databaseId, downloadId, file, etaInMilliSeconds, bytesDownloaded, totalSizeOfDownload,
|
databaseId, downloadId, file, etaInMilliSeconds, bytesDownloaded, totalSizeOfDownload,
|
||||||
status, error, progress, book
|
status, error, progress, book, false
|
||||||
)
|
)
|
||||||
|
|
||||||
fun downloadItem(
|
fun downloadItem(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user