diff options
author | RaindropsSys <raindrops@equestria.dev> | 2024-05-11 22:37:11 +0200 |
---|---|---|
committer | RaindropsSys <raindrops@equestria.dev> | 2024-05-11 22:37:11 +0200 |
commit | 8d04d4dbc82e7c1320abfebd411910cbe1cadc7a (patch) | |
tree | 246e4b4e5474864e4cfd0ef741e819c0e2e55929 | |
parent | 1071a4b93209047b262a6de6c89609cf0277782f (diff) | |
download | ponypush-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)
25 files changed, 428 insertions, 582 deletions
@@ -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' |