Merge branch 'constant-ring' into custom_notification_channels

This commit is contained in:
Philipp Heckel 2022-12-07 10:35:54 -05:00
commit 3e8ba28e63
20 changed files with 285 additions and 58 deletions

View file

@ -2,11 +2,11 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 13, "version": 13,
"identityHash": "39849793e1ed04fe89f0d71a59a56956", "identityHash": "44fc291d937fdf02b9bc2d0abb10d2e0",
"entities": [ "entities": [
{ {
"tableName": "Subscription", "tableName": "Subscription",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `topic` TEXT NOT NULL, `instant` INTEGER NOT NULL, `mutedUntil` INTEGER NOT NULL, `minPriority` INTEGER NOT NULL, `autoDelete` INTEGER NOT NULL, `lastNotificationId` TEXT, `icon` TEXT, `upAppId` TEXT, `upConnectorToken` TEXT, `displayName` TEXT, `dedicatedChannels` INTEGER NOT NULL, PRIMARY KEY(`id`))", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `topic` TEXT NOT NULL, `instant` INTEGER NOT NULL, `mutedUntil` INTEGER NOT NULL, `minPriority` INTEGER NOT NULL, `autoDelete` INTEGER NOT NULL, `insistent` INTEGER NOT NULL, `lastNotificationId` TEXT, `icon` TEXT, `upAppId` TEXT, `upConnectorToken` TEXT, `displayName` TEXT, `dedicatedChannels` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [ "fields": [
{ {
"fieldPath": "id", "fieldPath": "id",
@ -50,6 +50,12 @@
"affinity": "INTEGER", "affinity": "INTEGER",
"notNull": true "notNull": true
}, },
{
"fieldPath": "insistent",
"columnName": "insistent",
"affinity": "INTEGER",
"notNull": true
},
{ {
"fieldPath": "lastNotificationId", "fieldPath": "lastNotificationId",
"columnName": "lastNotificationId", "columnName": "lastNotificationId",
@ -344,7 +350,7 @@
"views": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '39849793e1ed04fe89f0d71a59a56956')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '44fc291d937fdf02b9bc2d0abb10d2e0')"
] ]
} }
} }

View file

@ -142,6 +142,13 @@
android:exported="false"> android:exported="false">
</receiver> </receiver>
<!-- Broadcast receiver for when the notification is swiped away (currently only to cancel the insistent sound) -->
<receiver
android:name=".msg.NotificationService$DeleteBroadcastReceiver"
android:enabled="true"
android:exported="false">
</receiver>
<!-- Firebase messaging (note that this is empty in the F-Droid flavor) --> <!-- Firebase messaging (note that this is empty in the F-Droid flavor) -->
<service <service
android:name=".firebase.FirebaseService" android:name=".firebase.FirebaseService"

View file

