diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadManagerMonitor.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadManagerMonitor.kt index 9d7a149e6..4caaf4385 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadManagerMonitor.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadManagerMonitor.kt @@ -41,7 +41,8 @@ const val DEFAULT_INT_VALUE = -1 class DownloadManagerMonitor @Inject constructor( val fetch: Fetch, val context: Context, - val downloadRoomDao: DownloadRoomDao + val downloadRoomDao: DownloadRoomDao, + private val fetchDownloadNotificationManager: FetchDownloadNotificationManager ) : DownloadMonitor { private val updater = PublishSubject.create<() -> Unit>() private var updaterDisposable: Disposable? = null @@ -122,7 +123,12 @@ class DownloadManagerMonitor @Inject constructor( } private fun update(download: Download) { - updater.onNext { downloadRoomDao.update(download) } + updater.onNext { + downloadRoomDao.update(download) + if (download.isPaused()) { + fetchDownloadNotificationManager.showDownloadPauseNotification(fetch, download) + } + } } private fun delete(download: Download) { diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadMonitorService.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadMonitorService.kt index bd1c5ce2c..6254d0269 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadMonitorService.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadMonitorService.kt @@ -116,12 +116,9 @@ class DownloadMonitorService : Service() { it.status == Status.NONE || it.status == Status.ADDED || it.status == Status.QUEUED || - it.status == Status.DOWNLOADING - }?.let { - val notificationBuilder = - fetchDownloadNotificationManager.getNotificationBuilder(it.id, it.id) - startForeground(it.id, notificationBuilder.build()) - } ?: kotlin.run { + it.status == Status.DOWNLOADING || + it.isPaused() + }?.let(::setForegroundNotificationForDownload) ?: kotlin.run { stopForegroundServiceForDownloads() // Cancel the last ongoing notification after detaching it from // the foreground service if no active downloads are found. @@ -131,6 +128,21 @@ class DownloadMonitorService : Service() { } } + private fun setForegroundNotificationForDownload(it: Download) { + val notificationBuilder = + fetchDownloadNotificationManager.getNotificationBuilder(it.id, it.id) + var foreGroundServiceNotification = notificationBuilder.build() + if (it.isPaused()) { + // Clear any pending actions on this notification builder. + notificationBuilder.clearActions() + // If a download is paused that means there is no notification for it, so we have to + // show our custom cancel notification. + foreGroundServiceNotification = + fetchDownloadNotificationManager.getCancelNotification(fetch, it, notificationBuilder) + } + startForeground(it.id, foreGroundServiceNotification) + } + private fun cancelNotificationForId(downloadId: Int) { notificationManager.cancel(downloadId) } @@ -165,7 +177,7 @@ class DownloadMonitorService : Service() { } override fun onPaused(download: Download) { - update(download, true) + update(download) } override fun onProgress( @@ -213,6 +225,13 @@ class DownloadMonitorService : Service() { downloadRoomDao.downloads().blockingFirst() } } + // If someone pause the Download then post a notification since fetch removes the + // notification for ongoing download when pause so we needs to show our custom notification. + if (download.isPaused()) { + fetchDownloadNotificationManager.showDownloadPauseNotification(fetch, download).also { + setForeGroundServiceNotificationIfNoActiveDownloads(fetch, download) + } + } if (shouldSetForegroundNotification) { setForegroundNotification(download.id) } @@ -227,6 +246,24 @@ class DownloadMonitorService : Service() { } } + private fun setForeGroundServiceNotificationIfNoActiveDownloads( + fetch: Fetch, + download: Download + ) { + updater.onNext { + // Check if there are any ongoing downloads. + // If the list is empty, it means no other downloads are running, + // so we need to promote this download to a foreground service. + fetch.getDownloadsWithStatus( + listOf(Status.NONE, Status.ADDED, Status.QUEUED, Status.DOWNLOADING) + ) { activeDownloads -> + if (activeDownloads.isEmpty()) { + setForegroundNotificationForDownload(download) + } + } + } + } + @Suppress("MagicNumber") private fun showDownloadCompletedNotification(download: Download) { downloadNotificationChannel() diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/FetchDownloadNotificationManager.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/FetchDownloadNotificationManager.kt index af734d1b2..e18abada1 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/FetchDownloadNotificationManager.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/FetchDownloadNotificationManager.kt @@ -19,8 +19,10 @@ package org.kiwix.kiwixmobile.core.downloader.downloadManager import android.annotation.SuppressLint +import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager +import android.app.PendingIntent import android.app.PendingIntent.FLAG_IMMUTABLE import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.app.PendingIntent.getActivity @@ -30,12 +32,36 @@ import android.content.IntentFilter import android.os.Build import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationCompat.Builder +import com.tonyodev.fetch2.ACTION_TYPE_CANCEL +import com.tonyodev.fetch2.ACTION_TYPE_DELETE +import com.tonyodev.fetch2.ACTION_TYPE_INVALID +import com.tonyodev.fetch2.ACTION_TYPE_PAUSE +import com.tonyodev.fetch2.ACTION_TYPE_RESUME +import com.tonyodev.fetch2.ACTION_TYPE_RETRY import com.tonyodev.fetch2.DefaultFetchNotificationManager +import com.tonyodev.fetch2.Download import com.tonyodev.fetch2.DownloadNotification +import com.tonyodev.fetch2.DownloadNotification.ActionType.CANCEL +import com.tonyodev.fetch2.DownloadNotification.ActionType.DELETE +import com.tonyodev.fetch2.DownloadNotification.ActionType.PAUSE +import com.tonyodev.fetch2.DownloadNotification.ActionType.RESUME +import com.tonyodev.fetch2.DownloadNotification.ActionType.RETRY +import com.tonyodev.fetch2.EXTRA_ACTION_TYPE +import com.tonyodev.fetch2.EXTRA_DOWNLOAD_ID +import com.tonyodev.fetch2.EXTRA_GROUP_ACTION +import com.tonyodev.fetch2.EXTRA_NAMESPACE +import com.tonyodev.fetch2.EXTRA_NOTIFICATION_GROUP_ID +import com.tonyodev.fetch2.EXTRA_NOTIFICATION_ID import com.tonyodev.fetch2.Fetch import com.tonyodev.fetch2.R.drawable import com.tonyodev.fetch2.R.string +import com.tonyodev.fetch2.Status import com.tonyodev.fetch2.util.DEFAULT_NOTIFICATION_TIMEOUT_AFTER_RESET +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.kiwix.kiwixmobile.core.CoreApp import org.kiwix.kiwixmobile.core.Intents import org.kiwix.kiwixmobile.core.R @@ -46,9 +72,13 @@ import javax.inject.Inject const val DOWNLOAD_NOTIFICATION_TITLE = "OPEN_ZIM_FILE" class FetchDownloadNotificationManager @Inject constructor( - context: Context, + val context: Context, private val downloadRoomDao: DownloadRoomDao ) : DefaultFetchNotificationManager(context) { + private val downloadNotificationManager: NotificationManager by lazy { + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + } + override fun getFetchInstanceForNamespace(namespace: String): Fetch = Fetch.getDefaultInstance() override fun registerBroadcastReceiver() { @@ -176,4 +206,78 @@ class FetchDownloadNotificationManager @Inject constructor( notificationBuilder.setAutoCancel(true) } } + + fun showDownloadPauseNotification(fetch: Fetch, download: Download) { + CoroutineScope(Dispatchers.IO).launch { + val notificationBuilder = getNotificationBuilder(download.id, download.id) + val cancelNotification = getCancelNotification(fetch, download, notificationBuilder) + downloadNotificationManager.notify(download.id, cancelNotification) + } + } + + fun getCancelNotification( + fetch: Fetch, + download: Download, + notificationBuilder: Builder + ): Notification { + val downloadTitle = getDownloadNotificationTitle(download) + val notificationTitle = + runBlocking(Dispatchers.IO) { + downloadRoomDao.getEntityForFileName(downloadTitle)?.title + ?: downloadTitle + } + return notificationBuilder.setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setSmallIcon(android.R.drawable.stat_sys_download_done) + .setContentTitle(notificationTitle) + .setContentText(context.getString(string.fetch_notification_download_paused)) + // Set the ongoing true so that could not cancel the pause notification. + // However, on Android 14 and above user can cancel the notification by swipe right so we + // can't control that see https://developer.android.com/about/versions/14/behavior-changes-all#non-dismissable-notifications + .setOngoing(true) + .setGroup(download.id.toString()) + .setGroupSummary(false) + .setProgress(HUNDERED, download.progress, false) + .addAction( + drawable.fetch_notification_cancel, + context.getString(R.string.cancel), + getActionPendingIntent(fetch, download, DownloadNotification.ActionType.DELETE) + ) + .addAction( + drawable.fetch_notification_resume, + context.getString(R.string.notification_resume_button_text), + getActionPendingIntent(fetch, download, DownloadNotification.ActionType.RESUME) + ) + .build() + } + + private fun getActionPendingIntent( + fetch: Fetch, + download: Download, + actionType: DownloadNotification.ActionType + ): PendingIntent { + val intent = Intent(notificationManagerAction).apply { + putExtra(EXTRA_NAMESPACE, fetch.namespace) + putExtra(EXTRA_DOWNLOAD_ID, download.id) + putExtra(EXTRA_NOTIFICATION_ID, download.id) + putExtra(EXTRA_GROUP_ACTION, false) + putExtra(EXTRA_NOTIFICATION_GROUP_ID, download.id) + } + val action = when (actionType) { + CANCEL -> ACTION_TYPE_CANCEL + DELETE -> ACTION_TYPE_DELETE + RESUME -> ACTION_TYPE_RESUME + PAUSE -> ACTION_TYPE_PAUSE + RETRY -> ACTION_TYPE_RETRY + else -> ACTION_TYPE_INVALID + } + intent.putExtra(EXTRA_ACTION_TYPE, action) + return PendingIntent.getBroadcast( + context, + download.id + action, + intent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + } } + +fun Download.isPaused() = status == Status.PAUSED