diff --git a/app/build.gradle b/app/build.gradle
index 06a6598..026d482 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -58,6 +58,7 @@ dependencies {
// WorkManager
implementation "androidx.work:work-runtime-ktx:2.6.0"
+ implementation 'androidx.preference:preference:1.1.1'
// Room (SQLite)
def roomVersion = "2.3.0"
diff --git a/app/schemas/io.heckel.ntfy.data.Database/3.json b/app/schemas/io.heckel.ntfy.data.Database/3.json
new file mode 100644
index 0000000..42b369c
--- /dev/null
+++ b/app/schemas/io.heckel.ntfy.data.Database/3.json
@@ -0,0 +1,118 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 3,
+ "identityHash": "7b0ef556331f6d2dd3515425837c3d3a",
+ "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, 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
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_Subscription_baseUrl_topic",
+ "unique": true,
+ "columnNames": [
+ "baseUrl",
+ "topic"
+ ],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Subscription_baseUrl_topic` ON `${TABLE_NAME}` (`baseUrl`, `topic`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Notification",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `subscriptionId` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `message` TEXT NOT NULL, `notificationId` INTEGER NOT NULL, `deleted` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "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": "message",
+ "columnName": "message",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notificationId",
+ "columnName": "notificationId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "deleted",
+ "columnName": "deleted",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [],
+ "foreignKeys": []
+ }
+ ],
+ "views": [],
+ "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, '7b0ef556331f6d2dd3515425837c3d3a')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 369e85d..bc21bac 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -7,15 +7,11 @@
- WAKE_LOCK & RECEIVE_BOOT_COMPLETED are required to restart the foreground service
if it is stopped; see https://robertohuertas.com/2019/06/29/android_foreground_services/
-->
-
-
-
+
+
+
-
-
+
+
-
-
-
+
-
-
-
-
-
-
-
+ android:value=".ui.MainActivity"/>
+
+
+
-
-
-
+
diff --git a/app/src/main/java/io/heckel/ntfy/data/Database.kt b/app/src/main/java/io/heckel/ntfy/data/Database.kt
index 0704aad..c7af4b5 100644
--- a/app/src/main/java/io/heckel/ntfy/data/Database.kt
+++ b/app/src/main/java/io/heckel/ntfy/data/Database.kt
@@ -5,6 +5,7 @@ import androidx.room.*
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import kotlinx.coroutines.flow.Flow
+import java.util.*
@Entity(indices = [Index(value = ["baseUrl", "topic"], unique = true)])
data class Subscription(
@@ -12,12 +13,16 @@ data class Subscription(
@ColumnInfo(name = "baseUrl") val baseUrl: String,
@ColumnInfo(name = "topic") val topic: String,
@ColumnInfo(name = "instant") val instant: Boolean,
+ @ColumnInfo(name = "mutedUntil") val mutedUntil: Long,
+ //val notificationSchedule: String,
+ //val notificationSound: String,
@Ignore val totalCount: Int = 0, // Total notifications
@Ignore val newCount: Int = 0, // New notifications
@Ignore val lastActive: Long = 0, // Unix timestamp
@Ignore val state: ConnectionState = ConnectionState.NOT_APPLICABLE
) {
- constructor(id: Long, baseUrl: String, topic: String, instant: Boolean) : this(id, baseUrl, topic, instant, 0, 0, 0, ConnectionState.NOT_APPLICABLE)
+ constructor(id: Long, baseUrl: String, topic: String, instant: Boolean, mutedUntil: Long) :
+ this(id, baseUrl, topic, instant, mutedUntil, 0, 0, 0, ConnectionState.NOT_APPLICABLE)
}
enum class ConnectionState {
@@ -29,6 +34,7 @@ data class SubscriptionWithMetadata(
val baseUrl: String,
val topic: String,
val instant: Boolean,
+ val mutedUntil: Long,
val totalCount: Int,
val newCount: Int,
val lastActive: Long
@@ -44,7 +50,7 @@ data class Notification(
@ColumnInfo(name = "deleted") val deleted: Boolean,
)
-@androidx.room.Database(entities = [Subscription::class, Notification::class], version = 2)
+@androidx.room.Database(entities = [Subscription::class, Notification::class], version = 3)
abstract class Database : RoomDatabase() {
abstract fun subscriptionDao(): SubscriptionDao
abstract fun notificationDao(): NotificationDao
@@ -79,6 +85,12 @@ abstract class Database : RoomDatabase() {
db.execSQL("ALTER TABLE Notification ADD COLUMN deleted INTEGER NOT NULL DEFAULT('0')")
}
}
+
+ private val MIGRATION_2_3 = object : Migration(2, 3) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ db.execSQL("ALTER TABLE Subscription ADD COLUMN mutedUntil INTEGER NOT NULL DEFAULT('0')")
+ }
+ }
}
}
@@ -86,7 +98,7 @@ abstract class Database : RoomDatabase() {
interface SubscriptionDao {
@Query("""
SELECT
- s.id, s.baseUrl, s.topic, s.instant,
+ s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil,
COUNT(n.id) totalCount,
COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount,
IFNULL(MAX(n.timestamp),0) AS lastActive
@@ -99,7 +111,7 @@ interface SubscriptionDao {
@Query("""
SELECT
- s.id, s.baseUrl, s.topic, s.instant,
+ s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil,
COUNT(n.id) totalCount,
COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount,
IFNULL(MAX(n.timestamp),0) AS lastActive
@@ -112,7 +124,7 @@ interface SubscriptionDao {
@Query("""
SELECT
- s.id, s.baseUrl, s.topic, s.instant,
+ s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil,
COUNT(n.id) totalCount,
COUNT(CASE n.notificationId WHEN 0 THEN NULL ELSE n.id END) newCount,
IFNULL(MAX(n.timestamp),0) AS lastActive
@@ -125,7 +137,7 @@ interface SubscriptionDao {
@Query("""
SELECT
- s.id, s.baseUrl, s.topic, s.instant,
+ s.id, s.baseUrl, s.topic, s.instant, s.mutedUntil,
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/data/Repository.kt b/app/src/main/java/io/heckel/ntfy/data/Repository.kt
index 348b40a..72915f3 100644
--- a/app/src/main/java/io/heckel/ntfy/data/Repository.kt
+++ b/app/src/main/java/io/heckel/ntfy/data/Repository.kt
@@ -113,6 +113,7 @@ class Repository(private val subscriptionDao: SubscriptionDao, private val notif
baseUrl = s.baseUrl,
topic = s.topic,
instant = s.instant,
+ mutedUntil = s.mutedUntil,
totalCount = s.totalCount,
newCount = s.newCount,
lastActive = s.lastActive,
@@ -130,6 +131,7 @@ class Repository(private val subscriptionDao: SubscriptionDao, private val notif
baseUrl = s.baseUrl,
topic = s.topic,
instant = s.instant,
+ mutedUntil = s.mutedUntil,
totalCount = s.totalCount,
newCount = s.newCount,
lastActive = s.lastActive,
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 df00a25..a62f1f0 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt
@@ -33,7 +33,7 @@ import kotlinx.coroutines.launch
import java.util.*
import java.util.concurrent.atomic.AtomicLong
-class DetailActivity : AppCompatActivity(), ActionMode.Callback {
+class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFragment.NotificationSettingsListener {
private val viewModel by viewModels {
DetailViewModelFactory((application as Application).repository)
}
@@ -187,6 +187,10 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback {
onTestClick()
true
}
+ R.id.detail_menu_notification -> {
+ onNotificationSettingsClick()
+ true
+ }
R.id.detail_menu_enable_instant -> {
onInstantEnableClick(enable = true)
true
@@ -228,6 +232,38 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback {
}
}
+ private fun onNotificationSettingsClick() {
+ Log.d(TAG, "Showing notification settings dialog for ${topicShortUrl(subscriptionBaseUrl, subscriptionTopic)}")
+ val intent = Intent(this, SubscriptionSettingsActivity::class.java)
+ startActivityForResult(intent, /*XXXXXX*/MainActivity.REQUEST_CODE_DELETE_SUBSCRIPTION)
+/*
+ val notificationFragment = NotificationFragment()
+ notificationFragment.show(supportFragmentManager, NotificationFragment.TAG)*/
+ }
+
+ override fun onNotificationSettingsChanged(mutedUntil: Long) {
+ lifecycleScope.launch(Dispatchers.IO) {
+ val subscription = repository.getSubscription(subscriptionId)
+ val newSubscription = subscription?.copy(mutedUntil = mutedUntil)
+ newSubscription?.let { repository.updateSubscription(newSubscription) }
+ runOnUiThread {
+ when (mutedUntil) {
+ 0L -> Toast.makeText(this@DetailActivity, getString(R.string.notification_dialog_enabled_toast_message), Toast.LENGTH_SHORT).show()
+ 1L -> Toast.makeText(this@DetailActivity, getString(R.string.notification_dialog_muted_forever_toast_message), Toast.LENGTH_SHORT).show()
+ else -> {
+ val mutedUntilDate = Date(mutedUntil).toString()
+ Toast.makeText(
+ this@DetailActivity,
+ getString(R.string.notification_dialog_muted_until_toast_message, mutedUntilDate),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+ }
+
+ }
+
private fun onCopyUrlClick() {
val url = topicUrl(subscriptionBaseUrl, subscriptionTopic)
Log.d(TAG, "Copying topic URL $url to clipboard ")
@@ -478,6 +514,5 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback {
companion object {
const val TAG = "NtfyDetailActivity"
- const val CANCEL_NOTIFICATION_DELAY_MILLIS = 20_000L
}
}
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 3c294ad..2c89315 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
@@ -165,6 +165,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
baseUrl = baseUrl,
topic = topic,
instant = instant,
+ mutedUntil = 0,
totalCount = 0,
newCount = 0,
lastActive = Date().time/1000
diff --git a/app/src/main/java/io/heckel/ntfy/ui/NotificationFragment.kt b/app/src/main/java/io/heckel/ntfy/ui/NotificationFragment.kt
new file mode 100644
index 0000000..f59e93e
--- /dev/null
+++ b/app/src/main/java/io/heckel/ntfy/ui/NotificationFragment.kt
@@ -0,0 +1,73 @@
+package io.heckel.ntfy.ui
+
+import android.app.AlertDialog
+import android.app.Dialog
+import android.content.Context
+import android.os.Bundle
+import android.text.Editable
+import android.text.TextWatcher
+import android.util.Log
+import android.view.View
+import android.widget.Button
+import android.widget.CheckBox
+import androidx.fragment.app.DialogFragment
+import androidx.lifecycle.lifecycleScope
+import com.google.android.material.textfield.TextInputEditText
+import io.heckel.ntfy.R
+import io.heckel.ntfy.data.Database
+import io.heckel.ntfy.data.Repository
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+class NotificationFragment : DialogFragment() {
+ private lateinit var repository: Repository
+ private lateinit var settingsListener: NotificationSettingsListener
+
+ interface NotificationSettingsListener {
+ fun onNotificationSettingsChanged(mutedUntil: Long)
+ }
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ settingsListener = activity as NotificationSettingsListener
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ if (activity == null) {
+ throw IllegalStateException("Activity cannot be null")
+ }
+
+ // Dependencies
+ val database = Database.getInstance(activity!!.applicationContext)
+ repository = Repository.getInstance(database.subscriptionDao(), database.notificationDao())
+
+ // Build root view
+ val view = requireActivity().layoutInflater.inflate(R.layout.notification_dialog_fragment, null)
+ // topicNameText = view.findViewById(R.id.add_dialog_topic_text) as TextInputEditText
+
+ // Build dialog
+ val alert = AlertDialog.Builder(activity)
+ .setView(view)
+ .setPositiveButton(R.string.notification_dialog_save) { _, _ ->
+ ///
+ settingsListener.onNotificationSettingsChanged(0L)
+ }
+ .setNegativeButton(R.string.notification_dialog_cancel) { _, _ ->
+ dialog?.cancel()
+ }
+ .create()
+
+ // Add logic to disable "Subscribe" button on invalid input
+ alert.setOnShowListener {
+ val dialog = it as AlertDialog
+ ///
+ }
+
+ return alert
+ }
+
+
+ companion object {
+ const val TAG = "NtfyNotificationFragment"
+ }
+}
diff --git a/app/src/main/java/io/heckel/ntfy/ui/SettingsFragment.kt b/app/src/main/java/io/heckel/ntfy/ui/SettingsFragment.kt
new file mode 100644
index 0000000..01204fe
--- /dev/null
+++ b/app/src/main/java/io/heckel/ntfy/ui/SettingsFragment.kt
@@ -0,0 +1,5 @@
+package io.heckel.ntfy.ui
+
+import android.os.Bundle
+import androidx.preference.PreferenceFragmentCompat
+import io.heckel.ntfy.R
diff --git a/app/src/main/java/io/heckel/ntfy/ui/SubscriptionSettingsActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/SubscriptionSettingsActivity.kt
new file mode 100644
index 0000000..1f80505
--- /dev/null
+++ b/app/src/main/java/io/heckel/ntfy/ui/SubscriptionSettingsActivity.kt
@@ -0,0 +1,25 @@
+package io.heckel.ntfy.ui
+
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+import androidx.preference.PreferenceFragmentCompat
+import io.heckel.ntfy.R
+
+class SubscriptionSettingsActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_subscription_settings)
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ supportActionBar?.setDisplayShowHomeEnabled(true)
+ supportFragmentManager
+ .beginTransaction()
+ .replace(R.id.subscription_settings_content, SubscriptionSettingsFragment())
+ .commit()
+ }
+
+ class SubscriptionSettingsFragment : PreferenceFragmentCompat() {
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ setPreferencesFromResource(R.xml.root_preferences, rootKey)
+ }
+ }
+}
diff --git a/app/src/main/res/drawable/ic_notifications_white_24dp.xml b/app/src/main/res/drawable/ic_notifications_white_24dp.xml
new file mode 100644
index 0000000..6410bf0
--- /dev/null
+++ b/app/src/main/res/drawable/ic_notifications_white_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_subscription_settings.xml b/app/src/main/res/layout/activity_subscription_settings.xml
new file mode 100644
index 0000000..11b43d5
--- /dev/null
+++ b/app/src/main/res/layout/activity_subscription_settings.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/notification_dialog_fragment.xml b/app/src/main/res/layout/notification_dialog_fragment.xml
new file mode 100644
index 0000000..d133cea
--- /dev/null
+++ b/app/src/main/res/layout/notification_dialog_fragment.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/detail_action_bar_menu.xml b/app/src/main/res/menu/detail_action_bar_menu.xml
index 959e77b..081be9a 100644
--- a/app/src/main/res/menu/detail_action_bar_menu.xml
+++ b/app/src/main/res/menu/detail_action_bar_menu.xml
@@ -1,4 +1,6 @@