mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-08-03 10:46:53 -04:00
Merge pull request #4108 from kiwix/Fixes#4106
Fixed: Download notification was disappearing when the application is in background.
This commit is contained in:
commit
308fe39d8c
@ -8,9 +8,6 @@
|
||||
tools:ignore="CoarseFineLocation" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
<uses-permission android:name="${permission}" />
|
||||
<!-- 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" />
|
||||
<uses-permission
|
||||
android:name="android.permission.NEARBY_WIFI_DEVICES"
|
||||
android:usesPermissionFlags="neverForLocation"
|
||||
|
@ -24,7 +24,6 @@ import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import org.kiwix.kiwixmobile.core.qr.GenerateQR
|
||||
import org.kiwix.kiwixmobile.core.read_aloud.ReadAloudNotificationManger
|
||||
import org.kiwix.kiwixmobile.di.ServiceScope
|
||||
import org.kiwix.kiwixmobile.webserver.KiwixServer
|
||||
import org.kiwix.kiwixmobile.webserver.WebServerHelper
|
||||
@ -34,12 +33,6 @@ import org.kiwix.kiwixmobile.webserver.wifi_hotspot.IpAddressCallbacks
|
||||
|
||||
@Module
|
||||
class ServiceModule {
|
||||
@Provides
|
||||
@ServiceScope
|
||||
fun providesReadAloudNotificationManager(
|
||||
notificationManager: NotificationManager,
|
||||
context: Context
|
||||
): ReadAloudNotificationManger = ReadAloudNotificationManger(notificationManager, context)
|
||||
|
||||
@Provides
|
||||
@ServiceScope
|
||||
|
@ -16,6 +16,9 @@
|
||||
<uses-permission
|
||||
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>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.TTS_SERVICE" />
|
||||
@ -90,5 +93,8 @@
|
||||
android:name=".error.DiagnosticReportActivity"
|
||||
android:exported="false" />
|
||||
<service android:name=".read_aloud.ReadAloudService" />
|
||||
<service
|
||||
android:name=".downloader.downloadManager.DownloadMonitorService"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
@ -52,7 +52,6 @@ import org.kiwix.kiwixmobile.core.di.modules.NetworkModule
|
||||
import org.kiwix.kiwixmobile.core.di.modules.SearchModule
|
||||
import org.kiwix.kiwixmobile.core.downloader.Downloader
|
||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadManagerBroadcastReceiver
|
||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationActionsBroadcastReceiver
|
||||
import org.kiwix.kiwixmobile.core.error.ErrorActivity
|
||||
import org.kiwix.kiwixmobile.core.main.KiwixWebView
|
||||
import org.kiwix.kiwixmobile.core.reader.ZimFileReader
|
||||
@ -117,8 +116,6 @@ interface CoreComponent {
|
||||
fun mutex(): Mutex
|
||||
|
||||
fun downloadManagerBroadCastReceiver(): DownloadManagerBroadcastReceiver
|
||||
fun downloadNotificationActionBroadCastReceiver(): DownloadNotificationActionsBroadcastReceiver
|
||||
|
||||
fun inject(application: CoreApp)
|
||||
fun inject(kiwixWebView: KiwixWebView)
|
||||
fun inject(storageSelectDialog: StorageSelectDialog)
|
||||
|
@ -23,12 +23,14 @@ import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import org.kiwix.kiwixmobile.core.di.CoreServiceScope
|
||||
import org.kiwix.kiwixmobile.core.di.modules.CoreServiceModule
|
||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadMonitorService
|
||||
import org.kiwix.kiwixmobile.core.read_aloud.ReadAloudService
|
||||
|
||||
@Subcomponent(modules = [CoreServiceModule::class])
|
||||
@CoreServiceScope
|
||||
interface CoreServiceComponent {
|
||||
fun inject(readAloudService: ReadAloudService)
|
||||
fun inject(downloadMonitorService: DownloadMonitorService)
|
||||
|
||||
@Subcomponent.Builder
|
||||
interface Builder {
|
||||
|
@ -30,7 +30,6 @@ import org.kiwix.kiwixmobile.core.downloader.DownloaderImpl
|
||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadManagerBroadcastReceiver
|
||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadManagerMonitor
|
||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadManagerRequester
|
||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationActionsBroadcastReceiver
|
||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager
|
||||
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
|
||||
import javax.inject.Singleton
|
||||
@ -71,13 +70,6 @@ object DownloaderModule {
|
||||
callback: DownloadManagerBroadcastReceiver.Callback
|
||||
): DownloadManagerBroadcastReceiver = DownloadManagerBroadcastReceiver(callback)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesDownloadNotificationActionsBroadcastReceiver(
|
||||
downloadManagerMonitor: DownloadManagerMonitor
|
||||
): DownloadNotificationActionsBroadcastReceiver =
|
||||
DownloadNotificationActionsBroadcastReceiver(downloadManagerMonitor)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesDownloadNotificationManager(
|
||||
|
@ -35,8 +35,8 @@ const val CONNECTION_TIMEOUT = 10L
|
||||
// increase the read and call timeout since the content is 19MB large so it takes
|
||||
// more time to read on slow internet connection, and due to less read timeout
|
||||
// the request is canceled.
|
||||
const val READ_TIMEOUT = 180L
|
||||
const val CALL_TIMEOUT = 180L
|
||||
const val READ_TIMEOUT = 300L
|
||||
const val CALL_TIMEOUT = 300L
|
||||
const val USER_AGENT = "kiwix-android-version:${BuildConfig.VERSION_CODE}"
|
||||
const val KIWIX_DOWNLOAD_URL = "https://mirror.download.kiwix.org/"
|
||||
|
||||
|
@ -18,527 +18,83 @@
|
||||
|
||||
package org.kiwix.kiwixmobile.core.downloader.downloadManager
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.DownloadManager
|
||||
import android.app.DownloadManager.COLUMN_STATUS
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao
|
||||
import org.kiwix.kiwixmobile.core.dao.entities.DownloadRoomEntity
|
||||
import org.kiwix.kiwixmobile.core.downloader.DownloadMonitor
|
||||
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 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 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 DownloadManagerMonitor @Inject constructor(
|
||||
private val downloadManager: DownloadManager,
|
||||
val downloadRoomDao: DownloadRoomDao,
|
||||
private val context: Context,
|
||||
private val downloadNotificationManager: DownloadNotificationManager
|
||||
private val context: Context
|
||||
) : DownloadMonitor, DownloadManagerBroadcastReceiver.Callback {
|
||||
|
||||
private val updater = PublishSubject.create<() -> Unit>()
|
||||
private val lock = Any()
|
||||
private val downloadInfoMap = mutableMapOf<Long, DownloadInfo>()
|
||||
private var monitoringDisposable: Disposable? = null
|
||||
|
||||
init {
|
||||
startMonitoringDownloads()
|
||||
setupUpdater()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
if (getActiveDownloads().isNotEmpty()) {
|
||||
startService()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getActiveDownloads(): List<DownloadRoomEntity> =
|
||||
downloadRoomDao.downloadRoomEntity().blockingFirst().filter {
|
||||
it.status != Status.PAUSED && it.status != Status.CANCELLED
|
||||
}
|
||||
|
||||
override fun downloadCompleteOrCancelled(intent: Intent) {
|
||||
synchronized(lock) {
|
||||
intent.extras?.let {
|
||||
val downloadId = it.getLong(DownloadManager.EXTRA_DOWNLOAD_ID, -1L)
|
||||
if (downloadId != -1L) {
|
||||
queryDownloadStatus(downloadId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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")
|
||||
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 {
|
||||
// dispose to avoid unnecessary request to downloadManager
|
||||
// when there is no download ongoing.
|
||||
monitoringDisposable?.dispose()
|
||||
}
|
||||
}
|
||||
} catch (ignore: Exception) {
|
||||
Log.i(
|
||||
"DOWNLOAD_MONITOR",
|
||||
"Couldn't get the downloads update. Original exception = $ignore"
|
||||
context.startService(
|
||||
getDownloadMonitorIntent(
|
||||
ACTION_QUERY_DOWNLOAD_STATUS,
|
||||
downloadId.toInt()
|
||||
)
|
||||
}
|
||||
},
|
||||
Throwable::printStackTrace
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("CheckResult")
|
||||
private fun setupUpdater() {
|
||||
updater.subscribeOn(Schedulers.io()).observeOn(Schedulers.io()).subscribe(
|
||||
{
|
||||
synchronized(lock) { it.invoke() }
|
||||
},
|
||||
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")
|
||||
private fun queryDownloadStatus(downloadId: Long) {
|
||||
synchronized(lock) {
|
||||
downloadManager.query(DownloadManager.Query().setFilterById(downloadId)).use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
handleDownloadStatus(cursor, downloadId)
|
||||
} else {
|
||||
handleCancelledDownload(downloadId)
|
||||
}
|
||||
}
|
||||
}
|
||||
fun startMonitoringDownloads() {
|
||||
startService()
|
||||
}
|
||||
|
||||
@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.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
|
||||
val totalBytes = cursor.getInt(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: Int,
|
||||
totalBytes: Int
|
||||
) {
|
||||
val error = mapDownloadError(reason)
|
||||
updateDownloadStatus(
|
||||
downloadId,
|
||||
Status.FAILED,
|
||||
error,
|
||||
progress,
|
||||
etaInMilliSeconds,
|
||||
bytesDownloaded,
|
||||
totalBytes
|
||||
)
|
||||
}
|
||||
|
||||
private fun handlePausedDownload(
|
||||
downloadId: Long,
|
||||
progress: Int,
|
||||
bytesDownloaded: Int,
|
||||
totalSizeOfDownload: Int,
|
||||
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: Int,
|
||||
totalSizeOfDownload: Int
|
||||
) {
|
||||
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: Int, totalBytes: Int): Int =
|
||||
if (totalBytes > ZERO) {
|
||||
(bytesDownloaded / totalBytes.toDouble()).times(HUNDERED).toInt()
|
||||
} else {
|
||||
ZERO
|
||||
}
|
||||
|
||||
private fun calculateETA(downloadedFileId: Long, bytesDownloaded: Int, totalBytes: Int): 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: Int = DEFAULT_INT_VALUE,
|
||||
totalSizeOfDownload: Int = DEFAULT_INT_VALUE
|
||||
) {
|
||||
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) {
|
||||
this.bytesDownloaded = bytesDownloaded.toLong()
|
||||
}
|
||||
if (totalSizeOfDownload != DEFAULT_INT_VALUE) {
|
||||
this.totalSizeOfDownload = totalSizeOfDownload.toLong()
|
||||
}
|
||||
}
|
||||
downloadRoomDao.update(downloadModel)
|
||||
updateNotification(downloadModel, downloadEntity.title, downloadEntity.description)
|
||||
return@let
|
||||
}
|
||||
cancelNotification(downloadId)
|
||||
} ?: run {
|
||||
// already downloaded/cancelled so cancel the notification if any running.
|
||||
cancelNotification(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 cancelNotification(downloadId: Long) {
|
||||
downloadNotificationManager.cancelNotification(downloadId.toInt())
|
||||
}
|
||||
|
||||
private fun updateNotification(
|
||||
downloadModel: DownloadModel,
|
||||
title: String,
|
||||
description: String?
|
||||
) {
|
||||
downloadNotificationManager.updateNotification(
|
||||
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(context).toString()
|
||||
)
|
||||
)
|
||||
private fun startService() {
|
||||
context.startService(Intent(context, DownloadMonitorService::class.java))
|
||||
}
|
||||
|
||||
fun pauseDownload(downloadId: Long) {
|
||||
synchronized(lock) {
|
||||
updater.onNext {
|
||||
if (pauseResumeDownloadInDownloadManagerContentResolver(
|
||||
downloadId,
|
||||
CONTROL_PAUSE,
|
||||
STATUS_PAUSED_BY_APP
|
||||
)
|
||||
) {
|
||||
updateDownloadStatus(downloadId, Status.PAUSED, Error.NONE)
|
||||
}
|
||||
}
|
||||
}
|
||||
context.startService(getDownloadMonitorIntent(ACTION_PAUSE, downloadId.toInt()))
|
||||
}
|
||||
|
||||
fun resumeDownload(downloadId: Long) {
|
||||
synchronized(lock) {
|
||||
updater.onNext {
|
||||
if (pauseResumeDownloadInDownloadManagerContentResolver(
|
||||
downloadId,
|
||||
CONTROL_RUN,
|
||||
STATUS_RUNNING
|
||||
)
|
||||
) {
|
||||
updateDownloadStatus(downloadId, Status.QUEUED, Error.NONE)
|
||||
}
|
||||
}
|
||||
}
|
||||
context.startService(getDownloadMonitorIntent(ACTION_RESUME, downloadId.toInt()))
|
||||
}
|
||||
|
||||
fun cancelDownload(downloadId: Long) {
|
||||
synchronized(lock) {
|
||||
downloadManager.remove(downloadId)
|
||||
handleCancelledDownload(downloadId)
|
||||
}
|
||||
context.startService(getDownloadMonitorIntent(ACTION_CANCEL, downloadId.toInt()))
|
||||
}
|
||||
|
||||
@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(COLUMN_STATUS, status)
|
||||
}
|
||||
val uri = ContentUris.withAppendedId(downloadBaseUri, downloadId)
|
||||
context.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 getDownloadMonitorIntent(action: String, downloadId: Int): Intent =
|
||||
Intent(context, DownloadMonitorService::class.java).apply {
|
||||
putExtra(DownloadNotificationManager.NOTIFICATION_ACTION, action)
|
||||
putExtra(DownloadNotificationManager.EXTRA_DOWNLOAD_ID, downloadId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldUpdateDownloadStatus(downloadRoomEntity: DownloadRoomEntity) =
|
||||
downloadRoomEntity.status != Status.COMPLETED
|
||||
|
||||
override fun init() {
|
||||
// empty method to so class does not get reported unused
|
||||
}
|
||||
}
|
||||
|
||||
data class DownloadInfo(
|
||||
var startTime: Long,
|
||||
var initialBytesDownloaded: Int
|
||||
)
|
||||
|
@ -0,0 +1,622 @@
|
||||
/*
|
||||
* 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 -> 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.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
|
||||
val totalBytes = cursor.getInt(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: Int,
|
||||
totalBytes: Int
|
||||
) {
|
||||
val error = mapDownloadError(reason)
|
||||
updateDownloadStatus(
|
||||
downloadId,
|
||||
Status.FAILED,
|
||||
error,
|
||||
progress,
|
||||
etaInMilliSeconds,
|
||||
bytesDownloaded,
|
||||
totalBytes
|
||||
)
|
||||
}
|
||||
|
||||
private fun handlePausedDownload(
|
||||
downloadId: Long,
|
||||
progress: Int,
|
||||
bytesDownloaded: Int,
|
||||
totalSizeOfDownload: Int,
|
||||
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: Int,
|
||||
totalSizeOfDownload: Int
|
||||
) {
|
||||
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: Int, totalBytes: Int): Int =
|
||||
if (totalBytes > ZERO) {
|
||||
(bytesDownloaded / totalBytes.toDouble()).times(HUNDERED).toInt()
|
||||
} else {
|
||||
ZERO
|
||||
}
|
||||
|
||||
private fun calculateETA(downloadedFileId: Long, bytesDownloaded: Int, totalBytes: Int): 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: Int = DEFAULT_INT_VALUE,
|
||||
totalSizeOfDownload: Int = DEFAULT_INT_VALUE
|
||||
) {
|
||||
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) {
|
||||
this.bytesDownloaded = bytesDownloaded.toLong()
|
||||
}
|
||||
if (totalSizeOfDownload != DEFAULT_INT_VALUE) {
|
||||
this.totalSizeOfDownload = totalSizeOfDownload.toLong()
|
||||
}
|
||||
}
|
||||
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) {
|
||||
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: Int
|
||||
)
|
||||
|
||||
interface AssignNewForegroundServiceNotification {
|
||||
fun assignNewForegroundServiceNotification(downloadId: Long)
|
||||
}
|
@ -1,49 +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.content.Context
|
||||
import android.content.Intent
|
||||
import org.kiwix.kiwixmobile.core.base.BaseBroadcastReceiver
|
||||
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_RESUME
|
||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager.Companion.EXTRA_DOWNLOAD_ID
|
||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager.Companion.NOTIFICATION_ACTION
|
||||
import javax.inject.Inject
|
||||
|
||||
const val DOWNLOAD_NOTIFICATION_ACTION = "org.kiwix.kiwixmobile.download_notification_action"
|
||||
|
||||
class DownloadNotificationActionsBroadcastReceiver @Inject constructor(
|
||||
private val downloadManagerMonitor: DownloadManagerMonitor
|
||||
) : BaseBroadcastReceiver() {
|
||||
|
||||
override val action: String = DOWNLOAD_NOTIFICATION_ACTION
|
||||
override fun onIntentWithActionReceived(context: Context, intent: Intent) {
|
||||
val downloadId = intent.getIntExtra(EXTRA_DOWNLOAD_ID, -1)
|
||||
val notificationAction = intent.getStringExtra(NOTIFICATION_ACTION)
|
||||
if (downloadId != -1) {
|
||||
when (notificationAction) {
|
||||
ACTION_PAUSE -> downloadManagerMonitor.pauseDownload(downloadId.toLong())
|
||||
ACTION_RESUME -> downloadManagerMonitor.resumeDownload(downloadId.toLong())
|
||||
ACTION_CANCEL -> downloadManagerMonitor.cancelDownload(downloadId.toLong())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -19,6 +19,7 @@
|
||||
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
|
||||
@ -53,71 +54,80 @@ class DownloadNotificationManager @Inject constructor(
|
||||
}
|
||||
|
||||
fun updateNotification(
|
||||
downloadNotificationModel: DownloadNotificationModel
|
||||
downloadNotificationModel: DownloadNotificationModel,
|
||||
assignNewForegroundServiceNotification: AssignNewForegroundServiceNotification
|
||||
) {
|
||||
synchronized(downloadNotificationsBuilderMap) {
|
||||
if (shouldUpdateNotification(downloadNotificationModel)) {
|
||||
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)
|
||||
notificationManager.notify(
|
||||
downloadNotificationModel.downloadId,
|
||||
notificationBuilder.build()
|
||||
createNotification(downloadNotificationModel)
|
||||
)
|
||||
} else {
|
||||
// the download is cancelled/paused so remove the notification.
|
||||
cancelNotification(downloadNotificationModel.downloadId)
|
||||
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)
|
||||
@ -184,16 +194,16 @@ class DownloadNotificationManager @Inject constructor(
|
||||
}
|
||||
|
||||
private fun getActionPendingIntent(action: String, downloadId: Int): PendingIntent {
|
||||
val intent =
|
||||
Intent(DOWNLOAD_NOTIFICATION_ACTION).apply {
|
||||
val pendingIntent =
|
||||
Intent(context, DownloadMonitorService::class.java).apply {
|
||||
putExtra(NOTIFICATION_ACTION, action)
|
||||
putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
||||
}
|
||||
val requestCode = downloadId + action.hashCode()
|
||||
return PendingIntent.getBroadcast(
|
||||
return PendingIntent.getService(
|
||||
context,
|
||||
requestCode,
|
||||
intent,
|
||||
pendingIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
}
|
||||
@ -203,7 +213,7 @@ class DownloadNotificationManager @Inject constructor(
|
||||
NotificationChannel(
|
||||
DOWNLOAD_NOTIFICATION_CHANNEL_ID,
|
||||
context.getString(R.string.download_notification_channel_name),
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
).apply {
|
||||
setSound(null, null)
|
||||
enableVibration(false)
|
||||
@ -243,6 +253,7 @@ class DownloadNotificationManager @Inject constructor(
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
@ -51,7 +51,6 @@ import org.kiwix.kiwixmobile.core.data.remote.ObjectBoxToLibkiwixMigrator
|
||||
import org.kiwix.kiwixmobile.core.data.remote.ObjectBoxToRoomMigrator
|
||||
import org.kiwix.kiwixmobile.core.di.components.CoreActivityComponent
|
||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadManagerBroadcastReceiver
|
||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationActionsBroadcastReceiver
|
||||
import org.kiwix.kiwixmobile.core.error.ErrorActivity
|
||||
import org.kiwix.kiwixmobile.core.extensions.browserIntent
|
||||
import org.kiwix.kiwixmobile.core.extensions.getToolbarNavigationIcon
|
||||
@ -101,10 +100,6 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
|
||||
|
||||
@Inject
|
||||
lateinit var downloadManagerBroadcastReceiver: DownloadManagerBroadcastReceiver
|
||||
|
||||
@Inject
|
||||
lateinit var downloadNotificationActionsReceiver: DownloadNotificationActionsBroadcastReceiver
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setTheme(R.style.KiwixTheme)
|
||||
super.onCreate(savedInstanceState)
|
||||
@ -136,7 +131,6 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
|
||||
objectBoxToRoomMigrator.migrateObjectBoxDataToRoom()
|
||||
}
|
||||
downloadManagerBroadcastReceiver.let(::registerReceiver)
|
||||
downloadNotificationActionsReceiver.let(::registerReceiver)
|
||||
createApplicationShortcuts()
|
||||
}
|
||||
|
||||
@ -156,7 +150,6 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
|
||||
|
||||
override fun onDestroy() {
|
||||
downloadManagerBroadcastReceiver.let(::unregisterReceiver)
|
||||
downloadNotificationActionsReceiver.let(::unregisterReceiver)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
|
@ -123,7 +123,13 @@ class CustomFileValidator @Inject constructor(private val context: Context) {
|
||||
directoryList.add(dir)
|
||||
}
|
||||
}
|
||||
return scanDirs(directoryList.toTypedArray(), "zim")
|
||||
return scanDirs(directoryList.toTypedArray(), "zim").filterNot {
|
||||
// Excluding the demo.zim file from the list as it is used for demonstration purposes
|
||||
// on the ZimHostFragment for hosting the ZIM file on the server.
|
||||
// Since we are now using the "asset delivery mode", in this we are using the
|
||||
// assetFileDescriptor instead of a regular file.
|
||||
it.name.equals("demo.zim", ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scanDirs(dirs: Array<out File?>?, extensionToMatch: String): List<File> =
|
||||
|
Loading…
x
Reference in New Issue
Block a user