diff --git a/app/build.gradle b/app/build.gradle index 88407079..7a1f71f9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" 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 c3e0134a..8ef39a51 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailSettingsActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailSettingsActivity.kt @@ -1,23 +1,23 @@ 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 + + private lateinit var iconSetPref: Preference + private lateinit var iconSetLauncher: ActivityResultLauncher + 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 { + 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 { 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) { 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 d0d41f97..a275364d 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt @@ -1,9 +1,12 @@ 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" + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 898db0b2..e0400481 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -339,7 +339,10 @@ Instant delivery Notifications are delivered instantly. Requires a foreground service and consumes more battery. Notifications are delivered using Firebase. Delivery may be delayed, but consumes less battery. - Custom icon + Appearance + Subscription icon + This icon is displayed in notifications. Tap to remove it. + Set an icon to be displayed in notifications Use global setting global diff --git a/app/src/main/res/values/values.xml b/app/src/main/res/values/values.xml index 37663ed7..cff7e5d0 100644 --- a/app/src/main/res/values/values.xml +++ b/app/src/main/res/values/values.xml @@ -35,7 +35,8 @@ SubscriptionMutedUntil SubscriptionMinPriority SubscriptionAutoDelete - SubscriptionIcon + SubscriptionIconSet + SubscriptionIconRemove diff --git a/app/src/main/res/xml/detail_preferences.xml b/app/src/main/res/xml/detail_preferences.xml index 2b29b97e..41f07a00 100644 --- a/app/src/main/res/xml/detail_preferences.xml +++ b/app/src/main/res/xml/detail_preferences.xml @@ -27,10 +27,16 @@ app:defaultValue="-1" app:isPreferenceVisible="false"/> - + +