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 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-05 22:42:11 +02:00
1 parent 0e17d2e5db
commit 0ee2a62adb
1 file changed
+47 -3
@@ -2,9 +2,12 @@ package com.novasonic.app
import android.Manifest import android.Manifest
import android.content.ComponentName import android.content.ComponentName
import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import android.view.KeyEvent import android.view.KeyEvent
import android.webkit.WebChromeClient import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest import android.webkit.WebResourceRequest
@@ -37,6 +40,10 @@ class MainActivity : AppCompatActivity(), MusicService.StateListener {
webBridge = WebBridge { mediaController } webBridge = WebBridge { mediaController }
configureWebView() configureWebView()
requestNotificationPermission() 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() connectToService()
val url = BuildConfig.NOVASONIC_URL.ifEmpty { "https://demo.navidrome.org" } val url = BuildConfig.NOVASONIC_URL.ifEmpty { "https://demo.navidrome.org" }
@@ -176,13 +183,50 @@ class MainActivity : AppCompatActivity(), MusicService.StateListener {
// ─── Permissions ────────────────────────────────────────────────────────── // ─── Permissions ──────────────────────────────────────────────────────────
private fun requestNotificationPermission() { 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) ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED == 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( ActivityCompat.requestPermissions(
this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 42 this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 42
) )
} }
} }
} }
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<out String>, 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()
}