diff --git a/app/src/main/java/io/heckel/ntfy/service/Connection.kt b/app/src/main/java/io/heckel/ntfy/service/Connection.kt index 71a98be4..9994dbe4 100644 --- a/app/src/main/java/io/heckel/ntfy/service/Connection.kt +++ b/app/src/main/java/io/heckel/ntfy/service/Connection.kt @@ -6,7 +6,14 @@ interface Connection { fun since(): String? } +/** + * Represents a unique connection identifier that changes every time a + * connection needs to be re-established. + */ data class ConnectionId( val baseUrl: String, - val topicsToSubscriptionIds: Map + val topicsToSubscriptionIds: Map, + val connectionProtocol: String, + val credentialsHash: Int, // Hash of "username:password" or 0 if no user + val headersHash: Int // Hash of sorted headers or 0 if none ) 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 59daf8e5..d54083d5 100644 --- a/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt +++ b/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt @@ -206,9 +206,27 @@ class SubscriberService : Service() { val instantSubscriptions = repository.getSubscriptions() .filter { s -> s.instant } val activeConnectionIds = connections.keys().toList().toSet() + val connectionProtocol = repository.getConnectionProtocol() val desiredConnectionIds = instantSubscriptions // Set - .groupBy { s -> ConnectionId(s.baseUrl, emptyMap()) } - .map { entry -> entry.key.copy(topicsToSubscriptionIds = entry.value.associate { s -> s.topic to s.id }) } + .groupBy { s -> s.baseUrl } + .map { (baseUrl, subs) -> + // Create a unique connection ID for each base URL. Each change in the connection ID will + // trigger a new connection, and close existing connections. We want to make sure that when the + // connection protocol (JSON/WS), the user or the custom headers are updated, that we kill existing + // connections and start new ones. + val credentialsHash = repository.getUser(baseUrl)?.let { "${it.username}:${it.password}".hashCode() } ?: 0 + val headersHash = repository.getCustomHeadersForServer(baseUrl) + .sortedBy { "${it.name}:${it.value}" } + .joinToString(",") { "${it.name}:${it.value}" } + .hashCode() + ConnectionId( + baseUrl = baseUrl, + topicsToSubscriptionIds = subs.associate { s -> s.topic to s.id }, + connectionProtocol = connectionProtocol, + credentialsHash = credentialsHash, + headersHash = headersHash + ) + } .toSet() val newConnectionIds = desiredConnectionIds.subtract(activeConnectionIds) val obsoleteConnectionIds = activeConnectionIds.subtract(desiredConnectionIds) @@ -237,7 +255,7 @@ class SubscriberService : Service() { val since = sinceByBaseUrl[connectionId.baseUrl] ?: "none" val serviceActive = { isServiceStarted } val user = repository.getUser(connectionId.baseUrl) - val connection = if (repository.getConnectionProtocol() == Repository.CONNECTION_PROTOCOL_WS) { + val connection = if (connectionId.connectionProtocol == Repository.CONNECTION_PROTOCOL_WS) { val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager WsConnection(this, connectionId, repository, user, since, ::onStateChanged, ::onNotificationReceived, alarmManager) } else { diff --git a/app/src/main/java/io/heckel/ntfy/service/SubscriberServiceManager.kt b/app/src/main/java/io/heckel/ntfy/service/SubscriberServiceManager.kt index c51d3039..71f51b48 100644 --- a/app/src/main/java/io/heckel/ntfy/service/SubscriberServiceManager.kt +++ b/app/src/main/java/io/heckel/ntfy/service/SubscriberServiceManager.kt @@ -26,12 +26,6 @@ class SubscriberServiceManager(private val context: Context) { workManager.enqueueUniqueWork(WORK_NAME_ONCE, ExistingWorkPolicy.KEEP, startServiceRequest) // Unique avoids races! } - fun restart() { - Intent(context, SubscriberService::class.java).also { intent -> - context.stopService(intent) // Service will auto-restart - } - } - /** * Starts or stops the foreground service by figuring out how many instant delivery subscriptions * exist. If there's > 0, then we need a foreground service. 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 8c8c1020..f364e16c 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt @@ -225,7 +225,7 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific // Update battery banner + WebSocket banner + websocket reconnect banner showHideBatteryBanner(subscriptions) showHideWebSocketBanner(subscriptions) - showHideWebSocketReconnectBanner(subscriptions) + showHideWebSocketReconnectBanner() } } @@ -304,13 +304,13 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific } wsEnableButton.setOnClickListener { repository.setConnectionProtocol(Repository.CONNECTION_PROTOCOL_WS) - SubscriberServiceManager(this).restart() + SubscriberServiceManager(this).refresh() wsBanner.visibility = View.GONE // Maybe show WebSocketReconnectBanner viewModel.list().observe(this) { it?.let { subscriptions -> - showHideWebSocketReconnectBanner(subscriptions) + showHideWebSocketReconnectBanner() } } } @@ -404,15 +404,14 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific } } - private fun showHideWebSocketReconnectBanner(subscriptions: List) { + private fun showHideWebSocketReconnectBanner() { val wsReconnectBanner = findViewById(R.id.main_banner_websocket_reconnect) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - val hasSelfHostedSubscriptions = subscriptions.count { it.baseUrl != appBaseUrl } > 0 val usingWebSockets = repository.getConnectionProtocol() == Repository.CONNECTION_PROTOCOL_WS val wsReconnectRemindTimeReached = repository.getWebSocketReconnectRemindTime() < System.currentTimeMillis() val canScheduleExactAlarms = (getSystemService(ALARM_SERVICE) as AlarmManager).canScheduleExactAlarms() - val showBanner = hasSelfHostedSubscriptions && wsReconnectRemindTimeReached && usingWebSockets && !canScheduleExactAlarms - Log.d(TAG, "hasSelfHostedSubscriptions: ${hasSelfHostedSubscriptions}, wsReconnectRemindTimeReached: ${wsReconnectRemindTimeReached}, usingWebSockets: ${usingWebSockets}, canScheduleExactAlarms: ${canScheduleExactAlarms}") + val showBanner = wsReconnectRemindTimeReached && usingWebSockets && !canScheduleExactAlarms + Log.d(TAG, "wsReconnectRemindTimeReached: ${wsReconnectRemindTimeReached}, usingWebSockets: ${usingWebSockets}, canScheduleExactAlarms: ${canScheduleExactAlarms}") wsReconnectBanner.visibility = if (showBanner) View.VISIBLE else View.GONE } else { wsReconnectBanner.visibility = View.GONE 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 d916d7b9..c720af56 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt @@ -700,7 +700,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere override fun putString(key: String?, value: String?) { val proto = value ?: repository.getConnectionProtocol() repository.setConnectionProtocol(proto) - restartService() + serviceManager.refresh() // Refresh to switch connections between WS and JSON stream } override fun getString(key: String?, defValue: String?): String { return repository.getConnectionProtocol() @@ -737,10 +737,6 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere repository.setAutoDownloadMaxSize(autoDownloadSelectionCopy) } - private fun restartService() { - serviceManager.restart() // Service will auto-restart - } - private fun copyLogsToClipboard(scrub: Boolean) { lifecycleScope.launch(Dispatchers.IO) { val context = context ?: return@launch @@ -845,7 +841,8 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere } fun updateExactAlarmsPref() { - val exactAlarmsPrefId = context?.getString(R.string.settings_advanced_exact_alarms_key) ?: return + val context = context ?: return + val exactAlarmsPrefId = context.getString(R.string.settings_advanced_exact_alarms_key) val exactAlarmsPref: Preference? = findPreference(exactAlarmsPrefId) val canScheduleExactAlarms = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { (activity?.getSystemService(ALARM_SERVICE) as AlarmManager).canScheduleExactAlarms() @@ -859,6 +856,14 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere } true } + // Android doesn't show "ntfy" in the "Alarms & reminders" list if battery optimizations are disabled. + // + // In fact, if the user has granted the battery optimization exemption (see battery banner in MainActivity), + // the alarm manager's canScheduleExactAlarms() method will return true. + // + // This is undocumented behavior. See https://github.com/binwiederhier/ntfy/issues/1456#issuecomment-3707174262 + exactAlarmsPref?.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + && !isIgnoringBatteryOptimizations(context) } @Keep @@ -1047,7 +1052,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere override fun onUpdateUser(dialog: DialogFragment, user: User) { lifecycleScope.launch(Dispatchers.IO) { repository.updateUser(user) - serviceManager.restart() // Editing does not change the user ID + serviceManager.refresh() runOnUiThread { if (this@SettingsActivity::userSettingsFragment.isInitialized) { userSettingsFragment.reload() @@ -1059,7 +1064,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere override fun onDeleteUser(dialog: DialogFragment, baseUrl: String) { lifecycleScope.launch(Dispatchers.IO) { repository.deleteUser(baseUrl) - serviceManager.restart() + serviceManager.refresh() runOnUiThread { if (this@SettingsActivity::userSettingsFragment.isInitialized) { userSettingsFragment.reload() @@ -1071,7 +1076,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere override fun onAddCustomHeader(dialog: DialogFragment, header: io.heckel.ntfy.db.CustomHeader) { lifecycleScope.launch(Dispatchers.IO) { repository.addCustomHeader(header) - serviceManager.restart() // Restart to apply new headers + serviceManager.refresh() // Refresh to apply new headers runOnUiThread { if (this@SettingsActivity::customHeaderSettingsFragment.isInitialized) { customHeaderSettingsFragment.reload() @@ -1083,7 +1088,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere override fun onUpdateCustomHeader(dialog: DialogFragment, oldHeader: io.heckel.ntfy.db.CustomHeader, newHeader: io.heckel.ntfy.db.CustomHeader) { lifecycleScope.launch(Dispatchers.IO) { repository.updateCustomHeader(oldHeader, newHeader) - serviceManager.restart() // Restart to apply header changes + serviceManager.refresh() // Refresh to apply header changes runOnUiThread { if (this@SettingsActivity::customHeaderSettingsFragment.isInitialized) { customHeaderSettingsFragment.reload() @@ -1095,7 +1100,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere override fun onDeleteCustomHeader(dialog: DialogFragment, header: io.heckel.ntfy.db.CustomHeader) { lifecycleScope.launch(Dispatchers.IO) { repository.deleteCustomHeader(header) - serviceManager.restart() + serviceManager.refresh() runOnUiThread { if (this@SettingsActivity::customHeaderSettingsFragment.isInitialized) { customHeaderSettingsFragment.reload() diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index d7e8ad90..c0cc64ab 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -346,4 +346,63 @@ התראות מדויקות ntfy יכול לתזמן התראות מדויקות, התראות מדויקות משמשות כדי לחבר את ה־WebSockets מחדש ברקע. לחיצה כאן תשלול את ההרשאה. ntfy לא יכול לתזמן התראות מדויקות. התראות מדויקות נחוצות כדי לחבר את ה־WebSockets מחדש ברקע. לחיצה תעניק את ההרשאה. + פרסום אל %1$s + כותרת + למשל: מישהו בדלת + הודעה + תגיות + למשל: אזהרה, גולגולת + עדיפות + פרסום + לא ניתן לפרסם הודעה: %1$s + לא ניתן לפרסם הודעה: %1$s (קוד %2$d) + ההודעה פורסמה + העלאה: %1$s (%2$s / %3$s) + ההעלאה בוטלה + כותרת + תגיות + עדיפות + לחיצה על כתובת + דוא״ל + השהיה + Markdown + צירוף לפי כתובת + צירוף קובץ מקומי + שיחת טלפון + לחיצה על כתובת + למשל: https://example.com/alerts/1234 + דוא״ל + למשל: phil@example.com + השהיית מסירה + למשל: https://example.com/flowers.jpg + למשל: lilies.jpg + שיחת טלפון + למשל: ‎+1234567890 + נא למלא הודעה כאן + פרסום הודעה + אפשרויות נוספות + התראת פרסום + שפה + להשתמש בברירת המחדל של המערכת + ברירת המחדל של המערכת + צבעים דינמיים + מחיקה + שמירה + הוספה + כבר יש כותרת בשם הזה בשרת הזה + הכותרת הזאת שמורה ומוגדרת על ידי ntfy + שם הכותרת מכיל תווים שגויים + ערך כותרת (למשל: 9f3c2e4a1b2d4e) + שם כותרת (למשל: CF-Access-Client-Id) + כתובת שירות + עריכת כותרת מותאמת אישית + הוספת כותרת מותאמת אישית + הוספת כותרת לשרת + הוספת כותרת + כותרות מותאמות אישית + כפתור הפרסום מופיע בתחתית תצוגת הנושאים + סרגל הודעות מופיע בתחתית תצוגת הנושאים + הצגת סרגל הודעות + להשתמש בצבעי המערכת הדינמיים + להשתמש בצבעי ערכת העיצוב של ntfy diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 58f21fc0..2e7c33b2 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -347,4 +347,28 @@ Değişken renkler Değişken sistem renkleri kullanılıyor ntfy tema renkleri kullanılıyor + %1$s\'a yayınla + Başlık + Örneğin, \'Kapıda birisi var\' + Mesaj + Etiketler + Orneğin, \'İkaz, Tehlike\' + Öncelik + Yayınla + Mesaj: %1$s yayınlanamadı + Mesaj: %1$s yayınlanamadı (kod %2$d) + Mesaj yayınlandı + Yüklenme: %1$s (%2$s / %3$s) + Yüklenme iptal edildi + Başlık + Etiketler + Öncelik + Tıklama URL\'i + E-posta adresi + Erteleme + \"Markdown\" + URL\'le ekle + Yerli dosya ekle + Telefon görüşmesi + Tıklama URL\'i diff --git a/fastlane/metadata/android/en-US/changelog/NEXT.txt b/fastlane/metadata/android/en-US/changelog/NEXT.txt new file mode 100644 index 00000000..913de553 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelog/NEXT.txt @@ -0,0 +1,3 @@ +Maintenance + bug fixes: +* Hide "Exact alarms" setting if battery optimization exemption has been granted (#1456, thanks for reporting @HappyLer) +* Fix ForegroundServiceDidNotStartInTimeException (#1520, attempt 3, d064e75)