Add cancel to downloads

This commit is contained in:
Philipp Heckel 2022-01-11 18:21:30 -05:00
parent 1cf781b27b
commit 40d8d20cc5
7 changed files with 86 additions and 40 deletions

View file

@ -0,0 +1,39 @@
package io.heckel.ntfy.msg
import android.content.Context
import android.util.Log
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import androidx.work.workDataOf
/**
* Download attachment in the background via WorkManager
*
* The indirection via WorkManager is required since this code may be executed
* in a doze state and Internet may not be available. It's also best practice apparently.
*/
class DownloadManager {
companion object {
private const val TAG = "NtfyDownloadManager"
private const val DOWNLOAD_WORK_NAME_PREFIX = "io.heckel.ntfy.DOWNLOAD_FILE_"
fun enqueue(context: Context, id: String) {
val workManager = WorkManager.getInstance(context)
val workName = DOWNLOAD_WORK_NAME_PREFIX + id
Log.d(TAG,"Enqueuing work to download attachment for notification $id, work: $workName")
val workRequest = OneTimeWorkRequest.Builder(DownloadWorker::class.java)
.setInputData(workDataOf("id" to id))
.build()
workManager.enqueueUniqueWork(workName, ExistingWorkPolicy.KEEP, workRequest)
}
fun cancel(context: Context, id: String) {
val workManager = WorkManager.getInstance(context)
val workName = DOWNLOAD_WORK_NAME_PREFIX + id
Log.d(TAG, "Cancelling download for notification $id, work: $workName")
workManager.cancelUniqueWork(workName)
}
}
}

View file

