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>