diff --git a/app/schemas/io.heckel.ntfy.data.Database/6.json b/app/schemas/io.heckel.ntfy.data.Database/6.json index c6efc44..f37de76 100644 --- a/app/schemas/io.heckel.ntfy.data.Database/6.json +++ b/app/schemas/io.heckel.ntfy.data.Database/6.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 6, - "identityHash": "1ab02dd84a7f2655b4fc651574b24240", + "identityHash": "fc725df9153ee7088ae8024428b7f2cf", "entities": [ { "tableName": "Subscription", @@ -80,7 +80,7 @@ }, { "tableName": "Notification", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `subscriptionId` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `title` TEXT NOT NULL, `message` TEXT NOT NULL, `notificationId` INTEGER NOT NULL, `priority` INTEGER NOT NULL DEFAULT 3, `tags` TEXT NOT NULL, `click` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `attachment_name` TEXT, `attachment_type` TEXT, `attachment_size` INTEGER, `attachment_expires` INTEGER, `attachment_previewUrl` TEXT, `attachment_url` TEXT, `attachment_contentUri` TEXT, `attachment_previewFile` TEXT, `attachment_progress` INTEGER, PRIMARY KEY(`id`, `subscriptionId`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `subscriptionId` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `title` TEXT NOT NULL, `message` TEXT NOT NULL, `notificationId` INTEGER NOT NULL, `priority` INTEGER NOT NULL DEFAULT 3, `tags` TEXT NOT NULL, `click` TEXT NOT NULL, `deleted` INTEGER NOT NULL, `attachment_name` TEXT, `attachment_type` TEXT, `attachment_size` INTEGER, `attachment_expires` INTEGER, `attachment_url` TEXT, `attachment_contentUri` TEXT, `attachment_progress` INTEGER, PRIMARY KEY(`id`, `subscriptionId`))", "fields": [ { "fieldPath": "id", @@ -167,12 +167,6 @@ "affinity": "INTEGER", "notNull": false }, - { - "fieldPath": "attachment.previewUrl", - "columnName": "attachment_previewUrl", - "affinity": "TEXT", - "notNull": false - }, { "fieldPath": "attachment.url", "columnName": "attachment_url", @@ -185,12 +179,6 @@ "affinity": "TEXT", "notNull": false }, - { - "fieldPath": "attachment.previewFile", - "columnName": "attachment_previewFile", - "affinity": "TEXT", - "notNull": false - }, { "fieldPath": "attachment.progress", "columnName": "attachment_progress", @@ -212,7 +200,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1ab02dd84a7f2655b4fc651574b24240')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fc725df9153ee7088ae8024428b7f2cf')" ] } } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c9f7771..f869faa 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -108,5 +108,6 @@ + diff --git a/app/src/main/java/io/heckel/ntfy/data/Database.kt b/app/src/main/java/io/heckel/ntfy/data/Database.kt index 033411e..cd0e20e 100644 --- a/app/src/main/java/io/heckel/ntfy/data/Database.kt +++ b/app/src/main/java/io/heckel/ntfy/data/Database.kt @@ -63,14 +63,12 @@ data class Attachment( @ColumnInfo(name = "type") val type: String?, // MIME type @ColumnInfo(name = "size") val size: Long?, // Size in bytes @ColumnInfo(name = "expires") val expires: Long?, // Unix timestamp - @ColumnInfo(name = "previewUrl") val previewUrl: String?, @ColumnInfo(name = "url") val url: String, @ColumnInfo(name = "contentUri") val contentUri: String?, - @ColumnInfo(name = "previewFile") val previewFile: String?, @ColumnInfo(name = "progress") val progress: Int, ) { - constructor(name: String?, type: String?, size: Long?, expires: Long?, previewUrl: String?, url: String) : - this(name, type, size, expires, previewUrl, url, null, null, 0) + constructor(name: String?, type: String?, size: Long?, expires: Long?, url: String) : + this(name, type, size, expires, url, null, 0) } @androidx.room.Database(entities = [Subscription::class, Notification::class], version = 6) diff --git a/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt b/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt index a108222..c5b301f 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/ApiService.kt @@ -7,11 +7,7 @@ import com.google.gson.Gson import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.data.Attachment import io.heckel.ntfy.data.Notification -import io.heckel.ntfy.util.topicUrl -import io.heckel.ntfy.util.topicUrlJson -import io.heckel.ntfy.util.topicUrlJsonPoll -import io.heckel.ntfy.util.toPriority -import io.heckel.ntfy.util.joinTags +import io.heckel.ntfy.util.* import okhttp3.* import okhttp3.RequestBody.Companion.toRequestBody import java.io.IOException @@ -119,7 +115,6 @@ class ApiService { type = message.attachment.type, size = message.attachment.size, expires = message.attachment.expires, - previewUrl = message.attachment.preview_url, url = message.attachment.url, ) } else null @@ -160,7 +155,6 @@ class ApiService { type = message.attachment.type, size = message.attachment.size, expires = message.attachment.expires, - previewUrl = message.attachment.preview_url, url = message.attachment.url, ) } else null @@ -174,7 +168,7 @@ class ApiService { tags = joinTags(message.tags), click = message.click ?: "", attachment = attachment, - notificationId = 0, // zero! + notificationId = 0, // zero: when we poll, we do not want a notificationId! deleted = false ) } @@ -192,16 +186,15 @@ class ApiService { val click: String?, val title: String?, val message: String, - val attachment: Attachment?, + val attachment: MessageAttachment?, ) @Keep - private data class Attachment( + private data class MessageAttachment( val name: String, val type: String?, val size: Long?, val expires: Long?, - val preview_url: String?, val url: String, ) diff --git a/app/src/main/java/io/heckel/ntfy/msg/AttachmentDownloaderWorker.kt b/app/src/main/java/io/heckel/ntfy/msg/AttachmentDownloaderWorker.kt index 86c6fa6..cb5e864 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/AttachmentDownloaderWorker.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/AttachmentDownloaderWorker.kt @@ -2,7 +2,6 @@ package io.heckel.ntfy.msg import android.content.ContentValues import android.content.Context -import android.graphics.BitmapFactory import android.os.Environment import android.provider.MediaStore import android.util.Log @@ -13,10 +12,9 @@ import io.heckel.ntfy.data.Attachment import io.heckel.ntfy.data.Notification import io.heckel.ntfy.data.Repository import io.heckel.ntfy.data.Subscription +import io.heckel.ntfy.msg.NotificationService.Companion.PROGRESS_DONE import okhttp3.OkHttpClient import okhttp3.Request -import java.io.File -import java.io.FileOutputStream import java.util.concurrent.TimeUnit class AttachmentDownloadWorker(private val context: Context, params: WorkerParameters) : Worker(context, params) { @@ -36,35 +34,10 @@ class AttachmentDownloadWorker(private val context: Context, params: WorkerParam val notification = repository.getNotification(notificationId) ?: return Result.failure() val subscription = repository.getSubscription(notification.subscriptionId) ?: return Result.failure() val attachment = notification.attachment ?: return Result.failure() - if (attachment.previewUrl != null) { - downloadPreview(subscription, notification, attachment) - } downloadAttachment(repository, subscription, notification, attachment) return Result.success() } - private fun downloadPreview(subscription: Subscription, notification: Notification, attachment: Attachment) { - val url = attachment.previewUrl ?: return - Log.d(TAG, "Downloading preview from $url") - - val request = Request.Builder() - .url(url) - .addHeader("User-Agent", ApiService.USER_AGENT) - .build() - client.newCall(request).execute().use { response -> - if (!response.isSuccessful || response.body == null) { - throw Exception("Preview download failed: ${response.code}") - } - val previewFile = File(applicationContext.cacheDir.absolutePath, "preview-" + notification.id) - Log.d(TAG, "Downloading preview to cache file: $previewFile") - FileOutputStream(previewFile).use { fileOut -> - response.body!!.byteStream().copyTo(fileOut) - } - Log.d(TAG, "Preview downloaded; updating notification") - notifier.update(subscription, notification) - } - } - private fun downloadAttachment(repository: Repository, subscription: Subscription, notification: Notification, attachment: Attachment) { Log.d(TAG, "Downloading attachment from ${attachment.url}") @@ -111,7 +84,7 @@ class AttachmentDownloadWorker(private val context: Context, params: WorkerParam val newAttachment = attachment.copy(contentUri = uri.toString()) val newNotification = notification.copy(attachment = newAttachment) repository.updateNotification(newNotification) - notifier.update(subscription, newNotification) + notifier.update(subscription, newNotification, progress = PROGRESS_DONE) } } diff --git a/app/src/main/java/io/heckel/ntfy/msg/NotificationDispatcher.kt b/app/src/main/java/io/heckel/ntfy/msg/NotificationDispatcher.kt index 7a8743d..ebdda36 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/NotificationDispatcher.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationDispatcher.kt @@ -42,7 +42,7 @@ class NotificationDispatcher(val context: Context, val repository: Repository) { } } if (download) { - // Download attachment (+ preview if available) in the background via WorkManager + // 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) diff --git a/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt b/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt index 1f69582..22b708a 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt @@ -1,12 +1,7 @@ package io.heckel.ntfy.msg -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.app.TaskStackBuilder -import android.content.Context -import android.content.Intent -import android.graphics.Bitmap +import android.app.* +import android.content.* import android.graphics.BitmapFactory import android.media.RingtoneManager import android.net.Uri @@ -14,18 +9,15 @@ 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.Notification -import io.heckel.ntfy.data.Repository import io.heckel.ntfy.data.Subscription import io.heckel.ntfy.ui.DetailActivity import io.heckel.ntfy.ui.MainActivity +import io.heckel.ntfy.util.formatBytes +import io.heckel.ntfy.util.formatDateShort import io.heckel.ntfy.util.formatMessage import io.heckel.ntfy.util.formatTitle -import java.io.File class NotificationService(val context: Context) { private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -54,7 +46,7 @@ class NotificationService(val context: Context) { private fun displayInternal(subscription: Subscription, notification: Notification, update: Boolean = false, progress: Int = PROGRESS_NONE) { val title = formatTitle(subscription, notification) - val message = formatMessage(notification) + val message = maybeWithAttachmentInfo(formatMessage(notification), notification, progress) val channelId = toChannelId(notification.priority) val builder = NotificationCompat.Builder(context, channelId) .setSmallIcon(R.drawable.ic_notification) @@ -68,12 +60,25 @@ class NotificationService(val context: Context) { maybeSetSound(builder, update) maybeSetProgress(builder, progress) maybeAddOpenAction(builder, notification) - maybeAddCopyUrlAction(builder, notification) + maybeAddBrowseAction(builder, notification) maybeCreateNotificationChannel(notification.priority) notificationManager.notify(notification.notificationId, builder.build()) } + private fun maybeWithAttachmentInfo(message: String, notification: Notification, progress: Int): String { + if (progress < 0 || notification.attachment == null) return message + val att = notification.attachment + val infos = mutableListOf() + if (att.name != null) infos.add(att.name) + if (att.size != null) infos.add(formatBytes(att.size)) + //if (att.expires != null && att.expires != 0L) infos.add(formatDateShort(att.expires)) + if (progress >= 0) infos.add("${progress}%") + if (infos.size == 0) return message + if (progress < 100) return "Downloading ${infos.joinToString(", ")}\n${message}" + return "${message}\nFile: ${infos.joinToString(", ")}" + } + private fun maybeSetSound(builder: NotificationCompat.Builder, update: Boolean) { if (!update) { val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) @@ -84,10 +89,13 @@ class NotificationService(val context: Context) { } private fun setStyle(builder: NotificationCompat.Builder, notification: Notification, message: String) { - val previewFile = File(context.applicationContext.cacheDir.absolutePath, "preview-" + notification.id) - if (previewFile.exists()) { + val contentUri = notification.attachment?.contentUri + val isImage = listOf("image/jpeg", "image/png").contains(notification.attachment?.type) + if (contentUri != null && isImage) { try { - val bitmap = BitmapFactory.decodeFile(previewFile.absolutePath) + val resolver = context.applicationContext.contentResolver + val bitmapStream = resolver.openInputStream(Uri.parse(contentUri)) + val bitmap = BitmapFactory.decodeStream(bitmapStream) builder .setLargeIcon(bitmap) .setStyle(NotificationCompat.BigPictureStyle() @@ -116,26 +124,27 @@ class NotificationService(val context: Context) { } private fun maybeSetProgress(builder: NotificationCompat.Builder, progress: Int) { - if (progress >= 0) { + if (progress in 0..99) { builder.setProgress(100, progress, false) } else { builder.setProgress(0, 0, false) // Remove progress bar } } - private fun maybeAddOpenAction(notificationBuilder: NotificationCompat.Builder, notification: Notification) { + private fun maybeAddOpenAction(builder: NotificationCompat.Builder, notification: Notification) { if (notification.attachment?.contentUri != null) { val contentUri = Uri.parse(notification.attachment.contentUri) - val openIntent = PendingIntent.getActivity(context, 0, Intent(Intent.ACTION_VIEW, contentUri), 0) - notificationBuilder.addAction(NotificationCompat.Action.Builder(0, "Open", openIntent).build()) + val intent = Intent(Intent.ACTION_VIEW, contentUri) + val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0) + builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_open), pendingIntent).build()) } } - private fun maybeAddCopyUrlAction(builder: NotificationCompat.Builder, notification: Notification) { - if (notification.attachment?.url != null) { - // XXXXXXXXx - val copyUrlIntent = PendingIntent.getActivity(context, 0, Intent(Intent.ACTION_VIEW, Uri.parse(notification.attachment.url)), 0) - builder.addAction(NotificationCompat.Action.Builder(0, "Copy URL", copyUrlIntent).build()) + private fun maybeAddBrowseAction(builder: NotificationCompat.Builder, notification: Notification) { + if (notification.attachment?.contentUri != null) { + val intent = Intent(DownloadManager.ACTION_VIEW_DOWNLOADS) + val pendingIntent = PendingIntent.getActivity(context, 0, intent, 0) + builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_browse), pendingIntent).build()) } } @@ -202,8 +211,10 @@ class NotificationService(val context: Context) { companion object { const val PROGRESS_NONE = -1 const val PROGRESS_INDETERMINATE = -2 + const val PROGRESS_DONE = 100 private const val TAG = "NtfyNotifService" + private const val CHANNEL_ID_MIN = "ntfy-min" private const val CHANNEL_ID_LOW = "ntfy-low" private const val CHANNEL_ID_DEFAULT = "ntfy" diff --git a/app/src/main/java/io/heckel/ntfy/util/Util.kt b/app/src/main/java/io/heckel/ntfy/util/Util.kt index 61a3d36..04a19d1 100644 --- a/app/src/main/java/io/heckel/ntfy/util/Util.kt +++ b/app/src/main/java/io/heckel/ntfy/util/Util.kt @@ -7,6 +7,7 @@ import io.heckel.ntfy.data.Notification import io.heckel.ntfy.data.Subscription import java.security.SecureRandom import java.text.DateFormat +import java.text.StringCharacterIterator import java.util.* fun topicUrl(baseUrl: String, topic: String) = "${baseUrl}/${topic}" @@ -19,8 +20,8 @@ fun topicShortUrl(baseUrl: String, topic: String) = .replace("https://", "") fun formatDateShort(timestampSecs: Long): String { - val mutedUntilDate = Date(timestampSecs*1000) - return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(mutedUntilDate) + val date = Date(timestampSecs*1000) + return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(date) } fun toPriority(priority: Int?): Int { @@ -126,3 +127,20 @@ fun randomString(len: Int): String { inline fun safeLet(p1: T1?, p2: T2?, block: (T1, T2)->R?): R? { return if (p1 != null && p2 != null) block(p1, p2) else null } + +fun formatBytes(bytes: Long): String { + val absB = if (bytes == Long.MIN_VALUE) Long.MAX_VALUE else Math.abs(bytes) + if (absB < 1024) { + return "$bytes B" + } + var value = absB + val ci = StringCharacterIterator("KMGTPE") + var i = 40 + while (i >= 0 && absB > 0xfffccccccccccccL shr i) { + value = value shr 10 + ci.next() + i -= 10 + } + value *= java.lang.Long.signum(bytes).toLong() + return java.lang.String.format("%.1f %ciB", value / 1024.0, ci.current()) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1767b78..f3459ed 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -144,6 +144,10 @@ Until tomorrow Forever + + Open + Browse + Settings Notifications diff --git a/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt b/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt index 2bd5142..b7f062b 100644 --- a/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt +++ b/app/src/play/java/io/heckel/ntfy/firebase/FirebaseService.kt @@ -62,7 +62,6 @@ class FirebaseService : FirebaseMessagingService() { val attachmentType = data["attachment_type"] val attachmentSize = data["attachment_size"]?.toLongOrNull() val attachmentExpires = data["attachment_expires"]?.toLongOrNull() - val attachmentPreviewUrl = data["attachment_preview_url"] val attachmentUrl = data["attachment_url"] val truncated = (data["truncated"] ?: "") == "1" if (id == null || topic == null || message == null || timestamp == null) { @@ -88,7 +87,6 @@ class FirebaseService : FirebaseMessagingService() { type = attachmentType, size = attachmentSize, expires = attachmentExpires, - previewUrl = attachmentPreviewUrl, url = attachmentUrl, ) } else null