Use Content-Type and Content-Length response headers
This commit is contained in:
parent
3b9f3f2e52
commit
0fce663975
6 changed files with 137 additions and 65 deletions
|
@ -59,7 +59,7 @@ data class Notification(
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
data class Attachment(
|
data class Attachment(
|
||||||
@ColumnInfo(name = "name") val name: String, // Filename (mandatory, see ntfy server)
|
@ColumnInfo(name = "name") val name: String, // Filename
|
||||||
@ColumnInfo(name = "type") val type: String?, // MIME type
|
@ColumnInfo(name = "type") val type: String?, // MIME type
|
||||||
@ColumnInfo(name = "size") val size: Long?, // Size in bytes
|
@ColumnInfo(name = "size") val size: Long?, // Size in bytes
|
||||||
@ColumnInfo(name = "expires") val expires: Long?, // Unix timestamp
|
@ColumnInfo(name = "expires") val expires: Long?, // Unix timestamp
|
||||||
|
|
|
@ -18,12 +18,15 @@ class DownloadManager {
|
||||||
private const val TAG = "NtfyDownloadManager"
|
private const val TAG = "NtfyDownloadManager"
|
||||||
private const val DOWNLOAD_WORK_NAME_PREFIX = "io.heckel.ntfy.DOWNLOAD_FILE_"
|
private const val DOWNLOAD_WORK_NAME_PREFIX = "io.heckel.ntfy.DOWNLOAD_FILE_"
|
||||||
|
|
||||||
fun enqueue(context: Context, id: String) {
|
fun enqueue(context: Context, id: String, userAction: Boolean) {
|
||||||
val workManager = WorkManager.getInstance(context)
|
val workManager = WorkManager.getInstance(context)
|
||||||
val workName = DOWNLOAD_WORK_NAME_PREFIX + id
|
val workName = DOWNLOAD_WORK_NAME_PREFIX + id
|
||||||
Log.d(TAG,"Enqueuing work to download attachment for notification $id, work: $workName")
|
Log.d(TAG,"Enqueuing work to download attachment for notification $id, work: $workName")
|
||||||
val workRequest = OneTimeWorkRequest.Builder(DownloadWorker::class.java)
|
val workRequest = OneTimeWorkRequest.Builder(DownloadWorker::class.java)
|
||||||
.setInputData(workDataOf("id" to id))
|
.setInputData(workDataOf(
|
||||||
|
DownloadWorker.INPUT_DATA_ID to id,
|
||||||
|
DownloadWorker.INPUT_DATA_USER_ACTION to userAction
|
||||||
|
))
|
||||||
.build()
|
.build()
|
||||||
workManager.enqueueUniqueWork(workName, ExistingWorkPolicy.KEEP, workRequest)
|
workManager.enqueueUniqueWork(workName, ExistingWorkPolicy.KEEP, workRequest)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
|
@ -20,40 +21,48 @@ import io.heckel.ntfy.data.*
|
||||||
import io.heckel.ntfy.util.queryFilename
|
import io.heckel.ntfy.util.queryFilename
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
|
||||||
class DownloadWorker(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()
|
private val client = OkHttpClient.Builder()
|
||||||
.callTimeout(5, TimeUnit.MINUTES) // Total timeout for entire request
|
.callTimeout(15, TimeUnit.MINUTES) // Total timeout for entire request
|
||||||
.connectTimeout(15, TimeUnit.SECONDS)
|
.connectTimeout(15, TimeUnit.SECONDS)
|
||||||
.readTimeout(15, TimeUnit.SECONDS)
|
.readTimeout(15, TimeUnit.SECONDS)
|
||||||
.writeTimeout(15, TimeUnit.SECONDS)
|
.writeTimeout(15, TimeUnit.SECONDS)
|
||||||
.build()
|
.build()
|
||||||
private val notifier = NotificationService(context)
|
private val notifier = NotificationService(context)
|
||||||
|
private lateinit var repository: Repository
|
||||||
|
private lateinit var subscription: Subscription
|
||||||
|
private lateinit var notification: Notification
|
||||||
|
private lateinit var attachment: Attachment
|
||||||
private var uri: Uri? = null
|
private var uri: Uri? = null
|
||||||
|
|
||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
if (context.applicationContext !is Application) return Result.failure()
|
if (context.applicationContext !is Application) return Result.failure()
|
||||||
val notificationId = inputData.getString("id") ?: return Result.failure()
|
val notificationId = inputData.getString(INPUT_DATA_ID) ?: return Result.failure()
|
||||||
|
val userAction = inputData.getBoolean(INPUT_DATA_USER_ACTION, false)
|
||||||
val app = context.applicationContext as Application
|
val app = context.applicationContext as Application
|
||||||
val repository = app.repository
|
repository = app.repository
|
||||||
val notification = repository.getNotification(notificationId) ?: return Result.failure()
|
notification = repository.getNotification(notificationId) ?: return Result.failure()
|
||||||
val subscription = repository.getSubscription(notification.subscriptionId) ?: return Result.failure()
|
subscription = repository.getSubscription(notification.subscriptionId) ?: return Result.failure()
|
||||||
downloadAttachment(repository, subscription, notification)
|
attachment = notification.attachment ?: return Result.failure()
|
||||||
|
try {
|
||||||
|
downloadAttachment(userAction)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
failed(e)
|
||||||
|
}
|
||||||
return Result.success()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStopped() {
|
override fun onStopped() {
|
||||||
val uriCopy = uri
|
Log.d(TAG, "Attachment download was canceled")
|
||||||
if (uriCopy != null) {
|
maybeDeleteFile()
|
||||||
val resolver = applicationContext.contentResolver
|
|
||||||
resolver.delete(uriCopy, null, null)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadAttachment(repository: Repository, subscription: Subscription, notification: Notification) {
|
private fun downloadAttachment(userAction: Boolean) {
|
||||||
val attachment = notification.attachment ?: return
|
|
||||||
Log.d(TAG, "Downloading attachment from ${attachment.url}")
|
Log.d(TAG, "Downloading attachment from ${attachment.url}")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -63,77 +72,80 @@ class DownloadWorker(private val context: Context, params: WorkerParameters) : W
|
||||||
.build()
|
.build()
|
||||||
client.newCall(request).execute().use { response ->
|
client.newCall(request).execute().use { response ->
|
||||||
if (!response.isSuccessful || response.body == null) {
|
if (!response.isSuccessful || response.body == null) {
|
||||||
throw Exception("Attachment download failed: ${response.code}")
|
throw Exception("Unexpected response: ${response.code}")
|
||||||
|
}
|
||||||
|
save(updateAttachmentFromResponse(response))
|
||||||
|
if (!userAction && shouldAbortDownload()) {
|
||||||
|
Log.d(TAG, "Aborting download: Content-Length is larger than auto-download setting")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
val name = attachment.name
|
|
||||||
val size = attachment.size ?: 0
|
|
||||||
val resolver = applicationContext.contentResolver
|
val resolver = applicationContext.contentResolver
|
||||||
val uri = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
val values = ContentValues().apply {
|
||||||
val file = ensureSafeNewFile(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), name)
|
put(MediaStore.MediaColumns.DISPLAY_NAME, attachment.name)
|
||||||
FileProvider.getUriForFile(context, FILE_PROVIDER_AUTHORITY, file)
|
|
||||||
} else {
|
|
||||||
val details = ContentValues().apply {
|
|
||||||
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
|
|
||||||
if (attachment.type != null) {
|
if (attachment.type != null) {
|
||||||
put(MediaStore.MediaColumns.MIME_TYPE, attachment.type)
|
put(MediaStore.MediaColumns.MIME_TYPE, attachment.type)
|
||||||
}
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||||
put(MediaStore.MediaColumns.IS_DOWNLOAD, 1)
|
put(MediaStore.MediaColumns.IS_DOWNLOAD, 1)
|
||||||
|
put(MediaStore.MediaColumns.IS_PENDING, 1) // While downloading
|
||||||
}
|
}
|
||||||
resolver.insert(MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL), details)
|
}
|
||||||
?: throw Exception("Cannot get content URI")
|
val uri = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
||||||
|
val file = ensureSafeNewFile(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), attachment.name)
|
||||||
|
FileProvider.getUriForFile(context, FILE_PROVIDER_AUTHORITY, file)
|
||||||
|
} else {
|
||||||
|
val contentUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
|
||||||
|
resolver.insert(contentUri, values) ?: throw Exception("Cannot insert content")
|
||||||
}
|
}
|
||||||
this.uri = uri // Required for cleanup in onStopped()
|
this.uri = uri // Required for cleanup in onStopped()
|
||||||
|
|
||||||
Log.d(TAG, "Starting download to content URI: $uri")
|
Log.d(TAG, "Starting download to content URI: $uri")
|
||||||
|
val contentLength = response.headers["Content-Length"]?.toLongOrNull()
|
||||||
var bytesCopied: Long = 0
|
var bytesCopied: Long = 0
|
||||||
val out = resolver.openOutputStream(uri) ?: throw Exception("Cannot open output stream")
|
val outFile = resolver.openOutputStream(uri) ?: throw Exception("Cannot open output stream")
|
||||||
out.use { fileOut ->
|
outFile.use { fileOut ->
|
||||||
val fileIn = response.body!!.byteStream()
|
val fileIn = response.body!!.byteStream()
|
||||||
val buffer = ByteArray(BUFFER_SIZE)
|
val buffer = ByteArray(BUFFER_SIZE)
|
||||||
var bytes = fileIn.read(buffer)
|
var bytes = fileIn.read(buffer)
|
||||||
var lastProgress = 0L
|
var lastProgress = 0L
|
||||||
while (bytes >= 0) {
|
while (bytes >= 0) {
|
||||||
if (System.currentTimeMillis() - lastProgress > NOTIFICATION_UPDATE_INTERVAL_MILLIS) {
|
if (System.currentTimeMillis() - lastProgress > NOTIFICATION_UPDATE_INTERVAL_MILLIS) {
|
||||||
if (isStopped) {
|
if (isStopped) { // Canceled by user
|
||||||
Log.d(TAG, "Attachment download was canceled")
|
save(attachment.copy(progress = PROGRESS_NONE))
|
||||||
val newAttachment = attachment.copy(progress = PROGRESS_NONE)
|
|
||||||
val newNotification = notification.copy(attachment = newAttachment)
|
|
||||||
notifier.update(subscription, newNotification)
|
|
||||||
repository.updateNotification(newNotification)
|
|
||||||
return // File will be deleted in onStopped()
|
return // File will be deleted in onStopped()
|
||||||
}
|
}
|
||||||
val progress = if (size > 0) (bytesCopied.toFloat()/size.toFloat()*100).toInt() else PROGRESS_INDETERMINATE
|
val progress = if (attachment.size != null && attachment.size!! > 0) {
|
||||||
val newAttachment = attachment.copy(progress = progress)
|
(bytesCopied.toFloat()/attachment.size!!.toFloat()*100).toInt()
|
||||||
val newNotification = notification.copy(attachment = newAttachment)
|
} else {
|
||||||
notifier.update(subscription, newNotification)
|
PROGRESS_INDETERMINATE
|
||||||
repository.updateNotification(newNotification)
|
}
|
||||||
|
save(attachment.copy(progress = progress))
|
||||||
lastProgress = System.currentTimeMillis()
|
lastProgress = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
if (contentLength != null && bytesCopied > contentLength) {
|
||||||
|
throw Exception("Attachment is longer than response headers said.")
|
||||||
|
}
|
||||||
fileOut.write(buffer, 0, bytes)
|
fileOut.write(buffer, 0, bytes)
|
||||||
bytesCopied += bytes
|
bytesCopied += bytes
|
||||||
bytes = fileIn.read(buffer)
|
bytes = fileIn.read(buffer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.d(TAG, "Attachment download: successful response, proceeding with download")
|
Log.d(TAG, "Attachment download: successful response, proceeding with download")
|
||||||
val actualName = queryFilename(context, uri.toString(), name)
|
val actualName = queryFilename(context, uri.toString(), attachment.name)
|
||||||
val newAttachment = attachment.copy(
|
save(attachment.copy(
|
||||||
name = actualName,
|
name = actualName,
|
||||||
size = bytesCopied,
|
size = bytesCopied,
|
||||||
contentUri = uri.toString(),
|
contentUri = uri.toString(),
|
||||||
progress = PROGRESS_DONE
|
progress = PROGRESS_DONE
|
||||||
)
|
))
|
||||||
val newNotification = notification.copy(attachment = newAttachment)
|
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
|
||||||
repository.updateNotification(newNotification)
|
values.put(MediaStore.MediaColumns.IS_PENDING, 0)
|
||||||
notifier.update(subscription, newNotification)
|
resolver.update(uri, values, null, null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Attachment download failed", e)
|
failed(e)
|
||||||
|
|
||||||
// Mark attachment download as failed
|
|
||||||
val newAttachment = attachment.copy(progress = PROGRESS_FAILED)
|
|
||||||
val newNotification = notification.copy(attachment = newAttachment)
|
|
||||||
notifier.update(subscription, newNotification)
|
|
||||||
repository.updateNotification(newNotification)
|
|
||||||
|
|
||||||
// Toast in a Worker: https://stackoverflow.com/a/56428145/1440785
|
// Toast in a Worker: https://stackoverflow.com/a/56428145/1440785
|
||||||
val handler = Handler(Looper.getMainLooper())
|
val handler = Handler(Looper.getMainLooper())
|
||||||
|
@ -145,6 +157,64 @@ class DownloadWorker(private val context: Context, params: WorkerParameters) : W
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateAttachmentFromResponse(response: Response): Attachment {
|
||||||
|
val size = if (response.headers["Content-Length"]?.toLongOrNull() != null) {
|
||||||
|
response.headers["Content-Length"]?.toLong()
|
||||||
|
} else {
|
||||||
|
attachment.size // May be null!
|
||||||
|
}
|
||||||
|
val mimeType = if (response.headers["Content-Type"] != null) {
|
||||||
|
response.headers["Content-Type"]
|
||||||
|
} else {
|
||||||
|
val ext = MimeTypeMap.getFileExtensionFromUrl(attachment.url)
|
||||||
|
if (ext != null) {
|
||||||
|
val typeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext)
|
||||||
|
typeFromExt ?: attachment.type // May be null!
|
||||||
|
} else {
|
||||||
|
attachment.type // May be null!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return attachment.copy(
|
||||||
|
size = size,
|
||||||
|
type = mimeType
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun failed(e: Exception) {
|
||||||
|
Log.w(TAG, "Attachment download failed", e)
|
||||||
|
save(attachment.copy(progress = PROGRESS_FAILED))
|
||||||
|
maybeDeleteFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun maybeDeleteFile() {
|
||||||
|
val uriCopy = uri
|
||||||
|
if (uriCopy != null) {
|
||||||
|
Log.d(TAG, "Deleting leftover attachment $uriCopy")
|
||||||
|
val resolver = applicationContext.contentResolver
|
||||||
|
resolver.delete(uriCopy, null, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun save(newAttachment: Attachment) {
|
||||||
|
Log.d(TAG, "Updating attachment: $newAttachment")
|
||||||
|
attachment = newAttachment
|
||||||
|
notification = notification.copy(attachment = newAttachment)
|
||||||
|
notifier.update(subscription, notification)
|
||||||
|
repository.updateNotification(notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shouldAbortDownload(): Boolean {
|
||||||
|
val maxAutoDownloadSize = repository.getAutoDownloadMaxSize()
|
||||||
|
when (maxAutoDownloadSize) {
|
||||||
|
Repository.AUTO_DOWNLOAD_NEVER -> return true
|
||||||
|
Repository.AUTO_DOWNLOAD_ALWAYS -> return false
|
||||||
|
else -> {
|
||||||
|
val size = attachment.size ?: return true // Abort if size unknown
|
||||||
|
return size > maxAutoDownloadSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun ensureSafeNewFile(dir: File, name: String): File {
|
private fun ensureSafeNewFile(dir: File, name: String): File {
|
||||||
val safeName = name.replace("[^-_.()\\w]+".toRegex(), "_");
|
val safeName = name.replace("[^-_.()\\w]+".toRegex(), "_");
|
||||||
val file = File(dir, safeName)
|
val file = File(dir, safeName)
|
||||||
|
@ -165,6 +235,9 @@ class DownloadWorker(private val context: Context, params: WorkerParameters) : W
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
const val INPUT_DATA_ID = "id"
|
||||||
|
const val INPUT_DATA_USER_ACTION = "userAction"
|
||||||
|
|
||||||
private const val TAG = "NtfyAttachDownload"
|
private const val TAG = "NtfyAttachDownload"
|
||||||
private const val FILE_PROVIDER_AUTHORITY = BuildConfig.APPLICATION_ID + ".provider" // See AndroidManifest.xml
|
private const val FILE_PROVIDER_AUTHORITY = BuildConfig.APPLICATION_ID + ".provider" // See AndroidManifest.xml
|
||||||
private const val BUFFER_SIZE = 8 * 1024
|
private const val BUFFER_SIZE = 8 * 1024
|
||||||
|
|
|
@ -41,7 +41,7 @@ class NotificationDispatcher(val context: Context, val repository: Repository) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (download) {
|
if (download) {
|
||||||
DownloadManager.enqueue(context, notification.id)
|
DownloadManager.enqueue(context, notification.id, userAction = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ class NotificationDispatcher(val context: Context, val repository: Repository) {
|
||||||
Repository.AUTO_DOWNLOAD_NEVER -> return false
|
Repository.AUTO_DOWNLOAD_NEVER -> return false
|
||||||
else -> {
|
else -> {
|
||||||
if (notification.attachment.size == null) {
|
if (notification.attachment.size == null) {
|
||||||
return false
|
return true // DownloadWorker will bail out if attachment is too large!
|
||||||
}
|
}
|
||||||
return notification.attachment.size <= maxAutoDownloadSize
|
return notification.attachment.size <= maxAutoDownloadSize
|
||||||
}
|
}
|
||||||
|
|
|
@ -199,7 +199,7 @@ class NotificationService(val context: Context) {
|
||||||
val id = intent.getStringExtra("id") ?: return
|
val id = intent.getStringExtra("id") ?: return
|
||||||
val action = intent.getStringExtra("action") ?: return
|
val action = intent.getStringExtra("action") ?: return
|
||||||
when (action) {
|
when (action) {
|
||||||
DOWNLOAD_ACTION_START -> DownloadManager.enqueue(context, id)
|
DOWNLOAD_ACTION_START -> DownloadManager.enqueue(context, id, userAction = true)
|
||||||
DOWNLOAD_ACTION_CANCEL -> DownloadManager.cancel(context, id)
|
DOWNLOAD_ACTION_CANCEL -> DownloadManager.cancel(context, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -236,7 +236,7 @@ class NotificationService(val context: Context) {
|
||||||
channel
|
channel
|
||||||
}
|
}
|
||||||
5 -> {
|
5 -> {
|
||||||
val channel = NotificationChannel(CHANNEL_ID_MAX, context.getString(R.string.channel_notifications_max_name), NotificationManager.IMPORTANCE_MAX)
|
val channel = NotificationChannel(CHANNEL_ID_MAX, context.getString(R.string.channel_notifications_max_name), NotificationManager.IMPORTANCE_HIGH) // IMPORTANCE_MAX does not exist
|
||||||
channel.enableLights(true)
|
channel.enableLights(true)
|
||||||
channel.enableVibration(true)
|
channel.enableVibration(true)
|
||||||
channel.vibrationPattern = longArrayOf(
|
channel.vibrationPattern = longArrayOf(
|
||||||
|
|
|
@ -18,14 +18,10 @@ import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.work.OneTimeWorkRequest
|
|
||||||
import androidx.work.WorkManager
|
|
||||||
import androidx.work.workDataOf
|
|
||||||
import com.stfalcon.imageviewer.StfalconImageViewer
|
import com.stfalcon.imageviewer.StfalconImageViewer
|
||||||
import io.heckel.ntfy.R
|
import io.heckel.ntfy.R
|
||||||
import io.heckel.ntfy.data.*
|
import io.heckel.ntfy.data.*
|
||||||
import io.heckel.ntfy.msg.DownloadManager
|
import io.heckel.ntfy.msg.DownloadManager
|
||||||
import io.heckel.ntfy.msg.DownloadWorker
|
|
||||||
import io.heckel.ntfy.util.*
|
import io.heckel.ntfy.util.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
@ -254,7 +250,7 @@ class DetailAdapter(private val activity: Activity, private val repository: Repo
|
||||||
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_CODE_WRITE_STORAGE_PERMISSION_FOR_DOWNLOAD)
|
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), REQUEST_CODE_WRITE_STORAGE_PERMISSION_FOR_DOWNLOAD)
|
||||||
return@setOnMenuItemClickListener true
|
return@setOnMenuItemClickListener true
|
||||||
}
|
}
|
||||||
DownloadManager.enqueue(context, notification.id)
|
DownloadManager.enqueue(context, notification.id, userAction = true)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
cancelItem.setOnMenuItemClickListener {
|
cancelItem.setOnMenuItemClickListener {
|
||||||
|
|
Loading…
Reference in a new issue