forked from akai/readest
fix(android): deliver Open-with intents reliably on cold start and re-mount (#4527)
Tapping an EPUB in the system file browser and choosing Readest could
silently fail to open the book in two distinct scenarios on Android:
1. Cold launch — the system delivers the ACTION_VIEW intent to
onCreate / onNewIntent before the JS layer has finished hydrating
and called addPluginListener('native-bridge', 'shared-intent', ...).
The upstream Tauri Plugin.trigger() drops events when the per-event
listener list is empty, so the intent vanishes. Fix this in
NativeBridgePlugin by queueing emits whose event has no listener,
then overriding registerListener so the queue is drained whenever
a listener becomes available.
2. React strict-mode re-mount — useAppUrlIngress had a one-shot
listened.current ref guard meant to avoid double registration. In
strict mode (and any subsequent effect re-run) the cleanup
unregister()'d the underlying native plugin listener but the next
mount short-circuited on the ref and never re-registered. The
shared-intent listener list ended up empty for the rest of the
session, so any subsequent Open-with intent went into the queue and
never came out. Drop the guard and let the effect register on every
mount; cleanup balances each registration.
This commit is contained in:
+57
-1
@@ -243,6 +243,22 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
|
||||
return list ?: emptyList()
|
||||
}
|
||||
|
||||
// shared-intent events emitted before the JS side has registered its
|
||||
// listener via addPluginListener("native-bridge", "shared-intent", ...)
|
||||
// would otherwise vanish — the upstream Plugin.trigger() drops events
|
||||
// when the per-event listener list is empty. This is exactly what
|
||||
// happens on cold launch via "Open with Readest": Android delivers the
|
||||
// ACTION_VIEW intent to onCreate / onNewIntent (the WebView is already
|
||||
// up but the JS app is mid-hydration), we emit it through triggerEvent
|
||||
// immediately, and useAppUrlIngress's addPluginListener call lands a
|
||||
// few hundred ms later — too late to receive the now-discarded event.
|
||||
//
|
||||
// To fix this we queue events whenever no listener is registered, then
|
||||
// replay the queue when a registerListener call lands for this event
|
||||
// (see overridden registerListener below).
|
||||
private val pendingEvents: MutableMap<String, MutableList<JSObject>> = mutableMapOf()
|
||||
private val pendingEventsLock = Any()
|
||||
|
||||
private fun emitSharedIntent(action: String, uris: List<Uri>) {
|
||||
val payload = JSObject().apply {
|
||||
put("action", action)
|
||||
@@ -250,7 +266,47 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
|
||||
uris.forEach { arr.put(it.toString()) }
|
||||
put("urls", arr)
|
||||
}
|
||||
triggerEvent("shared-intent", payload)
|
||||
emitOrQueue("shared-intent", payload)
|
||||
}
|
||||
|
||||
private fun emitOrQueue(eventName: String, payload: JSObject) {
|
||||
if (hasListener(eventName)) {
|
||||
triggerEvent(eventName, payload)
|
||||
} else {
|
||||
synchronized(pendingEventsLock) {
|
||||
val list = pendingEvents.getOrPut(eventName) { mutableListOf() }
|
||||
list.add(payload)
|
||||
}
|
||||
Log.d("NativeBridgePlugin", "Queued $eventName payload (no listener yet); pending size=${pendingEvents[eventName]?.size}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun registerListener(invoke: Invoke) {
|
||||
super.registerListener(invoke)
|
||||
// After super.registerListener, the listener is now wired up.
|
||||
// Drain any queued events for the same name so the JS side gets
|
||||
// events that were emitted between native start and listener
|
||||
// registration.
|
||||
// The event name lives on the invoke args, not directly accessible
|
||||
// post-resolve; instead, drain every queued bucket whose key has a
|
||||
// listener now. Cheap because there's at most one or two events.
|
||||
val toReplay = mutableListOf<Pair<String, JSObject>>()
|
||||
synchronized(pendingEventsLock) {
|
||||
val toRemove = mutableListOf<String>()
|
||||
for ((event, list) in pendingEvents) {
|
||||
if (hasListener(event)) {
|
||||
list.forEach { toReplay.add(event to it) }
|
||||
toRemove.add(event)
|
||||
}
|
||||
}
|
||||
toRemove.forEach { pendingEvents.remove(it) }
|
||||
}
|
||||
if (toReplay.isNotEmpty()) {
|
||||
Log.d("NativeBridgePlugin", "Replaying ${toReplay.size} queued event(s) after registerListener")
|
||||
for ((event, payload) in toReplay) {
|
||||
triggerEvent(event, payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Command
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { addPluginListener, PluginListener } from '@tauri-apps/api/core';
|
||||
import { onOpenUrl } from '@tauri-apps/plugin-deep-link';
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
@@ -55,12 +55,22 @@ interface SharedIntentPayload {
|
||||
*/
|
||||
export function useAppUrlIngress() {
|
||||
const { appService } = useEnv();
|
||||
const listened = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isTauriAppPlatform() || !appService) return;
|
||||
if (listened.current) return;
|
||||
listened.current = true;
|
||||
// Note: removed an old `listened.current` ref guard that tried to
|
||||
// make this effect a one-shot. In React strict mode (dev) the effect
|
||||
// mounts → cleans up → mounts again. With the guard, the second
|
||||
// mount short-circuited (ref still true) and DID NOT re-register
|
||||
// any listeners — but the previous cleanup had already
|
||||
// `unregister()`ed the underlying native plugin listener
|
||||
// (NativeBridgePlugin's listeners["shared-intent"] map). Net result:
|
||||
// the app ended up with zero shared-intent listeners on the native
|
||||
// side, so any "Open with Readest" intent that arrived AFTER cold
|
||||
// start was silently dropped (event got queued by our pending-events
|
||||
// workaround but, with no future register call, never replayed).
|
||||
// Letting the effect re-run on every mount cycle keeps the JS-side
|
||||
// and native-side listener bookkeeping in lockstep.
|
||||
|
||||
const dispatch = (urls: string[], action?: 'VIEW' | 'SEND') => {
|
||||
if (!urls.length) return;
|
||||
|
||||
Reference in New Issue
Block a user