WIP subscription icon

This commit is contained in:
Philipp Heckel 2022-05-08 16:04:52 -04:00
parent 2909d877f7
commit a498d68bcf
6 changed files with 98 additions and 23 deletions

View file

@ -81,7 +81,7 @@ dependencies {
implementation 'com.squareup.okhttp3:okhttp:4.9.3' implementation 'com.squareup.okhttp3:okhttp:4.9.3'
// Firebase, sigh ... (only Google Play) // Firebase, sigh ... (only Google Play)
playImplementation 'com.google.firebase:firebase-messaging:23.0.3' playImplementation 'com.google.firebase:firebase-messaging:23.0.4'
// RecyclerView // RecyclerView
implementation "androidx.recyclerview:recyclerview:1.3.0-alpha02" implementation "androidx.recyclerview:recyclerview:1.3.0-alpha02"
@ -90,7 +90,7 @@ dependencies {
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
// Material design // Material design
implementation "com.google.android.material:material:1.5.0" implementation "com.google.android.material:material:1.6.0"
// LiveData // LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1" implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1"

View file

@ -1,23 +1,23 @@
package io.heckel.ntfy.ui package io.heckel.ntfy.ui
import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.graphics.drawable.toDrawable
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.* import androidx.preference.*
import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.R import io.heckel.ntfy.R
import io.heckel.ntfy.db.Notification
import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.Subscription import io.heckel.ntfy.db.Subscription
import io.heckel.ntfy.msg.DownloadWorker import io.heckel.ntfy.msg.DownloadWorker
import io.heckel.ntfy.service.SubscriberServiceManager import io.heckel.ntfy.service.SubscriberServiceManager
import io.heckel.ntfy.util.* import io.heckel.ntfy.util.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import okio.source
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.* import java.util.*
@ -70,7 +70,10 @@ class DetailSettingsActivity : AppCompatActivity() {
private lateinit var repository: Repository private lateinit var repository: Repository
private lateinit var serviceManager: SubscriberServiceManager private lateinit var serviceManager: SubscriberServiceManager
private lateinit var subscription: Subscription 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?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.detail_preferences, rootKey) setPreferencesFromResource(R.xml.detail_preferences, rootKey)
@ -80,7 +83,7 @@ class DetailSettingsActivity : AppCompatActivity() {
serviceManager = SubscriberServiceManager(requireActivity()) serviceManager = SubscriberServiceManager(requireActivity())
// Create result launcher for custom icon (must be created in onCreatePreferences() directly) // Create result launcher for custom icon (must be created in onCreatePreferences() directly)
pickIconLauncher = createCustomIconPickLauncher() iconSetLauncher = createIconPickLauncher()
// Load subscription and users // Load subscription and users
val subscriptionId = arguments?.getLong(DetailActivity.EXTRA_SUBSCRIPTION_ID) ?: return val subscriptionId = arguments?.getLong(DetailActivity.EXTRA_SUBSCRIPTION_ID) ?: return
@ -99,7 +102,8 @@ class DetailSettingsActivity : AppCompatActivity() {
loadMutedUntilPref() loadMutedUntilPref()
loadMinPriorityPref() loadMinPriorityPref()
loadAutoDeletePref() loadAutoDeletePref()
loadCustomIconsPref() loadIconSetPref()
loadIconRemovePref()
} }
private fun loadInstantPref() { private fun loadInstantPref() {
@ -233,41 +237,78 @@ class DetailSettingsActivity : AppCompatActivity() {
} }
} }
private fun loadCustomIconsPref() { private fun loadIconSetPref() {
val prefId = context?.getString(R.string.detail_settings_general_icon_key) ?: return val prefId = context?.getString(R.string.detail_settings_appearance_icon_set_key) ?: return
val pref: Preference? = findPreference(prefId) iconSetPref = findPreference(prefId) ?: return
pref?.isVisible = true // Hack: Show all settings at once, because subscription is loaded asynchronously iconSetPref.isVisible = subscription.icon == null
pref?.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting iconSetPref.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting
pref?.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ -> iconSetPref.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
pickIconLauncher.launch("image/*") iconSetLauncher.launch("image/*")
false 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 -> return registerForActivityResult(ActivityResultContracts.GetContent()) { inputUri ->
if (inputUri == null) { if (inputUri == null) {
return@registerForActivityResult return@registerForActivityResult
} }
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
try { try {
// Write to cache storage
val resolver = requireContext().applicationContext.contentResolver val resolver = requireContext().applicationContext.contentResolver
val inputStream = resolver.openInputStream(inputUri) ?: throw IOException("Couldn't open content URI for reading") val inputStream = resolver.openInputStream(inputUri) ?: throw IOException("Couldn't open content URI for reading")
val outputUri = createUri() val outputUri = createUri()
val outputStream = resolver.openOutputStream(outputUri) ?: throw IOException("Couldn't open content URI for writing") val outputStream = resolver.openOutputStream(outputUri) ?: throw IOException("Couldn't open content URI for writing")
inputStream.copyTo(outputStream) inputStream.copyTo(outputStream)
save(subscription.copy(icon = outputUri.toString())) 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) { } catch (e: Exception) {
Log.w(TAG, "Saving icon failed", e) Log.w(TAG, "Saving icon failed", e)
requireActivity().runOnUiThread { requireActivity().runOnUiThread {
// FIXME // FIXME TOAST
} }
} }
} }
} }
} }
private fun createUri(): Uri { private fun createUri(): Uri {
val dir = File(requireContext().cacheDir, SUBSCRIPTION_ICONS) val dir = File(requireContext().cacheDir, SUBSCRIPTION_ICONS)
if (!dir.exists() && !dir.mkdirs()) { if (!dir.exists() && !dir.mkdirs()) {
@ -277,6 +318,10 @@ class DetailSettingsActivity : AppCompatActivity() {
return FileProvider.getUriForFile(requireContext(), DownloadWorker.FILE_PROVIDER_AUTHORITY, file) return FileProvider.getUriForFile(requireContext(), DownloadWorker.FILE_PROVIDER_AUTHORITY, file)
} }
private fun loadBitmap() {
// FIXME
}
private fun save(newSubscription: Subscription, refresh: Boolean = false) { private fun save(newSubscription: Subscription, refresh: Boolean = false) {
subscription = newSubscription subscription = newSubscription
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {

View file

@ -1,9 +1,12 @@
package io.heckel.ntfy.ui package io.heckel.ntfy.ui
import android.content.Context import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter 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.ConnectionState
import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.Subscription 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 io.heckel.ntfy.util.topicShortUrl
import java.text.DateFormat import java.text.DateFormat
import java.util.* import java.util.*
@ -47,6 +52,7 @@ class MainAdapter(private val repository: Repository, private val onClick: (Subs
RecyclerView.ViewHolder(itemView) { RecyclerView.ViewHolder(itemView) {
private var subscription: Subscription? = null private var subscription: Subscription? = null
private val context: Context = itemView.context 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 nameView: TextView = itemView.findViewById(R.id.main_item_text)
private val statusView: TextView = itemView.findViewById(R.id.main_item_status) private val statusView: TextView = itemView.findViewById(R.id.main_item_status)
private val dateView: TextView = itemView.findViewById(R.id.main_item_date) 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 globalMutedUntil = repository.getGlobalMutedUntil()
val showMutedForeverIcon = (subscription.mutedUntil == 1L || globalMutedUntil == 1L) && !isUnifiedPush val showMutedForeverIcon = (subscription.mutedUntil == 1L || globalMutedUntil == 1L) && !isUnifiedPush
val showMutedUntilIcon = !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) nameView.text = topicShortUrl(subscription.baseUrl, subscription.topic)
statusView.text = statusMessage statusView.text = statusMessage
dateView.text = dateText dateView.text = dateText
@ -114,4 +130,8 @@ class MainAdapter(private val repository: Repository, private val onClick: (Subs
return oldItem == newItem return oldItem == newItem
} }
} }
companion object {
const val TAG = "NtfyMainAdapter"
}
} }

View file

@ -339,7 +339,10 @@
<string name="detail_settings_notifications_instant_title">Instant delivery</string> <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_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_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_title">Use global setting</string>
<string name="detail_settings_global_setting_suffix">global</string> <string name="detail_settings_global_setting_suffix">global</string>

View file

@ -35,7 +35,8 @@
<string name="detail_settings_notifications_muted_until_key" translatable="false">SubscriptionMutedUntil</string> <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_min_priority_key" translatable="false">SubscriptionMinPriority</string>
<string name="detail_settings_notifications_auto_delete_key" translatable="false">SubscriptionAutoDelete</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 --> <!-- Main settings -->
<string-array name="settings_notifications_muted_until_entries"> <string-array name="settings_notifications_muted_until_entries">

View file

@ -27,10 +27,16 @@
app:defaultValue="-1" app:defaultValue="-1"
app:isPreferenceVisible="false"/> <!-- Same as Repository.AUTO_DELETE_USE_GLOBAL --> app:isPreferenceVisible="false"/> <!-- Same as Repository.AUTO_DELETE_USE_GLOBAL -->
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory app:title="@string/settings_general_header"> <PreferenceCategory app:title="@string/detail_settings_appearance_header">
<Preference <Preference
app:key="@string/detail_settings_general_icon_key" app:key="@string/detail_settings_appearance_icon_set_key"
app:title="@string/detail_settings_general_icon_title" app:title="@string/detail_settings_appearance_icon_title"
app:summary="@string/detail_settings_appearance_icon_set_summary_no_set"
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:isPreferenceVisible="false"/> app:isPreferenceVisible="false"/>
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>