From e88f87390e0107dd3aa8c2d0a0faede687200aab Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Sun, 9 Jan 2022 22:08:29 -0500 Subject: [PATCH] - Auto download toggle - Do not update notification if not visible - Detail view menu --- .../main/java/io/heckel/ntfy/data/Database.kt | 6 +- .../java/io/heckel/ntfy/data/Repository.kt | 11 +++ .../ntfy/msg/AttachmentDownloaderWorker.kt | 23 +++-- .../heckel/ntfy/msg/NotificationDispatcher.kt | 8 +- .../io/heckel/ntfy/msg/NotificationService.kt | 40 ++++---- .../ntfy/service/SubscriberConnection.kt | 4 +- .../java/io/heckel/ntfy/ui/DetailActivity.kt | 2 +- .../java/io/heckel/ntfy/ui/DetailAdapter.kt | 96 ++++++++++++++++--- .../java/io/heckel/ntfy/ui/MainActivity.kt | 4 +- .../io/heckel/ntfy/ui/SettingsActivity.kt | 20 ++++ app/src/main/java/io/heckel/ntfy/util/Util.kt | 33 +++++++ .../res/drawable/ic_more_horiz_gray_24dp.xml | 9 ++ .../main/res/layout/fragment_detail_item.xml | 56 ++++++++--- .../main/res/menu/menu_detail_attachment.xml | 7 ++ app/src/main/res/values/strings.xml | 10 ++ app/src/main/res/values/styles.xml | 6 ++ app/src/main/res/xml/main_preferences.xml | 4 + .../heckel/ntfy/firebase/FirebaseService.kt | 1 + assets/more_horiz_black_24dp.svg | 1 + 19 files changed, 273 insertions(+), 68 deletions(-) create mode 100644 app/src/main/res/drawable/ic_more_horiz_gray_24dp.xml create mode 100644 app/src/main/res/menu/menu_detail_attachment.xml create mode 100644 assets/more_horiz_black_24dp.svg 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 cd0e20e..3be8c71 100644 --- a/app/src/main/java/io/heckel/ntfy/data/Database.kt +++ b/app/src/main/java/io/heckel/ntfy/data/Database.kt @@ -68,9 +68,13 @@ data class Attachment( @ColumnInfo(name = "progress") val progress: Int, ) { constructor(name: String?, type: String?, size: Long?, expires: Long?, url: String) : - this(name, type, size, expires, url, null, 0) + this(name, type, size, expires, url, null, PROGRESS_NONE) } +const val PROGRESS_NONE = -1 +const val PROGRESS_INDETERMINATE = -2 +const val PROGRESS_DONE = 100 + @androidx.room.Database(entities = [Subscription::class, Notification::class], version = 6) abstract class Database : RoomDatabase() { abstract fun subscriptionDao(): SubscriptionDao diff --git a/app/src/main/java/io/heckel/ntfy/data/Repository.kt b/app/src/main/java/io/heckel/ntfy/data/Repository.kt index f8cb3bf..abf7688 100644 --- a/app/src/main/java/io/heckel/ntfy/data/Repository.kt +++ b/app/src/main/java/io/heckel/ntfy/data/Repository.kt @@ -161,6 +161,16 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri return sharedPrefs.getInt(SHARED_PREFS_MIN_PRIORITY, 1) // 1/low means all priorities } + fun getAutoDownloadEnabled(): Boolean { + return sharedPrefs.getBoolean(SHARED_PREFS_AUTO_DOWNLOAD_ENABLED, true) // Enabled by default + } + + fun setAutoDownloadEnabled(enabled: Boolean) { + sharedPrefs.edit() + .putBoolean(SHARED_PREFS_AUTO_DOWNLOAD_ENABLED, enabled) + .apply() + } + fun getBroadcastEnabled(): Boolean { return sharedPrefs.getBoolean(SHARED_PREFS_BROADCAST_ENABLED, true) // Enabled by default } @@ -291,6 +301,7 @@ class Repository(private val sharedPrefs: SharedPreferences, private val subscri const val SHARED_PREFS_AUTO_RESTART_WORKER_VERSION = "AutoRestartWorkerVersion" const val SHARED_PREFS_MUTED_UNTIL_TIMESTAMP = "MutedUntil" const val SHARED_PREFS_MIN_PRIORITY = "MinPriority" + const val SHARED_PREFS_AUTO_DOWNLOAD_ENABLED = "AutoDownload" const val SHARED_PREFS_BROADCAST_ENABLED = "BroadcastEnabled" const val SHARED_PREFS_UNIFIED_PUSH_ENABLED = "UnifiedPushEnabled" const val SHARED_PREFS_UNIFIED_PUSH_BASE_URL = "UnifiedPushBaseURL" 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 cb5e864..0459ece 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/AttachmentDownloaderWorker.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/AttachmentDownloaderWorker.kt @@ -8,11 +8,7 @@ import android.util.Log import androidx.work.Worker import androidx.work.WorkerParameters import io.heckel.ntfy.app.Application -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 io.heckel.ntfy.data.* import okhttp3.OkHttpClient import okhttp3.Request import java.util.concurrent.TimeUnit @@ -33,12 +29,12 @@ class AttachmentDownloadWorker(private val context: Context, params: WorkerParam val repository = app.repository val notification = repository.getNotification(notificationId) ?: return Result.failure() val subscription = repository.getSubscription(notification.subscriptionId) ?: return Result.failure() - val attachment = notification.attachment ?: return Result.failure() - downloadAttachment(repository, subscription, notification, attachment) + downloadAttachment(repository, subscription, notification) return Result.success() } - private fun downloadAttachment(repository: Repository, subscription: Subscription, notification: Notification, attachment: Attachment) { + private fun downloadAttachment(repository: Repository, subscription: Subscription, notification: Notification) { + val attachment = notification.attachment ?: return Log.d(TAG, "Downloading attachment from ${attachment.url}") val request = Request.Builder() @@ -71,8 +67,11 @@ class AttachmentDownloadWorker(private val context: Context, params: WorkerParam var lastProgress = 0L while (bytes >= 0) { if (System.currentTimeMillis() - lastProgress > 500) { - val progress = if (size > 0) (bytesCopied.toFloat()/size.toFloat()*100).toInt() else NotificationService.PROGRESS_INDETERMINATE - notifier.update(subscription, notification, progress = progress) + 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) + notifier.update(subscription, newNotification) + repository.updateNotification(newNotification) lastProgress = System.currentTimeMillis() } fileOut.write(buffer, 0, bytes) @@ -81,10 +80,10 @@ class AttachmentDownloadWorker(private val context: Context, params: WorkerParam } } Log.d(TAG, "Attachment download: successful response, proceeding with download") - val newAttachment = attachment.copy(contentUri = uri.toString()) + val newAttachment = attachment.copy(contentUri = uri.toString(), progress = PROGRESS_DONE) val newNotification = notification.copy(attachment = newAttachment) repository.updateNotification(newNotification) - notifier.update(subscription, newNotification, progress = PROGRESS_DONE) + notifier.update(subscription, newNotification) } } 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 ebdda36..5f134fd 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/NotificationDispatcher.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationDispatcher.kt @@ -50,7 +50,7 @@ class NotificationDispatcher(val context: Context, val repository: Repository) { } private fun shouldDownload(subscription: Subscription, notification: Notification): Boolean { - return notification.attachment != null + return notification.attachment != null && repository.getAutoDownloadEnabled() } private fun shouldNotify(subscription: Subscription, notification: Notification, muted: Boolean): Boolean { @@ -84,12 +84,10 @@ class NotificationDispatcher(val context: Context, val repository: Repository) { } private fun scheduleAttachmentDownload(notification: Notification) { - Log.d(TAG, "Enqueuing work to download attachment (+ preview if available)") + 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, - )) + .setInputData(workDataOf("id" to notification.id)) .build() workManager.enqueue(workRequest) } 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 e69c83b..7bd1b13 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt @@ -24,15 +24,21 @@ class NotificationService(val context: Context) { displayInternal(subscription, notification) } - fun update(subscription: Subscription, notification: Notification, progress: Int = PROGRESS_NONE) { - Log.d(TAG, "Updating notification $notification") - displayInternal(subscription, notification, update = true, progress = progress) + fun update(subscription: Subscription, notification: Notification) { + val active = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + notificationManager.activeNotifications.find { it.id == notification.notificationId } != null + } else { + true + } + if (active) { + Log.d(TAG, "Updating notification $notification") + displayInternal(subscription, notification, update = true) + } } fun cancel(notification: Notification) { if (notification.notificationId != 0) { Log.d(TAG, "Cancelling notification ${notification.id}: ${notification.message}") - val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.cancel(notification.notificationId) } } @@ -41,9 +47,9 @@ class NotificationService(val context: Context) { (1..5).forEach { priority -> maybeCreateNotificationChannel(priority) } } - private fun displayInternal(subscription: Subscription, notification: Notification, update: Boolean = false, progress: Int = PROGRESS_NONE) { + private fun displayInternal(subscription: Subscription, notification: Notification, update: Boolean = false) { val title = formatTitle(subscription, notification) - val message = maybeWithAttachmentInfo(formatMessage(notification), notification, progress) + val message = maybeWithAttachmentInfo(formatMessage(notification), notification) val channelId = toChannelId(notification.priority) val builder = NotificationCompat.Builder(context, channelId) .setSmallIcon(R.drawable.ic_notification) @@ -55,7 +61,7 @@ class NotificationService(val context: Context) { setStyle(builder, notification, message) // Preview picture or big text style setContentIntent(builder, subscription, notification) maybeSetSound(builder, update) - maybeSetProgress(builder, progress) + maybeSetProgress(builder, notification) maybeAddOpenAction(builder, notification) maybeAddBrowseAction(builder, notification) @@ -63,16 +69,17 @@ class NotificationService(val context: Context) { 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 + // FIXME duplicate code + private fun maybeWithAttachmentInfo(message: String, notification: Notification): String { + val att = notification.attachment ?: return message + if (att.progress < 0) return message 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 in 0..99) infos.add("${progress}%") + if (att.progress in 0..99) infos.add("${att.progress}%") if (infos.size == 0) return message - if (progress < 100) return "Downloading ${infos.joinToString(", ")}\n${message}" + if (att.progress < 100) return "Downloading ${infos.joinToString(", ")}\n${message}" return "${message}\nFile: ${infos.joinToString(", ")}" } @@ -120,9 +127,10 @@ class NotificationService(val context: Context) { } } - private fun maybeSetProgress(builder: NotificationCompat.Builder, progress: Int) { + private fun maybeSetProgress(builder: NotificationCompat.Builder, notification: Notification) { + val progress = notification.attachment?.progress if (progress in 0..99) { - builder.setProgress(100, progress, false) + builder.setProgress(100, progress!!, false) } else { builder.setProgress(0, 0, false) // Remove progress bar } @@ -206,10 +214,6 @@ 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" diff --git a/app/src/main/java/io/heckel/ntfy/service/SubscriberConnection.kt b/app/src/main/java/io/heckel/ntfy/service/SubscriberConnection.kt index e8ae782..a5ed860 100644 --- a/app/src/main/java/io/heckel/ntfy/service/SubscriberConnection.kt +++ b/app/src/main/java/io/heckel/ntfy/service/SubscriberConnection.kt @@ -87,8 +87,8 @@ class SubscriberConnection( fun cancel() { Log.d(TAG, "[$url] Cancelling connection") - job?.cancel() - call?.cancel() + if (this::job.isInitialized) job?.cancel() + if (this::call.isInitialized) call?.cancel() } private fun nextRetryMillis(retryMillis: Long, startTime: Long): Long { diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt index 599d5fb..21d6a8b 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt @@ -351,7 +351,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra Log.e(TAG, "Error fetching notifications for ${topicShortUrl(subscriptionBaseUrl, subscriptionTopic)}: ${e.stackTrace}", e) runOnUiThread { Toast - .makeText(this@DetailActivity, getString(R.string.refresh_message_error, e.message), Toast.LENGTH_LONG) + .makeText(this@DetailActivity, getString(R.string.refresh_message_error_one, e.message), Toast.LENGTH_LONG) .show() mainListContainer.isRefreshing = false } diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt index dc85524..7166103 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt @@ -1,23 +1,32 @@ package io.heckel.ntfy.ui +import android.app.DownloadManager +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent import android.graphics.BitmapFactory import android.net.Uri import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ImageView -import android.widget.TextView -import androidx.core.app.NotificationCompat +import android.widget.* import androidx.core.content.ContextCompat import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView +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.msg.AttachmentDownloadWorker +import io.heckel.ntfy.msg.NotificationDispatcher import io.heckel.ntfy.util.* import java.util.* + class DetailAdapter(private val onClick: (Notification) -> Unit, private val onLongClick: (Notification) -> Unit) : ListAdapter(TopicDiffCallback) { val selected = mutableSetOf() // Notification IDs @@ -51,13 +60,15 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL private val titleView: TextView = itemView.findViewById(R.id.detail_item_title_text) private val messageView: TextView = itemView.findViewById(R.id.detail_item_message_text) private val newImageView: View = itemView.findViewById(R.id.detail_item_new_dot) - private val tagsView: TextView = itemView.findViewById(R.id.detail_item_tags) + private val tagsView: TextView = itemView.findViewById(R.id.detail_item_tags_text) private val imageView: ImageView = itemView.findViewById(R.id.detail_item_image) + private val attachmentView: TextView = itemView.findViewById(R.id.detail_item_attachment_text) + private val menuButton: ImageButton = itemView.findViewById(R.id.detail_item_menu_button) fun bind(notification: Notification) { this.notification = notification - val ctx = itemView.context + val context = itemView.context val unmatchedTags = unmatchedTags(splitTags(notification.tags)) dateView.text = Date(notification.timestamp * 1000).toString() @@ -73,7 +84,7 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL } if (unmatchedTags.isNotEmpty()) { tagsView.visibility = View.VISIBLE - tagsView.text = ctx.getString(R.string.detail_item_tags, unmatchedTags.joinToString(", ")) + tagsView.text = context.getString(R.string.detail_item_tags, unmatchedTags.joinToString(", ")) } else { tagsView.visibility = View.GONE } @@ -83,29 +94,29 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL when (notification.priority) { 1 -> { priorityImageView.visibility = View.VISIBLE - priorityImageView.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.ic_priority_1_24dp)) + priorityImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_priority_1_24dp)) } 2 -> { priorityImageView.visibility = View.VISIBLE - priorityImageView.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.ic_priority_2_24dp)) + priorityImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_priority_2_24dp)) } 3 -> { priorityImageView.visibility = View.GONE } 4 -> { priorityImageView.visibility = View.VISIBLE - priorityImageView.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.ic_priority_4_24dp)) + priorityImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_priority_4_24dp)) } 5 -> { priorityImageView.visibility = View.VISIBLE - priorityImageView.setImageDrawable(ContextCompat.getDrawable(ctx, R.drawable.ic_priority_5_24dp)) + priorityImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_priority_5_24dp)) } } - // đź“„ val contentUri = notification.attachment?.contentUri - if (contentUri != null && supportedImage(notification.attachment.type)) { + val fileExists = if (contentUri != null) fileExists(context, contentUri) else false + if (contentUri != null && fileExists && supportedImage(notification.attachment.type)) { try { - val resolver = itemView.context.applicationContext.contentResolver + val resolver = context.applicationContext.contentResolver val bitmapStream = resolver.openInputStream(Uri.parse(contentUri)) val bitmap = BitmapFactory.decodeStream(bitmapStream) imageView.setImageBitmap(bitmap) @@ -116,6 +127,61 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL } else { imageView.visibility = View.GONE } + if (notification.attachment != null) { + attachmentView.text = formatAttachmentInfo(notification, fileExists) + attachmentView.visibility = View.VISIBLE + menuButton.visibility = View.VISIBLE + menuButton.setOnClickListener { menuView -> + val popup = PopupMenu(context, menuView) + popup.menuInflater.inflate(R.menu.menu_detail_attachment, popup.menu) + + val downloadItem = popup.menu.findItem(R.id.detail_item_menu_download) + 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) + if (contentUri != null && fileExists) { + openItem.setOnMenuItemClickListener { + context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(contentUri))) // FIXME try/catch + true + } + browseItem.setOnMenuItemClickListener { + context.startActivity(Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)) + true + } + copyUrlItem.setOnMenuItemClickListener { + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("attachment url", notification.attachment.url) + clipboard.setPrimaryClip(clip) + Toast + .makeText(context, context.getString(R.string.detail_copied_to_clipboard_message), Toast.LENGTH_LONG) + .show() + true + } + downloadItem.isVisible = false + } else { + openItem.isVisible = false + browseItem.isVisible = false + downloadItem.setOnMenuItemClickListener { + scheduleAttachmentDownload(context, notification) + true + } + } + + popup.show() + } + } else { + attachmentView.visibility = View.GONE + menuButton.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) } } @@ -128,4 +194,8 @@ class DetailAdapter(private val onClick: (Notification) -> Unit, private val onL return oldItem == newItem } } + + companion object { + const val TAG = "NtfyDetailAdapter" + } } diff --git a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt index 5979d39..c2df4b8 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt @@ -118,12 +118,12 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc startPeriodicPollWorker() startPeriodicServiceRestartWorker() - if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { + /*if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1234); } else { Toast.makeText(this, "Permission already granted", Toast.LENGTH_SHORT).show(); - } + }*/ } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, diff --git a/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt index 8adecf0..49be1f6 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt @@ -106,6 +106,26 @@ class SettingsActivity : AppCompatActivity() { } } + // Auto download + val autoDownloadPrefId = context?.getString(R.string.settings_notifications_auto_download_key) ?: return + val autoDownload: SwitchPreference? = findPreference(autoDownloadPrefId) + autoDownload?.isChecked = repository.getAutoDownloadEnabled() + autoDownload?.preferenceDataStore = object : PreferenceDataStore() { + override fun putBoolean(key: String?, value: Boolean) { + repository.setAutoDownloadEnabled(value) + } + override fun getBoolean(key: String?, defValue: Boolean): Boolean { + return repository.getAutoDownloadEnabled() + } + } + autoDownload?.summaryProvider = Preference.SummaryProvider { pref -> + if (pref.isChecked) { + getString(R.string.settings_notifications_auto_download_summary_on) + } else { + getString(R.string.settings_notifications_auto_download_summary_off) + } + } + // Broadcast enabled val broadcastEnabledPrefId = context?.getString(R.string.settings_advanced_broadcast_key) ?: return val broadcastEnabled: SwitchPreference? = findPreference(broadcastEnabledPrefId) 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 b39c89b..dc19ec3 100644 --- a/app/src/main/java/io/heckel/ntfy/util/Util.kt +++ b/app/src/main/java/io/heckel/ntfy/util/Util.kt @@ -2,8 +2,11 @@ package io.heckel.ntfy.util import android.animation.ArgbEvaluator import android.animation.ValueAnimator +import android.content.Context +import android.net.Uri import android.view.Window import io.heckel.ntfy.data.Notification +import io.heckel.ntfy.data.PROGRESS_NONE import io.heckel.ntfy.data.Subscription import java.security.SecureRandom import java.text.DateFormat @@ -106,6 +109,36 @@ fun formatTitle(notification: Notification): String { } } +// FIXME duplicate code +fun formatAttachmentInfo(notification: Notification, fileExists: Boolean): String { + if (notification.attachment == null) return "" + 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 (att.progress in 0..99) infos.add("${att.progress}%") + if (!fileExists) { + if (att.progress == PROGRESS_NONE) infos.add("not downloaded") + else infos.add("deleted") + } + if (infos.size == 0) return "" + if (att.progress < 100) return "Downloading ${infos.joinToString(", ")}" + return "\uD83D\uDCC4 " + infos.joinToString(", ") +} + +// Checks in the most horrible way if a content URI exists; I couldn't find a better way +fun fileExists(context: Context, uri: String): Boolean { + val resolver = context.applicationContext.contentResolver + return try { + val fileIS = resolver.openInputStream(Uri.parse(uri)) + fileIS?.close() + true + } catch (_: Exception) { + false + } +} + // Status bar color fading to match action bar, see https://stackoverflow.com/q/51150077/1440785 fun fadeStatusBarColor(window: Window, fromColor: Int, toColor: Int) { val statusBarColorAnimation = ValueAnimator.ofObject(ArgbEvaluator(), fromColor, toColor) diff --git a/app/src/main/res/drawable/ic_more_horiz_gray_24dp.xml b/app/src/main/res/drawable/ic_more_horiz_gray_24dp.xml new file mode 100644 index 0000000..df4621b --- /dev/null +++ b/app/src/main/res/drawable/ic_more_horiz_gray_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_detail_item.xml b/app/src/main/res/layout/fragment_detail_item.xml index 994e645..bcb41e2 100644 --- a/app/src/main/res/layout/fragment_detail_item.xml +++ b/app/src/main/res/layout/fragment_detail_item.xml @@ -29,6 +29,14 @@ android:layout_marginTop="1dp" app:layout_constraintStart_toEndOf="@id/detail_item_priority_image" android:layout_marginStart="5dp"/> + - - + android:scaleType="centerCrop" + android:adjustViewBounds="true" android:maxHeight="150dp" android:layout_marginTop="5dp" + app:layout_constraintBottom_toTopOf="@id/detail_item_attachment_text" android:layout_marginBottom="5dp" + app:shapeAppearanceOverlay="@style/roundedCornersImageView" android:visibility="visible"/> + + + diff --git a/app/src/main/res/menu/menu_detail_attachment.xml b/app/src/main/res/menu/menu_detail_attachment.xml new file mode 100644 index 0000000..0b12998 --- /dev/null +++ b/app/src/main/res/menu/menu_detail_attachment.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f3459ed..16c7cb1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -24,6 +24,7 @@ %1$d notification(s) received Everything is up-to-date %1$d subscription(s) could not be refreshed\n\n%2$s + Subscription could not be refreshed: %1$s Subscribed topics @@ -108,6 +109,11 @@ Instant delivery disabled Instant delivery is enabled Tags: %1$s + Open file + Browse file + Download file + Copy URL + Copied URL to clipboard Notifications enabled @@ -166,6 +172,10 @@ Default priority and higher High priority and higher Only max priority + AutoDownload + Auto download attachments + Attachments are automatically downloaded + Attachments are not automatically downloaded UnifiedPush Allows other apps to use ntfy as a message distributor. Find out more at unifiedpush.org. UnifiedPushEnabled diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index ce6d2d0..cd451da 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -6,4 +6,10 @@ @color/primaryColor @color/primaryDarkColor + + + diff --git a/app/src/main/res/xml/main_preferences.xml b/app/src/main/res/xml/main_preferences.xml index 5295d26..49561d2 100644 --- a/app/src/main/res/xml/main_preferences.xml +++ b/app/src/main/res/xml/main_preferences.xml @@ -13,6 +13,10 @@ app:entries="@array/settings_notifications_min_priority_entries" app:entryValues="@array/settings_notifications_min_priority_values" app:defaultValue="1"/> + \ No newline at end of file