mirror of
https://github.com/binwiederhier/ntfy-android.git
synced 2024-05-21 04:52:32 +12:00
219 lines
9 KiB
Kotlin
219 lines
9 KiB
Kotlin
package io.heckel.ntfy.msg
|
|
|
|
import android.content.Context
|
|
import android.net.Uri
|
|
import android.os.Handler
|
|
import android.os.Looper
|
|
import android.webkit.MimeTypeMap
|
|
import android.widget.Toast
|
|
import androidx.core.content.FileProvider
|
|
import androidx.work.Worker
|
|
import androidx.work.WorkerParameters
|
|
import io.heckel.ntfy.BuildConfig
|
|
import io.heckel.ntfy.R
|
|
import io.heckel.ntfy.app.Application
|
|
import io.heckel.ntfy.db.*
|
|
import io.heckel.ntfy.util.Log
|
|
import io.heckel.ntfy.util.ensureSafeNewFile
|
|
import okhttp3.OkHttpClient
|
|
import okhttp3.Request
|
|
import okhttp3.Response
|
|
import java.io.File
|
|
import java.util.concurrent.TimeUnit
|
|
|
|
class DownloadAttachmentWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) {
|
|
private val client = OkHttpClient.Builder()
|
|
.callTimeout(15, TimeUnit.MINUTES) // Total timeout for entire request
|
|
.connectTimeout(15, TimeUnit.SECONDS)
|
|
.readTimeout(15, TimeUnit.SECONDS)
|
|
.writeTimeout(15, TimeUnit.SECONDS)
|
|
.build()
|
|
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
|
|
|
|
override fun doWork(): Result {
|
|
if (context.applicationContext !is Application) 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
|
|
repository = app.repository
|
|
notification = repository.getNotification(notificationId) ?: return Result.failure()
|
|
subscription = repository.getSubscription(notification.subscriptionId) ?: return Result.failure()
|
|
attachment = notification.attachment ?: return Result.failure()
|
|
try {
|
|
downloadAttachment(userAction)
|
|
} catch (e: Exception) {
|
|
failed(e)
|
|
}
|
|
return Result.success()
|
|
}
|
|
|
|
override fun onStopped() {
|
|
Log.d(TAG, "Attachment download was canceled")
|
|
maybeDeleteFile()
|
|
}
|
|
|
|
private fun downloadAttachment(userAction: Boolean) {
|
|
Log.d(TAG, "Downloading attachment from ${attachment.url}")
|
|
|
|
try {
|
|
val request = Request.Builder()
|
|
.url(attachment.url)
|
|
.addHeader("User-Agent", ApiService.USER_AGENT)
|
|
.build()
|
|
client.newCall(request).execute().use { response ->
|
|
Log.d(TAG, "Download: headers received: $response")
|
|
if (!response.isSuccessful || response.body == null) {
|
|
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 resolver = applicationContext.contentResolver
|
|
val uri = createUri(notification)
|
|
this.uri = uri // Required for cleanup in onStopped()
|
|
|
|
Log.d(TAG, "Starting download to content URI: $uri")
|
|
var bytesCopied: Long = 0
|
|
val outFile = resolver.openOutputStream(uri) ?: throw Exception("Cannot open output stream")
|
|
val downloadLimit = getDownloadLimit(userAction)
|
|
outFile.use { fileOut ->
|
|
val fileIn = response.body!!.byteStream()
|
|
val buffer = ByteArray(BUFFER_SIZE)
|
|
var bytes = fileIn.read(buffer)
|
|
var lastProgress = 0L
|
|
while (bytes >= 0) {
|
|
if (System.currentTimeMillis() - lastProgress > NOTIFICATION_UPDATE_INTERVAL_MILLIS) {
|
|
if (isStopped) { // Canceled by user
|
|
save(attachment.copy(progress = ATTACHMENT_PROGRESS_NONE))
|
|
return // File will be deleted in onStopped()
|
|
}
|
|
val progress = if (attachment.size != null && attachment.size!! > 0) {
|
|
(bytesCopied.toFloat()/attachment.size!!.toFloat()*100).toInt()
|
|
} else {
|
|
ATTACHMENT_PROGRESS_INDETERMINATE
|
|
}
|
|
save(attachment.copy(progress = progress))
|
|
lastProgress = System.currentTimeMillis()
|
|
}
|
|
if (downloadLimit != null && bytesCopied > downloadLimit) {
|
|
throw Exception("Attachment is longer than max download size.")
|
|
}
|
|
fileOut.write(buffer, 0, bytes)
|
|
bytesCopied += bytes
|
|
bytes = fileIn.read(buffer)
|
|
}
|
|
}
|
|
Log.d(TAG, "Attachment download: successful response, proceeding with download")
|
|
save(attachment.copy(
|
|
size = bytesCopied,
|
|
contentUri = uri.toString(),
|
|
progress = ATTACHMENT_PROGRESS_DONE
|
|
))
|
|
}
|
|
} catch (e: Exception) {
|
|
failed(e)
|
|
|
|
// Toast in a Worker: https://stackoverflow.com/a/56428145/1440785
|
|
val handler = Handler(Looper.getMainLooper())
|
|
handler.postDelayed({
|
|
Toast
|
|
.makeText(context, context.getString(R.string.detail_item_download_failed, e.message), Toast.LENGTH_LONG)
|
|
.show()
|
|
}, 200)
|
|
}
|
|
}
|
|
|
|
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 = ATTACHMENT_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 false // Don't abort if size unknown
|
|
return size > maxAutoDownloadSize
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun getDownloadLimit(userAction: Boolean): Long? {
|
|
return if (userAction || repository.getAutoDownloadMaxSize() == Repository.AUTO_DOWNLOAD_ALWAYS) {
|
|
null
|
|
} else {
|
|
repository.getAutoDownloadMaxSize()
|
|
}
|
|
}
|
|
|
|
private fun createUri(notification: Notification): Uri {
|
|
val attachmentDir = File(context.cacheDir, ATTACHMENT_CACHE_DIR)
|
|
if (!attachmentDir.exists() && !attachmentDir.mkdirs()) {
|
|
throw Exception("Cannot create cache directory for attachments: $attachmentDir")
|
|
}
|
|
val file = ensureSafeNewFile(attachmentDir, notification.id)
|
|
return FileProvider.getUriForFile(context, FILE_PROVIDER_AUTHORITY, file)
|
|
}
|
|
|
|
companion object {
|
|
const val INPUT_DATA_ID = "id"
|
|
const val INPUT_DATA_USER_ACTION = "userAction"
|
|
const val FILE_PROVIDER_AUTHORITY = BuildConfig.APPLICATION_ID + ".provider" // See AndroidManifest.xml
|
|
|
|
private const val TAG = "NtfyAttachDownload"
|
|
private const val ATTACHMENT_CACHE_DIR = "attachments"
|
|
private const val BUFFER_SIZE = 8 * 1024
|
|
private const val NOTIFICATION_UPDATE_INTERVAL_MILLIS = 800
|
|
}
|
|
}
|