From ac3496d7fa544bdf49f1f6634371a642d899c9ad Mon Sep 17 00:00:00 2001
From: Philipp Heckel <pheckel@datto.com>
Date: Sun, 8 May 2022 22:57:52 -0400
Subject: [PATCH] Polishing

---
 .../io/heckel/ntfy/msg/NotificationService.kt | 30 +++++--------------
 .../heckel/ntfy/service/SubscriberService.kt  |  4 +++
 .../java/io/heckel/ntfy/ui/DetailAdapter.kt   |  4 +--
 .../heckel/ntfy/ui/DetailSettingsActivity.kt  | 10 ++-----
 .../java/io/heckel/ntfy/ui/MainAdapter.kt     | 10 ++-----
 .../java/io/heckel/ntfy/ui/ShareActivity.kt   |  5 +---
 app/src/main/java/io/heckel/ntfy/util/Util.kt | 21 +++++++++++++
 app/src/main/res/values/strings.xml           | 11 +++++--
 app/src/main/res/xml/detail_preferences.xml   |  8 ++---
 9 files changed, 52 insertions(+), 51 deletions(-)

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 f614b1d..06b8ad1 100644
--- a/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt
+++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt
@@ -5,7 +5,6 @@ import android.content.ActivityNotFoundException
 import android.content.BroadcastReceiver
 import android.content.Context
 import android.content.Intent
-import android.graphics.BitmapFactory
 import android.media.RingtoneManager
 import android.net.Uri
 import android.os.Build
