From 6e6820891c95b0b4365332548ac89903d21f1d27 Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Thu, 20 Mar 2025 22:15:23 -0600 Subject: [PATCH 1/6] target android 34 --- app/build.gradle | 26 +++++++++++++------ app/src/main/AndroidManifest.xml | 3 +-- .../main/java/io/heckel/ntfy/db/Database.kt | 6 ++--- app/src/main/java/io/heckel/ntfy/ui/Colors.kt | 2 +- app/src/main/java/io/heckel/ntfy/util/Util.kt | 11 ++++---- build.gradle | 10 ++++--- gradle/wrapper/gradle-wrapper.properties | 4 +-- 7 files changed, 38 insertions(+), 24 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 90dc1edb..2990a460 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,18 +1,23 @@ +plugins { + id 'com.google.devtools.ksp' +} + repositories { mavenCentral() } apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' +//apply plugin: 'kotlin-kapt' apply plugin: 'com.google.gms.google-services' android { - compileSdkVersion 33 + namespace "io.heckel.ntfy" + compileSdkVersion 34 defaultConfig { applicationId "io.heckel.ntfy" minSdkVersion 21 - targetSdkVersion 33 + targetSdkVersion 34 versionCode 33 versionName "1.17.0" @@ -25,6 +30,10 @@ android { arguments += ["room.schemaLocation": "$projectDir/schemas".toString()] } } + + buildFeatures { + buildConfig true + } } buildTypes { @@ -54,12 +63,12 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() + jvmTarget = JavaVersion.VERSION_17.toString() freeCompilerArgs += [ '-Xjvm-default=all-compatibility' // https://stackoverflow.com/a/71234042/1440785 ] @@ -103,9 +112,10 @@ dependencies { implementation 'com.google.code.gson:gson:2.10' // Room (SQLite) - def room_version = "2.5.1" + def room_version = "2.6.1" + implementation "androidx.room:room-runtime:$room_version" + ksp "androidx.room:room-compiler:$room_version" 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' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0950b404..67246fe5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + diff --git a/app/src/main/java/io/heckel/ntfy/db/Database.kt b/app/src/main/java/io/heckel/ntfy/db/Database.kt index 5f397969..f7e3a2ad 100644 --- a/app/src/main/java/io/heckel/ntfy/db/Database.kt +++ b/app/src/main/java/io/heckel/ntfy/db/Database.kt @@ -120,7 +120,7 @@ data class Attachment( @ColumnInfo(name = "contentUri") val contentUri: String?, // After it's downloaded, the content:// location @ColumnInfo(name = "progress") val progress: Int, // Progress during download, -1 if not downloaded ) { - constructor(name: String, type: String?, size: Long?, expires: Long?, url: String) : + @Ignore constructor(name: String, type: String?, size: Long?, expires: Long?, url: String) : this(name, type, size, expires, url, null, ATTACHMENT_PROGRESS_NONE) } @@ -135,7 +135,7 @@ data class Icon( @ColumnInfo(name = "url") val url: String, // URL (mandatory, see ntfy server) @ColumnInfo(name = "contentUri") val contentUri: String?, // After it's downloaded, the content:// location ) { - constructor(url:String) : + @Ignore constructor(url:String) : this(url, null) } @@ -192,7 +192,7 @@ data class LogEntry( @ColumnInfo(name = "message") val message: String, @ColumnInfo(name = "exception") val exception: String? ) { - constructor(timestamp: Long, tag: String, level: Int, message: String, exception: String?) : + @Ignore constructor(timestamp: Long, tag: String, level: Int, message: String, exception: String?) : this(0, timestamp, tag, level, message, exception) } diff --git a/app/src/main/java/io/heckel/ntfy/ui/Colors.kt b/app/src/main/java/io/heckel/ntfy/ui/Colors.kt index ada14cbf..47e8e164 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/Colors.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/Colors.kt @@ -7,7 +7,7 @@ import io.heckel.ntfy.util.isDarkThemeOn class Colors { companion object { - const val refreshProgressIndicator = R.color.teal + val refreshProgressIndicator = R.color.teal fun notificationIcon(context: Context): Int { return if (isDarkThemeOn(context)) R.color.teal_light else R.color.teal diff --git a/app/src/main/java/io/heckel/ntfy/util/Util.kt b/app/src/main/java/io/heckel/ntfy/util/Util.kt index 2ffbb766..1db243fe 100644 --- a/app/src/main/java/io/heckel/ntfy/util/Util.kt +++ b/app/src/main/java/io/heckel/ntfy/util/Util.kt @@ -393,13 +393,14 @@ class ContentUriRequestBody( } } +// TODO: make this work in Android 34+ // Hack: Make end icon for drop down smaller, see https://stackoverflow.com/a/57098715/1440785 fun View.makeEndIconSmaller(resources: Resources) { - val dimension = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30f, resources.displayMetrics) - val endIconImageView = findViewById(R.id.text_input_end_icon) - endIconImageView.minimumHeight = dimension.toInt() - endIconImageView.minimumWidth = dimension.toInt() - requestLayout() +// val dimension = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30f, resources.displayMetrics) +// val endIconImageView = findViewById(R.id.text_input_end_icon) +// endIconImageView.minimumHeight = dimension.toInt() +// endIconImageView.minimumWidth = dimension.toInt() +// requestLayout() } // Shows the ripple effect on the view, if it is ripple-able, see https://stackoverflow.com/a/56314062/1440785 diff --git a/build.gradle b/build.gradle index fd508003..8cc4348b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,11 +1,11 @@ buildscript { - ext.kotlin_version = '1.8.20' + ext.kotlin_version = '2.0.21' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.android.tools.build:gradle:8.7.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.google.gms:google-services:4.3.15' // This is removed in the "fdroid" flavor @@ -14,6 +14,10 @@ buildscript { } } +plugins { + id 'com.google.devtools.ksp' version '2.0.21-1.0.27' +} + allprojects { repositories { google() @@ -24,4 +28,4 @@ allprojects { task clean(type: Delete) { delete rootProject.buildDir -} +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index edb3793b..dcbbf23b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Sep 22 10:13:03 PDT 2020 +#Sat Feb 22 14:52:01 MST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip From 6f907a28cd521a111179e11934173fc815fb1102 Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Thu, 20 Mar 2025 22:17:07 -0600 Subject: [PATCH 2/6] run SubscriberService as special foreground service --- app/src/main/AndroidManifest.xml | 9 ++++++++- .../java/io/heckel/ntfy/service/SubscriberService.kt | 7 ++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 67246fe5..bee873d0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ + @@ -94,7 +95,13 @@ - + + + = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + startForeground(NOTIFICATION_SERVICE_ID, serviceNotification!!, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE) + } else { + startForeground(NOTIFICATION_SERVICE_ID, serviceNotification) + } } override fun onDestroy() { From 5ecd84855f0f9e2ccd5b3cb271386f7ff9c86818 Mon Sep 17 00:00:00 2001 From: Hunter Kehoe Date: Thu, 20 Mar 2025 22:17:33 -0600 Subject: [PATCH 3/6] request schedule_exact_alarm permissions --- app/src/main/AndroidManifest.xml | 1 + .../main/java/io/heckel/ntfy/db/Repository.kt | 14 ++++ .../io/heckel/ntfy/service/WsConnection.kt | 22 +++++- .../java/io/heckel/ntfy/ui/MainActivity.kt | 55 ++++++++++++++- .../io/heckel/ntfy/ui/SettingsActivity.kt | 27 ++++++++ app/src/main/res/layout/activity_main.xml | 69 ++++++++++++++++++- app/src/main/res/values/strings.xml | 9 +++ app/src/main/res/values/values.xml | 1 + app/src/main/res/xml/main_preferences.xml | 3 + 9 files changed, 198 insertions(+), 3 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bee873d0..cb991fa1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -110,6 +110,7 @@ android:exported="true"> + diff --git a/app/src/main/java/io/heckel/ntfy/db/Repository.kt b/app/src/main/java/io/heckel/ntfy/db/Repository.kt index 71b03092..15f76db7 100644 --- a/app/src/main/java/io/heckel/ntfy/db/Repository.kt +++ b/app/src/main/java/io/heckel/ntfy/db/Repository.kt @@ -342,6 +342,16 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas .apply() } + fun getWebSocketReconnectRemindTime(): Long { + return sharedPrefs.getLong(SHARED_PREFS_WEBSOCKET_RECONNECT_REMIND_TIME, WEBSOCKET_RECONNECT_REMIND_TIME_ALWAYS) + } + + fun setWebSocketReconnectRemindTime(timeMillis: Long) { + sharedPrefs.edit() + .putLong(SHARED_PREFS_WEBSOCKET_RECONNECT_REMIND_TIME, timeMillis) + .apply() + } + fun getDefaultBaseUrl(): String? { return sharedPrefs.getString(SHARED_PREFS_DEFAULT_BASE_URL, null) ?: sharedPrefs.getString(SHARED_PREFS_UNIFIED_PUSH_BASE_URL, null) // Fall back to UP URL, removed when default is set! @@ -492,6 +502,7 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas const val SHARED_PREFS_RECORD_LOGS_ENABLED = "RecordLogs" const val SHARED_PREFS_BATTERY_OPTIMIZATIONS_REMIND_TIME = "BatteryOptimizationsRemindTime" const val SHARED_PREFS_WEBSOCKET_REMIND_TIME = "JsonStreamRemindTime" // "Use WebSocket" banner (used to be JSON stream deprecation banner) + const val SHARED_PREFS_WEBSOCKET_RECONNECT_REMIND_TIME = "WebSocketReconnectRemindTime" const val SHARED_PREFS_UNIFIED_PUSH_BASE_URL = "UnifiedPushBaseURL" // Legacy key required for migration to DefaultBaseURL const val SHARED_PREFS_DEFAULT_BASE_URL = "DefaultBaseURL" const val SHARED_PREFS_LAST_TOPICS = "LastTopics" @@ -532,6 +543,9 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas const val WEBSOCKET_REMIND_TIME_ALWAYS = 1L const val WEBSOCKET_REMIND_TIME_NEVER = Long.MAX_VALUE + const val WEBSOCKET_RECONNECT_REMIND_TIME_ALWAYS = 1L + const val WEBSOCKET_RECONNECT_REMIND_TIME_NEVER = Long.MAX_VALUE + private const val TAG = "NtfyRepository" private var instance: Repository? = null diff --git a/app/src/main/java/io/heckel/ntfy/service/WsConnection.kt b/app/src/main/java/io/heckel/ntfy/service/WsConnection.kt index 080e8482..20059d5c 100644 --- a/app/src/main/java/io/heckel/ntfy/service/WsConnection.kt +++ b/app/src/main/java/io/heckel/ntfy/service/WsConnection.kt @@ -114,7 +114,27 @@ class WsConnection( Log.d(TAG,"$shortUrl (gid=$globalId): Scheduling a restart in $seconds seconds (via alarm manager)") val reconnectTime = Calendar.getInstance() reconnectTime.add(Calendar.SECOND, seconds) - alarmManager.setExact(AlarmManager.RTC_WAKEUP, reconnectTime.timeInMillis, RECONNECT_TAG, { start() }, null) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (alarmManager.canScheduleExactAlarms()) { + alarmManager.setExact( + AlarmManager.RTC_WAKEUP, + reconnectTime.timeInMillis, + RECONNECT_TAG, + { start() }, + null + ) + } else { + Log.d(TAG, "SCHEDULE_EXACT_ALARM permission denied: Failed to reschedule websocket connection") + } + } else { + alarmManager.setExact( + AlarmManager.RTC_WAKEUP, + reconnectTime.timeInMillis, + RECONNECT_TAG, + { start() }, + null + ) + } } else { Log.d(TAG, "$shortUrl (gid=$globalId): Scheduling a restart in $seconds seconds (via handler)") val handler = Handler(Looper.getMainLooper()) 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 e131f32c..5ac2f7ad 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt @@ -3,6 +3,7 @@ package io.heckel.ntfy.ui import android.Manifest import android.animation.Animator import android.animation.AnimatorListenerAdapter +import android.app.AlarmManager import android.app.AlertDialog import android.content.ActivityNotFoundException import android.content.Intent @@ -11,6 +12,7 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.Settings +import android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM import android.text.method.LinkMovementMethod import android.view.ActionMode import android.view.Menu @@ -125,9 +127,10 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc Log.addScrubTerm(s.topic) } - // Update banner + WebSocket banner + // Update battery banner + WebSocket banner + websocket reconnect banner showHideBatteryBanner(subscriptions) showHideWebSocketBanner(subscriptions) + showHideWebSocketReconnectBanner(subscriptions) } } @@ -194,6 +197,38 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc repository.setConnectionProtocol(Repository.CONNECTION_PROTOCOL_WS) SubscriberServiceManager(this).restart() wsBanner.visibility = View.GONE + + // maybe show WebSocketReconnectBanner + viewModel.list().observe(this) { + it?.let { subscriptions -> + showHideWebSocketReconnectBanner(subscriptions) + } + } + } + + // WebSocket Reconnect banner + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val wsReconnectBanner = findViewById(R.id.main_banner_websocket_reconnect) + val wsReconnectText = findViewById(R.id.main_banner_websocket_reconnect_text) + val wsReconnectDismissButton = + findViewById