@ -17,12 +17,13 @@ import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.*
import io.heckel.ntfy.util.queryFilename
import kotlinx.coroutines.delay
import okhttp3.OkHttpClient
import okhttp3.Request
import java.io.File
import java.util.concurrent.TimeUnit
class AttachmentDownloadWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) {
class DownloadWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) {
private val client = OkHttpClient.Builder()
.callTimeout(5, TimeUnit.MINUTES) // Total timeout for entire request
.connectTimeout(15, TimeUnit.SECONDS)
@ -83,6 +84,14 @@ class AttachmentDownloadWorker(private val context: Context, params: WorkerParam
var lastProgress = 0L
while (bytes >= 0) {
if (System.currentTimeMillis() - lastProgress > 500) {
if (isStopped) {
Log.d(TAG, "Attachment download was canceled")
val newAttachment = attachment.copy(progress = PROGRESS_NONE)
val newNotification = notification.copy(attachment = newAttachment)
notifier.update(subscription, newNotification)
repository.updateNotification(newNotification)
return
}
val progress = if (size > 0) (bytesCopied.toFloat()/size.toFloat()*100).toInt() else PROGRESS_INDETERMINATE
val newAttachment = attachment.copy(progress = progress)
val newNotification = notification.copy(attachment = newAttachment)

View file

@ -44,10 +44,7 @@ class NotificationDispatcher(val context: Context, val repository: Repository) {
}
}
if (download) {
// Download attachment in the background via WorkManager
// The indirection via WorkManager is required since this code may be executed
// in a doze state and Internet may not be available. It's also best practice apparently.
scheduleAttachmentDownload(notification)
DownloadManager.enqueue(context, notification.id)
}
}
@ -85,15 +82,6 @@ class NotificationDispatcher(val context: Context, val repository: Repository) {
return subscription.mutedUntil == 1L || (subscription.mutedUntil > 1L && subscription.mutedUntil > System.currentTimeMillis()/1000)
}
private fun scheduleAttachmentDownload(notification: Notification) {
Log.d(TAG, "Enqueuing work to download attachment")
val workManager = WorkManager.getInstance(context)
val workRequest = OneTimeWorkRequest.Builder(AttachmentDownloadWorker::class.java)
.setInputData(workDataOf("id" to notification.id))
.build()
workManager.enqueue(workRequest)
}
companion object {
private const val TAG = "NtfyNotifDispatch"
}

View file

@ -9,9 +9,6 @@ import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import androidx.work.workDataOf
import io.heckel.ntfy.R
import io.heckel.ntfy.data.*
import io.heckel.ntfy.data.Notification
@ -66,6 +63,7 @@ class NotificationService(val context: Context) {
maybeAddOpenAction(builder, notification)
maybeAddBrowseAction(builder, notification)
maybeAddDownloadAction(builder, notification)
maybeAddCancelAction(builder, notification)
maybeCreateNotificationChannel(notification.priority)
notificationManager.notify(notification.notificationId, builder.build())
@ -164,7 +162,7 @@ class NotificationService(val context: Context) {
private fun maybeAddBrowseAction(builder: NotificationCompat.Builder, notification: Notification) {
if (notification.attachment?.contentUri != null) {
val intent = Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)
val intent = Intent(android.app.DownloadManager.ACTION_VIEW_DOWNLOADS)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_browse), pendingIntent).build())
@ -174,21 +172,31 @@ class NotificationService(val context: Context) {
private fun maybeAddDownloadAction(builder: NotificationCompat.Builder, notification: Notification) {
if (notification.attachment?.contentUri == null && listOf(PROGRESS_NONE, PROGRESS_FAILED).contains(notification.attachment?.progress)) {
val intent = Intent(context, DownloadBroadcastReceiver::class.java)
intent.putExtra("action", DOWNLOAD_ACTION_START)
intent.putExtra("id", notification.id)
val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_download), pendingIntent).build())
}
}
private fun maybeAddCancelAction(builder: NotificationCompat.Builder, notification: Notification) {
if (notification.attachment?.contentUri == null && notification.attachment?.progress in 0..99) {
val intent = Intent(context, DownloadBroadcastReceiver::class.java)
intent.putExtra("action", DOWNLOAD_ACTION_CANCEL)
intent.putExtra("id", notification.id)
val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_cancel), pendingIntent).build())
}
}
class DownloadBroadcastReceiver : android.content.BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val id = intent.getStringExtra("id") ?: return
Log.d(TAG, "Enqueuing work to download attachment for notification $id")
val workManager = WorkManager.getInstance(context)
val workRequest = OneTimeWorkRequest.Builder(AttachmentDownloadWorker::class.java)
.setInputData(workDataOf("id" to id))
.build()
workManager.enqueue(workRequest)
val action = intent.getStringExtra("action") ?: return
when (action) {
DOWNLOAD_ACTION_START -> DownloadManager.enqueue(context, id)
DOWNLOAD_ACTION_CANCEL -> DownloadManager.cancel(context, id)
}
}
}
@ -254,7 +262,8 @@ class NotificationService(val context: Context) {
companion object {
private const val TAG = "NtfyNotifService"
private const val DOWNLOAD_ATTACHMENT_ACTION = "io.heckel.ntfy.DOWNLOAD_ATTACHMENT"
private const val DOWNLOAD_ACTION_START = "io.heckel.ntfy.DOWNLOAD_ACTION_START"
private const val DOWNLOAD_ACTION_CANCEL = "io.heckel.ntfy.DOWNLOAD_ACTION_CANCEL"
private const val CHANNEL_ID_MIN = "ntfy-min"
private const val CHANNEL_ID_LOW = "ntfy-low"

View file

@ -2,7 +2,6 @@ package io.heckel.ntfy.ui
import android.Manifest
import android.app.Activity
import android.app.DownloadManager
import android.content.*
import android.content.pm.PackageManager
import android.graphics.Bitmap
@ -25,7 +24,8 @@ import androidx.work.workDataOf
import com.stfalcon.imageviewer.StfalconImageViewer
import io.heckel.ntfy.R
import io.heckel.ntfy.data.*
import io.heckel.ntfy.msg.AttachmentDownloadWorker
import io.heckel.ntfy.msg.DownloadManager
import io.heckel.ntfy.msg.DownloadWorker
import io.heckel.ntfy.util.*
import java.util.*
@ -182,10 +182,12 @@ class DetailAdapter(private val activity: Activity, private val onClick: (Notifi
val popup = PopupMenu(context, anchor)
popup.menuInflater.inflate(R.menu.menu_detail_attachment, popup.menu)
val downloadItem = popup.menu.findItem(R.id.detail_item_menu_download)
val cancelItem = popup.menu.findItem(R.id.detail_item_menu_cancel)
val openItem = popup.menu.findItem(R.id.detail_item_menu_open)
val browseItem = popup.menu.findItem(R.id.detail_item_menu_browse)
val copyUrlItem = popup.menu.findItem(R.id.detail_item_menu_copy_url)
val expired = attachment.expires != null && attachment.expires < System.currentTimeMillis()/1000
val inProgress = attachment.progress in 0..99
if (attachment.contentUri != null) {
openItem.setOnMenuItemClickListener {
try {
@ -207,7 +209,7 @@ class DetailAdapter(private val activity: Activity, private val onClick: (Notifi
}
}
browseItem.setOnMenuItemClickListener {
val intent = Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)
val intent = Intent(android.app.DownloadManager.ACTION_VIEW_DOWNLOADS)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
context.startActivity(intent)
true
@ -227,14 +229,19 @@ class DetailAdapter(private val activity: Activity, private val onClick: (Notifi
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_CODE_WRITE_STORAGE_PERMISSION_FOR_DOWNLOAD)
return@setOnMenuItemClickListener true
}
scheduleAttachmentDownload(context, notification)
DownloadManager.enqueue(context, notification.id)
true
}
cancelItem.setOnMenuItemClickListener {
DownloadManager.cancel(context, notification.id)
true
}
openItem.isVisible = exists
browseItem.isVisible = exists
downloadItem.isVisible = !exists && !expired
downloadItem.isVisible = !exists && !expired && !inProgress
copyUrlItem.isVisible = !expired
val noOptions = !openItem.isVisible && !browseItem.isVisible && !downloadItem.isVisible && !copyUrlItem.isVisible
cancelItem.isVisible = inProgress
val noOptions = !openItem.isVisible && !browseItem.isVisible && !downloadItem.isVisible && !copyUrlItem.isVisible && !cancelItem.isVisible
if (noOptions) {
return null
}
@ -304,15 +311,6 @@ class DetailAdapter(private val activity: Activity, private val onClick: (Notifi
attachmentImageView.visibility = View.GONE
}
}
private fun scheduleAttachmentDownload(context: Context, notification: Notification) {
Log.d(TAG, "Enqueuing work to download attachment")
val workManager = WorkManager.getInstance(context)
val workRequest = OneTimeWorkRequest.Builder(AttachmentDownloadWorker::class.java)
.setInputData(workDataOf("id" to notification.id))
.build()
workManager.enqueue(workRequest)
}
}
object TopicDiffCallback : DiffUtil.ItemCallback<Notification>() {

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/detail_item_menu_download" android:title="@string/detail_item_menu_download"/>
<item android:id="@+id/detail_item_menu_cancel" android:title="@string/detail_item_menu_cancel"/>
<item android:id="@+id/detail_item_menu_open" android:title="@string/detail_item_menu_open"/>
<item android:id="@+id/detail_item_menu_browse" android:title="@string/detail_item_menu_browse"/>
<item android:id="@+id/detail_item_menu_copy_url" android:title="@string/detail_item_menu_copy_url"/>

View file

@ -112,6 +112,7 @@
<string name="detail_item_menu_open">Open file</string>
<string name="detail_item_menu_browse">Browse file</string>
<string name="detail_item_menu_download">Download file</string>
<string name="detail_item_menu_cancel">Cancel download</string>
<string name="detail_item_menu_copy_url">Copy URL</string>
<string name="detail_item_menu_copy_url_copied">Copied URL to clipboard</string>
<string name="detail_item_cannot_download">Cannot open or download attachment. Link expired and no local file found.</string>
@ -166,6 +167,7 @@
<string name="notification_popup_action_open">Open</string>
<string name="notification_popup_action_browse">Browse</string>
<string name="notification_popup_action_download">Download</string>
<string name="notification_popup_action_cancel">Cancel</string>
<string name="notification_popup_file">%1$s\nFile: %2$s</string>
<string name="notification_popup_file_downloading">Downloading %1$s, %2$d%%\n%3$s</string>
<string name="notification_popup_file_download_successful">%1$s\nFile: %2$s, download successful</string>