mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-09-08 06:42:21 -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" />
|
tools:ignore="CoarseFineLocation" />
|
||||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||||
<uses-permission android:name="${permission}" />
|
<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
|
<uses-permission
|
||||||
android:name="android.permission.NEARBY_WIFI_DEVICES"
|
android:name="android.permission.NEARBY_WIFI_DEVICES"
|
||||||
android:usesPermissionFlags="neverForLocation"
|
android:usesPermissionFlags="neverForLocation"
|
||||||
|
@ -24,7 +24,6 @@ import android.content.Context
|
|||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import org.kiwix.kiwixmobile.core.qr.GenerateQR
|
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.di.ServiceScope
|
||||||
import org.kiwix.kiwixmobile.webserver.KiwixServer
|
import org.kiwix.kiwixmobile.webserver.KiwixServer
|
||||||
import org.kiwix.kiwixmobile.webserver.WebServerHelper
|
import org.kiwix.kiwixmobile.webserver.WebServerHelper
|
||||||
@ -34,12 +33,6 @@ import org.kiwix.kiwixmobile.webserver.wifi_hotspot.IpAddressCallbacks
|
|||||||
|
|
||||||
@Module
|
@Module
|
||||||
class ServiceModule {
|
class ServiceModule {
|
||||||
@Provides
|
|
||||||
@ServiceScope
|
|
||||||
fun providesReadAloudNotificationManager(
|
|
||||||
notificationManager: NotificationManager,
|
|
||||||
context: Context
|
|
||||||
): ReadAloudNotificationManger = ReadAloudNotificationManger(notificationManager, context)
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@ServiceScope
|
@ServiceScope
|
||||||
|
@ -16,6 +16,9 @@
|
|||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
|
android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
|
||||||
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
|
<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" />
|
||||||
@ -90,5 +93,8 @@
|
|||||||
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>
|
||||||
|
@ -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.di.modules.SearchModule
|
||||||
import org.kiwix.kiwixmobile.core.downloader.Downloader
|
import org.kiwix.kiwixmobile.core.downloader.Downloader
|
||||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadManagerBroadcastReceiver
|
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.error.ErrorActivity
|
||||||
import org.kiwix.kiwixmobile.core.main.KiwixWebView
|
import org.kiwix.kiwixmobile.core.main.KiwixWebView
|
||||||
import org.kiwix.kiwixmobile.core.reader.ZimFileReader
|
import org.kiwix.kiwixmobile.core.reader.ZimFileReader
|
||||||
@ -117,8 +116,6 @@ interface CoreComponent {
|
|||||||
fun mutex(): Mutex
|
fun mutex(): Mutex
|
||||||
|
|
||||||
fun downloadManagerBroadCastReceiver(): DownloadManagerBroadcastReceiver
|
fun downloadManagerBroadCastReceiver(): DownloadManagerBroadcastReceiver
|
||||||
fun downloadNotificationActionBroadCastReceiver(): DownloadNotificationActionsBroadcastReceiver
|
|
||||||
|
|
||||||
fun inject(application: CoreApp)
|
fun inject(application: CoreApp)
|
||||||
fun inject(kiwixWebView: KiwixWebView)
|
fun inject(kiwixWebView: KiwixWebView)
|
||||||
fun inject(storageSelectDialog: StorageSelectDialog)
|
fun inject(storageSelectDialog: StorageSelectDialog)
|
||||||
|
@ -23,12 +23,14 @@ 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 {
|
||||||
|
@ -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.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.DownloadNotificationActionsBroadcastReceiver
|
|
||||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager
|
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
|
||||||
@ -71,13 +70,6 @@ object DownloaderModule {
|
|||||||
callback: DownloadManagerBroadcastReceiver.Callback
|
callback: DownloadManagerBroadcastReceiver.Callback
|
||||||
): DownloadManagerBroadcastReceiver = DownloadManagerBroadcastReceiver(callback)
|
): DownloadManagerBroadcastReceiver = DownloadManagerBroadcastReceiver(callback)
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun providesDownloadNotificationActionsBroadcastReceiver(
|
|
||||||
downloadManagerMonitor: DownloadManagerMonitor
|
|
||||||
): DownloadNotificationActionsBroadcastReceiver =
|
|
||||||
DownloadNotificationActionsBroadcastReceiver(downloadManagerMonitor)
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesDownloadNotificationManager(
|
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
|
// 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
|
// more time to read on slow internet connection, and due to less read timeout
|
||||||
// the request is canceled.
|
// the request is canceled.
|
||||||
const val READ_TIMEOUT = 180L
|
const val READ_TIMEOUT = 300L
|
||||||
const val CALL_TIMEOUT = 180L
|
const val CALL_TIMEOUT = 300L
|
||||||
const val USER_AGENT = "kiwix-android-version:${BuildConfig.VERSION_CODE}"
|
const val USER_AGENT = "kiwix-android-version:${BuildConfig.VERSION_CODE}"
|
||||||
const val KIWIX_DOWNLOAD_URL = "https://mirror.download.kiwix.org/"
|
const val KIWIX_DOWNLOAD_URL = "https://mirror.download.kiwix.org/"
|
||||||
|
|
||||||
|
@ -18,527 +18,83 @@
|
|||||||
|
|
||||||
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.app.DownloadManager.COLUMN_STATUS
|
|
||||||
import android.content.ContentUris
|
|
||||||
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 kotlinx.coroutines.CoroutineScope
|
||||||
import android.net.Uri
|
import kotlinx.coroutines.Dispatchers
|
||||||
import io.reactivex.Observable
|
import kotlinx.coroutines.launch
|
||||||
import io.reactivex.disposables.Disposable
|
|
||||||
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.model.DownloadModel
|
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager.Companion.ACTION_CANCEL
|
||||||
import org.kiwix.kiwixmobile.core.downloader.model.DownloadState
|
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager.Companion.ACTION_PAUSE
|
||||||
import org.kiwix.kiwixmobile.core.utils.files.Log
|
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager.Companion.ACTION_QUERY_DOWNLOAD_STATUS
|
||||||
import java.util.concurrent.TimeUnit
|
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager.Companion.ACTION_RESUME
|
||||||
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")
|
|
||||||
|
|
||||||
class DownloadManagerMonitor @Inject constructor(
|
class DownloadManagerMonitor @Inject constructor(
|
||||||
private val downloadManager: DownloadManager,
|
|
||||||
val downloadRoomDao: DownloadRoomDao,
|
val downloadRoomDao: DownloadRoomDao,
|
||||||
private val context: Context,
|
private val context: Context
|
||||||
private val downloadNotificationManager: DownloadNotificationManager
|
|
||||||
) : DownloadMonitor, DownloadManagerBroadcastReceiver.Callback {
|
) : DownloadMonitor, DownloadManagerBroadcastReceiver.Callback {
|
||||||
|
|
||||||
private val updater = PublishSubject.create<() -> Unit>()
|
|
||||||
private val lock = Any()
|
private val lock = Any()
|
||||||
private val downloadInfoMap = mutableMapOf<Long, DownloadInfo>()
|
|
||||||
private var monitoringDisposable: Disposable? = null
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
startMonitoringDownloads()
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
setupUpdater()
|
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) {
|
override fun downloadCompleteOrCancelled(intent: Intent) {
|
||||||
synchronized(lock) {
|
synchronized(lock) {
|
||||||
intent.extras?.let {
|
intent.extras?.let {
|
||||||
val downloadId = it.getLong(DownloadManager.EXTRA_DOWNLOAD_ID, -1L)
|
val downloadId = it.getLong(DownloadManager.EXTRA_DOWNLOAD_ID, -1L)
|
||||||
if (downloadId != -1L) {
|
if (downloadId != -1L) {
|
||||||
queryDownloadStatus(downloadId)
|
context.startService(
|
||||||
}
|
getDownloadMonitorIntent(
|
||||||
}
|
ACTION_QUERY_DOWNLOAD_STATUS,
|
||||||
}
|
downloadId.toInt()
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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"
|
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
},
|
|
||||||
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")
|
fun startMonitoringDownloads() {
|
||||||
private fun queryDownloadStatus(downloadId: Long) {
|
startService()
|
||||||
synchronized(lock) {
|
|
||||||
downloadManager.query(DownloadManager.Query().setFilterById(downloadId)).use { cursor ->
|
|
||||||
if (cursor.moveToFirst()) {
|
|
||||||
handleDownloadStatus(cursor, downloadId)
|
|
||||||
} else {
|
|
||||||
handleCancelledDownload(downloadId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("Range")
|
private fun startService() {
|
||||||
private fun handleDownloadStatus(cursor: Cursor, downloadId: Long) {
|
context.startService(Intent(context, DownloadMonitorService::class.java))
|
||||||
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()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pauseDownload(downloadId: Long) {
|
fun pauseDownload(downloadId: Long) {
|
||||||
synchronized(lock) {
|
context.startService(getDownloadMonitorIntent(ACTION_PAUSE, downloadId.toInt()))
|
||||||
updater.onNext {
|
|
||||||
if (pauseResumeDownloadInDownloadManagerContentResolver(
|
|
||||||
downloadId,
|
|
||||||
CONTROL_PAUSE,
|
|
||||||
STATUS_PAUSED_BY_APP
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
updateDownloadStatus(downloadId, Status.PAUSED, Error.NONE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resumeDownload(downloadId: Long) {
|
fun resumeDownload(downloadId: Long) {
|
||||||
synchronized(lock) {
|
context.startService(getDownloadMonitorIntent(ACTION_RESUME, downloadId.toInt()))
|
||||||
updater.onNext {
|
|
||||||
if (pauseResumeDownloadInDownloadManagerContentResolver(
|
|
||||||
downloadId,
|
|
||||||
CONTROL_RUN,
|
|
||||||
STATUS_RUNNING
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
updateDownloadStatus(downloadId, Status.QUEUED, Error.NONE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancelDownload(downloadId: Long) {
|
fun cancelDownload(downloadId: Long) {
|
||||||
synchronized(lock) {
|
context.startService(getDownloadMonitorIntent(ACTION_CANCEL, downloadId.toInt()))
|
||||||
downloadManager.remove(downloadId)
|
|
||||||
handleCancelledDownload(downloadId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("Range")
|
private fun getDownloadMonitorIntent(action: String, downloadId: Int): Intent =
|
||||||
private fun pauseResumeDownloadInDownloadManagerContentResolver(
|
Intent(context, DownloadMonitorService::class.java).apply {
|
||||||
downloadId: Long,
|
putExtra(DownloadNotificationManager.NOTIFICATION_ACTION, action)
|
||||||
control: Int,
|
putExtra(DownloadNotificationManager.EXTRA_DOWNLOAD_ID, downloadId)
|
||||||
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 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: 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
|
package org.kiwix.kiwixmobile.core.downloader.downloadManager
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Notification
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
@ -53,71 +54,80 @@ class DownloadNotificationManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun updateNotification(
|
fun updateNotification(
|
||||||
downloadNotificationModel: DownloadNotificationModel
|
downloadNotificationModel: DownloadNotificationModel,
|
||||||
|
assignNewForegroundServiceNotification: AssignNewForegroundServiceNotification
|
||||||
) {
|
) {
|
||||||
synchronized(downloadNotificationsBuilderMap) {
|
synchronized(downloadNotificationsBuilderMap) {
|
||||||
if (shouldUpdateNotification(downloadNotificationModel)) {
|
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(
|
notificationManager.notify(
|
||||||
downloadNotificationModel.downloadId,
|
downloadNotificationModel.downloadId,
|
||||||
notificationBuilder.build()
|
createNotification(downloadNotificationModel)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// the download is cancelled/paused so remove the notification.
|
// 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 {
|
private fun getPauseOrResumeTitle(isPause: Boolean): String {
|
||||||
val pauseOrResumeTitle = if (isPause) {
|
val pauseOrResumeTitle = if (isPause) {
|
||||||
context.getString(R.string.tts_pause)
|
context.getString(R.string.tts_pause)
|
||||||
@ -184,16 +194,16 @@ class DownloadNotificationManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun getActionPendingIntent(action: String, downloadId: Int): PendingIntent {
|
private fun getActionPendingIntent(action: String, downloadId: Int): PendingIntent {
|
||||||
val intent =
|
val pendingIntent =
|
||||||
Intent(DOWNLOAD_NOTIFICATION_ACTION).apply {
|
Intent(context, DownloadMonitorService::class.java).apply {
|
||||||
putExtra(NOTIFICATION_ACTION, action)
|
putExtra(NOTIFICATION_ACTION, action)
|
||||||
putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
putExtra(EXTRA_DOWNLOAD_ID, downloadId)
|
||||||
}
|
}
|
||||||
val requestCode = downloadId + action.hashCode()
|
val requestCode = downloadId + action.hashCode()
|
||||||
return PendingIntent.getBroadcast(
|
return PendingIntent.getService(
|
||||||
context,
|
context,
|
||||||
requestCode,
|
requestCode,
|
||||||
intent,
|
pendingIntent,
|
||||||
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -203,7 +213,7 @@ class DownloadNotificationManager @Inject constructor(
|
|||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
DOWNLOAD_NOTIFICATION_CHANNEL_ID,
|
DOWNLOAD_NOTIFICATION_CHANNEL_ID,
|
||||||
context.getString(R.string.download_notification_channel_name),
|
context.getString(R.string.download_notification_channel_name),
|
||||||
NotificationManager.IMPORTANCE_DEFAULT
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
).apply {
|
).apply {
|
||||||
setSound(null, null)
|
setSound(null, null)
|
||||||
enableVibration(false)
|
enableVibration(false)
|
||||||
@ -243,6 +253,7 @@ class DownloadNotificationManager @Inject constructor(
|
|||||||
const val ACTION_PAUSE = "action_pause"
|
const val ACTION_PAUSE = "action_pause"
|
||||||
const val ACTION_RESUME = "action_resume"
|
const val ACTION_RESUME = "action_resume"
|
||||||
const val ACTION_CANCEL = "action_cancel"
|
const val ACTION_CANCEL = "action_cancel"
|
||||||
|
const val ACTION_QUERY_DOWNLOAD_STATUS = "action_query_download_status"
|
||||||
const val EXTRA_DOWNLOAD_ID = "extra_download_id"
|
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.data.remote.ObjectBoxToRoomMigrator
|
||||||
import org.kiwix.kiwixmobile.core.di.components.CoreActivityComponent
|
import org.kiwix.kiwixmobile.core.di.components.CoreActivityComponent
|
||||||
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadManagerBroadcastReceiver
|
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.error.ErrorActivity
|
||||||
import org.kiwix.kiwixmobile.core.extensions.browserIntent
|
import org.kiwix.kiwixmobile.core.extensions.browserIntent
|
||||||
import org.kiwix.kiwixmobile.core.extensions.getToolbarNavigationIcon
|
import org.kiwix.kiwixmobile.core.extensions.getToolbarNavigationIcon
|
||||||
@ -101,10 +100,6 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
|
|||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var downloadManagerBroadcastReceiver: DownloadManagerBroadcastReceiver
|
lateinit var downloadManagerBroadcastReceiver: DownloadManagerBroadcastReceiver
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var downloadNotificationActionsReceiver: DownloadNotificationActionsBroadcastReceiver
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
setTheme(R.style.KiwixTheme)
|
setTheme(R.style.KiwixTheme)
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@ -136,7 +131,6 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
|
|||||||
objectBoxToRoomMigrator.migrateObjectBoxDataToRoom()
|
objectBoxToRoomMigrator.migrateObjectBoxDataToRoom()
|
||||||
}
|
}
|
||||||
downloadManagerBroadcastReceiver.let(::registerReceiver)
|
downloadManagerBroadcastReceiver.let(::registerReceiver)
|
||||||
downloadNotificationActionsReceiver.let(::registerReceiver)
|
|
||||||
createApplicationShortcuts()
|
createApplicationShortcuts()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,7 +150,6 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
downloadManagerBroadcastReceiver.let(::unregisterReceiver)
|
downloadManagerBroadcastReceiver.let(::unregisterReceiver)
|
||||||
downloadNotificationActionsReceiver.let(::unregisterReceiver)
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,7 +123,13 @@ class CustomFileValidator @Inject constructor(private val context: Context) {
|
|||||||
directoryList.add(dir)
|
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> =
|
private fun scanDirs(dirs: Array<out File?>?, extensionToMatch: String): List<File> =
|
||||||
|
Loading…
x
Reference in New Issue
Block a user