mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-19 00:02:15 +02:00
fix(download): route verification notifications to pending challenges
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user