Merge branch 'constant-ring' into custom_notification_channels
This commit is contained in:
commit
3e8ba28e63
20 changed files with 285 additions and 58 deletions
|
@ -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')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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"
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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?,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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 = ""
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
11
app/src/main/java/io/heckel/ntfy/util/Constants.kt
Normal file
11
app/src/main/java/io/heckel/ntfy/util/Constants.kt
Normal 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)
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue