diff --git a/app/schemas/io.heckel.ntfy.db.Database/19.json b/app/schemas/io.heckel.ntfy.db.Database/19.json new file mode 100644 index 000000000..5e0b8a9f2 --- /dev/null +++ b/app/schemas/io.heckel.ntfy.db.Database/19.json @@ -0,0 +1,447 @@ +{ + "formatVersion": 1, + "database": { + "version": 19, + "identityHash": "7428d57caf586048d40e1af61c5d65e6", + "entities": [ + { + "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, `insistent` INTEGER NOT NULL, `lastNotificationId` TEXT, `icon` TEXT, `upAppId` TEXT, `upConnectorToken` TEXT, `displayName` TEXT, `dedicatedChannels` INTEGER NOT NULL, `layout` INTEGER NOT NULL, `linkHandler` TEXT NOT NULL, `notificationButtons` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "topic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "instant", + "columnName": "instant", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mutedUntil", + "columnName": "mutedUntil", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minPriority", + "columnName": "minPriority", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "autoDelete", + "columnName": "autoDelete", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "insistent", + "columnName": "insistent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT" + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT" + }, + { + "fieldPath": "upAppId", + "columnName": "upAppId", + "affinity": "TEXT" + }, + { + "fieldPath": "upConnectorToken", + "columnName": "upConnectorToken", + "affinity": "TEXT" + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT" + }, + { + "fieldPath": "dedicatedChannels", + "columnName": "dedicatedChannels", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "layout", + "columnName": "layout", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "linkHandler", + "columnName": "linkHandler", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationButtons", + "columnName": "notificationButtons", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Subscription_baseUrl_topic", + "unique": true, + "columnNames": [ + "baseUrl", + "topic" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Subscription_baseUrl_topic` ON `${TABLE_NAME}` (`baseUrl`, `topic`)" + }, + { + "name": "index_Subscription_upConnectorToken", + "unique": true, + "columnNames": [ + "upConnectorToken" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Subscription_upConnectorToken` ON `${TABLE_NAME}` (`upConnectorToken`)" + } + ] + }, + { + "tableName": "Notification", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `subscriptionId` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `sequenceId` TEXT NOT NULL, `title` TEXT NOT NULL, `message` TEXT NOT NULL, `contentType` TEXT NOT NULL, `encoding` TEXT NOT NULL, `notificationId` INTEGER NOT NULL, `priority` INTEGER NOT NULL DEFAULT 3, `tags` TEXT NOT NULL, `click` TEXT NOT NULL, `actions` TEXT, `deleted` INTEGER NOT NULL, `icon_url` TEXT, `icon_contentUri` TEXT, `attachment_name` TEXT, `attachment_type` TEXT, `attachment_size` INTEGER, `attachment_expires` INTEGER, `attachment_url` TEXT, `attachment_contentUri` TEXT, `attachment_progress` INTEGER, PRIMARY KEY(`id`, `subscriptionId`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subscriptionId", + "columnName": "subscriptionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sequenceId", + "columnName": "sequenceId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "contentType", + "columnName": "contentType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "encoding", + "columnName": "encoding", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationId", + "columnName": "notificationId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "3" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "click", + "columnName": "click", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "actions", + "columnName": "actions", + "affinity": "TEXT" + }, + { + "fieldPath": "deleted", + "columnName": "deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "icon.url", + "columnName": "icon_url", + "affinity": "TEXT" + }, + { + "fieldPath": "icon.contentUri", + "columnName": "icon_contentUri", + "affinity": "TEXT" + }, + { + "fieldPath": "attachment.name", + "columnName": "attachment_name", + "affinity": "TEXT" + }, + { + "fieldPath": "attachment.type", + "columnName": "attachment_type", + "affinity": "TEXT" + }, + { + "fieldPath": "attachment.size", + "columnName": "attachment_size", + "affinity": "INTEGER" + }, + { + "fieldPath": "attachment.expires", + "columnName": "attachment_expires", + "affinity": "INTEGER" + }, + { + "fieldPath": "attachment.url", + "columnName": "attachment_url", + "affinity": "TEXT" + }, + { + "fieldPath": "attachment.contentUri", + "columnName": "attachment_contentUri", + "affinity": "TEXT" + }, + { + "fieldPath": "attachment.progress", + "columnName": "attachment_progress", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "subscriptionId" + ] + } + }, + { + "tableName": "User", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`baseUrl` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, PRIMARY KEY(`baseUrl`))", + "fields": [ + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "baseUrl" + ] + } + }, + { + "tableName": "Log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `tag` TEXT NOT NULL, `level` INTEGER NOT NULL, `message` TEXT NOT NULL, `exception` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tag", + "columnName": "tag", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "level", + "columnName": "level", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "exception", + "columnName": "exception", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "CustomHeader", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`baseUrl` TEXT NOT NULL, `name` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`baseUrl`, `name`))", + "fields": [ + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "baseUrl", + "name" + ] + } + }, + { + "tableName": "TrustedCertificate", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`baseUrl` TEXT NOT NULL, `pem` TEXT NOT NULL, PRIMARY KEY(`baseUrl`))", + "fields": [ + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pem", + "columnName": "pem", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "baseUrl" + ] + } + }, + { + "tableName": "ClientCertificate", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`baseUrl` TEXT NOT NULL, `p12Base64` TEXT NOT NULL, `password` TEXT NOT NULL, PRIMARY KEY(`baseUrl`))", + "fields": [ + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "p12Base64", + "columnName": "p12Base64", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "baseUrl" + ] + } + } + ], + "setupQueries": [ + "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, '7428d57caf586048d40e1af61c5d65e6')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt b/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt index 2dafa8397..ed937d45c 100644 --- a/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt +++ b/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt @@ -118,6 +118,9 @@ class Backuper(val context: Context) { upAppId = s.upAppId, upConnectorToken = s.upConnectorToken, displayName = s.displayName, + layout = s.layout ?: Repository.LAYOUT_DEFAULT, + linkHandler = s.linkHandler ?: Repository.LINK_HANDLER_DEFAULT, + notificationButtons = s.notificationButtons ?: Repository.NOTIFICATION_BUTTONS_DEFAULT ) repository.addSubscription(subscription) @@ -298,7 +301,10 @@ class Backuper(val context: Context) { icon = s.icon, upAppId = s.upAppId, upConnectorToken = s.upConnectorToken, - displayName = s.displayName + displayName = s.displayName, + layout = s.layout, + linkHandler = s.linkHandler, + notificationButtons = s.notificationButtons ) } } @@ -438,7 +444,10 @@ data class Subscription( val icon: String?, val upAppId: String?, val upConnectorToken: String?, - val displayName: String? + val displayName: String?, + val layout: Int?, + val linkHandler: String?, + val notificationButtons: String?, ) data class Notification( diff --git a/app/src/main/java/io/heckel/ntfy/db/Database.kt b/app/src/main/java/io/heckel/ntfy/db/Database.kt index 16ab22318..fbab7d989 100644 --- a/app/src/main/java/io/heckel/ntfy/db/Database.kt +++ b/app/src/main/java/io/heckel/ntfy/db/Database.kt @@ -44,6 +44,9 @@ data class Subscription( @ColumnInfo(name = "upConnectorToken") val upConnectorToken: String?, // UnifiedPush connector token @ColumnInfo(name = "displayName") val displayName: String?, @ColumnInfo(name = "dedicatedChannels") val dedicatedChannels: Boolean, + @ColumnInfo(name = "layout") val layout: Int, + @ColumnInfo(name = "linkHandler") val linkHandler: String, + @ColumnInfo(name = "notificationButtons") val notificationButtons: String, @Ignore val totalCount: Int = 0, // Total notifications @Ignore val newCount: Int = 0, // New notifications @Ignore val lastActive: Long = 0, // Unix timestamp @@ -63,7 +66,10 @@ data class Subscription( upAppId: String, upConnectorToken: String, displayName: String?, - dedicatedChannels: Boolean + dedicatedChannels: Boolean, + layout: Int, + linkHandler: String, + notificationButtons: String, ) : this( id, @@ -80,6 +86,9 @@ data class Subscription( upConnectorToken, displayName, dedicatedChannels, + layout, + linkHandler, + notificationButtons, totalCount = 0, newCount = 0, lastActive = 0, @@ -136,6 +145,9 @@ data class SubscriptionWithMetadata( val upConnectorToken: String?, val displayName: String?, val dedicatedChannels: Boolean, + val layout: Int, + val linkHandler: String, + val notificationButtons: String, val totalCount: Int, val newCount: Int, val lastActive: Long @@ -298,7 +310,7 @@ data class LogEntry( } @androidx.room.Database( - version = 18, + version = 19, entities = [ Subscription::class, Notification::class, @@ -344,6 +356,7 @@ abstract class Database : RoomDatabase() { .addMigrations(MIGRATION_15_16) .addMigrations(MIGRATION_16_17) .addMigrations(MIGRATION_17_18) + .addMigrations(MIGRATION_18_19) .fallbackToDestructiveMigration(true) .build() this.instance = instance @@ -484,6 +497,15 @@ abstract class Database : RoomDatabase() { db.execSQL("UPDATE Notification SET sequenceId = id WHERE sequenceId = ''") } } + + private val MIGRATION_18_19 = object : Migration(18, 19) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE Subscription ADD COLUMN layout INT NOT NULL DEFAULT (0)") + db.execSQL("ALTER TABLE Subscription ADD COLUMN linkHandler TEXT NOT NULL DEFAULT 'web'") + db.execSQL("ALTER TABLE Subscription ADD COLUMN notificationButtons TEXT NOT NULL DEFAULT 'open|browse'") + } + } + } } @@ -491,7 +513,7 @@ abstract class Database : RoomDatabase() { interface SubscriptionDao { @Query(""" SELECT - 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, + 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, s.layout, s.linkHandler, s.notificationButtons, COUNT(n.id) totalCount, COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount, IFNULL(MAX(n.timestamp),0) AS lastActive @@ -504,7 +526,7 @@ interface SubscriptionDao { @Query(""" SELECT - 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, + 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, s.layout, s.linkHandler, s.notificationButtons, COUNT(n.id) totalCount, COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount, IFNULL(MAX(n.timestamp),0) AS lastActive @@ -517,7 +539,7 @@ interface SubscriptionDao { @Query(""" SELECT - 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, + 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, s.layout, s.linkHandler, s.notificationButtons, COUNT(n.id) totalCount, COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount, IFNULL(MAX(n.timestamp),0) AS lastActive @@ -530,7 +552,7 @@ interface SubscriptionDao { @Query(""" SELECT - 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, + 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, s.layout, s.linkHandler, s.notificationButtons, COUNT(n.id) totalCount, COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount, IFNULL(MAX(n.timestamp),0) AS lastActive @@ -543,7 +565,7 @@ interface SubscriptionDao { @Query(""" SELECT - 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, + 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, s.layout, s.linkHandler, s.notificationButtons, COUNT(n.id) totalCount, COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount, IFNULL(MAX(n.timestamp),0) AS lastActive diff --git a/app/src/main/java/io/heckel/ntfy/db/Repository.kt b/app/src/main/java/io/heckel/ntfy/db/Repository.kt index 43c8c8581..62f81a33e 100644 --- a/app/src/main/java/io/heckel/ntfy/db/Repository.kt +++ b/app/src/main/java/io/heckel/ntfy/db/Repository.kt @@ -539,7 +539,10 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database) totalCount = s.totalCount, newCount = s.newCount, lastActive = s.lastActive, - connectionDetails = connectionDetails[s.baseUrl] ?: ConnectionDetails() + connectionDetails = connectionDetails[s.baseUrl] ?: ConnectionDetails(), + layout = s.layout, + linkHandler = s.linkHandler, + notificationButtons = s.notificationButtons ) } } @@ -566,7 +569,10 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database) totalCount = s.totalCount, newCount = s.newCount, lastActive = s.lastActive, - connectionDetails = connectionDetails[s.baseUrl] ?: ConnectionDetails() + connectionDetails = connectionDetails[s.baseUrl] ?: ConnectionDetails(), + layout = s.layout, + linkHandler = s.linkHandler, + notificationButtons = s.notificationButtons ) } @@ -664,6 +670,12 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database) const val WEBSOCKET_RECONNECT_REMIND_TIME_ALWAYS = 1L const val WEBSOCKET_RECONNECT_REMIND_TIME_NEVER = Long.MAX_VALUE + const val LAYOUT_DEFAULT = 0 + + const val LINK_HANDLER_DEFAULT = "web" + + const val NOTIFICATION_BUTTONS_DEFAULT = "open|browse" + private const val TAG = "NtfyRepository" private var instance: Repository? = null diff --git a/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt b/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt index ad489dde5..ebe81dd2a 100644 --- a/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt +++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt @@ -100,8 +100,14 @@ class NotificationService(val context: Context) { maybeSetDeleteIntent(builder, insistent) maybeSetSound(builder, insistent, update) maybeSetProgress(builder, notification) - maybeAddOpenAction(builder, notification) - maybeAddBrowseAction(builder, notification) + + if(subscription.notificationButtons.contains("open")){ + maybeAddOpenAction(builder, notification) + } + if(subscription.notificationButtons.contains("browse")){ + maybeAddBrowseAction(builder, notification) + } + maybeAddDownloadAction(builder, notification) maybeAddCancelAction(builder, notification) maybeAddUserActions(builder, notification) diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt index b2554a875..78021a0f3 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt @@ -83,6 +83,8 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet private var subscriptionTopic: String = "" // Set in onCreate() private var subscriptionDisplayName: String = "" // Set in onCreate() & updated by options menu! private var subscriptionMutedUntil: Long = 0L // Set in onCreate() & updated by options menu! + private var subscriptionLayout: Int = Repository.LAYOUT_DEFAULT + private var subscriptionLinkHandler: String = Repository.LINK_HANDLER_DEFAULT // UI elements private lateinit var adapter: DetailAdapter @@ -234,7 +236,10 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet displayName = displayName, totalCount = 0, newCount = 0, - lastActive = Date().time/1000 + lastActive = Date().time/1000, + layout = Repository.LAYOUT_DEFAULT, + linkHandler = Repository.LINK_HANDLER_DEFAULT, + notificationButtons = Repository.NOTIFICATION_BUTTONS_DEFAULT ) repository.addSubscription(subscription) @@ -264,6 +269,8 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_DISPLAY_NAME, displayName(appBaseUrl, subscription)) intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_INSTANT, subscription.instant) intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_MUTED_UNTIL, subscription.mutedUntil) + intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_LAYOUT, subscription.layout) + intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_LINK_HANDLER, subscription.linkHandler) runOnUiThread { loadView() @@ -285,6 +292,8 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet subscriptionTopic = intent.getStringExtra(MainActivity.EXTRA_SUBSCRIPTION_TOPIC) ?: return subscriptionDisplayName = intent.getStringExtra(MainActivity.EXTRA_SUBSCRIPTION_DISPLAY_NAME) ?: return subscriptionMutedUntil = intent.getLongExtra(MainActivity.EXTRA_SUBSCRIPTION_MUTED_UNTIL, 0L) + subscriptionLayout = intent.getIntExtra(MainActivity.EXTRA_SUBSCRIPTION_LAYOUT, Repository.LAYOUT_DEFAULT) + subscriptionLinkHandler = intent.getStringExtra(MainActivity.EXTRA_SUBSCRIPTION_LINK_HANDLER) ?: return // Set title val subscriptionBaseUrl = intent.getStringExtra(MainActivity.EXTRA_SUBSCRIPTION_BASE_URL) ?: return @@ -540,6 +549,15 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet showHideMenuItems() updateTitle(subscriptionDisplayName) + + if(subscriptionLayout != subscription.layout || subscriptionLinkHandler != subscription.linkHandler){ + intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_LAYOUT, subscription.layout) + intent.putExtra(MainActivity.EXTRA_SUBSCRIPTION_LINK_HANDLER, subscription.linkHandler) + + runOnUiThread { + loadView() + } + } } } diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt index dd2187f87..d2cec0394 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt @@ -50,8 +50,15 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope: /* Creates and inflates view and return TopicViewHolder. */ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DetailViewHolder { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.fragment_detail_item, parent, false) + val layout = activity.intent.getIntExtra(MainActivity.EXTRA_SUBSCRIPTION_LAYOUT, 0) + + val layoutMap = mapOf( + 1 to R.layout.fragment_detail_item_layout_1, + ) + + val layoutId = layoutMap[layout] ?: R.layout.fragment_detail_item + + val view = LayoutInflater.from(parent.context).inflate(layoutId, parent, false) return DetailViewHolder(activity, lifecycleScope, repository, markwon, view, selected, onClick, onLongClick) } @@ -120,7 +127,16 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope: messageView.autoLinkMask = 0 markwon.setMarkdown(messageView, message.toString()) } else { - messageView.autoLinkMask = Linkify.WEB_URLS or Linkify.EMAIL_ADDRESSES or Linkify.PHONE_NUMBERS + val linkHandler = activity.intent.getStringExtra(MainActivity.EXTRA_SUBSCRIPTION_LINK_HANDLER) + + val autoLinkMask = listOf( + Linkify.WEB_URLS to "web", + Linkify.EMAIL_ADDRESSES to "email", + Linkify.PHONE_NUMBERS to "phone" + ).filter { linkHandler?.contains(it.second) == true } + .fold(0) { acc, pair -> acc or pair.first } + + messageView.autoLinkMask = autoLinkMask messageView.text = message } messageView.movementMethod = BetterLinkMovementMethod.getInstance() diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailSettingsActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailSettingsActivity.kt index f0293c79c..c970e5b75 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailSettingsActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailSettingsActivity.kt @@ -13,10 +13,17 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.core.content.FileProvider import androidx.core.graphics.drawable.toDrawable +import androidx.core.net.toUri import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.lifecycleScope -import androidx.preference.* +import androidx.preference.EditTextPreference +import androidx.preference.ListPreference +import androidx.preference.MultiSelectListPreference +import androidx.preference.Preference import androidx.preference.Preference.OnPreferenceClickListener +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceDataStore +import androidx.preference.SwitchPreferenceCompat import com.google.android.material.appbar.AppBarLayout import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.R @@ -25,12 +32,25 @@ import io.heckel.ntfy.db.Subscription import io.heckel.ntfy.msg.DownloadAttachmentWorker import io.heckel.ntfy.msg.NotificationService import io.heckel.ntfy.service.SubscriberServiceManager -import io.heckel.ntfy.util.* -import kotlinx.coroutines.* +import io.heckel.ntfy.util.Log +import io.heckel.ntfy.util.PRIORITY_MAX +import io.heckel.ntfy.util.PRIORITY_MIN +import io.heckel.ntfy.util.copyToClipboard +import io.heckel.ntfy.util.displayName +import io.heckel.ntfy.util.fileStat +import io.heckel.ntfy.util.formatDateShort +import io.heckel.ntfy.util.isDarkThemeOn +import io.heckel.ntfy.util.readBitmapFromUri +import io.heckel.ntfy.util.supportedImage +import io.heckel.ntfy.util.toPriorityString +import io.heckel.ntfy.util.topicShortUrl +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import java.io.File import java.io.IOException -import java.util.* -import androidx.core.net.toUri +import java.util.Calendar /** * Subscription settings @@ -144,6 +164,9 @@ class DetailSettingsActivity : AppCompatActivity() { loadIconRemovePref() loadDedicatedChannelsPrefs() loadOpenChannelsPrefs() + loadLayoutPref() + loadLinkHandlerPref() + loadNotificationButtonsPref() } else { val notificationsHeaderId = context?.getString(R.string.detail_settings_notifications_header_key) ?: return val notificationsHeader: PreferenceCategory? = findPreference(notificationsHeaderId) @@ -437,6 +460,91 @@ class DetailSettingsActivity : AppCompatActivity() { } } + private fun loadLayoutPref() { + val prefId = context?.getString(R.string.detail_settings_appearance_layout_key) ?: return + val pref: ListPreference? = findPreference(prefId) + pref?.isVisible = true // Hack: Show all settings at once, because subscription is loaded asynchronously + pref?.value = subscription.layout.toString() + + pref?.preferenceDataStore = object : PreferenceDataStore() { + override fun putString(key: String?, value: String?) { + val layoutValue = value?.toIntOrNull() ?:return + save(subscription.copy(layout = layoutValue)) + } + override fun getString(key: String?, defValue: String?): String { + return subscription.layout.toString() + } + } + pref?.summaryProvider = Preference.SummaryProvider { + val layoutEntries = resources.getStringArray(R.array.detail_settings_appearance_layout_entries) + layoutEntries[subscription.layout] + } + } + + private fun loadLinkHandlerPref() { + val prefId = context?.getString(R.string.detail_settings_appearance_link_handler_key) ?: return + val pref: MultiSelectListPreference? = findPreference(prefId) + pref?.isVisible = true // Hack: Show all settings at once, because subscription is loaded asynchronously + pref?.values = subscription.linkHandler.split("|").toSet() + + pref?.preferenceDataStore = object : PreferenceDataStore() { + override fun putStringSet(key: String?, values: Set?) { + val valueString = values?.joinToString("|") ?: return + val cleanedValueString = if (valueString.startsWith("|")) valueString.substring(1) else valueString + + save(subscription.copy(linkHandler = cleanedValueString)) + } + override fun getStringSet(key: String?, defValues: Set?): Set { + val savedValue = subscription.linkHandler + val cleanedValue = if (savedValue.startsWith("|")) savedValue.substring(1) else savedValue + + return cleanedValue.split("|").toSet() + } + } + pref?.summaryProvider = Preference.SummaryProvider { preference -> + // Holen der Werte aus den Arrays + val entries = preference.context.resources.getStringArray(R.array.detail_settings_appearance_link_handler_entries) + val values = preference.context.resources.getStringArray(R.array.detail_settings_appearance_link_handler_values) + + // linkHandler-Werte aufsplitten und mit den entsprechenden Einträgen verbinden + subscription.linkHandler.split("|").mapNotNull { value -> + values.indexOf(value).takeIf { it != -1 }?.let { entries[it] } + }.joinToString(", ") ?: "" + } + } + + private fun loadNotificationButtonsPref() { + val prefId = context?.getString(R.string.detail_settings_appearance_notification_buttons_key) ?: return + val pref: MultiSelectListPreference? = findPreference(prefId) + pref?.isVisible = true // Hack: Show all settings at once, because subscription is loaded asynchronously + pref?.values = subscription.notificationButtons.split("|").toSet() + + pref?.preferenceDataStore = object : PreferenceDataStore() { + override fun putStringSet(key: String?, values: Set?) { + val valueString = values?.joinToString("|") ?: return + val cleanedValueString = if (valueString.startsWith("|")) valueString.substring(1) else valueString + + save(subscription.copy(notificationButtons = cleanedValueString)) + } + override fun getStringSet(key: String?, defValues: Set?): Set { + val savedValue = subscription.notificationButtons + val cleanedValue = if (savedValue.startsWith("|")) savedValue.substring(1) else savedValue + + return cleanedValue.split("|").toSet() + } + } + pref?.summaryProvider = Preference.SummaryProvider { preference -> + // Holen der Werte aus den Arrays + val entries = preference.context.resources.getStringArray(R.array.detail_settings_appearance_notification_buttons_entries) + val values = preference.context.resources.getStringArray(R.array.detail_settings_appearance_notification_buttons_values) + + // linkHandler-Werte aufsplitten und mit den entsprechenden Einträgen verbinden + subscription.notificationButtons.split("|").mapNotNull { value -> + values.indexOf(value).takeIf { it != -1 }?.let { entries[it] } + }.joinToString(", ") ?: "" + } + } + private fun createIconPickLauncher(): ActivityResultLauncher { return registerForActivityResult(ActivityResultContracts.GetContent()) { inputUri -> if (inputUri == null) { diff --git a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt index 251a4ba0e..390068004 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt @@ -678,7 +678,10 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific displayName = null, totalCount = 0, newCount = 0, - lastActive = Date().time/1000 + lastActive = Date().time/1000, + layout = Repository.LAYOUT_DEFAULT, + linkHandler = Repository.LINK_HANDLER_DEFAULT, + notificationButtons = Repository.NOTIFICATION_BUTTONS_DEFAULT ) viewModel.add(subscription) @@ -767,6 +770,8 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific intent.putExtra(EXTRA_SUBSCRIPTION_DISPLAY_NAME, displayName(appBaseUrl, subscription)) intent.putExtra(EXTRA_SUBSCRIPTION_INSTANT, subscription.instant) intent.putExtra(EXTRA_SUBSCRIPTION_MUTED_UNTIL, subscription.mutedUntil) + intent.putExtra(EXTRA_SUBSCRIPTION_LAYOUT, subscription.layout) + intent.putExtra(EXTRA_SUBSCRIPTION_LINK_HANDLER, subscription.linkHandler) startActivity(intent) } @@ -867,6 +872,8 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific const val EXTRA_SUBSCRIPTION_DISPLAY_NAME = "subscriptionDisplayName" const val EXTRA_SUBSCRIPTION_INSTANT = "subscriptionInstant" const val EXTRA_SUBSCRIPTION_MUTED_UNTIL = "subscriptionMutedUntil" + const val EXTRA_SUBSCRIPTION_LAYOUT = "subscriptionLayout" + const val EXTRA_SUBSCRIPTION_LINK_HANDLER = "subscriptionLinkHandler" const val ANIMATION_DURATION = 80L const val ONE_DAY_MILLIS = 86400000L diff --git a/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt index 8ebf2f874..6a553ec21 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt @@ -10,9 +10,7 @@ import android.os.Build import android.os.Bundle import android.provider.Settings import android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM -import android.text.TextUtils import android.view.View -import android.widget.Button import android.widget.Toast import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts @@ -42,7 +40,6 @@ import kotlinx.coroutines.launch import okhttp3.RequestBody.Companion.toRequestBody import java.text.SimpleDateFormat import java.util.* -import java.util.concurrent.TimeUnit /** * Main settings diff --git a/app/src/main/java/io/heckel/ntfy/up/BroadcastReceiver.kt b/app/src/main/java/io/heckel/ntfy/up/BroadcastReceiver.kt index b5a44a463..7d7184732 100644 --- a/app/src/main/java/io/heckel/ntfy/up/BroadcastReceiver.kt +++ b/app/src/main/java/io/heckel/ntfy/up/BroadcastReceiver.kt @@ -92,7 +92,10 @@ class BroadcastReceiver : android.content.BroadcastReceiver() { displayName = null, totalCount = 0, newCount = 0, - lastActive = Date().time/1000 + lastActive = Date().time/1000, + layout = Repository.LAYOUT_DEFAULT, + linkHandler = Repository.LINK_HANDLER_DEFAULT, + notificationButtons = Repository.NOTIFICATION_BUTTONS_DEFAULT ) Log.d(TAG, "Adding subscription with for app $appId (connectorToken $connectorToken): $subscription") try { diff --git a/app/src/main/res/layout/fragment_detail_item_layout_1.xml b/app/src/main/res/layout/fragment_detail_item_layout_1.xml new file mode 100644 index 000000000..e3602b806 --- /dev/null +++ b/app/src/main/res/layout/fragment_detail_item_layout_1.xml @@ -0,0 +1,218 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 087d54174..1baaff12d 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -304,11 +304,13 @@ Sechs Themen abonniert Benachrichtigungen werden über Firebase zugestellt. Die Zustellung kann verzögert werden, aber der Akkuverbrauch ist niedriger. Darstellung + Darstellung Benachrichtigungen Abo-Icon Ein Icon zur Darstellung in Benachrichtigungen auswählen Abo-Icon (entfernen durch Antippen) Kann Icon nicht speichern: %1$s Globale Einstellung verwenden + Layout-Vorlage verwendet globale Einstellung Sechs Sofortnachrichten-Themen abonniert Sofortnachrichten @@ -473,4 +475,22 @@ In Benachrichtigungen suchen Eine gültige Service-URL eingeben, z. B. https://ntfy.example.com Benachrichtigungen suchen + Wähle zwischen verschiedenen Layouts + Link-Interpreter + Wähle aus, wie die im Text enthaltenen Links interpretiert werden sollen. + + Links + Telefonnummern + E-Mail Adressen + + + Standard + Layout 1 + + Schaltflächen + Wähle aus, welche Schaltflächen in der Benachrichtigung angezeigt werden sollen. + + Öffnen + Durchsuchen + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2ac24ce1c..81f0052f9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -477,6 +477,7 @@ Keep alerting Alert only once Appearance + Appearance Notification Subscription icon Set an icon to be displayed in notifications Subscription icon (tap to remove) @@ -485,6 +486,26 @@ Display name Set a custom display name for this subscription. Leave blank for default (%1$s). %1$s (default) + Layout template + Choose between different layouts + Link interpreter + Choose how the links contained in the text should be interpreted + + link + phone number + e-mail address + + + Default + Layout 1 + + Buttons + Choose which buttons should be shown in the notification + + Open + Browse + + Use global setting using global setting About diff --git a/app/src/main/res/values/values.xml b/app/src/main/res/values/values.xml index 0b45baf59..6be557651 100644 --- a/app/src/main/res/values/values.xml +++ b/app/src/main/res/values/values.xml @@ -53,6 +53,30 @@ SubscriptionDisplayName SubscriptionAbout SubscriptionTopicURL + SubscriptionLayout + + 0 + 1 + + SubscriptionLinkHandler + + web + phone + mail + + + web + + SubscriptionAppearanceNotification + SubscriptionNotificationButtons + + open + browse + + + open + browse + diff --git a/app/src/main/res/xml/detail_preferences.xml b/app/src/main/res/xml/detail_preferences.xml index 4cb105b12..ddc148d38 100644 --- a/app/src/main/res/xml/detail_preferences.xml +++ b/app/src/main/res/xml/detail_preferences.xml @@ -64,7 +64,41 @@ app:dialogLayout="@layout/preference_dialog_edittext_edited" app:dialogMessage="@string/detail_settings_appearance_display_name_message" app:isPreferenceVisible="false"/> + + + + + + + diff --git a/build.gradle b/build.gradle index 5920c2950..4e84f735e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ buildscript { ext.kotlin_version = '2.2.10' dependencies { - classpath 'com.android.tools.build:gradle:9.0.0' + classpath 'com.android.tools.build:gradle:9.0.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.google.gms:google-services:4.4.4' // This is removed in the "fdroid" flavor