@@ -70,9 +69,8 @@ class NotificationService(val context: Context) {
             .setContentTitle(title)
             .setOnlyAlertOnce(true) // Do not vibrate or play sound if already showing (updates!)
             .setAutoCancel(true) // Cancel when notification is clicked
-        setStyleAndText(builder, notification) // Preview picture or big text style
+        setStyleAndText(builder, subscription, notification) // Preview picture or big text style
         setClickAction(builder, subscription, notification)
-        maybeSetIcon(builder, subscription)
         maybeSetSound(builder, update)
         maybeSetProgress(builder, notification)
         maybeAddOpenAction(builder, notification)
@@ -85,18 +83,6 @@ class NotificationService(val context: Context) {
         notificationManager.notify(notification.notificationId, builder.build())
     }
 
-    private fun maybeSetIcon(builder: NotificationCompat.Builder, subscription: Subscription) {
-        val icon = subscription.icon ?: return
-        try {
-            val resolver = context.applicationContext.contentResolver
-            val bitmapStream = resolver.openInputStream(Uri.parse(icon))
-            val bitmap = BitmapFactory.decodeStream(bitmapStream)
-            builder.setLargeIcon(bitmap)
-        } catch (e: Exception) {
-            Log.w(TAG, "Cannot load subscription icon", e)
-        }
-    }
-
     private fun maybeSetSound(builder: NotificationCompat.Builder, update: Boolean) {
         if (!update) {
             val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
@@ -106,20 +92,19 @@ class NotificationService(val context: Context) {
         }
     }
 
-    private fun setStyleAndText(builder: NotificationCompat.Builder, notification: Notification) {
+    private fun setStyleAndText(builder: NotificationCompat.Builder, subscription: Subscription, notification: Notification) {
         val contentUri = notification.attachment?.contentUri
         val isSupportedImage = supportedImage(notification.attachment?.type)
+        val subscriptionIcon = if (subscription.icon != null) subscription.icon.readBitmapFromUriOrNull(context) else null
         if (contentUri != null && isSupportedImage) {
             try {
-                val resolver = context.applicationContext.contentResolver
-                val bitmapStream = resolver.openInputStream(Uri.parse(contentUri))
-                val bitmap = BitmapFactory.decodeStream(bitmapStream)
+                val attachmentBitmap = contentUri.readBitmapFromUri(context)
                 builder
                     .setContentText(maybeAppendActionErrors(formatMessage(notification), notification))
-                    .setLargeIcon(bitmap)
+                    .setLargeIcon(attachmentBitmap)
                     .setStyle(NotificationCompat.BigPictureStyle()
-                        .bigPicture(bitmap)
-                        .bigLargeIcon(null))
+                        .bigPicture(attachmentBitmap)
+                        .bigLargeIcon(subscriptionIcon)) // May be null
             } catch (_: Exception) {
                 val message = maybeAppendActionErrors(formatMessageMaybeWithAttachmentInfos(notification), notification)
                 builder
@@ -131,6 +116,7 @@ class NotificationService(val context: Context) {
             builder
                 .setContentText(message)
                 .setStyle(NotificationCompat.BigTextStyle().bigText(message))
+                .setLargeIcon(subscriptionIcon) // May be null
         }
     }
 
diff --git a/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt b/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt
index 09d745d..b453e4d 100644
--- a/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt
+++ b/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt
@@ -233,6 +233,8 @@ class SubscriberService : Service() {
                     2 -> getString(R.string.channel_subscriber_notification_instant_text_two)
                     3 -> getString(R.string.channel_subscriber_notification_instant_text_three)
                     4 -> getString(R.string.channel_subscriber_notification_instant_text_four)
+                    5 -> getString(R.string.channel_subscriber_notification_instant_text_five)
+                    6 -> getString(R.string.channel_subscriber_notification_instant_text_six)
                     else -> getString(R.string.channel_subscriber_notification_instant_text_more, instantSubscriptions.size)
                 }
             } else {
@@ -241,6 +243,8 @@ class SubscriberService : Service() {
                     2 -> getString(R.string.channel_subscriber_notification_noinstant_text_two)
                     3 -> getString(R.string.channel_subscriber_notification_noinstant_text_three)
                     4 -> getString(R.string.channel_subscriber_notification_noinstant_text_four)
+                    5 -> getString(R.string.channel_subscriber_notification_noinstant_text_five)
+                    6 -> getString(R.string.channel_subscriber_notification_noinstant_text_six)
                     else -> getString(R.string.channel_subscriber_notification_noinstant_text_more, instantSubscriptions.size)
                 }
             }
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 a9dbc50..64e2d99 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt
@@ -289,9 +289,7 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
                 return
             }
             try {
-                val resolver = context.applicationContext.contentResolver
-                val bitmapStream = resolver.openInputStream(Uri.parse(attachment.contentUri))
-                val bitmap = BitmapFactory.decodeStream(bitmapStream)
+                val bitmap = attachment.contentUri?.readBitmapFromUri(context) ?: throw Exception("uri empty")
                 attachmentImageView.setImageBitmap(bitmap)
                 attachmentImageView.setOnClickListener {
                     val loadImage = { view: ImageView, image: Bitmap -> view.setImageBitmap(image) }
diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailSettingsActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailSettingsActivity.kt
index 350398e..8ead31a 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/DetailSettingsActivity.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/DetailSettingsActivity.kt
@@ -268,8 +268,7 @@ class DetailSettingsActivity : AppCompatActivity() {
             // Set icon (if it exists)
             if (subscription.icon != null) {
                 try {
-                    val bitmapStream = resolver.openInputStream(Uri.parse(subscription.icon))
-                    val bitmap = BitmapFactory.decodeStream(bitmapStream)
+                    val bitmap = subscription.icon!!.readBitmapFromUri(requireContext())
                     iconRemovePref.icon = bitmap.toDrawable(resources)
                 } catch (e: Exception) {
                     Log.w(TAG, "Unable to set icon ${subscription.icon}", e)
@@ -292,11 +291,8 @@ class DetailSettingsActivity : AppCompatActivity() {
                             it.copyTo(outputStream)
                         }
 
-                        // Read image and set as preference icon
-                        val bitmapStream = resolver.openInputStream(Uri.parse(outputUri.toString()))
-                        val bitmap = BitmapFactory.decodeStream(bitmapStream)
-
-                        // Display "remove" preference
+                        // Read image & display "remove" preference
+                        val bitmap = outputUri.readBitmapFromUri(requireContext())
                         iconRemovePref.icon = bitmap.toDrawable(resources)
                         iconRemovePref.isVisible = true
                         iconSetPref.isVisible = false
diff --git a/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt b/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt
index a275364..aebeadc 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt
@@ -18,6 +18,7 @@ import io.heckel.ntfy.db.Repository
 import io.heckel.ntfy.db.Subscription
 import io.heckel.ntfy.msg.NotificationService
 import io.heckel.ntfy.util.Log
+import io.heckel.ntfy.util.readBitmapFromUriOrNull
 import io.heckel.ntfy.util.topicShortUrl
 import java.text.DateFormat
 import java.util.*
@@ -91,14 +92,7 @@ class MainAdapter(private val repository: Repository, private val onClick: (Subs
             val showMutedForeverIcon = (subscription.mutedUntil == 1L || globalMutedUntil == 1L) && !isUnifiedPush
             val showMutedUntilIcon = !showMutedForeverIcon && (subscription.mutedUntil > 1L || globalMutedUntil > 1L) && !isUnifiedPush
             if (subscription.icon != null) {
-                try {
-                    val resolver = context.applicationContext.contentResolver
-                    val bitmapStream = resolver.openInputStream(Uri.parse(subscription.icon))
-                    val bitmap = BitmapFactory.decodeStream(bitmapStream)
-                    imageView.setImageBitmap(bitmap)
-                } catch (e: Exception) {
-                    Log.w(TAG, "Cannot load subscription icon", e)
-                }
+                imageView.setImageBitmap(subscription.icon.readBitmapFromUriOrNull(context))
             }
             nameView.text = topicShortUrl(subscription.baseUrl, subscription.topic)
             statusView.text = statusMessage
diff --git a/app/src/main/java/io/heckel/ntfy/ui/ShareActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/ShareActivity.kt
index be7b20c..5f31281 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/ShareActivity.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/ShareActivity.kt
@@ -193,10 +193,7 @@ class ShareActivity : AppCompatActivity() {
             return
         }
         try {
-            val resolver = applicationContext.contentResolver
-            val bitmapStream = resolver.openInputStream(fileUri!!)
-            val bitmap = BitmapFactory.decodeStream(bitmapStream)
-            contentImage.setImageBitmap(bitmap)
+            contentImage.setImageBitmap(fileUri!!.readBitmapFromUri(applicationContext))
             contentText.text = getString(R.string.share_content_image_text)
             show(image = true)
         } catch (e: Exception) {
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 5f39c0e..6676942 100644
--- a/app/src/main/java/io/heckel/ntfy/util/Util.kt
+++ b/app/src/main/java/io/heckel/ntfy/util/Util.kt
@@ -9,6 +9,8 @@ import android.content.Context
 import android.content.res.Configuration
 import android.content.res.Configuration.UI_MODE_NIGHT_YES
 import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
 import android.graphics.drawable.RippleDrawable
 import android.net.Uri
 import android.os.Build
@@ -394,6 +396,25 @@ fun View.ripple(scope: CoroutineScope) {
     }
 }
 
+
+fun Uri.readBitmapFromUri(context: Context): Bitmap {
+    val resolver = context.applicationContext.contentResolver
+    val bitmapStream = resolver.openInputStream(this)
+    return BitmapFactory.decodeStream(bitmapStream)
+}
+
+fun String.readBitmapFromUri(context: Context): Bitmap {
+    return Uri.parse(this).readBitmapFromUri(context)
+}
+
+fun String.readBitmapFromUriOrNull(context: Context): Bitmap? {
+    return try {
+        this.readBitmapFromUri(context)
+    } catch (_: Exception) {
+        null
+    }
+}
+
 // TextWatcher that only implements the afterTextChanged method
 class AfterChangedTextWatcher(val afterTextChangedFn: (s: Editable?) -> Unit) : TextWatcher {
     override fun afterTextChanged(s: Editable?) {
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 0545d56..faeadae 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -12,12 +12,16 @@
     <string name="channel_subscriber_notification_instant_text_two">Subscribed to two instant delivery topics</string>
     <string name="channel_subscriber_notification_instant_text_three">Subscribed to three instant delivery topics</string>
     <string name="channel_subscriber_notification_instant_text_four">Subscribed to four instant delivery topics</string>
+    <string name="channel_subscriber_notification_instant_text_five">Subscribed to five instant delivery topics</string>
+    <string name="channel_subscriber_notification_instant_text_six">Subscribed to six instant delivery topics</string>
     <string name="channel_subscriber_notification_instant_text_more">Subscribed to %1$d instant delivery topics</string>
     <string name="channel_subscriber_notification_noinstant_text">Subscribed to topics</string>
     <string name="channel_subscriber_notification_noinstant_text_one">Subscribed to one topic</string>
     <string name="channel_subscriber_notification_noinstant_text_two">Subscribed to two topics</string>
     <string name="channel_subscriber_notification_noinstant_text_three">Subscribed to three topics</string>
     <string name="channel_subscriber_notification_noinstant_text_four">Subscribed to four topics</string>
+    <string name="channel_subscriber_notification_noinstant_text_five">Subscribed to five topics</string>
+    <string name="channel_subscriber_notification_noinstant_text_six">Subscribed to six topics</string>
     <string name="channel_subscriber_notification_noinstant_text_more">Subscribed to %1$d topics</string>
 
     <!-- Common refresh toasts -->
@@ -340,9 +344,10 @@
     <string name="detail_settings_notifications_instant_summary_on">Notifications are delivered instantly. Requires a foreground service and consumes more battery.</string>
     <string name="detail_settings_notifications_instant_summary_off">Notifications are delivered using Firebase. Delivery may be delayed, but consumes less battery.</string>
     <string name="detail_settings_appearance_header">Appearance</string>
-    <string name="detail_settings_appearance_icon_title">Subscription icon</string>
-    <string name="detail_settings_appearance_icon_set_summary_set">This icon is displayed in notifications to this topic. Tap to remove it.</string>
-    <string name="detail_settings_appearance_icon_set_summary_no_set">Set an icon to be displayed in notifications</string>
+    <string name="detail_settings_appearance_icon_set_title">Subscription icon</string>
+    <string name="detail_settings_appearance_icon_set_summary">Set an icon to be displayed in notifications</string>
+    <string name="detail_settings_appearance_icon_remove_title">Subscription icon (tap to remove)</string>
+    <string name="detail_settings_appearance_icon_remove_summary">Icon displayed in notifications for this topic</string>
     <string name="detail_settings_appearance_icon_error_saving">Unable to save icon: %1$s</string>
     <string name="detail_settings_global_setting_title">Use global setting</string>
     <string name="detail_settings_global_setting_suffix">global</string>
diff --git a/app/src/main/res/xml/detail_preferences.xml b/app/src/main/res/xml/detail_preferences.xml
index 41f07a0..660635d 100644
--- a/app/src/main/res/xml/detail_preferences.xml
+++ b/app/src/main/res/xml/detail_preferences.xml
@@ -30,13 +30,13 @@
     <PreferenceCategory app:title="@string/detail_settings_appearance_header">
         <Preference
                 app:key="@string/detail_settings_appearance_icon_set_key"
-                app:title="@string/detail_settings_appearance_icon_title"
-                app:summary="@string/detail_settings_appearance_icon_set_summary_no_set"
+                app:title="@string/detail_settings_appearance_icon_set_title"
+                app:summary="@string/detail_settings_appearance_icon_set_summary"
                 app:isPreferenceVisible="false"/>
         <Preference
                 app:key="@string/detail_settings_appearance_icon_remove_key"
-                app:title="@string/detail_settings_appearance_icon_title"
-                app:summary="@string/detail_settings_appearance_icon_set_summary_set"
+                app:title="@string/detail_settings_appearance_icon_remove_title"
+                app:summary="@string/detail_settings_appearance_icon_remove_summary"
                 app:isPreferenceVisible="false"/>
     </PreferenceCategory>
 </PreferenceScreen>