aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRaindropsSys <raindrops@equestria.dev>2024-05-11 22:37:11 +0200
committerRaindropsSys <raindrops@equestria.dev>2024-05-11 22:37:11 +0200
commit8d04d4dbc82e7c1320abfebd411910cbe1cadc7a (patch)
tree246e4b4e5474864e4cfd0ef741e819c0e2e55929
parent1071a4b93209047b262a6de6c89609cf0277782f (diff)
downloadponypush-8d04d4dbc82e7c1320abfebd411910cbe1cadc7a.tar.gz
ponypush-8d04d4dbc82e7c1320abfebd411910cbe1cadc7a.tar.bz2
ponypush-8d04d4dbc82e7c1320abfebd411910cbe1cadc7a.zip
Updated 22 files, added app/schemas/io.heckel.haven.db.Database/13.json and deleted 2 files (automated)
-rw-r--r--README.md24
-rw-r--r--TESTING.md23
-rw-r--r--app/build.gradle68
-rw-r--r--app/schemas/io.heckel.haven.db.Database/13.json356
-rw-r--r--app/src/main/AndroidManifest.xml19
-rw-r--r--app/src/main/java/io/heckel/ntfy/backup/Backuper.kt9
-rw-r--r--app/src/main/java/io/heckel/ntfy/msg/BroadcastService.kt2
-rw-r--r--app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt49
-rw-r--r--app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt5
-rw-r--r--app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt8
-rw-r--r--app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt18
-rw-r--r--app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt2
-rw-r--r--app/src/main/java/io/heckel/ntfy/ui/DetailSettingsActivity.kt2
-rw-r--r--app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt30
-rw-r--r--app/src/main/java/io/heckel/ntfy/ui/NotificationFragment.kt1
-rw-r--r--app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt11
-rw-r--r--app/src/main/java/io/heckel/ntfy/ui/ShareActivity.kt360
-rw-r--r--app/src/main/java/io/heckel/ntfy/ui/UserFragment.kt4
-rw-r--r--app/src/main/java/io/heckel/ntfy/up/BroadcastReceiver.kt3
-rw-r--r--app/src/main/java/io/heckel/ntfy/util/Log.kt2
-rw-r--r--app/src/main/res/values/values.xml3
-rw-r--r--build.gradle5
-rw-r--r--gradle.properties2
-rw-r--r--gradle/wrapper/gradle-wrapper.properties2
-rw-r--r--settings.gradle2
25 files changed, 428 insertions, 582 deletions
diff --git a/README.md b/README.md
index a8cc41c..b76569f 100644
--- a/README.md
+++ b/README.md
@@ -1,25 +1,21 @@
-# Ponypush
-This is a fork of the [ntfy Android app](https://github.com/binwiederhier/ntfy) ([ntfy.sh](https://ntfy.sh)) made to work with Equestria.dev's notification servers and other technologies. It is not meant to be used outside of Equestria.dev's services.
+# Haven
+This app started as a fork of the [ntfy Android app](https://github.com/binwiederhier/ntfy) ([ntfy.sh](https://ntfy.sh)) made to work with Equestria.dev's notification servers and other technologies and eventually turned into a mobile app for various Equestria.dev services. It is not meant to be used outside of Equestria.dev and will not work without the Equestria.dev servers.
-## Build
+> **Note for Ponypush users:** Upgrading from Ponypush (`dev.equestria.notifications`) is not possible as Haven (`dev.equestria.haven`) uses a different package name. Therefore, you will need to uninstall Ponypush and then install Haven (or keep both installed).
-### Building without Firebase ("F-Droid flavor", using WebSockets)
+## Build
In Android Studio, go to Build > Generate Signed Bundle/APK and select `fdroidDebug` or `fdroidRelease`.
-### Building with Firebase ("Google Play flavor", using Firebase Cloud Messaging)
-To build your own version with Firebase, you must:
-* Create a Firebase account and create an Android app on it
-* Place your account file at `app/google-services.json`
-* In Android Studio, go to Build > Generate Signed Bundle/APK and select `playDebug` or `playRelease`.
-
-## Notable differences
+## Notable differences with upstream ntfy
- Revamped UI, with a lot of options removed
- Most notably, it is not possible to use a third-party server other than notifications.equestria.dev
- Integration with Equestria.dev's tags
-- Firebase Cloud Messaging support for notifications.equestria.dev
+- Targets Android 15 instead of the upstream Android 13
+- Requires Android 9 or later instead of Android 5.0 or later
+- Up-to-date dependencies
## Upstream integration
-When notable changes are made upstream (on the official ntfy app), Ponypush will attempt to integrate such changes as much as possible. However, we cannot guarantee that the implemented features will work the same as with the official ntfy app.
+When notable changes are made to the official ntfy app, Haven will attempt to integrate such changes as much as possible. However, we cannot guarantee that the implemented features will work the same as with the official ntfy app.
## License
-This is a fork of ntfy by [Philipp C. Heckel](https://heckel.io), distributed under the [Apache License 2.0](LICENSE) like the original project. \ No newline at end of file
+This is a fork of ntfy by [Philipp C. Heckel](https://heckel.io), originally distributed under the Apache License 2.0. This modified application is released under the [GNU Affero General Public License version 3](LICENSE) as per Equestria.dev's policy. \ No newline at end of file
diff --git a/TESTING.md b/TESTING.md
deleted file mode 100644
index b6d800a..0000000
--- a/TESTING.md
+++ /dev/null
@@ -1,23 +0,0 @@
-# Testing
-
-## Manual testing steps
-
-* Upgrade from old version
-* Subscribe to topic
- * With instant delivery
- * With other server
- * Deep link subscribe
-* Sending messages
- * Tags/emojis
- * Titles
- * Priority, in particular that high/max priority messages vibrate
-* Main view
- * Multi-delete
- * Toggle global mute
-* Detail view
- * Toggle per-topic mute
- * Toggle instant delivery
- * Send message while in detail view (should show notification)
- * Check if notifications get canceled when sending message
- * Clear notifications
- * Send test notification
diff --git a/app/build.gradle b/app/build.gradle
index 4517cdc..fb30e91 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -4,20 +4,22 @@ repositories {
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
-apply plugin: 'com.google.gms.google-services'
android {
- compileSdkVersion 33
-
defaultConfig {
- applicationId "dev.equestria.notifications"
+ applicationId "dev.equestria.haven"
minSdkVersion 28
- targetSdkVersion 33
+ targetSdkVersion 34
+ compileSdk 34
- versionCode 145
- versionName "3.1.8"
+ versionCode 146
+ versionName "4.0.0"
buildConfigField 'String', "NTFY_VERSION", '"1.16.0"'
+ buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'false'
+ buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'false'
+ buildConfigField 'boolean', 'INSTALL_PACKAGES_AVAILABLE', 'true'
+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
/* Required for Room schema migrations */
@@ -40,18 +42,10 @@ android {
}
}
- flavorDimensions "store"
- productFlavors {
- play {
- buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'true'
- buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'true'
- buildConfigField 'boolean', 'INSTALL_PACKAGES_AVAILABLE', 'false'
- }
- fdroid {
- buildConfigField 'boolean', 'FIREBASE_AVAILABLE', 'false'
- buildConfigField 'boolean', 'RATE_APP_AVAILABLE', 'false'
- buildConfigField 'boolean', 'INSTALL_PACKAGES_AVAILABLE', 'true'
- }
+ kotlinOptions {
+ freeCompilerArgs = [
+ '-opt-in=kotlin.RequiresOptIn'
+ ]
}
compileOptions {
@@ -66,13 +60,9 @@ android {
]
}
namespace 'io.heckel.ntfy'
-}
-
-// Disables GoogleServices tasks for F-Droid variant
-android.applicationVariants.all { variant ->
- def shouldProcessGoogleServices = variant.flavorName == "play"
- def googleTask = tasks.findByName("process${variant.name.capitalize()}GoogleServices")
- googleTask.enabled = shouldProcessGoogleServices
+ buildFeatures {
+ buildConfig true
+ }
}
// Strips out REQUEST_INSTALL_PACKAGES permission for Google Play variant
@@ -93,40 +83,36 @@ android.applicationVariants.all { variant ->
dependencies {
// AndroidX, The Basics
- implementation "androidx.appcompat:appcompat:1.5.1"
- implementation "androidx.core:core-ktx:1.9.0"
+ implementation "androidx.appcompat:appcompat:1.6.1"
+ implementation "androidx.core:core-ktx:1.13.1"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
- implementation "androidx.activity:activity-ktx:1.6.1"
- implementation "androidx.fragment:fragment-ktx:1.5.5"
- implementation "androidx.work:work-runtime-ktx:2.7.1"
- implementation 'androidx.preference:preference-ktx:1.2.0'
+ implementation "androidx.activity:activity-ktx:1.9.0"
+ implementation "androidx.fragment:fragment-ktx:1.7.0"
+ implementation "androidx.work:work-runtime-ktx:2.9.0"
+ implementation 'androidx.preference:preference-ktx:1.2.1'
// JSON serialization
- implementation 'com.google.code.gson:gson:2.10'
+ implementation 'com.google.code.gson:gson:2.10.1'
// Room (SQLite)
- def room_version = "2.4.3"
+ def room_version = "2.6.1"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// OkHttp (HTTP library)
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
- // Firebase, sigh ... (only Google Play)
- playImplementation 'com.google.firebase:firebase-messaging:23.1.1'
- playImplementation 'com.google.firebase:firebase-analytics:21.2.0'
-
// RecyclerView
- implementation "androidx.recyclerview:recyclerview:1.3.0-rc01"
+ implementation "androidx.recyclerview:recyclerview:1.3.2"
// Swipe down to refresh
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
// Material design
- implementation "com.google.android.material:material:1.7.0"
+ implementation "com.google.android.material:material:1.12.0"
// LiveData
- implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.5.1"
+ implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.7.0"
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
// Image viewer
diff --git a/app/schemas/io.heckel.haven.db.Database/13.json b/app/schemas/io.heckel.haven.db.Database/13.json
new file mode 100644
index 0000000..35dcab7
--- /dev/null
+++ b/app/schemas/io.heckel.haven.db.Database/13.json
@@ -0,0 +1,356 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 13,
+ "identityHash": "44fc291d937fdf02b9bc2d0abb10d2e0",
+ "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, 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",
+ "notNull": false
+ },
+ {
+ "fieldPath": "icon",
+ "columnName": "icon",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "upAppId",
+ "columnName": "upAppId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "upConnectorToken",
+ "columnName": "upConnectorToken",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "dedicatedChannels",
+ "columnName": "dedicatedChannels",
+ "affinity": "INTEGER",
+ "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`)"
+ }
+ ],
+ "foreignKeys": []
+ },
+ {
+ "tableName": "Notification",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `subscriptionId` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `title` TEXT NOT NULL, `message` 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": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "message",
+ "columnName": "message",
+ "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",
+ "notNull": false
+ },
+ {
+ "fieldPath": "deleted",
+ "columnName": "deleted",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "icon.url",
+ "columnName": "icon_url",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "icon.contentUri",
+ "columnName": "icon_contentUri",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "attachment.name",
+ "columnName": "attachment_name",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "attachment.type",
+ "columnName": "attachment_type",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "attachment.size",
+ "columnName": "attachment_size",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "attachment.expires",
+ "columnName": "attachment_expires",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "attachment.url",
+ "columnName": "attachment_url",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "attachment.contentUri",
+ "columnName": "attachment_contentUri",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "attachment.progress",
+ "columnName": "attachment_progress",
+ "affinity": "INTEGER",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id",
+ "subscriptionId"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "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"
+ ]
+ },
+ "indices": [],
+ "foreignKeys": []
+ },
+ {
+ "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",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "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, '44fc291d937fdf02b9bc2d0abb10d2e0')"
+ ]
+ }
+} \ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8ed2092..3f8c8a6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -4,6 +4,7 @@
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <!-- For instant delivery foregrounds service -->
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/> <!-- To keep foreground service awake; soon not needed anymore -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <!-- To restart service on reboot -->
<uses-permission android:name="android.permission.VIBRATE"/> <!-- Incoming notifications should be able to vibrate the phone -->
@@ -33,7 +34,6 @@
<!-- Main activity -->
<activity
android:name=".ui.MainActivity"
- android:label="@string/app_launch_name"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@@ -81,7 +81,7 @@
</activity>
<!-- Subscriber foreground service for hosts other than ntfy.sh -->
- <service android:name=".service.SubscriberService"/>
+ <service android:name=".service.SubscriberService" android:foregroundServiceType="remoteMessaging"/>
<!-- Subscriber service restart on reboot -->
<receiver
@@ -123,21 +123,6 @@
android:exported="false">
</receiver>
- <!-- Firebase messaging (note that this is empty in the F-Droid flavor) -->
- <service
- android:name=".firebase.FirebaseService"
- android:exported="false">
- <intent-filter>
- <action android:name="com.google.firebase.MESSAGING_EVENT"/>
- </intent-filter>
- </service>
- <meta-data
- android:name="firebase_analytics_collection_enabled"
- android:value="false"/>
- <meta-data
- android:name="com.google.firebase.messaging.default_notification_icon"
- android:resource="@drawable/ic_notification"/>
-
<!-- FileProvider required for older Android versions (<= P), to allow passing the file URI in the open intent.
Avoids "exposed beyond app through Intent.getData" exception, see see https://stackoverflow.com/a/57288352/1440785 -->
<provider
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 ef1546a..0a9648a 100644
--- a/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt
+++ b/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt
@@ -5,10 +5,8 @@ import android.net.Uri
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.stream.JsonReader
-import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.db.Repository
-import io.heckel.ntfy.firebase.FirebaseMessenger
import io.heckel.ntfy.msg.NotificationService
import io.heckel.ntfy.util.Log
import io.heckel.ntfy.util.topicUrl
@@ -18,7 +16,6 @@ class Backuper(val context: Context) {
private val gson = Gson()
private val resolver = context.applicationContext.contentResolver
private val repository = (context.applicationContext as Application).repository
- private val messenger = FirebaseMessenger()
private val notifier = NotificationService(context)
suspend fun backup(uri: Uri, withSettings: Boolean = true, withSubscriptions: Boolean = true, withUsers: Boolean = true) {
@@ -92,7 +89,6 @@ class Backuper(val context: Context) {
if (subscriptions == null) {
return
}
- val appBaseUrl = context.getString(R.string.app_base_url)
subscriptions.forEach { s ->
try {
// Add to database
@@ -114,11 +110,6 @@ class Backuper(val context: Context) {
)
repository.addSubscription(subscription)
- // Subscribe to Firebase topics
- if (s.baseUrl == appBaseUrl) {
- messenger.subscribe(s.topic)
- }
-
// Create dedicated channels
if (s.dedicatedChannels) {
notifier.createSubscriptionNotificationChannels(subscription)
diff --git a/app/src/main/java/io/heckel/ntfy/msg/BroadcastService.kt b/app/src/main/java/io/heckel/ntfy/msg/BroadcastService.kt
index b9e7737..8c27acc 100644
--- a/app/src/main/java/io/heckel/ntfy/msg/BroadcastService.kt
+++ b/app/src/main/java/io/heckel/ntfy/msg/BroadcastService.kt
@@ -8,6 +8,7 @@ import io.heckel.ntfy.db.Notification
import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.Subscription
import io.heckel.ntfy.util.*
+import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -66,6 +67,7 @@ class BroadcastService(private val ctx: Context) {
}
}
+ @OptIn(DelicateCoroutinesApi::class)
private fun send(ctx: Context, intent: Intent) {
val api = ApiService()
val baseUrl = getStringExtra(intent, "base_url") ?: ctx.getString(R.string.app_base_url)
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 bf13e98..69fdc6c 100644
--- a/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt
+++ b/app/src/main/java/io/heckel/ntfy/msg/NotificationService.kt
@@ -22,7 +22,6 @@ import io.heckel.ntfy.ui.DetailActivity
import io.heckel.ntfy.ui.MainActivity
import io.heckel.ntfy.util.*
import java.util.*
-import kotlin.reflect.typeOf
class NotificationService(val context: Context) {
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@@ -146,9 +145,6 @@ class NotificationService(val context: Context) {
maybeSetDeleteIntent(builder, insistent)
maybeSetSound(builder, insistent, update)
maybeSetProgress(builder, notification)
- maybeAddOpenAction(builder, notification)
- maybeAddBrowseAction(builder, notification)
- maybeAddDownloadAction(builder, notification)
maybeAddCancelAction(builder, notification)
maybeAddUserActions(builder, notification)
@@ -252,51 +248,6 @@ class NotificationService(val context: Context) {
}
}
- private fun maybeAddOpenAction(builder: NotificationCompat.Builder, notification: Notification) {
- // @ponypush - Disabled this because this should NOT be a default and non-configurable
- return
-
- if (!canOpenAttachment(notification.attachment)) {
- return
- }
- if (notification.attachment?.contentUri != null) {
- val contentUri = Uri.parse(notification.attachment.contentUri)
- val intent = Intent(Intent.ACTION_VIEW, contentUri).apply {
- setDataAndType(contentUri, notification.attachment.type ?: "application/octet-stream") // Required for Android <= P
- addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- }
- val pendingIntent = PendingIntent.getActivity(context, Random().nextInt(), intent, PendingIntent.FLAG_IMMUTABLE)
- builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_open), pendingIntent).build())
- }
- }
-
- private fun maybeAddBrowseAction(builder: NotificationCompat.Builder, notification: Notification) {
- // @ponypush - Disabled this because this should NOT be a default and non-configurable
- return
-
- if (notification.attachment?.contentUri != null) {
- val intent = Intent(android.app.DownloadManager.ACTION_VIEW_DOWNLOADS).apply {
- addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- }
- val pendingIntent = PendingIntent.getActivity(context, Random().nextInt(), intent, PendingIntent.FLAG_IMMUTABLE)
- builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_browse), pendingIntent).build())
- }
- }
-
- private fun maybeAddDownloadAction(builder: NotificationCompat.Builder, notification: Notification) {
- // @ponypush - Disabled this because this should NOT be a default and non-configurable
- return
-
- if (notification.attachment?.contentUri == null && listOf(ATTACHMENT_PROGRESS_NONE, ATTACHMENT_PROGRESS_FAILED).contains(notification.attachment?.progress)) {
- val intent = Intent(context, UserActionBroadcastReceiver::class.java).apply {
- putExtra(BROADCAST_EXTRA_TYPE, BROADCAST_TYPE_DOWNLOAD_START)
- putExtra(BROADCAST_EXTRA_NOTIFICATION_ID, notification.id)
- }
- val pendingIntent = PendingIntent.getBroadcast(context, Random().nextInt(), intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
- builder.addAction(NotificationCompat.Action.Builder(0, context.getString(R.string.notification_popup_action_download), pendingIntent).build())
- }
- }
-
private fun maybeAddCancelAction(builder: NotificationCompat.Builder, notification: Notification) {
if (notification.attachment?.contentUri == null && notification.attachment?.progress in 0..99) {
val intent = Intent(context, UserActionBroadcastReceiver::class.java).apply {
diff --git a/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt b/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt
index 45ae500..7ca20a6 100644
--- a/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt
+++ b/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt
@@ -23,6 +23,7 @@ import io.heckel.ntfy.ui.MainActivity
import io.heckel.ntfy.util.Log
import io.heckel.ntfy.util.topicUrl
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -138,7 +139,7 @@ class SubscriberService : Service() {
}
}
wakeLock = null
- stopForeground(true)
+ stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
} catch (e: Exception) {
Log.d(TAG, "Service stopped without being started: ${e.message}")
@@ -148,6 +149,7 @@ class SubscriberService : Service() {
saveServiceState(this, ServiceState.STOPPED)
}
+ @OptIn(DelicateCoroutinesApi::class)
private fun refreshConnections() {
GlobalScope.launch(Dispatchers.IO) {
if (!refreshMutex.tryLock()) {
@@ -251,6 +253,7 @@ class SubscriberService : Service() {
repository.updateState(subscriptionIds, state)
}
+ @OptIn(DelicateCoroutinesApi::class)
private fun onNotificationReceived(subscription: Subscription, notification: io.heckel.ntfy.db.Notification) {
// Wakelock while notifications are being dispatched
// Wakelocks are reference counted by default so that should work neatly here
diff --git a/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt b/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt
index 12dd482..49dfeb6 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/AddFragment.kt
@@ -13,7 +13,6 @@ import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import com.google.android.material.color.DynamicColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.google.android.material.elevation.SurfaceColors
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import io.heckel.ntfy.BuildConfig
@@ -138,7 +137,7 @@ class AddFragment : DialogFragment() {
loginPasswordText.addTextChangedListener(loginTextWatcher)
// Build dialog
- val dialog = MaterialAlertDialogBuilder(activity!!)
+ val dialog = MaterialAlertDialogBuilder(requireActivity())
.setView(view)
.setPositiveButton(R.string.add_dialog_button_subscribe) { _, _ ->
// This will be overridden below to avoid closing the dialog immediately
@@ -279,11 +278,6 @@ class AddFragment : DialogFragment() {
private fun validateInputSubscribeView() {
if (!this::positiveButton.isInitialized) return // As per crash seen in Google Play
- // Show/hide things: This logic is intentionally kept simple. Do not simplify "just because it's pretty".
- val instantToggleAllowed = if (!BuildConfig.FIREBASE_AVAILABLE) {
- false
- } else defaultBaseUrl == null
-
// Enable/disable "Subscribe" button
lifecycleScope.launch(Dispatchers.IO) {
val baseUrl = getBaseUrl()
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 d1286aa..114fc7b 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt
@@ -7,14 +7,11 @@ import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_VIEW
import android.net.Uri
-import android.os.Build
import android.os.Bundle
-import android.text.Html
import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
import android.view.View
-import android.widget.TextView
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
@@ -33,7 +30,6 @@ import io.heckel.ntfy.app.Application
import io.heckel.ntfy.db.Notification
import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.Subscription
-import io.heckel.ntfy.firebase.FirebaseMessenger
import io.heckel.ntfy.util.Log
import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.NotificationService
@@ -50,7 +46,6 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
}
private val repository by lazy { (application as Application).repository }
private val api = ApiService()
- private val messenger = FirebaseMessenger()
private var notifier: NotificationService? = null // Context-dependent
private var appBaseUrl: String? = null // Context-dependent
@@ -135,12 +130,6 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
)
repository.addSubscription(subscription)
- // Subscribe to Firebase topic if ntfy.sh (even if instant, just to be sure!)
- if (baseUrl == appBaseUrl) {
- Log.d(TAG, "Subscribing to Firebase topic $topic")
- messenger.subscribe(topic)
- }
-
// Fetch cached messages
try {
val user = repository.getUser(subscription.baseUrl) // May be null
@@ -180,8 +169,6 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
subscriptionMutedUntil = intent.getLongExtra(MainActivity.EXTRA_SUBSCRIPTION_MUTED_UNTIL, 0L)
// Set title
- val subscriptionBaseUrl = intent.getStringExtra(MainActivity.EXTRA_SUBSCRIPTION_BASE_URL) ?: return
- val topicUrl = topicShortUrl(subscriptionBaseUrl, subscriptionTopic)
title = subscriptionDisplayName
// Swipe to refresh
@@ -282,6 +269,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
}
}
+ @OptIn(DelicateCoroutinesApi::class)
override fun onPause() {
super.onPause()
Log.d(TAG, "onPause hook: Removing 'notificationId' from all notifications for $subscriptionId")
@@ -590,6 +578,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
startActivity(intent)
}
+ @OptIn(DelicateCoroutinesApi::class)
private fun onDeleteClick() {
Log.d(TAG, "Deleting subscription ${topicShortUrl(subscriptionBaseUrl, subscriptionTopic)}")
@@ -601,9 +590,6 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
GlobalScope.launch(Dispatchers.IO) {
repository.removeAllNotifications(subscriptionId)
repository.removeSubscription(subscriptionId)
- if (subscriptionBaseUrl == appBaseUrl) {
- messenger.unsubscribe(subscriptionTopic)
- }
}
finish()
}
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 b7bc58d..a5eebc8 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/DetailAdapter.kt
@@ -34,6 +34,7 @@ import io.heckel.ntfy.msg.NotificationService
import io.heckel.ntfy.msg.NotificationService.Companion.ACTION_VIEW
import io.heckel.ntfy.util.*
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -466,6 +467,7 @@ class DetailAdapter(private val activity: Activity, private val lifecycleScope:
return true
}
+ @OptIn(DelicateCoroutinesApi::class)
private fun deleteFile(context: Context, notification: Notification, attachment: Attachment): Boolean {
try {
val contentUri = Uri.parse(attachment.contentUri)
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 81c3a57..3c47c4d 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/DetailSettingsActivity.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/DetailSettingsActivity.kt
@@ -21,7 +21,6 @@ import androidx.preference.Preference.OnPreferenceClickListener
import com.google.android.material.color.DynamicColors
import com.google.android.material.elevation.SurfaceColors
import io.heckel.ntfy.BuildConfig
-import io.heckel.ntfy.R
import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.Subscription
import io.heckel.ntfy.msg.DownloadAttachmentWorker
@@ -32,6 +31,7 @@ import kotlinx.coroutines.*
import java.io.File
import java.io.IOException
import java.util.*
+import io.heckel.ntfy.R
/**
* Subscription settings
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 fa7b680..e7d3e08 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
@@ -4,20 +4,16 @@ import android.Manifest
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.app.AlertDialog
-import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.pm.PackageManager
-import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
-import android.text.method.LinkMovementMethod
import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.Button
-import android.widget.TextView
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
@@ -32,12 +28,9 @@ import com.google.android.material.color.DynamicColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.elevation.SurfaceColors
import com.google.android.material.floatingactionbutton.FloatingActionButton
-import io.heckel.ntfy.BuildConfig
-import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.Subscription
-import io.heckel.ntfy.firebase.FirebaseMessenger
import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.DownloadManager
import io.heckel.ntfy.msg.DownloadType
@@ -53,6 +46,7 @@ import kotlinx.coroutines.launch
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.random.Random
+import io.heckel.ntfy.R
class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.SubscribeListener, NotificationFragment.NotificationSettingsListener {
private val viewModel by viewModels<SubscriptionsViewModel> {
@@ -60,7 +54,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
}
private val repository by lazy { (application as Application).repository }
private val api = ApiService()
- private val messenger = FirebaseMessenger()
// UI elements
private lateinit var menu: Menu
@@ -186,9 +179,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
// Create notification channels right away, so we can configure them immediately after installing the app
dispatcher?.init()
- // Subscribe to control Firebase channel (so we can re-start the foreground service if it dies)
- messenger.subscribe(ApiService.CONTROL_TOPIC)
-
// Darrkkkk mode
AppCompatDelegate.setDefaultNightMode(repository.getDarkMode())
@@ -241,9 +231,9 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
Log.d(TAG, "Poll worker version matches: choosing KEEP as existing work policy")
ExistingPeriodicWorkPolicy.KEEP
} else {
- Log.d(TAG, "Poll worker version DOES NOT MATCH: choosing REPLACE as existing work policy")
+ Log.d(TAG, "Poll worker version DOES NOT MATCH: choosing UPDATE as existing work policy")
repository.setPollWorkerVersion(PollWorker.VERSION)
- ExistingPeriodicWorkPolicy.REPLACE
+ ExistingPeriodicWorkPolicy.UPDATE
}
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
@@ -263,9 +253,9 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
Log.d(TAG, "Delete worker version matches: choosing KEEP as existing work policy")
ExistingPeriodicWorkPolicy.KEEP
} else {
- Log.d(TAG, "Delete worker version DOES NOT MATCH: choosing REPLACE as existing work policy")
+ Log.d(TAG, "Delete worker version DOES NOT MATCH: choosing UPDATE as existing work policy")
repository.setDeleteWorkerVersion(DeleteWorker.VERSION)
- ExistingPeriodicWorkPolicy.REPLACE
+ ExistingPeriodicWorkPolicy.UPDATE
}
val work = PeriodicWorkRequestBuilder<DeleteWorker>(DELETE_WORKER_INTERVAL_MINUTES, TimeUnit.MINUTES)
.addTag(DeleteWorker.TAG)
@@ -281,9 +271,9 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
Log.d(TAG, "ServiceStartWorker version matches: choosing KEEP as existing work policy")
ExistingPeriodicWorkPolicy.KEEP
} else {
- Log.d(TAG, "ServiceStartWorker version DOES NOT MATCH: choosing REPLACE as existing work policy")
+ Log.d(TAG, "ServiceStartWorker version DOES NOT MATCH: choosing UPDATE as existing work policy")
repository.setAutoRestartWorkerVersion(SubscriberService.SERVICE_START_WORKER_VERSION)
- ExistingPeriodicWorkPolicy.REPLACE
+ ExistingPeriodicWorkPolicy.UPDATE
}
val work = PeriodicWorkRequestBuilder<SubscriberServiceManager.ServiceStartWorker>(SERVICE_START_WORKER_INTERVAL_MINUTES, TimeUnit.MINUTES)
.addTag(SubscriberService.TAG)
@@ -429,12 +419,6 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
)
viewModel.add(subscription)
- // Subscribe to Firebase topic if ntfy.sh (even if instant, just to be sure!)
- if (baseUrl == appBaseUrl) {
- Log.d(TAG, "Subscribing to Firebase topic $topic")
- messenger.subscribe(topic)
- }
-
// Fetch cached messages
lifecycleScope.launch(Dispatchers.IO) {
try {
diff --git a/app/src/main/java/io/heckel/ntfy/ui/NotificationFragment.kt b/app/src/main/java/io/heckel/ntfy/ui/NotificationFragment.kt
index c932fbc..b53c26b 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/NotificationFragment.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/NotificationFragment.kt
@@ -1,6 +1,5 @@
package io.heckel.ntfy.ui
-import android.app.AlertDialog
import android.app.Dialog
import android.content.Context
import android.os.Bundle
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 de2169e..c2b67fb 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt
@@ -27,7 +27,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.elevation.SurfaceColors
import com.google.gson.Gson
import io.heckel.ntfy.BuildConfig
-import io.heckel.ntfy.R
import io.heckel.ntfy.backup.Backuper
import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.User
@@ -41,6 +40,7 @@ import okhttp3.RequestBody.Companion.toRequestBody
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit
+import io.heckel.ntfy.R
/**
* Main settings
@@ -438,7 +438,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
val backupPrefId = context?.getString(R.string.settings_backup_restore_backup_key) ?: return
val backup: ListPreference? = findPreference(backupPrefId)
var backupSelection = BACKUP_EVERYTHING
- val backupResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument()) { uri ->
+ val backupResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("binary/octet-stream")) { uri ->
if (uri == null) {
return@registerForActivityResult
}
@@ -517,7 +517,6 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
connectionProtocol?.value = repository.getConnectionProtocol()
connectionProtocol?.preferenceDataStore = object : PreferenceDataStore() {
override fun putString(key: String?, value: String?) {
- val proto = value ?: repository.getConnectionProtocol()
repository.setConnectionProtocol()
restartService()
}
@@ -536,12 +535,6 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
val versionPrefId = context?.getString(R.string.settings_about_version_key) ?: return
val versionPref: Preference? = findPreference(versionPrefId)
- val type = if (BuildConfig.FIREBASE_AVAILABLE) {
- "Firebase Cloud Messaging"
- } else {
- "WebSocket"
- }
-
val version = "Ponypush " + BuildConfig.VERSION_NAME + " (ntfy " + BuildConfig.NTFY_VERSION + ")"
versionPref?.summary = version
versionPref?.onPreferenceClickListener = OnPreferenceClickListener {
diff --git a/app/src/main/java/io/heckel/ntfy/ui/ShareActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/ShareActivity.kt
deleted file mode 100644
index bfbb65b..0000000
--- a/app/src/main/java/io/heckel/ntfy/ui/ShareActivity.kt
+++ /dev/null
@@ -1,360 +0,0 @@
-package io.heckel.ntfy.ui
-
-import android.content.Intent
-import android.net.Uri
-import android.os.Bundle
-import android.os.Parcelable
-import android.text.Editable
-import android.text.TextWatcher
-import android.view.*
-import android.widget.*
-import androidx.appcompat.app.AppCompatActivity
-import androidx.lifecycle.lifecycleScope
-import androidx.recyclerview.widget.RecyclerView
-import com.google.android.material.textfield.TextInputLayout
-import io.heckel.ntfy.R
-import io.heckel.ntfy.app.Application
-import io.heckel.ntfy.msg.ApiService
-import io.heckel.ntfy.util.*
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-
-class ShareActivity : AppCompatActivity() {
- private val repository by lazy { (application as Application).repository }
- private val api = ApiService()
-
- // File to share
- private var fileUri: Uri? = null
-
- // Context-dependent things
- private lateinit var appBaseUrl: String
- private var defaultBaseUrl: String? = null
-
- // UI elements
- private lateinit var menu: Menu
- private lateinit var sendItem: MenuItem
- private lateinit var contentImage: ImageView
- private lateinit var contentFileBox: View
- private lateinit var contentFileInfo: TextView
- private lateinit var contentFileIcon: ImageView
- private lateinit var contentText: TextView
- private lateinit var topicText: TextView
- private lateinit var baseUrlLayout: TextInputLayout
- private lateinit var baseUrlText: AutoCompleteTextView
- private lateinit var useAnotherServerCheckbox: CheckBox
- private lateinit var suggestedTopicsList: RecyclerView
- private lateinit var progress: ProgressBar
- private lateinit var errorText: TextView
- private lateinit var errorImage: ImageView
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_share)
-
- Log.init(this) // Init logs in all entry points
- Log.d(TAG, "Create $this with intent $intent")
-
- // Action bar
- title = getString(R.string.share_title)
-
- // Show 'Back' button
- supportActionBar?.setDisplayHomeAsUpEnabled(true)
-
- // Context-dependent things
- appBaseUrl = getString(R.string.app_base_url)
- defaultBaseUrl = repository.getDefaultBaseUrl()
-
- // UI elements
- val root: View = findViewById(R.id.share_root_view)
- contentText = findViewById(R.id.share_content_text)
- contentImage = findViewById(R.id.share_content_image)
- contentFileBox = findViewById(R.id.share_content_file_box)
- contentFileInfo = findViewById(R.id.share_content_file_info)
- contentFileIcon = findViewById(R.id.share_content_file_icon)
- topicText = findViewById(R.id.share_topic_text)
- baseUrlLayout = findViewById(R.id.share_base_url_layout)
- baseUrlLayout.background = root.background
- baseUrlLayout.makeEndIconSmaller(resources) // Hack!
- baseUrlText = findViewById(R.id.share_base_url_text)
- baseUrlText.background = root.background
- baseUrlText.hint = defaultBaseUrl ?: appBaseUrl
- useAnotherServerCheckbox = findViewById(R.id.share_use_another_server_checkbox)
- suggestedTopicsList = findViewById(R.id.share_suggested_topics)
- progress = findViewById(R.id.share_progress)
- progress.visibility = View.GONE
- errorText = findViewById(R.id.share_error_text)
- errorText.visibility = View.GONE
- errorImage = findViewById(R.id.share_error_image)
- errorImage.visibility = View.GONE
-
- val textWatcher = object : TextWatcher {
- override fun afterTextChanged(s: Editable?) {
- validateInput()
- }
- override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
- // Nothing
- }
- override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
- // Nothing
- }
- }
- contentText.addTextChangedListener(textWatcher)
- topicText.addTextChangedListener(textWatcher)
- baseUrlText.addTextChangedListener(textWatcher)
-
- // Add behavior to "use another" checkbox
- useAnotherServerCheckbox.setOnCheckedChangeListener { _, isChecked ->
- baseUrlLayout.visibility = if (isChecked) View.VISIBLE else View.GONE
- validateInput()
- }
-
- // Things that need the database
- lifecycleScope.launch(Dispatchers.IO) {
- // Populate "suggested topics"
- val subscriptions = repository.getSubscriptions()
- val lastShareTopics = repository.getLastShareTopics()
- val subscribedTopics = subscriptions
- .map { topicUrl(it.baseUrl, it.topic) }
- .toSet()
- .subtract(lastShareTopics.toSet())
- val suggestedTopics = (lastShareTopics.reversed() + subscribedTopics).distinct()
- val baseUrlsRaw = suggestedTopics
- .mapNotNull {
- try { splitTopicUrl(it).first }
- catch (_: Exception) { null }
- }
- .distinct()
- val baseUrls = if (defaultBaseUrl != null) {
- baseUrlsRaw.filterNot { it == defaultBaseUrl }
- } else {
- baseUrlsRaw.filterNot { it == appBaseUrl }
- }
- suggestedTopicsList.adapter = TopicAdapter(suggestedTopics) { topicUrl ->
- try {
- val (baseUrl, topic) = splitTopicUrl(topicUrl)
- val defaultUrl = defaultBaseUrl ?: appBaseUrl
- topicText.text = topic
- if (baseUrl == defaultUrl) {
- useAnotherServerCheckbox.isChecked = false
- } else {
- useAnotherServerCheckbox.isChecked = true
- baseUrlText.setText(baseUrl)
- }
- } catch (e: Exception) {
- Log.w(TAG, "Invalid topicUrl $topicUrl", e)
- }
- }
-
- // Add baseUrl auto-complete behavior
- val activity = this@ShareActivity
- activity.runOnUiThread {
- initBaseUrlDropdown(baseUrls, baseUrlText, baseUrlLayout)
- useAnotherServerCheckbox.isChecked = if (suggestedTopics.isNotEmpty()) {
- try {
- val (baseUrl, _) = splitTopicUrl(suggestedTopics.first())
- val defaultUrl = defaultBaseUrl ?: appBaseUrl
- baseUrl != defaultUrl
- } catch (_: Exception) {
- false
- }
- } else {
- baseUrls.count() == 1
- }
- baseUrlLayout.visibility = if (useAnotherServerCheckbox.isChecked) View.VISIBLE else View.GONE
- }
- }
-
- // Incoming intent
- val intent = intent ?: return
- val type = intent.type ?: return
- if (intent.action != Intent.ACTION_SEND) return
- if (type == "text/plain") {
- handleSendText(intent)
- } else if (type.startsWith("image/")) {
- handleSendImage(intent)
- } else {
- handleSendFile(intent)
- }
- }
-
- private fun handleSendText(intent: Intent) {
- val text = intent.getStringExtra(Intent.EXTRA_TEXT) ?: "(no text)"
- Log.d(TAG, "Shared content is text: $text")
- contentText.text = text
- show()
- }
-
- private fun handleSendImage(intent: Intent) {
- fileUri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri
- Log.d(TAG, "Shared content is an image with URI $fileUri")
- if (fileUri == null) {
- Log.w(TAG, "Null URI is not allowed. Aborting.")
- return
- }
- try {
- contentImage.setImageBitmap(fileUri!!.readBitmapFromUri(applicationContext))
- contentText.text = getString(R.string.share_content_image_text)
- show(image = true)
- } catch (e: Exception) {
- fileUri = null
- contentText.text = ""
- errorText.text = getString(R.string.share_content_image_error, e.message)
- show(error = true)
- }
- }
-
- private fun handleSendFile(intent: Intent) {
- fileUri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri
- Log.d(TAG, "Shared content is a file with URI $fileUri")
- if (fileUri == null) {
- Log.w(TAG, "Null URI is not allowed. Aborting.")
- return
- }
- try {
- val resolver = applicationContext.contentResolver
- val info = fileStat(this, fileUri)
- val mimeType = resolver.getType(fileUri!!)
- contentText.text = getString(R.string.share_content_file_text)
- contentFileInfo.text = "${info.filename}\n${formatBytes(info.size)}"
- contentFileIcon.setImageResource(mimeTypeToIconResource(mimeType))
- show(file = true)
- } catch (e: Exception) {
- fileUri = null
- contentText.text = ""
- errorText.text = getString(R.string.share_content_file_error, e.message)
- show(error = true)
- }
- }
-
- override fun onSupportNavigateUp(): Boolean {
- finish()
- return true
- }
-
- override fun onCreateOptionsMenu(menu: Menu): Boolean {
- menuInflater.inflate(R.menu.menu_share_action_bar, menu)
- this.menu = menu
- sendItem = menu.findItem(R.id.share_menu_send)
- validateInput() // Disable icon
- return true
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- return when (item.itemId) {
- R.id.share_menu_send -> {
- onShareClick()
- true
- }
- else -> super.onOptionsItemSelected(item)
- }
- }
-
- private fun show(image: Boolean = false, file: Boolean = false, error: Boolean = false) {
- contentImage.visibility = if (image) View.VISIBLE else View.GONE
- contentFileBox.visibility = if (file) View.VISIBLE else View.GONE
- errorImage.visibility = if (error) View.VISIBLE else View.GONE
- errorText.visibility = if (error) View.VISIBLE else View.GONE
- }
-
- private fun onShareClick() {
- val baseUrl = getBaseUrl()
- val topic = topicText.text.toString()
- val message = contentText.text.toString()
- progress.visibility = View.VISIBLE
- contentText.isEnabled = false
- topicText.isEnabled = false
- useAnotherServerCheckbox.isEnabled = false
- baseUrlText.isEnabled = false
- suggestedTopicsList.isEnabled = false
- lifecycleScope.launch(Dispatchers.IO) {
- val user = repository.getUser(baseUrl)
- try {
- val (filename, body) = if (fileUri != null) {
- val stat = fileStat(this@ShareActivity, fileUri)
- val body = ContentUriRequestBody(applicationContext.contentResolver, fileUri!!, stat.size)
- Pair(stat.filename, body)
- } else {
- Pair("", null)
- }
- api.publish(
- baseUrl = baseUrl,
- topic = topic,
- user = user,
- message = message,
- body = body, // May be null
- filename = filename, // May be empty
- )
- runOnUiThread {
- repository.addLastShareTopic(topicUrl(baseUrl, topic))
- Log.addScrubTerm(shortUrl(baseUrl), Log.TermType.Domain)
- Log.addScrubTerm(topic, Log.TermType.Term)
- finish()
- Toast
- .makeText(this@ShareActivity, getString(R.string.share_successful), Toast.LENGTH_LONG)
- .show()
- }
- } catch (e: Exception) {
- val errorMessage = if (e is ApiService.UnauthorizedException) {
- if (e.user != null) {
- getString(R.string.detail_test_message_error_unauthorized_user, e.user.username)
- } else {
- getString(R.string.detail_test_message_error_unauthorized_anon)
- }
- } else if (e is ApiService.EntityTooLargeException) {
- getString(R.string.detail_test_message_error_too_large)
- } else {
- getString(R.string.detail_test_message_error, e.message)
- }
- runOnUiThread {
- progress.visibility = View.GONE
- errorText.text = errorMessage
- errorImage.visibility = View.VISIBLE
- errorText.visibility = View.VISIBLE
- }
- }
- }
- }
-
- private fun validateInput() {
- if (!this::sendItem.isInitialized || !this::useAnotherServerCheckbox.isInitialized || !this::contentText.isInitialized || !this::topicText.isInitialized) {
- return // sendItem is initialized late in onCreateOptionsMenu
- }
- val enabled = if (useAnotherServerCheckbox.isChecked) {
- contentText.text.isNotEmpty() && validTopic(topicText.text.toString()) && validUrl(baseUrlText.text.toString())
- } else {
- contentText.text.isNotEmpty() && topicText.text.isNotEmpty()
- }
- sendItem.isEnabled = enabled
- sendItem.icon?.alpha = if (enabled) 255 else 130
- }
-
- private fun getBaseUrl(): String {
- return if (useAnotherServerCheckbox.isChecked) {
- baseUrlText.text.toString()
- } else {
- defaultBaseUrl ?: appBaseUrl
- }
- }
-
- class TopicAdapter(private val topicUrls: List<String>, val onClick: (String) -> Unit) : RecyclerView.Adapter<TopicAdapter.ViewHolder>() {
- override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
- val view = LayoutInflater.from(viewGroup.context).inflate(R.layout.fragment_share_item, viewGroup, false)
- return ViewHolder(view)
- }
-
- override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
- viewHolder.topicName.text = shortUrl(topicUrls[position])
- viewHolder.view.setOnClickListener { onClick(topicUrls[position]) }
- }
-
- override fun getItemCount() = topicUrls.size
-
- class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
- val topicName: TextView = view.findViewById(R.id.share_item_text)
- }
- }
-
- companion object {
- const val TAG = "PonypushShareActivity"
- }
-}
diff --git a/app/src/main/java/io/heckel/ntfy/ui/UserFragment.kt b/app/src/main/java/io/heckel/ntfy/ui/UserFragment.kt
index 3673647..bf8423c 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/UserFragment.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/UserFragment.kt
@@ -102,9 +102,7 @@ class UserFragment : DialogFragment() {
val dialog = builder?.create()
if (dialog != null) {
dialog.setOnShowListener {
- if (dialog != null) {
- positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
- }
+ positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
// Delete button should be red
if (user != null) {
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 6f01999..f785ffc 100644
--- a/app/src/main/java/io/heckel/ntfy/up/BroadcastReceiver.kt
+++ b/app/src/main/java/io/heckel/ntfy/up/BroadcastReceiver.kt
@@ -8,6 +8,7 @@ import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.Subscription
import io.heckel.ntfy.service.SubscriberServiceManager
import io.heckel.ntfy.util.*
+import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -31,6 +32,7 @@ class BroadcastReceiver : android.content.BroadcastReceiver() {
}
}
+ @OptIn(DelicateCoroutinesApi::class)
private fun register(context: Context, intent: Intent) {
val appId = intent.getStringExtra(EXTRA_APPLICATION) ?: return
val connectorToken = intent.getStringExtra(EXTRA_TOKEN) ?: return
@@ -104,6 +106,7 @@ class BroadcastReceiver : android.content.BroadcastReceiver() {
}
}
+ @OptIn(DelicateCoroutinesApi::class)
private fun unregister(context: Context, intent: Intent) {
val connectorToken = intent.getStringExtra(EXTRA_TOKEN) ?: return
val app = context.applicationContext as Application
diff --git a/app/src/main/java/io/heckel/ntfy/util/Log.kt b/app/src/main/java/io/heckel/ntfy/util/Log.kt
index 8a236c9..d4e00b2 100644
--- a/app/src/main/java/io/heckel/ntfy/util/Log.kt
+++ b/app/src/main/java/io/heckel/ntfy/util/Log.kt
@@ -7,6 +7,7 @@ import io.heckel.ntfy.backup.Backuper
import io.heckel.ntfy.db.Database
import io.heckel.ntfy.db.LogDao
import io.heckel.ntfy.db.LogEntry
+import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -21,6 +22,7 @@ class Log(private val logsDao: LogDao) {
private val scrubNum: AtomicInteger = AtomicInteger(-1)
private val scrubTerms = Collections.synchronizedMap(mutableMapOf<String, ReplaceTerm>())
+ @OptIn(DelicateCoroutinesApi::class)
private fun log(level: Int, tag: String, message: String, exception: Throwable?) {
if (!record.get()) return
GlobalScope.launch(Dispatchers.IO) { // FIXME This does not guarantee the log order
diff --git a/app/src/main/res/values/values.xml b/app/src/main/res/values/values.xml
index 3c21410..57b9822 100644
--- a/app/src/main/res/values/values.xml
+++ b/app/src/main/res/values/values.xml
@@ -4,8 +4,7 @@
The translatable="false" attribute is just an additional safety. -->
<!-- Main app constants -->
- <string name="app_name" translatable="false">Equestria.dev Ponypush</string>
- <string name="app_launch_name" translatable="false">Ponypush</string>
+ <string name="app_name" translatable="false">Equestria.dev</string>
<string name="app_base_url" translatable="false">https://notifications.equestria.dev</string> <!-- If changed, you must also change google-services.json! -->
<!-- Main activity -->
diff --git a/build.gradle b/build.gradle
index e4abc03..83371f8 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,13 +1,12 @@
buildscript {
- ext.kotlin_version = '1.6.21'
+ ext.kotlin_version = '1.9.23'
repositories {
google()
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.0.2'
+ classpath 'com.android.tools.build:gradle:8.4.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- classpath 'com.google.gms:google-services:4.3.14' // This is removed in the "fdroid" flavor
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
diff --git a/gradle.properties b/gradle.properties
index 9aff27f..2f03129 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -14,6 +14,6 @@ org.gradle.jvmargs=-Xmx1536m
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
-android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false
+org.gradle.configuration-cache=true
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 292ead7..5868ef0 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
diff --git a/settings.gradle b/settings.gradle
index 231bc04..2461898 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,2 +1,2 @@
-rootProject.name='ntfy'
+rootProject.name='haven'
include ':app'