From 0ee2a62adbed9a91f314b8cd9778505e3dfe1c89 Mon Sep 17 00:00:00 2001 From: Honey Date: Tue, 5 May 2026 22:42:11 +0200 Subject: [PATCH] fix: ensure foreground service + notification permission on Android 14 - startService() before bindService() so Android 14 allows foreground promotion (bind-only no longer satisfies the "started from foreground" requirement since API 34) - smarter POST_NOTIFICATIONS flow: re-ask once, then redirect to app notification settings instead of silently doing nothing after denial - track first-ask via SharedPreferences so the settings redirect only triggers on a second-or-later launch after denial Co-Authored-By: Claude Sonnet 4.6 --- .../kotlin/com/novasonic/app/MainActivity.kt | 56 +++++++++++++++++-- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/android/app/src/main/kotlin/com/novasonic/app/MainActivity.kt b/android/app/src/main/kotlin/com/novasonic/app/MainActivity.kt index 69e6b76..99eb5cd 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/MainActivity.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/MainActivity.kt @@ -2,9 +2,12 @@ package com.novasonic.app import android.Manifest import android.content.ComponentName +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.view.KeyEvent import android.webkit.WebChromeClient import android.webkit.WebResourceRequest @@ -37,6 +40,10 @@ class MainActivity : AppCompatActivity(), MusicService.StateListener { webBridge = WebBridge { mediaController } configureWebView() requestNotificationPermission() + // Explicitly start the service before binding so Android 14 lets it + // promote to foreground (bindService alone doesn't satisfy the + // "started from foreground" requirement for foreground services). + startService(Intent(this, MusicService::class.java)) connectToService() val url = BuildConfig.NOVASONIC_URL.ifEmpty { "https://demo.navidrome.org" } @@ -176,13 +183,50 @@ class MainActivity : AppCompatActivity(), MusicService.StateListener { // ─── Permissions ────────────────────────────────────────────────────────── private fun requestNotificationPermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return + when { ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) - != PackageManager.PERMISSION_GRANTED - ) { - ActivityCompat.requestPermissions( - this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 42 - ) + == PackageManager.PERMISSION_GRANTED -> return + + // System won't show the dialog again after one denial — send user to settings. + !ActivityCompat.shouldShowRequestPermissionRationale( + this, Manifest.permission.POST_NOTIFICATIONS + ) && _notifPermissionAskedBefore() -> openNotificationSettings() + + else -> { + markNotifPermissionAsked() + ActivityCompat.requestPermissions( + this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 42 + ) + } } } + + override fun onRequestPermissionsResult( + requestCode: Int, permissions: Array, grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == 42 && + grantResults.firstOrNull() == PackageManager.PERMISSION_DENIED + ) { + // Denied — open settings so the user can flip the toggle themselves. + openNotificationSettings() + } + } + + private fun openNotificationSettings() { + startActivity( + Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", packageName, null) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + ) + } + + private fun _notifPermissionAskedBefore() = + getSharedPreferences("ns_prefs", MODE_PRIVATE).getBoolean("notif_asked", false) + + private fun markNotifPermissionAsked() = + getSharedPreferences("ns_prefs", MODE_PRIVATE).edit() + .putBoolean("notif_asked", true).apply() }