Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a498d68b authored by Philipp Heckel's avatar Philipp Heckel
Browse files

WIP subscription icon

parent 2909d877
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -81,7 +81,7 @@ dependencies {
    implementation 'com.squareup.okhttp3:okhttp:4.9.3'

    // Firebase, sigh ... (only Google Play)
    playImplementation 'com.google.firebase:firebase-messaging:23.0.3'
    playImplementation 'com.google.firebase:firebase-messaging:23.0.4'

    // RecyclerView
    implementation "androidx.recyclerview:recyclerview:1.3.0-alpha02"
@@ -90,7 +90,7 @@ dependencies {
    implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'

    // Material design
    implementation "com.google.android.material:material:1.5.0"
    implementation "com.google.android.material:material:1.6.0"

    // LiveData
    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1"
+61 −16
Original line number Diff line number Diff line
package io.heckel.ntfy.ui

import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import androidx.core.graphics.drawable.toDrawable
import androidx.lifecycle.lifecycleScope
import androidx.preference.*
import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.R
import io.heckel.ntfy.db.Notification
import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.Subscription
import io.heckel.ntfy.msg.DownloadWorker
import io.heckel.ntfy.service.SubscriberServiceManager
import io.heckel.ntfy.util.*
import kotlinx.coroutines.*
import okio.source
import java.io.File
import java.io.IOException
import java.util.*
@@ -70,7 +70,10 @@ class DetailSettingsActivity : AppCompatActivity() {
        private lateinit var repository: Repository
        private lateinit var serviceManager: SubscriberServiceManager
        private lateinit var subscription: Subscription
        private lateinit var pickIconLauncher: ActivityResultLauncher<String>

        private lateinit var iconSetPref: Preference
        private lateinit var iconSetLauncher: ActivityResultLauncher<String>
        private lateinit var iconRemovePref: Preference

        override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
            setPreferencesFromResource(R.xml.detail_preferences, rootKey)
@@ -80,7 +83,7 @@ class DetailSettingsActivity : AppCompatActivity() {
            serviceManager = SubscriberServiceManager(requireActivity())

            // Create result launcher for custom icon (must be created in onCreatePreferences() directly)
            pickIconLauncher = createCustomIconPickLauncher()
            iconSetLauncher = createIconPickLauncher()

            // Load subscription and users
            val subscriptionId = arguments?.getLong(DetailActivity.EXTRA_SUBSCRIPTION_ID) ?: return
@@ -99,7 +102,8 @@ class DetailSettingsActivity : AppCompatActivity() {
            loadMutedUntilPref()
            loadMinPriorityPref()
            loadAutoDeletePref()
            loadCustomIconsPref()
            loadIconSetPref()
            loadIconRemovePref()
        }

        private fun loadInstantPref() {
@@ -233,41 +237,78 @@ class DetailSettingsActivity : AppCompatActivity() {
            }
        }

        private fun loadCustomIconsPref() {
            val prefId = context?.getString(R.string.detail_settings_general_icon_key) ?: return
            val pref: Preference? = findPreference(prefId)
            pref?.isVisible = true // Hack: Show all settings at once, because subscription is loaded asynchronously
            pref?.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting
            pref?.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
                pickIconLauncher.launch("image/*")
                false
        private fun loadIconSetPref() {
            val prefId = context?.getString(R.string.detail_settings_appearance_icon_set_key) ?: return
            iconSetPref = findPreference(prefId) ?: return
            iconSetPref.isVisible = subscription.icon == null
            iconSetPref.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting
            iconSetPref.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
                iconSetLauncher.launch("image/*")
                true
            }
        }

        private fun createCustomIconPickLauncher(): ActivityResultLauncher<String> {
        private fun loadIconRemovePref() {
            val prefId = context?.getString(R.string.detail_settings_appearance_icon_remove_key) ?: return
            iconRemovePref = findPreference(prefId) ?: return

            // FIXME

            if (subscription.icon != null) {
                try {
                    val resolver = requireContext().applicationContext.contentResolver
                    val bitmapStream = resolver.openInputStream(Uri.parse(subscription.icon))
                    val bitmap = BitmapFactory.decodeStream(bitmapStream)
                    iconRemovePref.icon = bitmap.toDrawable(resources)
                } catch (e: Exception) {
                    // FIXME

                }
            }
            iconRemovePref.isVisible = subscription.icon != null
            iconRemovePref.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting
            iconRemovePref.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
                save(subscription.copy(icon = null))
                iconRemovePref.isVisible = false
                iconSetPref.isVisible = true
                true
            }
        }

        private fun createIconPickLauncher(): ActivityResultLauncher<String> {
            return registerForActivityResult(ActivityResultContracts.GetContent()) { inputUri ->
                if (inputUri == null) {
                    return@registerForActivityResult
                }
                lifecycleScope.launch(Dispatchers.IO) {
                    try {
                        // Write to cache storage
                        val resolver = requireContext().applicationContext.contentResolver
                        val inputStream = resolver.openInputStream(inputUri) ?: throw IOException("Couldn't open content URI for reading")
                        val outputUri = createUri()
                        val outputStream = resolver.openOutputStream(outputUri) ?: throw IOException("Couldn't open content URI for writing")
                        inputStream.copyTo(outputStream)
                        save(subscription.copy(icon = outputUri.toString()))

                        // FIXME
                        // FIXME

                        iconSetPref.isVisible = false

                        val bitmapStream = resolver.openInputStream(Uri.parse(outputUri.toString()))
                        val bitmap = BitmapFactory.decodeStream(bitmapStream)
                        iconRemovePref.icon = bitmap.toDrawable(resources)
                        iconRemovePref.isVisible = true
                    } catch (e: Exception) {
                        Log.w(TAG, "Saving icon failed", e)
                        requireActivity().runOnUiThread {
                            // FIXME
                            // FIXME TOAST
                        }
                    }
                }
            }
        }


        private fun createUri(): Uri {
            val dir = File(requireContext().cacheDir, SUBSCRIPTION_ICONS)
            if (!dir.exists() && !dir.mkdirs()) {
@@ -277,6 +318,10 @@ class DetailSettingsActivity : AppCompatActivity() {
            return FileProvider.getUriForFile(requireContext(), DownloadWorker.FILE_PROVIDER_AUTHORITY, file)
        }

        private fun loadBitmap() {
            // FIXME
        }

        private fun save(newSubscription: Subscription, refresh: Boolean = false) {
            subscription = newSubscription
            lifecycleScope.launch(Dispatchers.IO) {
+20 −0
Original line number Diff line number Diff line
package io.heckel.ntfy.ui

import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
@@ -13,6 +16,8 @@ import io.heckel.ntfy.R
import io.heckel.ntfy.db.ConnectionState
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.topicShortUrl
import java.text.DateFormat
import java.util.*
@@ -47,6 +52,7 @@ class MainAdapter(private val repository: Repository, private val onClick: (Subs
        RecyclerView.ViewHolder(itemView) {
        private var subscription: Subscription? = null
        private val context: Context = itemView.context
        private val imageView: ImageView = itemView.findViewById(R.id.main_item_image)
        private val nameView: TextView = itemView.findViewById(R.id.main_item_text)
        private val statusView: TextView = itemView.findViewById(R.id.main_item_status)
        private val dateView: TextView = itemView.findViewById(R.id.main_item_date)
@@ -84,6 +90,16 @@ class MainAdapter(private val repository: Repository, private val onClick: (Subs
            val globalMutedUntil = repository.getGlobalMutedUntil()
            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)
                }
            }
            nameView.text = topicShortUrl(subscription.baseUrl, subscription.topic)
            statusView.text = statusMessage
            dateView.text = dateText
@@ -114,4 +130,8 @@ class MainAdapter(private val repository: Repository, private val onClick: (Subs
            return oldItem == newItem
        }
    }

    companion object {
        const val TAG = "NtfyMainAdapter"
    }
}
+4 −1
Original line number Diff line number Diff line
@@ -339,7 +339,10 @@
    <string name="detail_settings_notifications_instant_title">Instant delivery</string>
    <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_general_icon_title">Custom icon</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. 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_global_setting_title">Use global setting</string>
    <string name="detail_settings_global_setting_suffix">global</string>

+2 −1
Original line number Diff line number Diff line
@@ -35,7 +35,8 @@
    <string name="detail_settings_notifications_muted_until_key" translatable="false">SubscriptionMutedUntil</string>
    <string name="detail_settings_notifications_min_priority_key" translatable="false">SubscriptionMinPriority</string>
    <string name="detail_settings_notifications_auto_delete_key" translatable="false">SubscriptionAutoDelete</string>
    <string name="detail_settings_general_icon_key" translatable="false">SubscriptionIcon</string>
    <string name="detail_settings_appearance_icon_set_key" translatable="false">SubscriptionIconSet</string>
    <string name="detail_settings_appearance_icon_remove_key" translatable="false">SubscriptionIconRemove</string>

    <!-- Main settings -->
    <string-array name="settings_notifications_muted_until_entries">
Loading