Remove preview; progress in notification

This commit is contained in:
Philipp Heckel 2022-01-08 21:32:43 -05:00
parent d440d0a633
commit dced0a2e06
10 changed files with 74 additions and 90 deletions

View file

@ -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')"
]
}
}

View file

@ -108,5 +108,6 @@
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_notification"/>
</application>
</manifest>

View file

@ -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)

View file

@ -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,
)

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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<String>()
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"

View file

@ -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 <T1: Any, T2: Any, R: Any> 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())
}

View file

@ -144,6 +144,10 @@
<string name="notification_dialog_tomorrow">Until tomorrow</string>
<string name="notification_dialog_forever">Forever</string>
<!-- Notification popup -->
<string name="notification_popup_action_open">Open</string>
<string name="notification_popup_action_browse">Browse</string>
<!-- Settings -->
<string name="settings_title">Settings</string>
<string name="settings_notifications_header">Notifications</string>

View file

@ -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