Compare commits
1 commit
main
...
e2e-dabble
Author | SHA1 | Date | |
---|---|---|---|
|
a4461bf47f |
18 changed files with 239 additions and 32 deletions
|
@ -2,11 +2,11 @@
|
||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 12,
|
"version": 12,
|
||||||
"identityHash": "9363ad5196e88862acceb1bb9ee91124",
|
"identityHash": "250db1985385d64d880124071eab96fc",
|
||||||
"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, 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, `lastNotificationId` TEXT, `icon` TEXT, `upAppId` TEXT, `upConnectorToken` TEXT, `displayName` TEXT, `encryptionKey` BLOB, PRIMARY KEY(`id`))",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "id",
|
"fieldPath": "id",
|
||||||
|
@ -79,6 +79,12 @@
|
||||||
"columnName": "displayName",
|
"columnName": "displayName",
|
||||||
"affinity": "TEXT",
|
"affinity": "TEXT",
|
||||||
"notNull": false
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "encryptionKey",
|
||||||
|
"columnName": "encryptionKey",
|
||||||
|
"affinity": "BLOB",
|
||||||
|
"notNull": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"primaryKey": {
|
"primaryKey": {
|
||||||
|
@ -326,7 +332,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, '9363ad5196e88862acceb1bb9ee91124')"
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '250db1985385d64d880124071eab96fc')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,6 +2,7 @@ package io.heckel.ntfy.backup
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.util.Base64
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.stream.JsonReader
|
import com.google.gson.stream.JsonReader
|
||||||
|
@ -102,6 +103,7 @@ class Backuper(val context: Context) {
|
||||||
upAppId = s.upAppId,
|
upAppId = s.upAppId,
|
||||||
upConnectorToken = s.upConnectorToken,
|
upConnectorToken = s.upConnectorToken,
|
||||||
displayName = s.displayName,
|
displayName = s.displayName,
|
||||||
|
encryptionKey = Base64.decode(s.encryptionKey, Base64.DEFAULT)
|
||||||
))
|
))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Unable to restore subscription ${s.id} (${topicUrl(s.baseUrl, s.topic)}): ${e.message}. Ignoring.", e)
|
Log.w(TAG, "Unable to restore subscription ${s.id} (${topicUrl(s.baseUrl, s.topic)}): ${e.message}. Ignoring.", e)
|
||||||
|
@ -226,7 +228,8 @@ class Backuper(val context: Context) {
|
||||||
icon = s.icon,
|
icon = s.icon,
|
||||||
upAppId = s.upAppId,
|
upAppId = s.upAppId,
|
||||||
upConnectorToken = s.upConnectorToken,
|
upConnectorToken = s.upConnectorToken,
|
||||||
displayName = s.displayName
|
displayName = s.displayName,
|
||||||
|
encryptionKey = if (s.encryptionKey != null) Base64.encodeToString(s.encryptionKey, Base64.DEFAULT) else null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -334,7 +337,8 @@ data class Subscription(
|
||||||
val icon: String?,
|
val icon: String?,
|
||||||
val upAppId: String?,
|
val upAppId: String?,
|
||||||
val upConnectorToken: String?,
|
val upConnectorToken: String?,
|
||||||
val displayName: String?
|
val displayName: String?,
|
||||||
|
val encryptionKey: String? // as base64
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Notification(
|
data class Notification(
|
||||||
|
@ -343,7 +347,7 @@ data class Notification(
|
||||||
val timestamp: Long,
|
val timestamp: Long,
|
||||||
val title: String,
|
val title: String,
|
||||||
val message: String,
|
val message: String,
|
||||||
val encoding: String, // "base64" or ""
|
val encoding: String, // "base64", "jwe", or ""
|
||||||
val priority: Int, // 1=min, 3=default, 5=max
|
val priority: Int, // 1=min, 3=default, 5=max
|
||||||
val tags: String,
|
val tags: String,
|
||||||
val click: String, // URL/intent to open on notification click
|
val click: String, // URL/intent to open on notification click
|
||||||
|
|
|
@ -23,13 +23,14 @@ data class Subscription(
|
||||||
@ColumnInfo(name = "upAppId") val upAppId: String?, // UnifiedPush application package name
|
@ColumnInfo(name = "upAppId") val upAppId: String?, // UnifiedPush application package name
|
||||||
@ColumnInfo(name = "upConnectorToken") val upConnectorToken: String?, // UnifiedPush connector token
|
@ColumnInfo(name = "upConnectorToken") val upConnectorToken: String?, // UnifiedPush connector token
|
||||||
@ColumnInfo(name = "displayName") val displayName: String?,
|
@ColumnInfo(name = "displayName") val displayName: String?,
|
||||||
|
@ColumnInfo(name = "encryptionKey") val encryptionKey: ByteArray?,
|
||||||
@Ignore val totalCount: Int = 0, // Total notifications
|
@Ignore val totalCount: Int = 0, // Total notifications
|
||||||
@Ignore val newCount: Int = 0, // New notifications
|
@Ignore val newCount: Int = 0, // New notifications
|
||||||
@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?) :
|
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?, encryptionKey: ByteArray?) :
|
||||||
this(id, baseUrl, topic, instant, mutedUntil, minPriority, autoDelete, lastNotificationId, icon, upAppId, upConnectorToken, displayName, 0, 0, 0, ConnectionState.NOT_APPLICABLE)
|
this(id, baseUrl, topic, instant, mutedUntil, minPriority, autoDelete, lastNotificationId, icon, upAppId, upConnectorToken, displayName, encryptionKey, 0, 0, 0, ConnectionState.NOT_APPLICABLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class ConnectionState {
|
enum class ConnectionState {
|
||||||
|
@ -49,6 +50,7 @@ data class SubscriptionWithMetadata(
|
||||||
val upAppId: String?,
|
val upAppId: String?,
|
||||||
val upConnectorToken: String?,
|
val upConnectorToken: String?,
|
||||||
val displayName: String?,
|
val displayName: String?,
|
||||||
|
val encryptionKey: ByteArray?,
|
||||||
val totalCount: Int,
|
val totalCount: Int,
|
||||||
val newCount: Int,
|
val newCount: Int,
|
||||||
val lastActive: Long
|
val lastActive: Long
|
||||||
|
@ -61,7 +63,7 @@ data class Notification(
|
||||||
@ColumnInfo(name = "timestamp") val timestamp: Long, // Unix timestamp
|
@ColumnInfo(name = "timestamp") val timestamp: Long, // Unix timestamp
|
||||||
@ColumnInfo(name = "title") val title: String,
|
@ColumnInfo(name = "title") val title: String,
|
||||||
@ColumnInfo(name = "message") val message: String,
|
@ColumnInfo(name = "message") val message: String,
|
||||||
@ColumnInfo(name = "encoding") val encoding: String, // "base64" or ""
|
@ColumnInfo(name = "encoding") val encoding: String, // "" (raw UTF-8), "base64", or "jwe" (encryption)
|
||||||
@ColumnInfo(name = "notificationId") val notificationId: Int, // Android notification popup ID
|
@ColumnInfo(name = "notificationId") val notificationId: Int, // Android notification popup ID
|
||||||
@ColumnInfo(name = "priority", defaultValue = "3") val priority: Int, // 1=min, 3=default, 5=max
|
@ColumnInfo(name = "priority", defaultValue = "3") val priority: Int, // 1=min, 3=default, 5=max
|
||||||
@ColumnInfo(name = "tags") val tags: String,
|
@ColumnInfo(name = "tags") val tags: String,
|
||||||
|
@ -269,6 +271,8 @@ abstract class Database : RoomDatabase() {
|
||||||
override fun migrate(db: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
db.execSQL("ALTER TABLE Subscription ADD COLUMN lastNotificationId TEXT")
|
db.execSQL("ALTER TABLE Subscription ADD COLUMN lastNotificationId TEXT")
|
||||||
db.execSQL("ALTER TABLE Subscription ADD COLUMN displayName TEXT")
|
db.execSQL("ALTER TABLE Subscription ADD COLUMN displayName TEXT")
|
||||||
|
db.execSQL("ALTER TABLE Subscription ADD COLUMN encryptionKey BLOB")
|
||||||
|
db.execSQL("ALTER TABLE Notification ADD COLUMN encryption TEXT NOT NULL DEFAULT('')")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -278,7 +282,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.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken, s.displayName, s.encryptionKey,
|
||||||
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
|
||||||
|
@ -291,7 +295,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.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken, s.displayName, s.encryptionKey,
|
||||||
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
|
||||||
|
@ -304,7 +308,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.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken, s.displayName, s.encryptionKey,
|
||||||
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
|
||||||
|
@ -317,7 +321,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.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken, s.displayName, s.encryptionKey,
|
||||||
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
|
||||||
|
@ -330,7 +334,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.id, s.baseUrl, s.topic, s.instant, s.mutedUntil, s.minPriority, s.autoDelete, s.lastNotificationId, s.icon, s.upAppId, s.upConnectorToken, s.displayName, s.encryptionKey,
|
||||||
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
|
||||||
|
|
|
@ -385,6 +385,7 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
|
||||||
upAppId = s.upAppId,
|
upAppId = s.upAppId,
|
||||||
upConnectorToken = s.upConnectorToken,
|
upConnectorToken = s.upConnectorToken,
|
||||||
displayName = s.displayName,
|
displayName = s.displayName,
|
||||||
|
encryptionKey = s.encryptionKey,
|
||||||
totalCount = s.totalCount,
|
totalCount = s.totalCount,
|
||||||
newCount = s.newCount,
|
newCount = s.newCount,
|
||||||
lastActive = s.lastActive,
|
lastActive = s.lastActive,
|
||||||
|
@ -410,6 +411,7 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
|
||||||
upAppId = s.upAppId,
|
upAppId = s.upAppId,
|
||||||
upConnectorToken = s.upConnectorToken,
|
upConnectorToken = s.upConnectorToken,
|
||||||
displayName = s.displayName,
|
displayName = s.displayName,
|
||||||
|
encryptionKey = s.encryptionKey,
|
||||||
totalCount = s.totalCount,
|
totalCount = s.totalCount,
|
||||||
newCount = s.newCount,
|
newCount = s.newCount,
|
||||||
lastActive = s.lastActive,
|
lastActive = s.lastActive,
|
||||||
|
|
|
@ -3,6 +3,7 @@ package io.heckel.ntfy.service
|
||||||
import io.heckel.ntfy.db.*
|
import io.heckel.ntfy.db.*
|
||||||
import io.heckel.ntfy.util.Log
|
import io.heckel.ntfy.util.Log
|
||||||
import io.heckel.ntfy.msg.ApiService
|
import io.heckel.ntfy.msg.ApiService
|
||||||
|
import io.heckel.ntfy.util.Encryption
|
||||||
import io.heckel.ntfy.util.topicUrl
|
import io.heckel.ntfy.util.topicUrl
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
|
@ -42,7 +43,8 @@ class JsonConnection(
|
||||||
since = notification.id
|
since = notification.id
|
||||||
val subscriptionId = topicsToSubscriptionIds[topic] ?: return@notify
|
val subscriptionId = topicsToSubscriptionIds[topic] ?: return@notify
|
||||||
val subscription = repository.getSubscription(subscriptionId) ?: return@notify
|
val subscription = repository.getSubscription(subscriptionId) ?: return@notify
|
||||||
val notificationWithSubscriptionId = notification.copy(subscriptionId = subscription.id)
|
val notificationDecrypted = Encryption.maybeDecrypt(subscription, notification)
|
||||||
|
val notificationWithSubscriptionId = notificationDecrypted.copy(subscriptionId = subscription.id)
|
||||||
notificationListener(subscription, notificationWithSubscriptionId)
|
notificationListener(subscription, notificationWithSubscriptionId)
|
||||||
}
|
}
|
||||||
val failed = AtomicBoolean(false)
|
val failed = AtomicBoolean(false)
|
||||||
|
|
|
@ -7,6 +7,7 @@ import android.os.Looper
|
||||||
import io.heckel.ntfy.db.*
|
import io.heckel.ntfy.db.*
|
||||||
import io.heckel.ntfy.msg.ApiService.Companion.requestBuilder
|
import io.heckel.ntfy.msg.ApiService.Companion.requestBuilder
|
||||||
import io.heckel.ntfy.msg.NotificationParser
|
import io.heckel.ntfy.msg.NotificationParser
|
||||||
|
import io.heckel.ntfy.util.Encryption
|
||||||
import io.heckel.ntfy.util.Log
|
import io.heckel.ntfy.util.Log
|
||||||
import io.heckel.ntfy.util.topicShortUrl
|
import io.heckel.ntfy.util.topicShortUrl
|
||||||
import io.heckel.ntfy.util.topicUrlWs
|
import io.heckel.ntfy.util.topicUrlWs
|
||||||
|
@ -144,7 +145,8 @@ class WsConnection(
|
||||||
val notification = notificationWithTopic.notification
|
val notification = notificationWithTopic.notification
|
||||||
val subscriptionId = topicsToSubscriptionIds[topic] ?: return@synchronize
|
val subscriptionId = topicsToSubscriptionIds[topic] ?: return@synchronize
|
||||||
val subscription = repository.getSubscription(subscriptionId) ?: return@synchronize
|
val subscription = repository.getSubscription(subscriptionId) ?: return@synchronize
|
||||||
val notificationWithSubscriptionId = notification.copy(subscriptionId = subscription.id)
|
val notificationDecrypted = Encryption.maybeDecrypt(subscription, notification)
|
||||||
|
val notificationWithSubscriptionId = notificationDecrypted.copy(subscriptionId = subscription.id)
|
||||||
notificationListener(subscription, notificationWithSubscriptionId)
|
notificationListener(subscription, notificationWithSubscriptionId)
|
||||||
since.set(notification.id)
|
since.set(notification.id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,6 +118,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
||||||
upAppId = null,
|
upAppId = null,
|
||||||
upConnectorToken = null,
|
upConnectorToken = null,
|
||||||
displayName = null,
|
displayName = null,
|
||||||
|
encryptionKey = null,
|
||||||
totalCount = 0,
|
totalCount = 0,
|
||||||
newCount = 0,
|
newCount = 0,
|
||||||
lastActive = Date().time/1000
|
lastActive = Date().time/1000
|
||||||
|
@ -133,7 +134,9 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
||||||
// Fetch cached messages
|
// Fetch cached messages
|
||||||
try {
|
try {
|
||||||
val user = repository.getUser(subscription.baseUrl) // May be null
|
val user = repository.getUser(subscription.baseUrl) // May be null
|
||||||
val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic, user)
|
val notifications = api
|
||||||
|
.poll(subscription.id, subscription.baseUrl, subscription.topic, user)
|
||||||
|
.map { n -> Encryption.maybeDecrypt(subscription, n) }
|
||||||
notifications.forEach { notification -> repository.addNotification(notification) }
|
notifications.forEach { notification -> repository.addNotification(notification) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to fetch notifications: ${e.message}", e)
|
Log.e(TAG, "Unable to fetch notifications: ${e.message}", e)
|
||||||
|
@ -466,7 +469,9 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
|
||||||
try {
|
try {
|
||||||
val subscription = repository.getSubscription(subscriptionId) ?: return@launch
|
val subscription = repository.getSubscription(subscriptionId) ?: return@launch
|
||||||
val user = repository.getUser(subscription.baseUrl) // May be null
|
val user = repository.getUser(subscription.baseUrl) // May be null
|
||||||
val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic, user, subscription.lastNotificationId)
|
val notifications = api
|
||||||
|
.poll(subscription.id, subscription.baseUrl, subscription.topic, user, subscription.lastNotificationId)
|
||||||
|
.map { n -> Encryption.maybeDecrypt(subscription, n) }
|
||||||
val newNotifications = repository.onlyNewNotifications(subscriptionId, notifications)
|
val newNotifications = repository.onlyNewNotifications(subscriptionId, notifications)
|
||||||
val toastMessage = if (newNotifications.isEmpty()) {
|
val toastMessage = if (newNotifications.isEmpty()) {
|
||||||
getString(R.string.refresh_message_no_results)
|
getString(R.string.refresh_message_no_results)
|
||||||
|
|
|
@ -4,7 +4,6 @@ import android.content.ContentResolver
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
|
@ -111,6 +110,7 @@ class DetailSettingsActivity : AppCompatActivity() {
|
||||||
loadMutedUntilPref()
|
loadMutedUntilPref()
|
||||||
loadMinPriorityPref()
|
loadMinPriorityPref()
|
||||||
loadAutoDeletePref()
|
loadAutoDeletePref()
|
||||||
|
loadPasswordPref()
|
||||||
loadIconSetPref()
|
loadIconSetPref()
|
||||||
loadIconRemovePref()
|
loadIconRemovePref()
|
||||||
} else {
|
} else {
|
||||||
|
@ -254,6 +254,30 @@ class DetailSettingsActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun loadPasswordPref() {
|
||||||
|
val prefId = context?.getString(R.string.detail_settings_notifications_password_key) ?: return
|
||||||
|
val pref: EditTextPreference? = findPreference(prefId)
|
||||||
|
pref?.isVisible = true // Hack: Show all settings at once, because subscription is loaded asynchronously
|
||||||
|
pref?.text = ""
|
||||||
|
pref?.preferenceDataStore = object : PreferenceDataStore() {
|
||||||
|
override fun putString(key: String?, value: String?) {
|
||||||
|
val newPassword = value ?: return
|
||||||
|
val encryptionKey = if (newPassword.trim().isEmpty()) null else Encryption.deriveKey(newPassword, topicUrl(subscription))
|
||||||
|
save(subscription.copy(encryptionKey = encryptionKey))
|
||||||
|
}
|
||||||
|
override fun getString(key: String?, defValue: String?): String {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pref?.summaryProvider = Preference.SummaryProvider<EditTextPreference> { pref ->
|
||||||
|
if (TextUtils.isEmpty(pref.text)) {
|
||||||
|
"No password set"
|
||||||
|
} else {
|
||||||
|
"Password saved"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -43,7 +43,6 @@ import io.heckel.ntfy.work.PollWorker
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.security.SecureRandom
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
@ -206,6 +205,20 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
||||||
schedulePeriodicPollWorker()
|
schedulePeriodicPollWorker()
|
||||||
schedulePeriodicServiceRestartWorker()
|
schedulePeriodicServiceRestartWorker()
|
||||||
schedulePeriodicDeleteWorker()
|
schedulePeriodicDeleteWorker()
|
||||||
|
|
||||||
|
testenc()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun testenc() {
|
||||||
|
try {
|
||||||
|
val key = Encryption.deriveKey("secr3t password", "https://ntfy.sh/mysecret")
|
||||||
|
Log.d("encryption", "key ${key.toHex()}")
|
||||||
|
val ciphertext = "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..vbe1Qv_-mKYbUgce.EfmOUIUi7lxXZG_o4bqXZ9pmpr1Rzs4Y5QLE2XD2_aw_SQ.y2hadrN5b2LEw7_PJHhbcA"
|
||||||
|
val plaintext = Encryption.decrypt(ciphertext, key)
|
||||||
|
Log.d("encryption", "decryptString: $plaintext")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("encryption", "failed", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
@ -439,6 +452,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
||||||
upAppId = null,
|
upAppId = null,
|
||||||
upConnectorToken = null,
|
upConnectorToken = null,
|
||||||
displayName = null,
|
displayName = null,
|
||||||
|
encryptionKey = null,
|
||||||
totalCount = 0,
|
totalCount = 0,
|
||||||
newCount = 0,
|
newCount = 0,
|
||||||
lastActive = Date().time/1000
|
lastActive = Date().time/1000
|
||||||
|
@ -455,7 +469,9 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
val user = repository.getUser(subscription.baseUrl) // May be null
|
val user = repository.getUser(subscription.baseUrl) // May be null
|
||||||
val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic, user)
|
val notifications = api
|
||||||
|
.poll(subscription.id, subscription.baseUrl, subscription.topic, user)
|
||||||
|
.map { n -> Encryption.maybeDecrypt(subscription, n) }
|
||||||
notifications.forEach { notification -> repository.addNotification(notification) }
|
notifications.forEach { notification -> repository.addNotification(notification) }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Unable to fetch notifications: ${e.message}", e)
|
Log.e(TAG, "Unable to fetch notifications: ${e.message}", e)
|
||||||
|
@ -492,7 +508,9 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
|
||||||
Log.d(TAG, "subscription: ${subscription}")
|
Log.d(TAG, "subscription: ${subscription}")
|
||||||
try {
|
try {
|
||||||
val user = repository.getUser(subscription.baseUrl) // May be null
|
val user = repository.getUser(subscription.baseUrl) // May be null
|
||||||
val notifications = api.poll(subscription.id, subscription.baseUrl, subscription.topic, user, subscription.lastNotificationId)
|
val notifications = api
|
||||||
|
.poll(subscription.id, subscription.baseUrl, subscription.topic, user, subscription.lastNotificationId)
|
||||||
|
.map { n -> Encryption.maybeDecrypt(subscription, n) }
|
||||||
val newNotifications = repository.onlyNewNotifications(subscription.id, notifications)
|
val newNotifications = repository.onlyNewNotifications(subscription.id, notifications)
|
||||||
newNotifications.forEach { notification ->
|
newNotifications.forEach { notification ->
|
||||||
newNotificationsCount++
|
newNotificationsCount++
|
||||||
|
|
|
@ -80,6 +80,7 @@ class BroadcastReceiver : android.content.BroadcastReceiver() {
|
||||||
upAppId = appId,
|
upAppId = appId,
|
||||||
upConnectorToken = connectorToken,
|
upConnectorToken = connectorToken,
|
||||||
displayName = null,
|
displayName = null,
|
||||||
|
encryptionKey = null,
|
||||||
totalCount = 0,
|
totalCount = 0,
|
||||||
newCount = 0,
|
newCount = 0,
|
||||||
lastActive = Date().time/1000
|
lastActive = Date().time/1000
|
||||||
|
|
68
app/src/main/java/io/heckel/ntfy/util/Encryption.kt
Normal file
68
app/src/main/java/io/heckel/ntfy/util/Encryption.kt
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
package io.heckel.ntfy.util
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import io.heckel.ntfy.db.Notification
|
||||||
|
import io.heckel.ntfy.db.Subscription
|
||||||
|
import io.heckel.ntfy.msg.NotificationParser
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.SecretKeyFactory
|
||||||
|
import javax.crypto.spec.GCMParameterSpec
|
||||||
|
import javax.crypto.spec.PBEKeySpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }
|
||||||
|
|
||||||
|
object Encryption {
|
||||||
|
private const val TAG = "NtfyEncryption"
|
||||||
|
private const val keyDerivIter = 50000
|
||||||
|
private const val keyLenBits = 256
|
||||||
|
private const val gcmTagLenBits = 128
|
||||||
|
private const val encodingJwe = "jwe"
|
||||||
|
private val parser = NotificationParser()
|
||||||
|
|
||||||
|
fun maybeDecrypt(subscription: Subscription, notification: Notification): Notification {
|
||||||
|
if (notification.encoding != encodingJwe) {
|
||||||
|
return notification
|
||||||
|
} else if (subscription.encryptionKey == null) {
|
||||||
|
Log.w(TAG, "Notification is encrypted, but key is missing: $notification; leaving encrypted message intact")
|
||||||
|
return notification
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
val plaintext = decrypt(notification.message, subscription.encryptionKey)
|
||||||
|
val decryptedNotification = parser.parse(plaintext) ?: throw Exception("Cannot parse decrypted message: $plaintext")
|
||||||
|
if (decryptedNotification.id != notification.id || decryptedNotification.timestamp != notification.timestamp) {
|
||||||
|
throw Exception("Message ID or timestamp mismatch in decrypted message: $plaintext")
|
||||||
|
}
|
||||||
|
decryptedNotification
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Unable to decrypt message, falling back to original", e)
|
||||||
|
notification
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deriveKey(password: String, topicUrl: String): ByteArray {
|
||||||
|
val salt = MessageDigest.getInstance("SHA-256").digest(topicUrl.toByteArray())
|
||||||
|
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
|
||||||
|
val spec = PBEKeySpec(password.toCharArray(), salt, keyDerivIter, keyLenBits)
|
||||||
|
return factory.generateSecret(spec).encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decrypt(input: String, key: ByteArray): String {
|
||||||
|
val parts = input.split(".")
|
||||||
|
if (parts.size != 5 || parts[1] != "") {
|
||||||
|
throw Exception("Unexpected format")
|
||||||
|
}
|
||||||
|
val encodedHeader = parts[0]
|
||||||
|
val iv = Base64.decode(parts[2], Base64.URL_SAFE)
|
||||||
|
val ciphertext = Base64.decode(parts[3], Base64.URL_SAFE)
|
||||||
|
val tag = Base64.decode(parts[4], Base64.URL_SAFE)
|
||||||
|
val ciphertextWithTag = ciphertext + tag
|
||||||
|
val secretKeySpec = SecretKeySpec(key, "AES")
|
||||||
|
val gcmParameterSpec = GCMParameterSpec(gcmTagLenBits, iv)
|
||||||
|
val cipher: Cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, gcmParameterSpec)
|
||||||
|
cipher.updateAAD(encodedHeader.toByteArray())
|
||||||
|
return String(cipher.doFinal(ciphertextWithTag))
|
||||||
|
}
|
||||||
|
}
|
|
@ -46,6 +46,7 @@ import kotlin.math.abs
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
fun topicUrl(baseUrl: String, topic: String) = "${baseUrl}/${topic}"
|
fun topicUrl(baseUrl: String, topic: String) = "${baseUrl}/${topic}"
|
||||||
|
fun topicUrl(subscription: Subscription) = topicUrl(subscription.baseUrl, subscription.topic)
|
||||||
fun topicUrlUp(baseUrl: String, topic: String) = "${baseUrl}/${topic}?up=1" // UnifiedPush
|
fun topicUrlUp(baseUrl: String, topic: String) = "${baseUrl}/${topic}?up=1" // UnifiedPush
|
||||||
fun topicUrlJson(baseUrl: String, topic: String, since: String) = "${topicUrl(baseUrl, topic)}/json?since=$since"
|
fun topicUrlJson(baseUrl: String, topic: String, since: String) = "${topicUrl(baseUrl, topic)}/json?since=$since"
|
||||||
fun topicUrlWs(baseUrl: String, topic: String, since: String) = "${topicUrl(baseUrl, topic)}/ws?since=$since"
|
fun topicUrlWs(baseUrl: String, topic: String, since: String) = "${topicUrl(baseUrl, topic)}/ws?since=$since"
|
||||||
|
|
|
@ -7,6 +7,7 @@ import io.heckel.ntfy.BuildConfig
|
||||||
import io.heckel.ntfy.db.Repository
|
import io.heckel.ntfy.db.Repository
|
||||||
import io.heckel.ntfy.msg.ApiService
|
import io.heckel.ntfy.msg.ApiService
|
||||||
import io.heckel.ntfy.msg.NotificationDispatcher
|
import io.heckel.ntfy.msg.NotificationDispatcher
|
||||||
|
import io.heckel.ntfy.util.Encryption
|
||||||
import io.heckel.ntfy.util.Log
|
import io.heckel.ntfy.util.Log
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -40,13 +41,15 @@ class PollWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx,
|
||||||
subscriptions.forEach{ subscription ->
|
subscriptions.forEach{ subscription ->
|
||||||
try {
|
try {
|
||||||
val user = repository.getUser(subscription.baseUrl)
|
val user = repository.getUser(subscription.baseUrl)
|
||||||
val notifications = api.poll(
|
val notifications = api
|
||||||
|
.poll(
|
||||||
subscriptionId = subscription.id,
|
subscriptionId = subscription.id,
|
||||||
baseUrl = subscription.baseUrl,
|
baseUrl = subscription.baseUrl,
|
||||||
topic = subscription.topic,
|
topic = subscription.topic,
|
||||||
user = user,
|
user = user,
|
||||||
since = subscription.lastNotificationId
|
since = subscription.lastNotificationId
|
||||||
)
|
)
|
||||||
|
.map { n -> Encryption.maybeDecrypt(subscription, n) }
|
||||||
val newNotifications = repository
|
val newNotifications = repository
|
||||||
.onlyNewNotifications(subscription.id, notifications)
|
.onlyNewNotifications(subscription.id, notifications)
|
||||||
.map { it.copy(notificationId = Random.nextInt()) }
|
.map { it.copy(notificationId = Random.nextInt()) }
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
ntfy EDIT:
|
||||||
|
This is a slightly edited copy of the original Android project layout
|
||||||
|
to reduce the marginBottom of the message to something reasonable (was: 48dp).
|
||||||
|
|
||||||
|
~ Copyright (C) 2015 The Android Open Source Project
|
||||||
|
~
|
||||||
|
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
~ you may not use this file except in compliance with the License.
|
||||||
|
~ You may obtain a copy of the License at
|
||||||
|
~
|
||||||
|
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
~
|
||||||
|
~ Unless required by applicable law or agreed to in writing, software
|
||||||
|
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
~ See the License for the specific language governing permissions and
|
||||||
|
~ limitations under the License
|
||||||
|
-->
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginTop="48dp"
|
||||||
|
android:layout_marginBottom="48dp"
|
||||||
|
android:overScrollMode="ifContentScrolls">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@android:id/message"
|
||||||
|
style="?android:attr/textAppearanceSmall"
|
||||||
|
android:layout_marginLeft="24dp"
|
||||||
|
android:layout_marginRight="24dp"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:layout_marginBottom="2dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"/>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@android:id/edit"
|
||||||
|
android:layout_marginLeft="20dp"
|
||||||
|
android:layout_marginRight="20dp"
|
||||||
|
android:layout_marginStart="20dp"
|
||||||
|
android:layout_marginEnd="20dp"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minHeight = "48dp"
|
||||||
|
android:inputType="textPassword"/>
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
|
@ -347,6 +347,8 @@
|
||||||
<string name="detail_settings_notifications_instant_title">Instant delivery</string>
|
<string name="detail_settings_notifications_instant_title">Instant delivery</string>
|
||||||
<string name="detail_settings_notifications_instant_summary_on">Notifications are delivered instantly. Requires a foreground service and consumes more battery.</string>
|
<string name="detail_settings_notifications_instant_summary_on">Notifications are delivered instantly. Requires a foreground service and consumes more battery.</string>
|
||||||
<string name="detail_settings_notifications_instant_summary_off">Notifications are delivered using Firebase. Delivery may be delayed, but consumes less battery.</string>
|
<string name="detail_settings_notifications_instant_summary_off">Notifications are delivered using Firebase. Delivery may be delayed, but consumes less battery.</string>
|
||||||
|
<string name="detail_settings_notifications_password_title">Password</string>
|
||||||
|
<string name="detail_settings_notifications_password_dialog_summary">Choose a password to use to decrypt incoming end-to-end encrypted notifications. The derived key will be stored locally. If the password is incorrect, messages will not decrypt correctly.</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>
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
<string name="detail_settings_notifications_muted_until_key" translatable="false">SubscriptionMutedUntil</string>
|
<string name="detail_settings_notifications_muted_until_key" translatable="false">SubscriptionMutedUntil</string>
|
||||||
<string name="detail_settings_notifications_min_priority_key" translatable="false">SubscriptionMinPriority</string>
|
<string name="detail_settings_notifications_min_priority_key" translatable="false">SubscriptionMinPriority</string>
|
||||||
<string name="detail_settings_notifications_auto_delete_key" translatable="false">SubscriptionAutoDelete</string>
|
<string name="detail_settings_notifications_auto_delete_key" translatable="false">SubscriptionAutoDelete</string>
|
||||||
|
<string name="detail_settings_notifications_password_key" translatable="false">SubscriptionPassword</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>
|
||||||
|
|
|
@ -28,6 +28,11 @@
|
||||||
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 -->
|
||||||
|
<EditTextPreference
|
||||||
|
app:key="@string/detail_settings_notifications_password_key"
|
||||||
|
app:title="@string/detail_settings_notifications_password_title"
|
||||||
|
app:dialogLayout="@layout/preference_dialog_editpass_edited"
|
||||||
|
app:dialogMessage="@string/detail_settings_notifications_password_dialog_summary"/>
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
app:key="@string/detail_settings_appearance_header_key"
|
app:key="@string/detail_settings_appearance_header_key"
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package io.heckel.ntfy.firebase
|
package io.heckel.ntfy.firebase
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.util.Base64
|
|
||||||
import androidx.work.*
|
import androidx.work.*
|
||||||
import com.google.firebase.messaging.FirebaseMessagingService
|
import com.google.firebase.messaging.FirebaseMessagingService
|
||||||
import com.google.firebase.messaging.RemoteMessage
|
import com.google.firebase.messaging.RemoteMessage
|
||||||
|
@ -11,10 +10,10 @@ import io.heckel.ntfy.db.Attachment
|
||||||
import io.heckel.ntfy.db.Notification
|
import io.heckel.ntfy.db.Notification
|
||||||
import io.heckel.ntfy.util.Log
|
import io.heckel.ntfy.util.Log
|
||||||
import io.heckel.ntfy.msg.ApiService
|
import io.heckel.ntfy.msg.ApiService
|
||||||
import io.heckel.ntfy.msg.MESSAGE_ENCODING_BASE64
|
|
||||||
import io.heckel.ntfy.msg.NotificationDispatcher
|
import io.heckel.ntfy.msg.NotificationDispatcher
|
||||||
import io.heckel.ntfy.msg.NotificationParser
|
import io.heckel.ntfy.msg.NotificationParser
|
||||||
import io.heckel.ntfy.service.SubscriberService
|
import io.heckel.ntfy.service.SubscriberService
|
||||||
|
import io.heckel.ntfy.util.Encryption
|
||||||
import io.heckel.ntfy.util.toPriority
|
import io.heckel.ntfy.util.toPriority
|
||||||
import io.heckel.ntfy.util.topicShortUrl
|
import io.heckel.ntfy.util.topicShortUrl
|
||||||
import io.heckel.ntfy.work.PollWorker
|
import io.heckel.ntfy.work.PollWorker
|
||||||
|
@ -124,7 +123,7 @@ class FirebaseService : FirebaseMessagingService() {
|
||||||
url = attachmentUrl,
|
url = attachmentUrl,
|
||||||
)
|
)
|
||||||
} else null
|
} else null
|
||||||
val notification = Notification(
|
val notificationOriginal = Notification(
|
||||||
id = id,
|
id = id,
|
||||||
subscriptionId = subscription.id,
|
subscriptionId = subscription.id,
|
||||||
timestamp = timestamp,
|
timestamp = timestamp,
|
||||||
|
@ -139,6 +138,7 @@ class FirebaseService : FirebaseMessagingService() {
|
||||||
notificationId = Random.nextInt(),
|
notificationId = Random.nextInt(),
|
||||||
deleted = false
|
deleted = false
|
||||||
)
|
)
|
||||||
|
val notification = Encryption.maybeDecrypt(subscription, notificationOriginal)
|
||||||
if (repository.addNotification(notification)) {
|
if (repository.addNotification(notification)) {
|
||||||
Log.d(TAG, "Dispatching notification: from=${remoteMessage.from}, fcmprio=${remoteMessage.priority}, fcmprio_orig=${remoteMessage.originalPriority}, data=${data}")
|
Log.d(TAG, "Dispatching notification: from=${remoteMessage.from}, fcmprio=${remoteMessage.priority}, fcmprio_orig=${remoteMessage.originalPriority}, data=${data}")
|
||||||
dispatcher.dispatch(subscription, notification)
|
dispatcher.dispatch(subscription, notification)
|
||||||
|
|
Loading…
Reference in a new issue