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'
// 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"

View file

@ -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<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) {

View file

@ -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"
}
}

View file

@ -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>

View file

@ -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">

View file

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