From fb0a0643c25b5a4a000d35e5e1cce5686e950bb4 Mon Sep 17 00:00:00 2001 From: Ivorisnoob Date: Mon, 16 Mar 2026 15:40:07 +0530 Subject: [PATCH 1/5] docs: bump version references to 3.0.0 across app and documentation --- CONTRIBUTING.md | 2 +- README.md | 2 +- .../airsync/presentation/ui/components/SettingsView.kt | 2 +- .../airsync/presentation/ui/screens/AirSyncMainScreen.kt | 2 +- app/src/main/java/com/sameerasw/airsync/utils/SyncManager.kt | 2 +- .../java/com/sameerasw/airsync/utils/WebSocketMessageHandler.kt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c1011287..b04602c2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Contributing to AirSync Android (2.0) +# Contributing to AirSync Android (3.0) Thank you for your interest in contributing! Before you submit a pull request, please read the following guidelines. diff --git a/README.md b/README.md index 15a30b1f..d90b65b1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # airsync-android -Android app for AirSync 2.0 built with Kotlin Jetpack Compose +Android app for AirSync 3.0 built with Kotlin Jetpack Compose Min : Android 11 diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/SettingsView.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/SettingsView.kt index 993a4add..227f591f 100644 --- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/SettingsView.kt +++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/components/SettingsView.kt @@ -394,7 +394,7 @@ fun SettingsView( deviceInfo.name, deviceInfo.localIp, uiState.port.toIntOrNull() ?: 6996, - versionName ?: "2.0.0", + versionName ?: "3.0.0", adbPorts ) onSendMessage(message) diff --git a/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/AirSyncMainScreen.kt b/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/AirSyncMainScreen.kt index 06d54097..0834e89b 100644 --- a/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/AirSyncMainScreen.kt +++ b/app/src/main/java/com/sameerasw/airsync/presentation/ui/screens/AirSyncMainScreen.kt @@ -154,7 +154,7 @@ fun AirSyncMainScreen( .getPackageInfo(context.packageName, 0) .versionName } catch (_: Exception) { - "2.0.0" + "3.0.0" } val viewModel: AirSyncViewModel = viewModel { AirSyncViewModel.create(context) } val uiState by viewModel.uiState.collectAsState() diff --git a/app/src/main/java/com/sameerasw/airsync/utils/SyncManager.kt b/app/src/main/java/com/sameerasw/airsync/utils/SyncManager.kt index 852e5ce2..c3758dae 100644 --- a/app/src/main/java/com/sameerasw/airsync/utils/SyncManager.kt +++ b/app/src/main/java/com/sameerasw/airsync/utils/SyncManager.kt @@ -164,7 +164,7 @@ object SyncManager { val localIp = DeviceInfoUtil.getWifiIpAddress(context) ?: "Unknown" val port = dataStoreManager.getPort().first().toIntOrNull() ?: 6996 val version = context.packageManager - .getPackageInfo(context.packageName, 0).versionName ?: "2.0.0" + .getPackageInfo(context.packageName, 0).versionName ?: "3.0.0" // Get discovered ADB ports from the running mDNS discovery val adbPorts = try { diff --git a/app/src/main/java/com/sameerasw/airsync/utils/WebSocketMessageHandler.kt b/app/src/main/java/com/sameerasw/airsync/utils/WebSocketMessageHandler.kt index 12dca51a..d4fca035 100644 --- a/app/src/main/java/com/sameerasw/airsync/utils/WebSocketMessageHandler.kt +++ b/app/src/main/java/com/sameerasw/airsync/utils/WebSocketMessageHandler.kt @@ -513,7 +513,7 @@ object WebSocketMessageHandler { val macName = data.optString("name", "") val isPlus = data.optBoolean("isPlusSubscription", false) - val macVersion = data.optString("version", "2.0.0") + val macVersion = data.optString("version", "3.0.0") Log.d( TAG, From 6c8ce3271df5d99353b717e36aa6209851fd1951 Mon Sep 17 00:00:00 2001 From: Ivorisnoob Date: Mon, 16 Mar 2026 16:15:08 +0530 Subject: [PATCH 2/5] Typos and also a Minor Discovery issue --- README.md | 2 +- app/src/main/java/com/sameerasw/airsync/MainActivity.kt | 8 ++++---- app/src/main/java/com/sameerasw/airsync/utils/JsonUtil.kt | 6 ------ .../sameerasw/airsync/utils/NotificationDismissalUtil.kt | 2 +- .../main/java/com/sameerasw/airsync/utils/SyncManager.kt | 2 +- .../com/sameerasw/airsync/utils/UDPDiscoveryManager.kt | 6 +++--- .../java/com/sameerasw/airsync/utils/WallpaperUtil.kt | 2 +- .../sameerasw/airsync/utils/WebSocketMessageHandler.kt | 2 +- app/src/main/res/values/strings.xml | 6 +++--- 9 files changed, 15 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index d90b65b1..01acdead 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Min : Android 11 ## How to connect? -Use your built-in camera or Google lense or anything that can scan a QR code. I twill prompt you to open the app. Once authorized, The last device will be saved on the mobile for now for easier re-connection. +Use your built-in camera or Google Lens or anything that can scan a QR code. It will prompt you to open the app. Once authorized, the last device will be saved on the mobile for now for easier reconnection. ## [Read Documentation and How-To](https://airsync.notion.site/) diff --git a/app/src/main/java/com/sameerasw/airsync/MainActivity.kt b/app/src/main/java/com/sameerasw/airsync/MainActivity.kt index 8da1a07d..8465d1c5 100644 --- a/app/src/main/java/com/sameerasw/airsync/MainActivity.kt +++ b/app/src/main/java/com/sameerasw/airsync/MainActivity.kt @@ -180,7 +180,7 @@ class MainActivity : ComponentActivity() { // Install and configure the splash screen before any UI rendering val splashScreen = installSplashScreen() - // Make activity draw behind system bars - let the theme handles the colors + // Make activity draw behind system bars - let the theme handle the colors androidx.core.view.WindowCompat.setDecorFitsSystemWindows(window, false) super.onCreate(savedInstanceState) @@ -221,7 +221,7 @@ class MainActivity : ComponentActivity() { if (splashIcon is ImageView && deviceIconRes != null) { // Fade out the original app icon val fadeOutIcon = ObjectAnimator.ofFloat(splashIcon, "alpha", 1f, 0f).apply { - duration = 150 // 0.5 seconds + duration = 150 // 0.15 seconds } fadeOutIcon.doOnEnd { @@ -239,7 +239,7 @@ class MainActivity : ComponentActivity() { // Fade in the new device icon val fadeInIcon = ObjectAnimator.ofFloat(splashIcon, "alpha", 0f, 1f).apply { - duration = 350 // 0.5 seconds + duration = 350 // 0.35 seconds } fadeInIcon.doOnEnd { @@ -251,7 +251,7 @@ class MainActivity : ComponentActivity() { splashIcon, splashScreenViewProvider ) - }, 250) // 0.5 second hold + }, 250) // 0.25 second hold } catch (e: Exception) { Log.e( "MainActivity", diff --git a/app/src/main/java/com/sameerasw/airsync/utils/JsonUtil.kt b/app/src/main/java/com/sameerasw/airsync/utils/JsonUtil.kt index e6536f1c..99bb5afb 100644 --- a/app/src/main/java/com/sameerasw/airsync/utils/JsonUtil.kt +++ b/app/src/main/java/com/sameerasw/airsync/utils/JsonUtil.kt @@ -26,9 +26,6 @@ object JsonUtil { return """{"type":"device","data":{"name":"$name","ipAddress":"$ipAddress","port":$port,"version":"$version","adbPorts":[]$targetIpJson}}""" } - /** - * Creates a single-line JSON string for device info with ADB ports - */ /** * Creates a single-line JSON string for device info with ADB ports */ @@ -47,9 +44,6 @@ object JsonUtil { return """{"type":"device","data":{"id":"$id","name":"$name","ipAddress":"$ipAddress","port":$port,"version":"$version","adbPorts":[$portsJson]$targetIpJson}}""" } - /** - * Creates a single-line JSON string for device info with wallpaper - */ /** * Creates a single-line JSON string for device info with wallpaper */ diff --git a/app/src/main/java/com/sameerasw/airsync/utils/NotificationDismissalUtil.kt b/app/src/main/java/com/sameerasw/airsync/utils/NotificationDismissalUtil.kt index 1a814d17..0329017a 100644 --- a/app/src/main/java/com/sameerasw/airsync/utils/NotificationDismissalUtil.kt +++ b/app/src/main/java/com/sameerasw/airsync/utils/NotificationDismissalUtil.kt @@ -198,7 +198,7 @@ object NotificationDismissalUtil { return suppressedIds.remove(notificationId) } - /** Remove from caches when we it's gone. */ + /** Remove from caches when it's gone. */ fun removeFromCaches(id: String) { activeNotifications.remove(id)?.let { sbn -> keyToId.remove(sbn.key) diff --git a/app/src/main/java/com/sameerasw/airsync/utils/SyncManager.kt b/app/src/main/java/com/sameerasw/airsync/utils/SyncManager.kt index c3758dae..4f1ea1cb 100644 --- a/app/src/main/java/com/sameerasw/airsync/utils/SyncManager.kt +++ b/app/src/main/java/com/sameerasw/airsync/utils/SyncManager.kt @@ -247,7 +247,7 @@ object SyncManager { val statusJson = DeviceInfoUtil.generateDeviceStatusJson(context) if (WebSocketUtil.sendMessage(statusJson)) { Log.d(TAG, "Device status sent") - // Update cache + // Update cache lastAudioInfo = DeviceInfoUtil.getAudioInfo(context, includeNowPlaying) lastBatteryInfo = DeviceInfoUtil.getBatteryInfo(context) } else { diff --git a/app/src/main/java/com/sameerasw/airsync/utils/UDPDiscoveryManager.kt b/app/src/main/java/com/sameerasw/airsync/utils/UDPDiscoveryManager.kt index 237e8126..d9bd2e07 100644 --- a/app/src/main/java/com/sameerasw/airsync/utils/UDPDiscoveryManager.kt +++ b/app/src/main/java/com/sameerasw/airsync/utils/UDPDiscoveryManager.kt @@ -25,10 +25,10 @@ data class DiscoveredDevice( val type: String, // "mac" or "android" val lastSeen: Long = System.currentTimeMillis() ) { - //check if it has a local IP (non-Tailscale) + // check if it has a local IP (non-Tailscale) fun hasLocalIp(): Boolean = ips.any { !it.startsWith("100.") } - //check if it has a Tailscale IP + // check if it has a Tailscale IP fun hasTailscaleIp(): Boolean = ips.any { it.startsWith("100.") } // Best IP for connection @@ -415,7 +415,7 @@ object UDPDiscoveryManager { val packet = DatagramPacket( data, data.size, - InetAddress.getByName("255.55.255.255"), + InetAddress.getByName("255.255.255.255"), BROADCAST_PORT ) DatagramSocket(0, InetAddress.getByName(bindIp)).use { sender -> diff --git a/app/src/main/java/com/sameerasw/airsync/utils/WallpaperUtil.kt b/app/src/main/java/com/sameerasw/airsync/utils/WallpaperUtil.kt index de098607..4390336c 100644 --- a/app/src/main/java/com/sameerasw/airsync/utils/WallpaperUtil.kt +++ b/app/src/main/java/com/sameerasw/airsync/utils/WallpaperUtil.kt @@ -26,7 +26,7 @@ object WallpaperUtil { return try { val wallpaperManager = WallpaperManager.getInstance(context) - // Check permissions + // Check permissions if (!hasWallpaperPermissions(context)) { Log.w(TAG, "Missing wallpaper permissions") return null diff --git a/app/src/main/java/com/sameerasw/airsync/utils/WebSocketMessageHandler.kt b/app/src/main/java/com/sameerasw/airsync/utils/WebSocketMessageHandler.kt index d4fca035..31bc146b 100644 --- a/app/src/main/java/com/sameerasw/airsync/utils/WebSocketMessageHandler.kt +++ b/app/src/main/java/com/sameerasw/airsync/utils/WebSocketMessageHandler.kt @@ -328,7 +328,7 @@ object WebSocketMessageHandler { val success = NotificationDismissalUtil.dismissNotification(notificationId) val message = - if (success) "Notification dismissed" else "Failed to dismiss notification or notification not found" + if (success) "Notification dismissed" else "Failed to dismiss notification or not found" sendNotificationDismissalResponse(notificationId, success, message) } catch (e: Exception) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d752d8c8..5c507559 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,7 +14,7 @@ Disconnect Device discovery Allow other devices to find this phone in the background - Psst... You\'ve been syncing with your mac for a while now, Do you like the app? Rate and give your feedback. ♥ + Psst... You\'ve been syncing with your Mac for a while now, Do you like the app? Rate and give your feedback. ♥ Rate Now Maybe Later No device connected @@ -60,10 +60,10 @@ by sameerasw.com Let\'s Begin Permissions - This app allows you to sync notifications, clipboard, and media controls with your Mac. To provide these features, AirSync requires certain permissions.\n\nYou have full control over which features are enabled and which permissions are granted. We do not collect or store any of your personal data.\n\nAirSync is open source and this app is completly free to use. Always ensure you download it from Play Store or GitHub. + This app allows you to sync notifications, clipboard, and media controls with your Mac. To provide these features, AirSync requires certain permissions.\n\nYou have full control over which features are enabled and which permissions are granted. We do not collect or store any of your personal data.\n\nAirSync is open source and this app is completely free to use. Always ensure you download it from Play Store or GitHub. If you need any help, feel free to check the guides or reach out to the developer. I Understand - From the scan button in app or by long pressing the connection quick settings tile, scan the QR code displayed on the app on MacOS. + From the scan button in app or by long pressing the connection quick settings tile, scan the QR code displayed on the app on macOS. You can always find help and guides in the app settings. Let Me In Already Preferences From 3d0efe461c5cae9662dfc41ff7fcb424dd5b2d52 Mon Sep 17 00:00:00 2001 From: "sameerasw.com" Date: Tue, 17 Mar 2026 11:54:09 +0530 Subject: [PATCH 3/5] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 01acdead..b99220f5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # airsync-android -Android app for AirSync 3.0 built with Kotlin Jetpack Compose +Android app for AirSync built with Kotlin Jetpack Compose Min : Android 11 From 3f3b494f5c069327d132c3b3e6ace76de6c38051 Mon Sep 17 00:00:00 2001 From: "sameerasw.com" Date: Sun, 26 Jul 2026 00:27:19 +0530 Subject: [PATCH 4/5] Create SECURITY.md --- SECURITY.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..5fc1f56f --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +# Security Policy + +## Reporting a Vulnerability + +Please reach out to mail@sameerasw.com or GitHub issues if acceptable. From a5844914e92b0c692ef584a64744f3e108cd413e Mon Sep 17 00:00:00 2001 From: Renato Araujo Carneiro Date: Fri, 31 Jul 2026 11:06:13 -0300 Subject: [PATCH 5/5] fix(notifications): make action buttons work and keep IDs stable across restarts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #136 and #137. Plain action buttons (#136) WebSocketMessageHandler read the optional "text" field with optString(), which returns "" when the field is absent. performNotificationAction() branched on `replyText != null`, so an absent text was still treated as an inline reply: every plain action button went down the reply path, failed the remoteInputs check and returned false. Any client that omits "text" for non-reply actions could never invoke a button. The guard now tests for a non-empty string, and the handler normalizes an absent field to null. BleTransportBridge also dropped the optional reply text entirely, so inline replies never worked over BLE. It now forwards it, treating empty as "no reply" for consistency with the WebSocket path. Actions after a process restart (#137) activeNotifications was only populated from onNotificationPosted, so after a process restart nothing already in the shade was actionable — clients got "not found" for notifications that were still visible. onListenerConnected() now re-registers what is currently posted. Re-registering alone is not enough: generated IDs embed postTime, and during a session the first-seen ID is preserved across updates via keyToId. Once that in-memory map is gone, regenerating from the current postTime produces a different ID than the one the client holds, so its requests would still miss. The key -> id mapping is therefore persisted (sbn.key is stable across updates and restarts) and consulted when re-registering, so IDs a client obtained before the restart keep resolving. Stale entries are pruned against the currently active notifications on each listener connect. Verified on a Galaxy Z Fold7 (Android 16) against the macOS client: invoking a plain button now reaches pendingIntent.send() and the app-side effect happens, and an ID held by the client before a force-stop is restored identically afterwards. Co-Authored-By: Claude Opus 5 (1M context) --- .../airsync/data/ble/BleTransportBridge.kt | 6 ++- .../service/MediaNotificationListener.kt | 31 ++++++++++++++- .../utils/NotificationDismissalUtil.kt | 38 ++++++++++++++++++- .../airsync/utils/WebSocketMessageHandler.kt | 6 ++- 4 files changed, 76 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/sameerasw/airsync/data/ble/BleTransportBridge.kt b/app/src/main/java/com/sameerasw/airsync/data/ble/BleTransportBridge.kt index f46d2b06..781354e2 100644 --- a/app/src/main/java/com/sameerasw/airsync/data/ble/BleTransportBridge.kt +++ b/app/src/main/java/com/sameerasw/airsync/data/ble/BleTransportBridge.kt @@ -186,9 +186,13 @@ object BleTransportBridge { if (parts.size >= 2) { val id = parts[0] val actionName = parts[1] + // Reply text is optional and was previously dropped, so inline replies + // never worked over BLE. Empty means "plain button", not an empty reply. + val replyText = parts.getOrNull(2)?.takeIf { it.isNotEmpty() } com.sameerasw.airsync.utils.NotificationDismissalUtil.performNotificationAction( id, - actionName + actionName, + replyText ) } } diff --git a/app/src/main/java/com/sameerasw/airsync/service/MediaNotificationListener.kt b/app/src/main/java/com/sameerasw/airsync/service/MediaNotificationListener.kt index 0e8eca0d..1420e663 100644 --- a/app/src/main/java/com/sameerasw/airsync/service/MediaNotificationListener.kt +++ b/app/src/main/java/com/sameerasw/airsync/service/MediaNotificationListener.kt @@ -454,6 +454,33 @@ class MediaNotificationListener : NotificationListenerService() { } catch (e: Exception) { Log.e(TAG, "Failed to start AirSyncService from listener", e) } + + // Re-register notifications that were already in the shade before this + // process started. Without this, actions and dismissals coming from the + // client fail with "not found" for every notification that predates the + // listener connecting. The persisted key->id mapping recovers the exact + // ID the client already holds even when the notification was updated + // since (its postTime — embedded in generated IDs — changes on update, + // while sbn.key stays stable). + try { + val currentKeys = mutableSetOf() + activeNotifications?.forEach { sbn -> + currentKeys.add(sbn.key) + val title = sbn.notification?.extras?.getString(Notification.EXTRA_TITLE) ?: "" + val notificationId = NotificationDismissalUtil.getIdBySystemKey(sbn.key) + ?: NotificationDismissalUtil.getPersistedIdBySystemKey(sbn.key) + ?: NotificationDismissalUtil.generateNotificationId( + sbn.packageName, + title, + sbn.postTime + ) + NotificationDismissalUtil.storeNotification(notificationId, sbn) + } + NotificationDismissalUtil.prunePersistedMappings(currentKeys) + } catch (e: Exception) { + Log.e(TAG, "Failed to restore active notifications on listener connect", e) + } + updateMediaInfo() } @@ -656,8 +683,10 @@ class MediaNotificationListener : NotificationListenerService() { return@launch } - // Retrieve existing notification ID or generate a new one + // Retrieve existing notification ID (in-memory, then persisted + // from a previous process) or generate a new one val notificationId = NotificationDismissalUtil.getIdBySystemKey(sbn.key) + ?: NotificationDismissalUtil.getPersistedIdBySystemKey(sbn.key) ?: NotificationDismissalUtil.generateNotificationId( sbn.packageName, title, diff --git a/app/src/main/java/com/sameerasw/airsync/utils/NotificationDismissalUtil.kt b/app/src/main/java/com/sameerasw/airsync/utils/NotificationDismissalUtil.kt index 0329017a..0ca66278 100644 --- a/app/src/main/java/com/sameerasw/airsync/utils/NotificationDismissalUtil.kt +++ b/app/src/main/java/com/sameerasw/airsync/utils/NotificationDismissalUtil.kt @@ -11,6 +11,11 @@ import java.util.concurrent.ConcurrentHashMap object NotificationDismissalUtil { private const val TAG = "NotificationDismissalUtil" + // Persisted sbn.key -> generated ID mappings. IDs embed postTime, which + // changes when a notification is updated, while sbn.key stays stable — + // so this is what keeps client-held IDs valid across process restarts. + private const val ID_MAP_PREFS = "notification_id_mappings" + // Store active notifications with their IDs for dismissal or actions private val activeNotifications = ConcurrentHashMap() @@ -38,6 +43,7 @@ object NotificationDismissalUtil { // Keep reverse lookup so we can map sbn.key -> id on removal try { keyToId[notification.key] = id + idMapPrefs()?.edit()?.putString(notification.key, id)?.apply() } catch (_: Exception) { } Log.d(TAG, "Stored notification with ID: $id") @@ -48,6 +54,7 @@ object NotificationDismissalUtil { oldestKeys.forEach { oldId -> activeNotifications.remove(oldId)?.let { sbn -> keyToId.remove(sbn.key) + idMapPrefs()?.edit()?.remove(sbn.key)?.apply() } } } @@ -76,6 +83,7 @@ object NotificationDismissalUtil { // Cleanup maps after cancel is requested (onNotificationRemoved may also do this) activeNotifications.remove(notificationId) keyToId.remove(notification.key) + idMapPrefs()?.edit()?.remove(notification.key)?.apply() Log.d(TAG, "Successfully dismissed notification: $notificationId") true } else { @@ -132,7 +140,7 @@ object NotificationDismissalUtil { } val pendingIntent = target.actionIntent - if (replyText != null) { + if (!replyText.isNullOrEmpty()) { // Inline reply path val remoteInputs = target.remoteInputs if (remoteInputs.isNullOrEmpty()) { @@ -180,6 +188,33 @@ object NotificationDismissalUtil { null } + /** + * Lookup a generated ID persisted from a previous process, by system key. + */ + fun getPersistedIdBySystemKey(systemKey: String): String? = try { + idMapPrefs()?.getString(systemKey, null) + } catch (_: Exception) { + null + } + + /** + * Drop persisted mappings whose notifications are no longer active. + * Called after re-registering on listener connect. + */ + fun prunePersistedMappings(activeKeys: Set) { + try { + val prefs = idMapPrefs() ?: return + val editor = prefs.edit() + prefs.all.keys.filter { it !in activeKeys }.forEach { editor.remove(it) } + editor.apply() + } catch (_: Exception) { + } + } + + private fun idMapPrefs(): android.content.SharedPreferences? = + getNotificationListenerService()?.applicationContext + ?.getSharedPreferences(ID_MAP_PREFS, android.content.Context.MODE_PRIVATE) + /** * Lookup generated ID by StatusBarNotification */ @@ -202,6 +237,7 @@ object NotificationDismissalUtil { fun removeFromCaches(id: String) { activeNotifications.remove(id)?.let { sbn -> keyToId.remove(sbn.key) + idMapPrefs()?.edit()?.remove(sbn.key)?.apply() } testNotificationIds.remove(id) } diff --git a/app/src/main/java/com/sameerasw/airsync/utils/WebSocketMessageHandler.kt b/app/src/main/java/com/sameerasw/airsync/utils/WebSocketMessageHandler.kt index c6dee5de..c7da09a7 100644 --- a/app/src/main/java/com/sameerasw/airsync/utils/WebSocketMessageHandler.kt +++ b/app/src/main/java/com/sameerasw/airsync/utils/WebSocketMessageHandler.kt @@ -393,7 +393,9 @@ object WebSocketMessageHandler { // We accept either "name" or legacy "action" for action name val actionName = data.optString("name", data.optString("action", "")).ifEmpty { "" } - val replyText = data.optString("text") + // Absent "text" must stay null: an empty string would be taken as an + // inline reply and plain action buttons would never be invoked. + val replyText = data.optString("text").takeIf { it.isNotEmpty() } if (actionName.isEmpty()) { sendNotificationActionResponse( @@ -411,7 +413,7 @@ object WebSocketMessageHandler { replyText ) val message = if (success) { - if (replyText.isNotEmpty()) "Reply sent" else "Action invoked" + if (!replyText.isNullOrEmpty()) "Reply sent" else "Action invoked" } else { "Failed to perform action or notification not found" }