Add cancel to downloads
This commit is contained in:
parent
1cf781b27b
commit
40d8d20cc5
7 changed files with 86 additions and 40 deletions
39
app/src/main/java/io/heckel/ntfy/msg/DownloadManager.kt
Normal file
39
app/src/main/java/io/heckel/ntfy/msg/DownloadManager.kt
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>() {
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue