fix(download): route verification notifications to pending challenges

This commit is contained in:
zarzet
2026-09-06 16:03:46 +07:00
parent 4fc837b94c
commit 1b0c28b91a
21 changed files with 621 additions and 65 deletions
@@ -1255,7 +1255,7 @@ class DownloadService : Service() {
settingsJson = settingsJson,
includeItems = true,
)
showNativeVerificationRequired()
showNativeVerificationRequired(request, result)
updateNotification(0L, 0L)
retryCurrentRequest = true
} else {
@@ -1622,7 +1622,7 @@ class DownloadService : Service() {
// replace this same notification ID while owning
// the interactive challenge; if Flutter is
// suspended, the native alert remains visible.
showNativeVerificationRequired()
showNativeVerificationRequired(request, result)
updateNotification(0L, 0L)
retryCurrentRequest = true
} else {
@@ -1987,11 +1987,14 @@ class DownloadService : Service() {
}
}
private fun showNativeVerificationRequired() {
private fun showNativeVerificationRequired(request: NativeDownloadRequest, result: JSONObject) {
val extensionId = result.optString("service").trim().ifEmpty {
JSONObject(request.requestJson).optString("service").trim()
}
val pendingIntent = PendingIntent.getActivity(
this,
0,
Intent(this, MainActivity::class.java),
VERIFICATION_REQUIRED_NOTIFICATION_ID,
VerificationNotificationIntent.create(this, extensionId, request.itemId),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
val builder = NotificationCompat.Builder(this, ALERT_CHANNEL_ID)
@@ -78,6 +78,7 @@ class MainActivity: FlutterFragmentActivity() {
private var backendChannel: MethodChannel? = null
private var libraryStorageReceiver: BroadcastReceiver? = null
private val pendingSessionGrantEvents = mutableListOf<Map<String, Any>>()
private var pendingVerificationNotification: String? = null
private var pendingSafTreeResult: MethodChannel.Result? = null
internal val safScanLock = Any()
internal var safScanProgress = SafScanProgress()
@@ -697,15 +698,27 @@ class MainActivity: FlutterFragmentActivity() {
// delegate looks it up by cached id (see getCachedEngineId above).
AudioServicePlugin.getFlutterEngine(this)
super.onCreate(savedInstanceState)
handleVerificationNotificationIntent(intent)
handleExtensionOAuthIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleVerificationNotificationIntent(intent)
handleExtensionOAuthIntent(intent)
}
private fun handleVerificationNotificationIntent(intent: Intent?) {
if (intent?.action != VerificationNotificationIntent.ACTION) return
val payload = intent.getStringExtra(VerificationNotificationIntent.PAYLOAD)
?.takeIf { it.isNotBlank() } ?: return
pendingVerificationNotification = payload
intent.removeExtra(VerificationNotificationIntent.PAYLOAD)
// Keep the payload until Dart is initialized and explicitly consumes it.
backendChannel?.invokeMethod("extensionVerificationNotificationTapped", null)
}
/**
* Deliver Spotify (or other) OAuth authorization code to the extension runtime
* and run its token exchange (e.g. completeSpotifyLogin). State is a one-time
@@ -951,6 +964,11 @@ class MainActivity: FlutterFragmentActivity() {
scope.launch {
try {
when (call.method) {
"consumeVerificationNotification" -> {
val payload = pendingVerificationNotification
pendingVerificationNotification = null
result.success(payload)
}
"ensureInstallMarker" -> {
val installState = withContext(Dispatchers.IO) {
ensureInstallMarker()
@@ -50,6 +50,10 @@ internal object NativeWorkerPolicy {
if (errorType.equals("verification_required", ignoreCase = true)) {
return true
}
when (errorType?.trim()?.lowercase()) {
"authentication_error", "provider_auth_failed",
"request_auth_invalid", "provider_reauth_required" -> return false
}
val message = errorMessage.orEmpty()
return message.contains("verification required", ignoreCase = true) ||
message.contains("challenge required", ignoreCase = true)
@@ -0,0 +1,23 @@
package com.zarz.spotiflac
import android.content.Context
import android.content.Intent
import org.json.JSONObject
import java.util.UUID
internal object VerificationNotificationIntent {
const val ACTION = "com.zarz.spotiflac.VERIFY_EXTENSION"
const val PAYLOAD = "verification_payload"
fun create(context: Context, extensionId: String, itemId: String): Intent {
val payload = JSONObject()
.put("kind", "extension_verification")
.put("extension_id", extensionId)
.put("item_id", itemId)
.put("tap_id", "native:${UUID.randomUUID()}")
return Intent(context, MainActivity::class.java)
.setAction(ACTION)
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
.putExtra(PAYLOAD, payload.toString())
}
}
@@ -106,6 +106,17 @@ class NativeWorkerPolicyTest {
@Test
fun verificationDetectionUsesTypeAndMessageFallback() {
for (errorType in listOf(
"authentication_error", "provider_auth_failed",
"request_auth_invalid", "provider_reauth_required",
)) {
assertFalse(
NativeWorkerPolicy.isVerificationRequired(
errorType = errorType,
errorMessage = "Provider unauthorized; verification required upstream",
),
)
}
assertTrue(
NativeWorkerPolicy.isVerificationRequired(
errorType = "verification_required",