@ -1,15 +1,10 @@
package io.heckel.ntfy.app package io.heckel.ntfy.app
import android.app.Application import android.app.Application
import io.heckel.ntfy.db.Database
import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.util.Log import io.heckel.ntfy.util.Log
class Application : Application() { class Application : Application() {
private val database by lazy {
Log.init(this) // What a hack, but this is super early and used everywhere
Database.getInstance(this)
}
val repository by lazy { val repository by lazy {
val repository = Repository.getInstance(applicationContext) val repository = Repository.getInstance(applicationContext)
if (repository.getRecordLogs()) { if (repository.getRecordLogs()) {

View file

@ -103,6 +103,7 @@ class Backuper(val context: Context) {
mutedUntil = s.mutedUntil, mutedUntil = s.mutedUntil,
minPriority = s.minPriority ?: Repository.MIN_PRIORITY_USE_GLOBAL, minPriority = s.minPriority ?: Repository.MIN_PRIORITY_USE_GLOBAL,
autoDelete = s.autoDelete ?: Repository.AUTO_DELETE_USE_GLOBAL, autoDelete = s.autoDelete ?: Repository.AUTO_DELETE_USE_GLOBAL,
insistent = s.insistent ?: Repository.INSISTENT_MAX_PRIORITY_USE_GLOBAL,
lastNotificationId = s.lastNotificationId, lastNotificationId = s.lastNotificationId,
icon = s.icon, icon = s.icon,
upAppId = s.upAppId, upAppId = s.upAppId,
@ -241,6 +242,7 @@ class Backuper(val context: Context) {
mutedUntil = s.mutedUntil, mutedUntil = s.mutedUntil,
minPriority = s.minPriority, minPriority = s.minPriority,
autoDelete = s.autoDelete, autoDelete = s.autoDelete,
insistent = s.insistent,
lastNotificationId = s.lastNotificationId, lastNotificationId = s.lastNotificationId,
icon = s.icon, icon = s.icon,
upAppId = s.upAppId, upAppId = s.upAppId,
@ -359,6 +361,7 @@ data class Subscription(
val mutedUntil: Long, val mutedUntil: Long,
val minPriority: Int?, val minPriority: Int?,
val autoDelete: Long?, val autoDelete: Long?,
val insistent: Int?,
val lastNotificationId: String?, val lastNotificationId: String?,
val icon: String?, val icon: String?,
val upAppId: String?, val upAppId: String?,

View file

@ -18,6 +18,7 @@ data class Subscription(
@ColumnInfo(name = "mutedUntil") val mutedUntil: Long, @ColumnInfo(name = "mutedUntil") val mutedUntil: Long,
@ColumnInfo(name = "minPriority") val minPriority: Int, @ColumnInfo(name = "minPriority") val minPriority: Int,
@ColumnInfo(name = "autoDelete") val autoDelete: Long, // Seconds @ColumnInfo(name = "autoDelete") val autoDelete: Long, // Seconds
@ColumnInfo(name = "insistent") val insistent: Int, // Ring constantly for max priority notifications (-1 = use global, 0 = off, 1 = on)
@ColumnInfo(name = "lastNotificationId") val lastNotificationId: String?, // Used for polling, with since=<id> @ColumnInfo(name = "lastNotificationId") val lastNotificationId: String?, // Used for polling, with since=<id>
@ColumnInfo(name = "icon") val icon: String?, // content://-URI (or later other identifier) @ColumnInfo(name = "icon") val icon: String?, // content://-URI (or later other identifier)
@ColumnInfo(name = "upAppId") val upAppId: String?, // UnifiedPush application package name @ColumnInfo(name = "upAppId") val upAppId: String?, // UnifiedPush application package name
@ -29,8 +30,42 @@ data class Subscription(
@Ignore val lastActive: Long = 0, // Unix timestamp @Ignore val lastActive: Long = 0, // Unix timestamp
@Ignore val state: ConnectionState = ConnectionState.NOT_APPLICABLE @Ignore val state: ConnectionState = ConnectionState.NOT_APPLICABLE
) { ) {
constructor(id: Long, baseUrl: String, topic: String, instant: Boolean, mutedUntil: Long, minPriority: Int, autoDelete: Long, lastNotificationId: String, icon: String, upAppId: String, upConnectorToken: String, displayName: String?, dedicatedChannels: Boolean?) : constructor(
this(id, baseUrl, topic, instant, mutedUntil, minPriority, autoDelete, lastNotificationId, icon, upAppId, upConnectorToken, displayName, dedicatedChannels == true, 0, 0, 0, ConnectionState.NOT_APPLICABLE) id: Long,
baseUrl: String,
topic: String,
instant: Boolean,
mutedUntil: Long,
minPriority: Int,
autoDelete: Long,
insistent: Int,
lastNotificationId: String,
icon: String,
upAppId: String,
upConnectorToken: String,
displayName: String?,
dedicatedChannels: Boolean
) :
this(
id,
baseUrl,
topic,
instant,
mutedUntil,
minPriority,
autoDelete,
insistent,
lastNotificationId,
icon,
upAppId,
upConnectorToken,
displayName,
dedicatedChannels,
totalCount = 0,
newCount = 0,
lastActive = 0,
state = ConnectionState.NOT_APPLICABLE
)
} }
enum class ConnectionState { enum class ConnectionState {
@ -45,6 +80,7 @@ data class SubscriptionWithMetadata(
val mutedUntil: Long, val mutedUntil: Long,
val autoDelete: Long, val autoDelete: Long,
val minPriority: Int, val minPriority: Int,
val insistent: Int,
val lastNotificationId: String?, val lastNotificationId: String?,
val icon: String?, val icon: String?,
val upAppId: String?, val upAppId: String?,
@ -289,7 +325,8 @@ abstract class Database : RoomDatabase() {
private val MIGRATION_12_13 = object : Migration(12, 13) { private val MIGRATION_12_13 = object : Migration(12, 13) {
override fun migrate(db: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE Subscription ADD COLUMN dedicatedChannels INTEGER NOT NULL DEFAULT('0')") db.execSQL("ALTER TABLE Subscription ADD COLUMN insistent INTEGER NOT NULL DEFAULT (-1)") // = Repository.INSISTENT_MAX_PRIORITY_USE_GLOBAL
db.execSQL("ALTER TABLE Subscription ADD COLUMN dedicatedChannels INTEGER NOT NULL DEFAULT (0)")
} }
} }
} }
@ -299,7 +336,7 @@ abstract class Database : RoomDatabase() {
interface SubscriptionDao { interface SubscriptionDao {
@Query(""" @Query("""
SELECT SELECT
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken, s.displayName, s.dedicatedChannels, s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.insistent, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken, s.displayName, s.dedicatedChannels,
COUNT(n.id) totalCount, COUNT(n.id) totalCount,
COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount, COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount,
IFNULL(MAX(n.timestamp),0) AS lastActive IFNULL(MAX(n.timestamp),0) AS lastActive
@ -312,7 +349,7 @@ interface SubscriptionDao {
@Query(""" @Query("""
SELECT SELECT
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken, s.displayName, s.dedicatedChannels, s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.insistent, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken, s.displayName, s.dedicatedChannels,
COUNT(n.id) totalCount, COUNT(n.id) totalCount,
COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount, COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount,
IFNULL(MAX(n.timestamp),0) AS lastActive IFNULL(MAX(n.timestamp),0) AS lastActive
@ -325,7 +362,7 @@ interface SubscriptionDao {
@Query(""" @Query("""
SELECT SELECT
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken, s.displayName, s.dedicatedChannels, s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.insistent, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken, s.displayName, s.dedicatedChannels,
COUNT(n.id) totalCount, COUNT(n.id) totalCount,
COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount, COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount,
IFNULL(MAX(n.timestamp),0) AS lastActive IFNULL(MAX(n.timestamp),0) AS lastActive
@ -338,7 +375,7 @@ interface SubscriptionDao {
@Query(""" @Query("""
SELECT SELECT
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken, s.displayName, s.dedicatedChannels, s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.insistent, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken, s.displayName, s.dedicatedChannels,
COUNT(n.id) totalCount, COUNT(n.id) totalCount,
COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount, COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount,
IFNULL(MAX(n.timestamp),0) AS lastActive IFNULL(MAX(n.timestamp),0) AS lastActive
@ -351,7 +388,7 @@ interface SubscriptionDao {
@Query(""" @Query("""
SELECT SELECT
s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken, s.displayName, s.dedicatedChannels, s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.insistent, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken, s.displayName, s.dedicatedChannels,
COUNT(n.id) totalCount, COUNT(n.id) totalCount,
COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount, COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount,
IFNULL(MAX(n.timestamp),0) AS lastActive IFNULL(MAX(n.timestamp),0) AS lastActive

View file

@ -2,6 +2,7 @@ package io.heckel.ntfy.db
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.media.MediaPlayer
import android.os.Build import android.os.Build
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
@ -18,7 +19,10 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
private val connectionStates = ConcurrentHashMap<Long, ConnectionState>() private val connectionStates = ConcurrentHashMap<Long, ConnectionState>()
private val connectionStatesLiveData = MutableLiveData(connectionStates) private val connectionStatesLiveData = MutableLiveData(connectionStates)
// TODO Move these into an ApplicationState singleton
val detailViewSubscriptionId = AtomicLong(0L) // Omg, what a hack ... val detailViewSubscriptionId = AtomicLong(0L) // Omg, what a hack ...
val mediaPlayer = MediaPlayer()
init { init {
Log.d(TAG, "Created $this") Log.d(TAG, "Created $this")
@ -288,6 +292,16 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
.apply() .apply()
} }
fun getInsistentMaxPriorityEnabled(): Boolean {
return sharedPrefs.getBoolean(SHARED_PREFS_INSISTENT_MAX_PRIORITY_ENABLED, false) // Disabled by default
}
fun setInsistentMaxPriorityEnabled(enabled: Boolean) {
sharedPrefs.edit()
.putBoolean(SHARED_PREFS_INSISTENT_MAX_PRIORITY_ENABLED, enabled)
.apply()
}
fun getRecordLogs(): Boolean { fun getRecordLogs(): Boolean {
return sharedPrefs.getBoolean(SHARED_PREFS_RECORD_LOGS_ENABLED, false) // Disabled by default return sharedPrefs.getBoolean(SHARED_PREFS_RECORD_LOGS_ENABLED, false) // Disabled by default
} }
@ -389,6 +403,7 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
mutedUntil = s.mutedUntil, mutedUntil = s.mutedUntil,
minPriority = s.minPriority, minPriority = s.minPriority,
autoDelete = s.autoDelete, autoDelete = s.autoDelete,
insistent = s.insistent,
lastNotificationId = s.lastNotificationId, lastNotificationId = s.lastNotificationId,
icon = s.icon, icon = s.icon,
upAppId = s.upAppId, upAppId = s.upAppId,
@ -415,6 +430,7 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
mutedUntil = s.mutedUntil, mutedUntil = s.mutedUntil,
minPriority = s.minPriority, minPriority = s.minPriority,
autoDelete = s.autoDelete, autoDelete = s.autoDelete,
insistent = s.insistent,
lastNotificationId = s.lastNotificationId, lastNotificationId = s.lastNotificationId,
icon = s.icon, icon = s.icon,
upAppId = s.upAppId, upAppId = s.upAppId,
@ -461,6 +477,7 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
const val SHARED_PREFS_CONNECTION_PROTOCOL = "ConnectionProtocol" const val SHARED_PREFS_CONNECTION_PROTOCOL = "ConnectionProtocol"
const val SHARED_PREFS_DARK_MODE = "DarkMode" const val SHARED_PREFS_DARK_MODE = "DarkMode"
const val SHARED_PREFS_BROADCAST_ENABLED = "BroadcastEnabled" const val SHARED_PREFS_BROADCAST_ENABLED = "BroadcastEnabled"
const val SHARED_PREFS_INSISTENT_MAX_PRIORITY_ENABLED = "InsistentMaxPriority"
const val SHARED_PREFS_RECORD_LOGS_ENABLED = "RecordLogs" const val SHARED_PREFS_RECORD_LOGS_ENABLED = "RecordLogs"
const val SHARED_PREFS_BATTERY_OPTIMIZATIONS_REMIND_TIME = "BatteryOptimizationsRemindTime" const val SHARED_PREFS_BATTERY_OPTIMIZATIONS_REMIND_TIME = "BatteryOptimizationsRemindTime"
const val SHARED_PREFS_WEBSOCKET_REMIND_TIME = "JsonStreamRemindTime" // "Use WebSocket" banner (used to be JSON stream deprecation banner) const val SHARED_PREFS_WEBSOCKET_REMIND_TIME = "JsonStreamRemindTime" // "Use WebSocket" banner (used to be JSON stream deprecation banner)
@ -492,6 +509,9 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
const val AUTO_DELETE_THREE_MONTHS_SECONDS = 90 * ONE_DAY_SECONDS const val AUTO_DELETE_THREE_MONTHS_SECONDS = 90 * ONE_DAY_SECONDS
const val AUTO_DELETE_DEFAULT_SECONDS = AUTO_DELETE_ONE_MONTH_SECONDS const val AUTO_DELETE_DEFAULT_SECONDS = AUTO_DELETE_ONE_MONTH_SECONDS
const val INSISTENT_MAX_PRIORITY_USE_GLOBAL = -1 // Values must match values.xml
const val INSISTENT_MAX_PRIORITY_ENABLED = 1 // 0 = Disabled (but not needed in code)
const val CONNECTION_PROTOCOL_JSONHTTP = "jsonhttp" const val CONNECTION_PROTOCOL_JSONHTTP = "jsonhttp"
const val CONNECTION_PROTOCOL_WS = "ws" const val CONNECTION_PROTOCOL_WS = "ws"

View file

@ -37,7 +37,7 @@ class ApiService {
user: User? = null, user: User? = null,
message: String, message: String,
title: String = "", title: String = "",
priority: Int = 3, priority: Int = PRIORITY_DEFAULT,
tags: List<String> = emptyList(), tags: List<String> = emptyList(),
delay: String = "", delay: String = "",
body: RequestBody? = null, body: RequestBody? = null,
@ -45,7 +45,7 @@ class ApiService {
) { ) {
val url = topicUrl(baseUrl, topic) val url = topicUrl(baseUrl, topic)
val query = mutableListOf<String>() val query = mutableListOf<String>()
if (priority in 1..5) { if (priority in ALL_PRIORITIES) {
query.add("priority=$priority") query.add("priority=$priority")
} }
if (tags.isNotEmpty()) { if (tags.isNotEmpty()) {

View file

@ -5,6 +5,8 @@ import android.content.ActivityNotFoundException
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.media.AudioAttributes
import android.media.AudioManager
import android.media.RingtoneManager import android.media.RingtoneManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
@ -21,9 +23,9 @@ import io.heckel.ntfy.ui.MainActivity
import io.heckel.ntfy.util.* import io.heckel.ntfy.util.*
import java.util.* import java.util.*
class NotificationService(val context: Context) { class NotificationService(val context: Context) {
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val repository = Repository.getInstance(context)
fun display(subscription: Subscription, notification: Notification) { fun display(subscription: Subscription, notification: Notification) {
Log.d(TAG, "Displaying notification $notification") Log.d(TAG, "Displaying notification $notification")
@ -58,18 +60,18 @@ class NotificationService(val context: Context) {
fun createDefaultNotificationChannels() { fun createDefaultNotificationChannels() {
maybeCreateNotificationGroup(DEFAULT_GROUP, context.getString(R.string.channel_notifications_group_default_name)) maybeCreateNotificationGroup(DEFAULT_GROUP, context.getString(R.string.channel_notifications_group_default_name))
(1..5).forEach { priority -> maybeCreateNotificationChannel(DEFAULT_GROUP, priority) } ALL_PRIORITIES.forEach { priority -> maybeCreateNotificationChannel(DEFAULT_GROUP, priority) }
} }
fun createSubscriptionNotificationChannels(subscription: Subscription) { fun createSubscriptionNotificationChannels(subscription: Subscription) {
val groupId = subscriptionGroupId(subscription) val groupId = subscriptionGroupId(subscription)
maybeCreateNotificationGroup(groupId, subscriptionGroupName(subscription)) maybeCreateNotificationGroup(groupId, subscriptionGroupName(subscription))
(1..5).forEach { priority -> maybeCreateNotificationChannel(groupId, priority) } ALL_PRIORITIES.forEach { priority -> maybeCreateNotificationChannel(groupId, priority) }
} }
fun deleteSubscriptionNotificationChannels(subscription: Subscription) { fun deleteSubscriptionNotificationChannels(subscription: Subscription) {
val groupId = subscriptionGroupId(subscription) val groupId = subscriptionGroupId(subscription)
(1..5).forEach { priority -> maybeDeleteNotificationChannel(groupId, priority) } ALL_PRIORITIES.forEach { priority -> maybeDeleteNotificationChannel(groupId, priority) }
maybeDeleteNotificationGroup(groupId) maybeDeleteNotificationGroup(groupId)
} }
@ -78,7 +80,7 @@ class NotificationService(val context: Context) {
} }
private fun subscriptionGroupId(subscription: Subscription): String { private fun subscriptionGroupId(subscription: Subscription): String {
return subscription.id.toString() return SUBSCRIPTION_GROUP_PREFIX + subscription.id.toString()
} }
private fun subscriptionGroupName(subscription: Subscription): String { private fun subscriptionGroupName(subscription: Subscription): String {
@ -88,7 +90,10 @@ class NotificationService(val context: Context) {
private fun displayInternal(subscription: Subscription, notification: Notification, update: Boolean = false) { private fun displayInternal(subscription: Subscription, notification: Notification, update: Boolean = false) {
val title = formatTitle(subscription, notification) val title = formatTitle(subscription, notification)
val groupId = if (subscription.dedicatedChannels) subscriptionGroupId(subscription) else DEFAULT_GROUP val groupId = if (subscription.dedicatedChannels) subscriptionGroupId(subscription) else DEFAULT_GROUP
val builder = NotificationCompat.Builder(context, toChannelId(groupId, notification.priority)) val channelId = toChannelId(groupId, notification.priority)
val insistent = notification.priority == PRIORITY_MAX &&
(repository.getInsistentMaxPriorityEnabled() || subscription.insistent == Repository.INSISTENT_MAX_PRIORITY_ENABLED)
val builder = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_notification) .setSmallIcon(R.drawable.ic_notification)
.setColor(ContextCompat.getColor(context, Colors.notificationIcon(context))) .setColor(ContextCompat.getColor(context, Colors.notificationIcon(context)))
.setContentTitle(title) .setContentTitle(title)
@ -96,7 +101,8 @@ class NotificationService(val context: Context) {
.setAutoCancel(true) // Cancel when notification is clicked .setAutoCancel(true) // Cancel when notification is clicked
setStyleAndText(builder, subscription, notification) // Preview picture or big text style setStyleAndText(builder, subscription, notification) // Preview picture or big text style
setClickAction(builder, subscription, notification) setClickAction(builder, subscription, notification)
maybeSetSound(builder, update) maybeSetDeleteIntent(builder, insistent)
maybeSetSound(builder, insistent, update)
maybeSetProgress(builder, notification) maybeSetProgress(builder, notification)
maybeAddOpenAction(builder, notification) maybeAddOpenAction(builder, notification)
maybeAddBrowseAction(builder, notification) maybeAddBrowseAction(builder, notification)
@ -106,12 +112,24 @@ class NotificationService(val context: Context) {
maybeCreateNotificationGroup(groupId, subscriptionGroupName(subscription)) maybeCreateNotificationGroup(groupId, subscriptionGroupName(subscription))
maybeCreateNotificationChannel(groupId, notification.priority) maybeCreateNotificationChannel(groupId, notification.priority)
maybePlayInsistentSound(groupId, insistent)
notificationManager.notify(notification.notificationId, builder.build()) notificationManager.notify(notification.notificationId, builder.build())
} }
private fun maybeSetSound(builder: NotificationCompat.Builder, update: Boolean) { private fun maybeSetDeleteIntent(builder: NotificationCompat.Builder, insistent: Boolean) {
if (!update) { if (!insistent) {
return
}
val intent = Intent(context, DeleteBroadcastReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(context, Random().nextInt(), intent, PendingIntent.FLAG_IMMUTABLE)
builder.setDeleteIntent(pendingIntent)
}
private fun maybeSetSound(builder: NotificationCompat.Builder, insistent: Boolean, update: Boolean) {
// Note that the sound setting is ignored in Android => O (26) in favor of notification channels
val hasSound = !update && !insistent
if (hasSound) {
val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
builder.setSound(defaultSoundUri) builder.setSound(defaultSoundUri)
} else { } else {
@ -327,6 +345,18 @@ class NotificationService(val context: Context) {
} }
} }
/**
* Receives a broadcast when a notification is swiped away. This is currently
* only called for notifications with an insistent sound.
*/
class DeleteBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.d(TAG, "Media player: Stopping insistent ring")
val mediaPlayer = Repository.getInstance(context).mediaPlayer
mediaPlayer.stop()
}
}
private fun detailActivityIntent(subscription: Subscription): PendingIntent? { private fun detailActivityIntent(subscription: Subscription): PendingIntent? {
val intent = Intent(context, DetailActivity::class.java).apply { val intent = Intent(context, DetailActivity::class.java).apply {
putExtra(MainActivity.EXTRA_SUBSCRIPTION_ID, subscription.id) putExtra(MainActivity.EXTRA_SUBSCRIPTION_ID, subscription.id)
@ -349,9 +379,9 @@ class NotificationService(val context: Context) {
val channelId = toChannelId(group, priority) val channelId = toChannelId(group, priority)
val pause = 300L val pause = 300L
val channel = when (priority) { val channel = when (priority) {
1 -> NotificationChannel(channelId, context.getString(R.string.channel_notifications_min_name), NotificationManager.IMPORTANCE_MIN) PRIORITY_MIN -> NotificationChannel(channelId, context.getString(R.string.channel_notifications_min_name), NotificationManager.IMPORTANCE_MIN)
2 -> NotificationChannel(channelId, context.getString(R.string.channel_notifications_low_name), NotificationManager.IMPORTANCE_LOW) PRIORITY_LOW -> NotificationChannel(channelId, context.getString(R.string.channel_notifications_low_name), NotificationManager.IMPORTANCE_LOW)
4 -> { PRIORITY_HIGH -> {
val channel = NotificationChannel(channelId, context.getString(R.string.channel_notifications_high_name), NotificationManager.IMPORTANCE_HIGH) val channel = NotificationChannel(channelId, context.getString(R.string.channel_notifications_high_name), NotificationManager.IMPORTANCE_HIGH)
channel.enableVibration(true) channel.enableVibration(true)
channel.vibrationPattern = longArrayOf( channel.vibrationPattern = longArrayOf(
@ -360,10 +390,11 @@ class NotificationService(val context: Context) {
) )
channel channel
} }
5 -> { PRIORITY_MAX -> {
val channel = NotificationChannel(channelId, context.getString(R.string.channel_notifications_max_name), NotificationManager.IMPORTANCE_HIGH) // IMPORTANCE_MAX does not exist val channel = NotificationChannel(channelId, context.getString(R.string.channel_notifications_max_name), NotificationManager.IMPORTANCE_HIGH) // IMPORTANCE_MAX does not exist
channel.enableLights(true) channel.enableLights(true)
channel.enableVibration(true) channel.enableVibration(true)
channel.setBypassDnd(true)
channel.vibrationPattern = longArrayOf( channel.vibrationPattern = longArrayOf(
pause, 100, pause, 100, pause, 100, pause, 100, pause, 100, pause, 100,
pause, 2000, pause, 2000,
@ -399,13 +430,46 @@ class NotificationService(val context: Context) {
} }
} }
private fun toChannelId(group: String, priority: Int): String { private fun toChannelId(groupId: String, priority: Int): String {
return when (priority) { return when (priority) {
1 -> group + GROUP_SUFFIX_PRIORITY_MIN PRIORITY_MIN -> groupId + GROUP_SUFFIX_PRIORITY_MIN
2 -> group + GROUP_SUFFIX_PRIORITY_LOW PRIORITY_LOW -> groupId + GROUP_SUFFIX_PRIORITY_LOW
4 -> group + GROUP_SUFFIX_PRIORITY_HIGH PRIORITY_HIGH -> groupId + GROUP_SUFFIX_PRIORITY_HIGH
5 -> group + GROUP_SUFFIX_PRIORITY_MAX PRIORITY_MAX -> groupId + GROUP_SUFFIX_PRIORITY_MAX
else -> group + GROUP_SUFFIX_PRIORITY_DEFAULT else -> groupId + GROUP_SUFFIX_PRIORITY_DEFAULT
}
}
private fun maybePlayInsistentSound(groupId: String, insistent: Boolean) {
if (!insistent) {
return
}
try {
val mediaPlayer = repository.mediaPlayer
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
if (audioManager.getStreamVolume(AudioManager.STREAM_ALARM) != 0) {
Log.d(TAG, "Media player: Playing insistent alarm on alarm channel")
mediaPlayer.reset()
mediaPlayer.setDataSource(context, getInsistentSound(groupId))
mediaPlayer.setAudioAttributes(AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_ALARM).build())
mediaPlayer.isLooping = true
mediaPlayer.prepare()
mediaPlayer.start()
} else {
Log.d(TAG, "Media player: Alarm volume is 0; not playing insistent alarm")
}
} catch (e: Exception) {
Log.w(TAG, "Media player: Failed to play insistent alarm", e)
}
}
private fun getInsistentSound(groupId: String): Uri {
return if (channelsSupported()) {
val channelId = toChannelId(groupId, PRIORITY_MAX)
val channel = notificationManager.getNotificationChannel(channelId)
channel.sound
} else {
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
} }
} }
@ -466,6 +530,7 @@ class NotificationService(val context: Context) {
private const val TAG = "NtfyNotifService" private const val TAG = "NtfyNotifService"
private const val DEFAULT_GROUP = "ntfy" private const val DEFAULT_GROUP = "ntfy"
private const val SUBSCRIPTION_GROUP_PREFIX = "ntfy-subscription-"
private const val GROUP_SUFFIX_PRIORITY_MIN = "-min" private const val GROUP_SUFFIX_PRIORITY_MIN = "-min"
private const val GROUP_SUFFIX_PRIORITY_LOW = "-low" private const val GROUP_SUFFIX_PRIORITY_LOW = "-low"
private const val GROUP_SUFFIX_PRIORITY_DEFAULT = "" private const val GROUP_SUFFIX_PRIORITY_DEFAULT = ""

View file

@ -115,6 +115,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
mutedUntil = 0, mutedUntil = 0,
minPriority = Repository.MIN_PRIORITY_USE_GLOBAL, minPriority = Repository.MIN_PRIORITY_USE_GLOBAL,
autoDelete = Repository.AUTO_DELETE_USE_GLOBAL, autoDelete = Repository.AUTO_DELETE_USE_GLOBAL,
insistent = Repository.INSISTENT_MAX_PRIORITY_USE_GLOBAL,
lastNotificationId = null, lastNotificationId = null,
icon = null, icon = null,
upAppId = null, upAppId = null,
@ -256,6 +257,13 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
// Mark this subscription as "open" so we don't receive notifications for it // Mark this subscription as "open" so we don't receive notifications for it
repository.detailViewSubscriptionId.set(subscriptionId) repository.detailViewSubscriptionId.set(subscriptionId)
// Stop insistent playback (if running, otherwise it'll throw)
try {
repository.mediaPlayer.stop()
} catch (_: Exception) {
// Ignore errors
}
} }
override fun onResume() { override fun onResume() {

View file

@ -145,22 +145,22 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
private fun renderPriority(context: Context, notification: Notification) { private fun renderPriority(context: Context, notification: Notification) {
when (notification.priority) { when (notification.priority) {
1 -> { PRIORITY_MIN -> {
priorityImageView.visibility = View.VISIBLE priorityImageView.visibility = View.VISIBLE
priorityImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_priority_1_24dp)) priorityImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_priority_1_24dp))
} }
2 -> { PRIORITY_LOW -> {
priorityImageView.visibility = View.VISIBLE priorityImageView.visibility = View.VISIBLE
priorityImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_priority_2_24dp)) priorityImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_priority_2_24dp))
} }
3 -> { PRIORITY_DEFAULT -> {
priorityImageView.visibility = View.GONE priorityImageView.visibility = View.GONE
} }
4 -> { PRIORITY_HIGH -> {
priorityImageView.visibility = View.VISIBLE priorityImageView.visibility = View.VISIBLE
priorityImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_priority_4_24dp)) priorityImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_priority_4_24dp))
} }
5 -> { PRIORITY_MAX -> {
priorityImageView.visibility = View.VISIBLE priorityImageView.visibility = View.VISIBLE
priorityImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_priority_5_24dp)) priorityImageView.setImageDrawable(ContextCompat.getDrawable(context, R.drawable.ic_priority_5_24dp))
} }

View file

@ -118,6 +118,7 @@ class DetailSettingsActivity : AppCompatActivity() {
loadMutedUntilPref() loadMutedUntilPref()
loadMinPriorityPref() loadMinPriorityPref()
loadAutoDeletePref() loadAutoDeletePref()
loadInsistentMaxPriorityPref()
loadIconSetPref() loadIconSetPref()
loadIconRemovePref() loadIconRemovePref()
if (notificationService.channelsSupported()) { if (notificationService.channelsSupported()) {
@ -261,8 +262,8 @@ class DetailSettingsActivity : AppCompatActivity() {
value = repository.getMinPriority() value = repository.getMinPriority()
} }
val summary = when (value) { val summary = when (value) {
1 -> getString(R.string.settings_notifications_min_priority_summary_any) PRIORITY_MIN -> getString(R.string.settings_notifications_min_priority_summary_any)
5 -> getString(R.string.settings_notifications_min_priority_summary_max) PRIORITY_MAX -> getString(R.string.settings_notifications_min_priority_summary_max)
else -> { else -> {
val minPriorityString = toPriorityString(requireContext(), value) val minPriorityString = toPriorityString(requireContext(), value)
getString(R.string.settings_notifications_min_priority_summary_x_or_higher, value, minPriorityString) getString(R.string.settings_notifications_min_priority_summary_x_or_higher, value, minPriorityString)
@ -289,7 +290,7 @@ class DetailSettingsActivity : AppCompatActivity() {
pref?.summaryProvider = Preference.SummaryProvider<ListPreference> { preference -> pref?.summaryProvider = Preference.SummaryProvider<ListPreference> { preference ->
var seconds = preference.value.toLongOrNull() ?: Repository.AUTO_DELETE_USE_GLOBAL var seconds = preference.value.toLongOrNull() ?: Repository.AUTO_DELETE_USE_GLOBAL
val global = seconds == Repository.AUTO_DELETE_USE_GLOBAL val global = seconds == Repository.AUTO_DELETE_USE_GLOBAL
if (seconds == Repository.AUTO_DELETE_USE_GLOBAL) { if (global) {
seconds = repository.getAutoDeleteSeconds() seconds = repository.getAutoDeleteSeconds()
} }
val summary = when (seconds) { val summary = when (seconds) {
@ -305,6 +306,33 @@ class DetailSettingsActivity : AppCompatActivity() {
} }
} }
private fun loadInsistentMaxPriorityPref() {
val prefId = context?.getString(R.string.detail_settings_notifications_insistent_max_priority_key) ?: return
val pref: ListPreference? = findPreference(prefId)
pref?.isVisible = true
pref?.value = subscription.insistent.toString()
pref?.preferenceDataStore = object : PreferenceDataStore() {
override fun putString(key: String?, value: String?) {
val intValue = value?.toIntOrNull() ?:return
save(subscription.copy(insistent = intValue))
}
override fun getString(key: String?, defValue: String?): String {
return subscription.insistent.toString()
}
}
pref?.summaryProvider = Preference.SummaryProvider<ListPreference> { preference ->
val value = preference.value.toIntOrNull() ?: Repository.INSISTENT_MAX_PRIORITY_USE_GLOBAL
val global = value == Repository.INSISTENT_MAX_PRIORITY_USE_GLOBAL
val enabled = if (global) repository.getInsistentMaxPriorityEnabled() else value == Repository.INSISTENT_MAX_PRIORITY_ENABLED
val summary = if (enabled) {
getString(R.string.settings_notifications_insistent_max_priority_summary_enabled)
} else {
getString(R.string.settings_notifications_insistent_max_priority_summary_disabled)
}
maybeAppendGlobal(summary, global)
}
}
private fun loadIconSetPref() { private fun loadIconSetPref() {
val prefId = context?.getString(R.string.detail_settings_appearance_icon_set_key) ?: return val prefId = context?.getString(R.string.detail_settings_appearance_icon_set_key) ?: return
iconSetPref = findPreference(prefId) ?: return iconSetPref = findPreference(prefId) ?: return

View file

@ -454,6 +454,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
mutedUntil = 0, mutedUntil = 0,
minPriority = Repository.MIN_PRIORITY_USE_GLOBAL, minPriority = Repository.MIN_PRIORITY_USE_GLOBAL,
autoDelete = Repository.AUTO_DELETE_USE_GLOBAL, autoDelete = Repository.AUTO_DELETE_USE_GLOBAL,
insistent = Repository.INSISTENT_MAX_PRIORITY_USE_GLOBAL,
lastNotificationId = null, lastNotificationId = null,
icon = null, icon = null,
upAppId = null, upAppId = null,

View file

@ -191,8 +191,8 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
} }
minPriority?.summaryProvider = Preference.SummaryProvider<ListPreference> { pref -> minPriority?.summaryProvider = Preference.SummaryProvider<ListPreference> { pref ->
when (val minPriorityValue = pref.value.toIntOrNull() ?: 1) { // 1/low means all priorities when (val minPriorityValue = pref.value.toIntOrNull() ?: 1) { // 1/low means all priorities
1 -> getString(R.string.settings_notifications_min_priority_summary_any) PRIORITY_MIN -> getString(R.string.settings_notifications_min_priority_summary_any)
5 -> getString(R.string.settings_notifications_min_priority_summary_max) PRIORITY_MAX -> getString(R.string.settings_notifications_min_priority_summary_max)
else -> { else -> {
val minPriorityString = toPriorityString(requireContext(), minPriorityValue) val minPriorityString = toPriorityString(requireContext(), minPriorityValue)
getString(R.string.settings_notifications_min_priority_summary_x_or_higher, minPriorityValue, minPriorityString) getString(R.string.settings_notifications_min_priority_summary_x_or_higher, minPriorityValue, minPriorityString)
@ -200,6 +200,26 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
} }
} }
// Keep alerting for max priority
val insistentMaxPriorityPrefId = context?.getString(R.string.settings_notifications_insistent_max_priority_key) ?: return
val insistentMaxPriority: SwitchPreference? = findPreference(insistentMaxPriorityPrefId)
insistentMaxPriority?.isChecked = repository.getInsistentMaxPriorityEnabled()
insistentMaxPriority?.preferenceDataStore = object : PreferenceDataStore() {
override fun putBoolean(key: String?, value: Boolean) {
repository.setInsistentMaxPriorityEnabled(value)
}
override fun getBoolean(key: String?, defValue: Boolean): Boolean {
return repository.getInsistentMaxPriorityEnabled()
}
}
insistentMaxPriority?.summaryProvider = Preference.SummaryProvider<SwitchPreference> { pref ->
if (pref.isChecked) {
getString(R.string.settings_notifications_insistent_max_priority_summary_enabled)
} else {
getString(R.string.settings_notifications_insistent_max_priority_summary_disabled)
}
}
// Channel settings // Channel settings
val channelPrefsPrefId = context?.getString(R.string.settings_notifications_channel_prefs_key) ?: return val channelPrefsPrefId = context?.getString(R.string.settings_notifications_channel_prefs_key) ?: return
val channelPrefs: Preference? = findPreference(channelPrefsPrefId) val channelPrefs: Preference? = findPreference(channelPrefsPrefId)

View file

@ -74,6 +74,7 @@ class BroadcastReceiver : android.content.BroadcastReceiver() {
mutedUntil = 0, mutedUntil = 0,
minPriority = Repository.MIN_PRIORITY_USE_GLOBAL, minPriority = Repository.MIN_PRIORITY_USE_GLOBAL,
autoDelete = Repository.AUTO_DELETE_USE_GLOBAL, autoDelete = Repository.AUTO_DELETE_USE_GLOBAL,
insistent = Repository.INSISTENT_MAX_PRIORITY_USE_GLOBAL,
lastNotificationId = null, lastNotificationId = null,
icon = null, icon = null,
upAppId = appId, upAppId = appId,

View file

@ -0,0 +1,11 @@
package io.heckel.ntfy.util
const val ANDROID_APP_MIME_TYPE = "application/vnd.android.package-archive"
const val PRIORITY_MIN = 1
const val PRIORITY_LOW = 2
const val PRIORITY_DEFAULT = 3
const val PRIORITY_HIGH = 4
const val PRIORITY_MAX = 5
val ALL_PRIORITIES = listOf(PRIORITY_MIN, PRIORITY_LOW, PRIORITY_DEFAULT, PRIORITY_HIGH, PRIORITY_MAX)

View file

@ -99,17 +99,16 @@ fun formatDateShort(timestampSecs: Long): String {
} }
fun toPriority(priority: Int?): Int { fun toPriority(priority: Int?): Int {
if (priority != null && (1..5).contains(priority)) return priority return if (priority != null && ALL_PRIORITIES.contains(priority)) priority else PRIORITY_DEFAULT
else return 3
} }
fun toPriorityString(context: Context, priority: Int): String { fun toPriorityString(context: Context, priority: Int): String {
return when (priority) { return when (priority) {
1 -> context.getString(R.string.settings_notifications_priority_min) PRIORITY_MIN -> context.getString(R.string.settings_notifications_priority_min)
2 -> context.getString(R.string.settings_notifications_priority_low) PRIORITY_LOW -> context.getString(R.string.settings_notifications_priority_low)
3 -> context.getString(R.string.settings_notifications_priority_default) PRIORITY_DEFAULT -> context.getString(R.string.settings_notifications_priority_default)
4 -> context.getString(R.string.settings_notifications_priority_high) PRIORITY_HIGH -> context.getString(R.string.settings_notifications_priority_high)
5 -> context.getString(R.string.settings_notifications_priority_max) PRIORITY_MAX -> context.getString(R.string.settings_notifications_priority_max)
else -> context.getString(R.string.settings_notifications_priority_default) else -> context.getString(R.string.settings_notifications_priority_default)
} }
} }
@ -319,8 +318,6 @@ fun formatBytes(bytes: Long, decimals: Int = 1): String {
return java.lang.String.format("%.${decimals}f %cB", value / 1024.0, ci.current()) return java.lang.String.format("%.${decimals}f %cB", value / 1024.0, ci.current())
} }
const val androidAppMimeType = "application/vnd.android.package-archive"
fun mimeTypeToIconResource(mimeType: String?): Int { fun mimeTypeToIconResource(mimeType: String?): Int {
return if (mimeType?.startsWith("image/") == true) { return if (mimeType?.startsWith("image/") == true) {
R.drawable.ic_file_image_red_24dp R.drawable.ic_file_image_red_24dp
@ -328,7 +325,7 @@ fun mimeTypeToIconResource(mimeType: String?): Int {
R.drawable.ic_file_video_orange_24dp R.drawable.ic_file_video_orange_24dp
} else if (mimeType?.startsWith("audio/") == true) { } else if (mimeType?.startsWith("audio/") == true) {
R.drawable.ic_file_audio_purple_24dp R.drawable.ic_file_audio_purple_24dp
} else if (mimeType == androidAppMimeType) { } else if (mimeType == ANDROID_APP_MIME_TYPE) {
R.drawable.ic_file_app_gray_24dp R.drawable.ic_file_app_gray_24dp
} else { } else {
R.drawable.ic_file_document_blue_24dp R.drawable.ic_file_document_blue_24dp
@ -342,7 +339,7 @@ fun supportedImage(mimeType: String?): Boolean {
// Google Play doesn't allow us to install received .apk files anymore. // Google Play doesn't allow us to install received .apk files anymore.
// See https://github.com/binwiederhier/ntfy/issues/531 // See https://github.com/binwiederhier/ntfy/issues/531
fun canOpenAttachment(attachment: Attachment?): Boolean { fun canOpenAttachment(attachment: Attachment?): Boolean {
if (attachment?.type == androidAppMimeType && !BuildConfig.INSTALL_PACKAGES_AVAILABLE) { if (attachment?.type == ANDROID_APP_MIME_TYPE && !BuildConfig.INSTALL_PACKAGES_AVAILABLE) {
return false return false
} }
return true return true

View file

@ -281,6 +281,9 @@
<string name="settings_notifications_auto_delete_one_week">After one week</string> <string name="settings_notifications_auto_delete_one_week">After one week</string>
<string name="settings_notifications_auto_delete_one_month">After one month</string> <string name="settings_notifications_auto_delete_one_month">After one month</string>
<string name="settings_notifications_auto_delete_three_months">After 3 months</string> <string name="settings_notifications_auto_delete_three_months">After 3 months</string>
<string name="settings_notifications_insistent_max_priority_title">Keep alerting for highest priority</string>
<string name="settings_notifications_insistent_max_priority_summary_enabled">Max priority notifications continuously alert until dismissed</string>
<string name="settings_notifications_insistent_max_priority_summary_disabled">Max priority notifications only alert once</string>
<string name="settings_general_header">General</string> <string name="settings_general_header">General</string>
<string name="settings_general_default_base_url_title">Default server</string> <string name="settings_general_default_base_url_title">Default server</string>
<string name="settings_general_default_base_url_message">Enter your server\'s root URL to use your own server as a default when subscribing to new topics and/or sharing to topics.</string> <string name="settings_general_default_base_url_message">Enter your server\'s root URL to use your own server as a default when subscribing to new topics and/or sharing to topics.</string>
@ -355,6 +358,8 @@
<string name="detail_settings_notifications_dedicated_channels_summary_off">Using default settings (sounds, Do Not Disturb override, etc.)</string> <string name="detail_settings_notifications_dedicated_channels_summary_off">Using default settings (sounds, Do Not Disturb override, etc.)</string>
<string name="detail_settings_notifications_open_channels_title">Configure notification settings</string> <string name="detail_settings_notifications_open_channels_title">Configure notification settings</string>
<string name="detail_settings_notifications_open_channels_summary">Do Not Disturb (DND) override, sounds, etc.</string> <string name="detail_settings_notifications_open_channels_summary">Do Not Disturb (DND) override, sounds, etc.</string>
<string name="detail_settings_notifications_insistent_max_priority_list_item_enabled">Keep alerting</string>
<string name="detail_settings_notifications_insistent_max_priority_list_item_disabled">Alert only once</string>
<string name="detail_settings_appearance_header">Appearance</string> <string name="detail_settings_appearance_header">Appearance</string>
<string name="detail_settings_appearance_icon_set_title">Subscription icon</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_set_summary">Set an icon to be displayed in notifications</string>

View file

@ -18,6 +18,7 @@
<string name="settings_notifications_channel_prefs_key" translatable="false">ChannelPrefs</string> <string name="settings_notifications_channel_prefs_key" translatable="false">ChannelPrefs</string>
<string name="settings_notifications_auto_download_key" translatable="false">AutoDownload</string> <string name="settings_notifications_auto_download_key" translatable="false">AutoDownload</string>
<string name="settings_notifications_auto_delete_key" translatable="false">AutoDelete</string> <string name="settings_notifications_auto_delete_key" translatable="false">AutoDelete</string>
<string name="settings_notifications_insistent_max_priority_key" translatable="false">InsistentMaxPriority</string>
<string name="settings_general_default_base_url_key" translatable="false">DefaultBaseURL</string> <string name="settings_general_default_base_url_key" translatable="false">DefaultBaseURL</string>
<string name="settings_general_users_key" translatable="false">ManageUsers</string> <string name="settings_general_users_key" translatable="false">ManageUsers</string>
<string name="settings_general_dark_mode_key" translatable="false">DarkMode</string> <string name="settings_general_dark_mode_key" translatable="false">DarkMode</string>
@ -38,6 +39,7 @@
<string name="detail_settings_notifications_open_channels_key" translatable="false">SubscriptionOpenChannels</string> <string name="detail_settings_notifications_open_channels_key" translatable="false">SubscriptionOpenChannels</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_notifications_insistent_max_priority_key" translatable="false">SubscriptionInsistentMaxPriority</string>
<string name="detail_settings_appearance_header_key" translatable="false">SubscriptionAppearance</string> <string name="detail_settings_appearance_header_key" translatable="false">SubscriptionAppearance</string>
<string name="detail_settings_appearance_icon_set_key" translatable="false">SubscriptionIconSet</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> <string name="detail_settings_appearance_icon_remove_key" translatable="false">SubscriptionIconRemove</string>
@ -148,6 +150,16 @@
<item>2592000</item> <item>2592000</item>
<item>7776000</item> <item>7776000</item>
</string-array> </string-array>
<string-array name="detail_settings_notifications_insistent_max_priority_entries">
<item>@string/detail_settings_global_setting_title</item>
<item>@string/detail_settings_notifications_insistent_max_priority_list_item_enabled</item>
<item>@string/detail_settings_notifications_insistent_max_priority_list_item_disabled</item>
</string-array>
<string-array name="detail_settings_notifications_insistent_max_priority_values">
<item>-1</item> <!-- Same as Repository.INSISTENT_MAX_PRIORITY_USE_GLOBAL -->
<item>1</item>
<item>0</item>
</string-array>
<string-array name="settings_advanced_connection_protocol_entries"> <string-array name="settings_advanced_connection_protocol_entries">
<item>@string/settings_advanced_connection_protocol_entry_jsonhttp</item> <item>@string/settings_advanced_connection_protocol_entry_jsonhttp</item>
<item>@string/settings_advanced_connection_protocol_entry_ws</item> <item>@string/settings_advanced_connection_protocol_entry_ws</item>

View file

@ -28,6 +28,13 @@
app:entryValues="@array/detail_settings_notifications_auto_delete_values" app:entryValues="@array/detail_settings_notifications_auto_delete_values"
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 -->
<ListPreference
app:key="@string/detail_settings_notifications_insistent_max_priority_key"
app:title="@string/settings_notifications_insistent_max_priority_title"
app:entries="@array/detail_settings_notifications_insistent_max_priority_entries"
app:entryValues="@array/detail_settings_notifications_insistent_max_priority_values"
app:defaultValue="-1"
app:isPreferenceVisible="false"/> <!-- Same as Repository.INSISTENT_MAX_PRIORITY_USE_GLOBAL -->
<SwitchPreference <SwitchPreference
app:key="@string/detail_settings_notifications_dedicated_channels_key" app:key="@string/detail_settings_notifications_dedicated_channels_key"
app:title="@string/detail_settings_notifications_dedicated_channels_title" app:title="@string/detail_settings_notifications_dedicated_channels_title"

View file

@ -25,6 +25,10 @@
app:entries="@array/settings_notifications_auto_delete_entries" app:entries="@array/settings_notifications_auto_delete_entries"
app:entryValues="@array/settings_notifications_auto_delete_values" app:entryValues="@array/settings_notifications_auto_delete_values"
app:defaultValue="2592000"/> app:defaultValue="2592000"/>
<SwitchPreference
app:key="@string/settings_notifications_insistent_max_priority_key"
app:title="@string/settings_notifications_insistent_max_priority_title"
app:defaultValue="false"/>
<Preference <Preference
app:key="@string/settings_notifications_channel_prefs_key" app:key="@string/settings_notifications_channel_prefs_key"
app:title="@string/settings_notifications_channel_prefs_title" app:title="@string/settings_notifications_channel_prefs_title"