improve app link banner handling
This commit is contained in:
+6
-1
@@ -134,7 +134,12 @@ class GeckoAppLinksApiImpl(
|
||||
try {
|
||||
val components = GlobalComponents.components
|
||||
val list = components
|
||||
?.let { pendingStoreFor(it).getPending(owner).map(PendingAppLinkRequest::toPigeon) }
|
||||
?.let {
|
||||
val store = pendingStoreFor(it)
|
||||
store.getPending(owner).map { request ->
|
||||
request.toPigeon(store.expiresInMs(request))
|
||||
}
|
||||
}
|
||||
?: emptyList()
|
||||
callback(Result.success(list))
|
||||
} catch (e: Exception) {
|
||||
|
||||
+37
-4
@@ -56,6 +56,7 @@ class NativeAppLinkPromptFeature(
|
||||
private val sessionUseCases: SessionUseCases,
|
||||
) : LifecycleAwareFeature {
|
||||
private var dialog: AlertDialog? = null
|
||||
private var shownRequestId: Long? = null
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
override fun start() {
|
||||
@@ -70,15 +71,33 @@ class NativeAppLinkPromptFeature(
|
||||
dialog?.setOnDismissListener(null)
|
||||
dialog?.dismiss()
|
||||
dialog = null
|
||||
shownRequestId = null
|
||||
}
|
||||
|
||||
/**
|
||||
* A new pending request may have been created for this tab (interceptor, engine thread) after
|
||||
* [start] already queried. Re-check on the main thread; [showNext] is idempotent (a no-op while a
|
||||
* dialog is up or when nothing pends).
|
||||
* The tab's pending requests changed (interceptor created one on an engine thread after [start]
|
||||
* already queried, or the navigation middleware invalidated one). Re-check on the main thread;
|
||||
* [showNext] is idempotent (a no-op while a live dialog is up or when nothing pends).
|
||||
*/
|
||||
fun onPromptAvailable() {
|
||||
mainHandler.post { showNext() }
|
||||
mainHandler.post {
|
||||
dismissStaleDialog()
|
||||
showNext()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop a dialog whose request has since been invalidated — otherwise it stays on screen as a
|
||||
* dud whose Open button consumes nothing. Not a user dismissal: nothing is suppressed.
|
||||
*/
|
||||
private fun dismissStaleDialog() {
|
||||
val shown = shownRequestId ?: return
|
||||
if (store.peek(shown) != null) return
|
||||
dialog?.setOnCancelListener(null)
|
||||
dialog?.setOnDismissListener(null)
|
||||
dialog?.dismiss()
|
||||
dialog = null
|
||||
shownRequestId = null
|
||||
}
|
||||
|
||||
private fun showNext() {
|
||||
@@ -107,6 +126,14 @@ class NativeAppLinkPromptFeature(
|
||||
}
|
||||
.setOnDismissListener { dialog = null }
|
||||
.show()
|
||||
shownRequestId = request.requestId
|
||||
|
||||
// Expiry in the store is lazy, so nothing would take this dialog down when the request
|
||||
// lapses — its buttons would consume nothing. Retire it on its own deadline.
|
||||
mainHandler.postDelayed(
|
||||
{ dismissStaleDialog() },
|
||||
store.expiresInMs(request).coerceAtLeast(MIN_EXPIRY_TICK_MS),
|
||||
)
|
||||
}
|
||||
|
||||
private fun resolveOpen(request: PendingAppLinkRequest) {
|
||||
@@ -142,6 +169,12 @@ class NativeAppLinkPromptFeature(
|
||||
|
||||
private fun afterResolve() {
|
||||
dialog = null
|
||||
shownRequestId = null
|
||||
showNext()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
/** Never schedule a zero-delay expiry tick; a lapsed request would reschedule in a spin. */
|
||||
const val MIN_EXPIRY_TICK_MS = 250L
|
||||
}
|
||||
}
|
||||
|
||||
+60
-70
@@ -51,7 +51,7 @@ data class PendingAppLinkRequest(
|
||||
val scopeKey: String,
|
||||
val createdAt: Long,
|
||||
) {
|
||||
fun toPigeon(): AppLinkPromptRequest = AppLinkPromptRequest(
|
||||
fun toPigeon(expiresInMs: Long): AppLinkPromptRequest = AppLinkPromptRequest(
|
||||
requestId = requestId,
|
||||
owner = owner,
|
||||
tabId = tabId,
|
||||
@@ -72,6 +72,7 @@ data class PendingAppLinkRequest(
|
||||
engineSupportsScheme = engineSupportsScheme,
|
||||
scopeKey = scopeKey,
|
||||
),
|
||||
expiresInMs = expiresInMs,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -120,10 +121,32 @@ object PendingAppLinkStores {
|
||||
* Query + consume: requests stay until resolved, invalidated, or expired. The store
|
||||
* never holds its lock across a side effect — [consume] returns the request and the
|
||||
* caller performs launch/fallback after the lock is released.
|
||||
*
|
||||
* **A request's lifetime is deliberately not derived from navigation.** Three attempts to infer
|
||||
* "the user has left the page this prompt belongs to" from the [mozilla.components.browser.state.store.BrowserStore]
|
||||
* action stream all failed the same way, because every available signal is *per document* while a
|
||||
* single user-visible navigation spans several:
|
||||
* - comparing the committed URL's host to the request's anchor killed a banner on its own redirect
|
||||
* chain (`youtu.be` → `youtube.com`, shortener → destination) and killed a modal on the commit of
|
||||
* the very load it had interrupted — leaving a denied navigation with no dialog to un-stall it;
|
||||
* - counting load starts ([mozilla.components.browser.state.action.ContentAction.UpdateLoadingStateAction])
|
||||
* dismissed banners seconds in, because each redirected document starts its own load;
|
||||
* - settling on idle only moved that to the first `onPageStop`, which multi-document pages reach
|
||||
* long before the user is done with them.
|
||||
*
|
||||
* So navigation is not consulted at all. A request ends when the user answers it, when its tab
|
||||
* closes, when a newer banner for the same tab replaces it, or when it expires ([BANNER_EXPIRY_MS]
|
||||
* for the passive banner, [REQUEST_EXPIRY_MS] for a modal that is holding a navigation). The
|
||||
* residual risk — a banner outliving the page it was raised on — is bounded by that expiry and is
|
||||
* strictly safer than the alternatives: a lingering banner still names its target and still opens
|
||||
* exactly that link, whereas the invalidation heuristics produced prompts that silently did
|
||||
* nothing. Do not reintroduce URL- or load-state-derived invalidation without a signal that is
|
||||
* per *navigation* rather than per document.
|
||||
*/
|
||||
class PendingAppLinkStore(
|
||||
private val clock: MonotonicClock = MonotonicClock.SYSTEM,
|
||||
private val requestExpiryMs: Long = REQUEST_EXPIRY_MS,
|
||||
private val bannerExpiryMs: Long = BANNER_EXPIRY_MS,
|
||||
private val suppressionExpiryMs: Long = SUPPRESSION_EXPIRY_MS,
|
||||
private val dedupeWindowMs: Long = DEDUPE_WINDOW_MS,
|
||||
private val fallbackReentryMs: Long = FALLBACK_REENTRY_MS,
|
||||
@@ -180,11 +203,26 @@ class PendingAppLinkStore(
|
||||
scopeKey = input.scopeKey,
|
||||
createdAt = clock.elapsedRealtime(),
|
||||
)
|
||||
// At most one live banner per tab: the surface renders one anyway, and a second
|
||||
// app-link site visited in the same tab should replace the offer, not stack behind it.
|
||||
if (request.urlClass == AppLinkUrlClass.BANNER) {
|
||||
requests.values.removeAll {
|
||||
it.tabId == request.tabId && it.urlClass == AppLinkUrlClass.BANNER
|
||||
}
|
||||
}
|
||||
requests[request.requestId] = request
|
||||
return request
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* How long [request] has left before [sweepExpiredLocked] drops it. Handed to the surface so it
|
||||
* can retire the prompt on time — expiry is lazy (it only runs on query/consume), so a prompt
|
||||
* left on screen past its deadline would still render buttons that resolve to `stale`.
|
||||
*/
|
||||
fun expiresInMs(request: PendingAppLinkRequest): Long =
|
||||
(expiryFor(request) - (clock.elapsedRealtime() - request.createdAt)).coerceAtLeast(0L)
|
||||
|
||||
/** Non-consuming query of live requests for [owner]. */
|
||||
fun getPending(owner: AppLinkPromptOwner): List<PendingAppLinkRequest> {
|
||||
synchronized(lock) {
|
||||
@@ -212,80 +250,23 @@ class PendingAppLinkStore(
|
||||
synchronized(lock) { requests.remove(requestId) }
|
||||
}
|
||||
|
||||
/** Invalidate every pending request for a tab (tab close / replacement). */
|
||||
fun invalidateTab(tabId: String) {
|
||||
/**
|
||||
* Invalidate every pending request for a tab (tab close / replacement).
|
||||
*
|
||||
* @return the owners that had a request removed, so the caller can tell those surfaces to
|
||||
* re-query instead of leaving a dead prompt on screen.
|
||||
*/
|
||||
fun invalidateTab(tabId: String): Set<AppLinkPromptOwner> {
|
||||
synchronized(lock) {
|
||||
val owners = requests.values
|
||||
.filter { it.tabId == tabId }
|
||||
.mapTo(mutableSetOf()) { it.owner }
|
||||
requests.values.removeAll { it.tabId == tabId }
|
||||
suppression.keys.removeAll { it.startsWith("$tabId\u0000") }
|
||||
return owners
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A committed top-level navigation in [tabId]. A request whose own page committed
|
||||
* stays alive (that commit is the page the prompt sits on); a commit to a
|
||||
* *different site* invalidates the tab's pending requests (§2.6).
|
||||
*
|
||||
* Matching is by **normalised host**, not exact URL: the initial load a banner
|
||||
* rides on almost always commits at a redirected/normalised URL (`www`, trailing
|
||||
* slash, tracking params) that never equals the intercepted URL, so an exact-URL
|
||||
* check would invalidate every banner on its own page load. The anchor is the
|
||||
* target host for a banner (the page it loads) and the source host for a modal
|
||||
* (the page it is shown over, since the modal's own navigation was denied). When
|
||||
* no host can be derived, the request is kept and left to expiry/tab-close.
|
||||
*/
|
||||
fun onCommittedNavigation(tabId: String, committedUrl: String) {
|
||||
val committedHost = siteKey(committedUrl)
|
||||
synchronized(lock) {
|
||||
val removed = mutableListOf<Long>()
|
||||
requests.values.removeAll { request ->
|
||||
if (request.tabId != tabId) return@removeAll false
|
||||
val anchorHost = siteKey(if (request.isModal) request.sourceUrl else request.url)
|
||||
val invalidate = anchorHost != null && committedHost != null && anchorHost != committedHost
|
||||
if (invalidate) removed.add(request.requestId)
|
||||
invalidate
|
||||
}
|
||||
if (removed.isNotEmpty()) {
|
||||
logger.info(
|
||||
"onCommittedNavigation tab=$tabId committedHost=$committedHost invalidated=$removed",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Normalised, subdomain-stripped host for same-site comparison; null if underivable. */
|
||||
private fun siteKey(url: String?): String? {
|
||||
val rawHost = extractHost(url) ?: return null
|
||||
val normalized = AppLinkHostNormalizer.normalizeHost(rawHost) ?: return null
|
||||
return stripCommonSubDomains(normalized)
|
||||
}
|
||||
|
||||
private fun extractHost(url: String?): String? {
|
||||
if (url.isNullOrEmpty()) return null
|
||||
val schemeSep = url.indexOf("://")
|
||||
if (schemeSep < 0) return null
|
||||
val afterScheme = url.substring(schemeSep + 3)
|
||||
val end = afterScheme.indexOfFirst { it == '/' || it == '?' || it == '#' }
|
||||
var authority = if (end >= 0) afterScheme.substring(0, end) else afterScheme
|
||||
val at = authority.lastIndexOf('@')
|
||||
if (at >= 0) authority = authority.substring(at + 1)
|
||||
// Preserve a bracketed IPv6 literal; AppLinkHostNormalizer canonicalises it.
|
||||
if (authority.startsWith("[")) {
|
||||
val close = authority.indexOf(']')
|
||||
return if (close >= 0) authority.substring(0, close + 1) else null
|
||||
}
|
||||
val colon = authority.lastIndexOf(':')
|
||||
if (colon >= 0) authority = authority.substring(0, colon)
|
||||
return authority.ifEmpty { null }
|
||||
}
|
||||
|
||||
private fun stripCommonSubDomains(host: String): String = when {
|
||||
host.startsWith("www.") -> host.removePrefix("www.")
|
||||
host.startsWith("m.") -> host.removePrefix("m.")
|
||||
host.startsWith("mobile.") -> host.removePrefix("mobile.")
|
||||
host.startsWith("maps.") -> host.removePrefix("maps.")
|
||||
else -> host
|
||||
}
|
||||
|
||||
// ---- Suppression (§2.6) ----
|
||||
|
||||
fun recordSuppression(tabId: String, fingerprint: String) {
|
||||
@@ -326,15 +307,24 @@ class PendingAppLinkStore(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A banner is a passive offer sitting over a page the user keeps reading, so it is bounded by
|
||||
* time rather than by navigation (see the class KDoc). A modal blocks a navigation until it is
|
||||
* answered, so it keeps the long window.
|
||||
*/
|
||||
private fun expiryFor(request: PendingAppLinkRequest): Long =
|
||||
if (request.urlClass == AppLinkUrlClass.BANNER) bannerExpiryMs else requestExpiryMs
|
||||
|
||||
private fun sweepExpiredLocked() {
|
||||
val now = clock.elapsedRealtime()
|
||||
requests.values.removeAll { now > it.createdAt + requestExpiryMs }
|
||||
requests.values.removeAll { now > it.createdAt + expiryFor(it) }
|
||||
suppression.values.removeAll { now > it }
|
||||
fallbackReentry.values.removeAll { now > it }
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val REQUEST_EXPIRY_MS = 10 * 60 * 1000L
|
||||
const val BANNER_EXPIRY_MS = 90 * 1000L
|
||||
const val SUPPRESSION_EXPIRY_MS = 10 * 60 * 1000L
|
||||
const val DEDUPE_WINDOW_MS = 2000L
|
||||
const val FALLBACK_REENTRY_MS = 10 * 1000L
|
||||
|
||||
+38
-20
@@ -6,9 +6,12 @@
|
||||
|
||||
package eu.weblibre.flutter_mozilla_components.middleware
|
||||
|
||||
import eu.weblibre.flutter_mozilla_components.GlobalComponents
|
||||
import eu.weblibre.flutter_mozilla_components.applinks.NativeAppLinkPromptNotifier
|
||||
import eu.weblibre.flutter_mozilla_components.applinks.PendingAppLinkStore
|
||||
import eu.weblibre.flutter_mozilla_components.ext.EventSequence
|
||||
import eu.weblibre.flutter_mozilla_components.pigeons.AppLinkPromptOwner
|
||||
import mozilla.components.browser.state.action.BrowserAction
|
||||
import mozilla.components.browser.state.action.ContentAction
|
||||
import mozilla.components.browser.state.action.CustomTabListAction
|
||||
import mozilla.components.browser.state.action.EngineAction
|
||||
import mozilla.components.browser.state.action.TabListAction
|
||||
@@ -17,17 +20,18 @@ import mozilla.components.lib.state.Middleware
|
||||
import mozilla.components.lib.state.Store
|
||||
|
||||
/**
|
||||
* Observes the [BrowserStore] and drives [PendingAppLinkStore] invalidation and
|
||||
* suppression clearing (APP_LINKS_OWN_IMPLEMENTATION_PLAN.md §2.6):
|
||||
* Observes the [BrowserStore] and drives [PendingAppLinkStore] tab teardown and suppression
|
||||
* clearing (APP_LINKS_OWN_IMPLEMENTATION_PLAN.md §2.6):
|
||||
*
|
||||
* - a committed top-level navigation whose URL is not a request's own target
|
||||
* invalidates that request (a banner-class request's target committing keeps it
|
||||
* alive — that commit is the page the banner sits on);
|
||||
* - tab close / Custom Tab removal invalidates the tab's pending requests and
|
||||
* suppression;
|
||||
* - a new user-initiated/direct navigation (omnibar, bookmark, typed URL — which
|
||||
* dispatch a `LoadUrlAction`) clears the tab's suppression. In-page redirects
|
||||
* do not dispatch these actions, so the redirect-loop defence stays intact.
|
||||
* - tab close / Custom Tab removal invalidates the tab's pending requests and suppression, and
|
||||
* tells the owning surface to re-query so no dead prompt is left on screen;
|
||||
* - a new user-initiated/direct navigation (omnibar, bookmark, typed URL — which dispatch a
|
||||
* `LoadUrlAction`) clears the tab's suppression. In-page redirects do not dispatch these
|
||||
* actions, so the redirect-loop defence stays intact.
|
||||
*
|
||||
* It deliberately does **not** invalidate prompts on navigation: see the [PendingAppLinkStore]
|
||||
* KDoc for the three per-document signals that were tried and why none of them can express
|
||||
* "the user left this page".
|
||||
*/
|
||||
class AppLinkNavigationMiddleware(
|
||||
private val store: PendingAppLinkStore,
|
||||
@@ -38,13 +42,7 @@ class AppLinkNavigationMiddleware(
|
||||
action: BrowserAction,
|
||||
) {
|
||||
when (action) {
|
||||
is ContentAction.UpdateUrlAction -> {
|
||||
// A committed top-level navigation.
|
||||
this.store.onCommittedNavigation(action.sessionId, action.url)
|
||||
}
|
||||
|
||||
is EngineAction.LoadUrlAction -> {
|
||||
// App-initiated (direct) navigation — clears suppression.
|
||||
this.store.clearSuppressionForTab(action.tabId)
|
||||
}
|
||||
|
||||
@@ -53,15 +51,17 @@ class AppLinkNavigationMiddleware(
|
||||
}
|
||||
|
||||
is TabListAction.RemoveTabAction -> {
|
||||
this.store.invalidateTab(action.tabId)
|
||||
notifyInvalidated(action.tabId, this.store.invalidateTab(action.tabId))
|
||||
}
|
||||
|
||||
is TabListAction.RemoveTabsAction -> {
|
||||
action.tabIds.forEach(this.store::invalidateTab)
|
||||
action.tabIds.forEach { tabId ->
|
||||
notifyInvalidated(tabId, this.store.invalidateTab(tabId))
|
||||
}
|
||||
}
|
||||
|
||||
is CustomTabListAction.RemoveCustomTabAction -> {
|
||||
this.store.invalidateTab(action.tabId)
|
||||
notifyInvalidated(action.tabId, this.store.invalidateTab(action.tabId))
|
||||
}
|
||||
|
||||
else -> {}
|
||||
@@ -69,4 +69,22 @@ class AppLinkNavigationMiddleware(
|
||||
|
||||
next(action)
|
||||
}
|
||||
|
||||
/**
|
||||
* Nudge each affected surface to re-query the pending store. The event is the same
|
||||
* "prompts changed" signal the interceptor sends on creation — the query is authoritative, so
|
||||
* a lost event only delays the cleanup to the next resume.
|
||||
*/
|
||||
private fun notifyInvalidated(tabId: String, owners: Set<AppLinkPromptOwner>) {
|
||||
for (owner in owners) {
|
||||
when (owner) {
|
||||
AppLinkPromptOwner.NATIVE_EXTERNAL ->
|
||||
NativeAppLinkPromptNotifier.notifyPromptAvailable(tabId)
|
||||
|
||||
AppLinkPromptOwner.FLUTTER_BROWSER ->
|
||||
GlobalComponents.appLinkEvents
|
||||
?.onAppLinkPromptAvailable(EventSequence.next(), owner) { _ -> }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
-4
@@ -5929,7 +5929,14 @@ data class AppLinkPromptRequest (
|
||||
* unsupported-scheme prompt.
|
||||
*/
|
||||
val isModal: Boolean,
|
||||
val target: AppLinkTarget
|
||||
val target: AppLinkTarget,
|
||||
/**
|
||||
* Milliseconds until the native store drops this request, measured at query
|
||||
* time. The surface showing it must stop offering it by then: resolving an
|
||||
* expired request is a no-op, so a prompt left on screen past this becomes a
|
||||
* button that silently does nothing.
|
||||
*/
|
||||
val expiresInMs: Long
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
@@ -5945,7 +5952,8 @@ data class AppLinkPromptRequest (
|
||||
val canRemember = pigeonVar_list[8] as Boolean
|
||||
val isModal = pigeonVar_list[9] as Boolean
|
||||
val target = pigeonVar_list[10] as AppLinkTarget
|
||||
return AppLinkPromptRequest(requestId, owner, tabId, contextId, sourceUrl, isPrivate, isWallet, isProtectedContext, canRemember, isModal, target)
|
||||
val expiresInMs = pigeonVar_list[11] as Long
|
||||
return AppLinkPromptRequest(requestId, owner, tabId, contextId, sourceUrl, isPrivate, isWallet, isProtectedContext, canRemember, isModal, target, expiresInMs)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
@@ -5961,6 +5969,7 @@ data class AppLinkPromptRequest (
|
||||
canRemember,
|
||||
isModal,
|
||||
target,
|
||||
expiresInMs,
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
@@ -5971,7 +5980,7 @@ data class AppLinkPromptRequest (
|
||||
return true
|
||||
}
|
||||
val other = other as AppLinkPromptRequest
|
||||
return GeckoPigeonUtils.deepEquals(this.requestId, other.requestId) && GeckoPigeonUtils.deepEquals(this.owner, other.owner) && GeckoPigeonUtils.deepEquals(this.tabId, other.tabId) && GeckoPigeonUtils.deepEquals(this.contextId, other.contextId) && GeckoPigeonUtils.deepEquals(this.sourceUrl, other.sourceUrl) && GeckoPigeonUtils.deepEquals(this.isPrivate, other.isPrivate) && GeckoPigeonUtils.deepEquals(this.isWallet, other.isWallet) && GeckoPigeonUtils.deepEquals(this.isProtectedContext, other.isProtectedContext) && GeckoPigeonUtils.deepEquals(this.canRemember, other.canRemember) && GeckoPigeonUtils.deepEquals(this.isModal, other.isModal) && GeckoPigeonUtils.deepEquals(this.target, other.target)
|
||||
return GeckoPigeonUtils.deepEquals(this.requestId, other.requestId) && GeckoPigeonUtils.deepEquals(this.owner, other.owner) && GeckoPigeonUtils.deepEquals(this.tabId, other.tabId) && GeckoPigeonUtils.deepEquals(this.contextId, other.contextId) && GeckoPigeonUtils.deepEquals(this.sourceUrl, other.sourceUrl) && GeckoPigeonUtils.deepEquals(this.isPrivate, other.isPrivate) && GeckoPigeonUtils.deepEquals(this.isWallet, other.isWallet) && GeckoPigeonUtils.deepEquals(this.isProtectedContext, other.isProtectedContext) && GeckoPigeonUtils.deepEquals(this.canRemember, other.canRemember) && GeckoPigeonUtils.deepEquals(this.isModal, other.isModal) && GeckoPigeonUtils.deepEquals(this.target, other.target) && GeckoPigeonUtils.deepEquals(this.expiresInMs, other.expiresInMs)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
@@ -5987,10 +5996,11 @@ data class AppLinkPromptRequest (
|
||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.canRemember)
|
||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.isModal)
|
||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.target)
|
||||
result = 31 * result + GeckoPigeonUtils.deepHash(this.expiresInMs)
|
||||
return result
|
||||
}
|
||||
override fun toString(): String {
|
||||
return "AppLinkPromptRequest(requestId=$requestId, owner=$owner, tabId=$tabId, contextId=$contextId, sourceUrl=$sourceUrl, isPrivate=$isPrivate, isWallet=$isWallet, isProtectedContext=$isProtectedContext, canRemember=$canRemember, isModal=$isModal, target=$target)"
|
||||
return "AppLinkPromptRequest(requestId=$requestId, owner=$owner, tabId=$tabId, contextId=$contextId, sourceUrl=$sourceUrl, isPrivate=$isPrivate, isWallet=$isWallet, isProtectedContext=$isProtectedContext, canRemember=$canRemember, isModal=$isModal, target=$target, expiresInMs=$expiresInMs)"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+71
-23
@@ -103,33 +103,82 @@ class PendingAppLinkStoreTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bannerTargetCommitKeepsRequestButUnrelatedCommitInvalidates() {
|
||||
val store = PendingAppLinkStore(FakeClock())
|
||||
val banner = store.createRequest(
|
||||
newRequest(urlClass = AppLinkUrlClass.BANNER, url = "https://youtu.be/x"),
|
||||
fun aNewerBannerForTheTabReplacesTheOlderOne() {
|
||||
val clock = FakeClock()
|
||||
val store = PendingAppLinkStore(clock)
|
||||
val first = store.createRequest(
|
||||
newRequest(urlClass = AppLinkUrlClass.BANNER, url = "https://a.example/x"),
|
||||
)
|
||||
// The banner's own target committing keeps it alive.
|
||||
store.onCommittedNavigation("tab1", "https://youtu.be/x")
|
||||
assertNotNull(store.peek(banner.requestId))
|
||||
// An unrelated commit invalidates it.
|
||||
store.onCommittedNavigation("tab1", "https://example.com/other")
|
||||
assertNull(store.peek(banner.requestId))
|
||||
clock.now = 5000L // past the dedupe window, so this is a genuinely new offer
|
||||
val second = store.createRequest(
|
||||
newRequest(
|
||||
urlClass = AppLinkUrlClass.BANNER,
|
||||
url = "https://b.example/y",
|
||||
fingerprint = "fp2",
|
||||
),
|
||||
)
|
||||
|
||||
// One live banner per tab: visiting a second app-link site replaces the offer rather than
|
||||
// stacking behind it.
|
||||
assertNull(store.peek(first.requestId))
|
||||
assertNotNull(store.peek(second.requestId))
|
||||
assertEquals(1, store.getPending(AppLinkPromptOwner.FLUTTER_BROWSER).size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bannerSurvivesSameSiteRedirectAndNormalisation() {
|
||||
val store = PendingAppLinkStore(FakeClock())
|
||||
// The intercepted URL is rarely byte-identical to the committed one: the initial
|
||||
// load redirects/normalises (www stripped, tracking params added, trailing slash).
|
||||
val banner = store.createRequest(
|
||||
newRequest(urlClass = AppLinkUrlClass.BANNER, url = "https://www.reddit.com/r/foo"),
|
||||
fun aBannerForAnotherTabIsUntouched() {
|
||||
val clock = FakeClock()
|
||||
val store = PendingAppLinkStore(clock)
|
||||
val other = store.createRequest(
|
||||
newRequest(tabId = "tab2", urlClass = AppLinkUrlClass.BANNER, url = "https://a.example/x"),
|
||||
)
|
||||
store.onCommittedNavigation("tab1", "https://reddit.com/r/foo/?utm_source=share")
|
||||
assertNotNull(store.peek(banner.requestId))
|
||||
clock.now = 5000L
|
||||
store.createRequest(
|
||||
newRequest(tabId = "tab1", urlClass = AppLinkUrlClass.BANNER, url = "https://b.example/y"),
|
||||
)
|
||||
assertNotNull(store.peek(other.requestId))
|
||||
}
|
||||
|
||||
// A commit to a genuinely different site still invalidates it.
|
||||
store.onCommittedNavigation("tab1", "https://twitter.com/reddit")
|
||||
@Test
|
||||
fun bannersExpireSoonerThanModals() {
|
||||
val clock = FakeClock()
|
||||
val store = PendingAppLinkStore(
|
||||
clock,
|
||||
requestExpiryMs = 10_000L,
|
||||
bannerExpiryMs = 1_000L,
|
||||
)
|
||||
val banner = store.createRequest(
|
||||
newRequest(urlClass = AppLinkUrlClass.BANNER, url = "https://a.example/x"),
|
||||
)
|
||||
val modal = store.createRequest(
|
||||
newRequest(urlClass = AppLinkUrlClass.MODAL, fingerprint = "fp-modal"),
|
||||
)
|
||||
|
||||
// The banner is a passive offer bounded by time; the modal is holding a navigation open
|
||||
// and keeps the long window.
|
||||
clock.now = 1001L
|
||||
assertNull(store.peek(banner.requestId))
|
||||
assertNotNull(store.peek(modal.requestId))
|
||||
|
||||
clock.now = 10_001L
|
||||
assertNull(store.peek(modal.requestId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun remainingTtlIsReportedSoTheSurfaceCanRetireThePromptOnTime() {
|
||||
val clock = FakeClock()
|
||||
val store = PendingAppLinkStore(clock, bannerExpiryMs = 1_000L)
|
||||
val banner = store.createRequest(
|
||||
newRequest(urlClass = AppLinkUrlClass.BANNER, url = "https://a.example/x"),
|
||||
)
|
||||
assertEquals(1_000L, store.expiresInMs(banner))
|
||||
|
||||
clock.now = 400L
|
||||
assertEquals(600L, store.expiresInMs(banner))
|
||||
|
||||
// Never negative: a surface schedules on this value, and expiry itself is lazy.
|
||||
clock.now = 5_000L
|
||||
assertEquals(0L, store.expiresInMs(banner))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -137,7 +186,7 @@ class PendingAppLinkStoreTest {
|
||||
val store = PendingAppLinkStore(FakeClock())
|
||||
val request = store.createRequest(newRequest())
|
||||
store.recordSuppression("tab1", "fp1")
|
||||
store.invalidateTab("tab1")
|
||||
assertEquals(setOf(AppLinkPromptOwner.FLUTTER_BROWSER), store.invalidateTab("tab1"))
|
||||
assertNull(store.peek(request.requestId))
|
||||
assertFalse(store.isSuppressed("tab1", "fp1"))
|
||||
}
|
||||
@@ -148,8 +197,7 @@ class PendingAppLinkStoreTest {
|
||||
val store = PendingAppLinkStore(clock, suppressionExpiryMs = 1000L)
|
||||
store.recordSuppression("tab1", "fp1")
|
||||
assertTrue(store.isSuppressed("tab1", "fp1"))
|
||||
// Ordinary committed navigation does not clear it.
|
||||
store.onCommittedNavigation("tab1", "https://redirect.example")
|
||||
// A redirect within the current load does not clear it (no load start is dispatched).
|
||||
assertTrue(store.isSuppressed("tab1", "fp1"))
|
||||
// Direct navigation clears it.
|
||||
store.clearSuppressionForTab("tab1")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3050,6 +3050,12 @@ class AppLinkPromptRequest {
|
||||
final bool isModal;
|
||||
final AppLinkTarget target;
|
||||
|
||||
/// Milliseconds until the native store drops this request, measured at query
|
||||
/// time. The surface showing it must stop offering it by then: resolving an
|
||||
/// expired request is a no-op, so a prompt left on screen past this becomes a
|
||||
/// button that silently does nothing.
|
||||
final int expiresInMs;
|
||||
|
||||
const AppLinkPromptRequest({
|
||||
required this.requestId,
|
||||
required this.owner,
|
||||
@@ -3062,6 +3068,7 @@ class AppLinkPromptRequest {
|
||||
required this.canRemember,
|
||||
required this.isModal,
|
||||
required this.target,
|
||||
required this.expiresInMs,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,9 @@ import 'package:flutter/services.dart';
|
||||
import 'package:meta/meta.dart' show immutable, protected, visibleForTesting;
|
||||
|
||||
Object? _extractReplyValueOrThrow(
|
||||
List<Object?>? replyList,
|
||||
String channelName, {
|
||||
required bool isNullValid,
|
||||
List<Object?>? replyList,
|
||||
String channelName, {
|
||||
required bool isNullValid,
|
||||
}) {
|
||||
if (replyList == null) {
|
||||
throw PlatformException(
|
||||
@@ -34,11 +34,8 @@ Object? _extractReplyValueOrThrow(
|
||||
return replyList.firstOrNull;
|
||||
}
|
||||
|
||||
List<Object?> wrapResponse({
|
||||
Object? result,
|
||||
PlatformException? error,
|
||||
bool empty = false,
|
||||
}) {
|
||||
|
||||
List<Object?> wrapResponse({Object? result, PlatformException? error, bool empty = false}) {
|
||||
if (empty) {
|
||||
return <Object?>[];
|
||||
}
|
||||
@@ -47,7 +44,6 @@ List<Object?> wrapResponse({
|
||||
}
|
||||
return <Object?>[error.code, error.message, error.details];
|
||||
}
|
||||
|
||||
bool _deepEquals(Object? a, Object? b) {
|
||||
if (identical(a, b)) {
|
||||
return true;
|
||||
@@ -60,9 +56,8 @@ bool _deepEquals(Object? a, Object? b) {
|
||||
}
|
||||
if (a is List && b is List) {
|
||||
return a.length == b.length &&
|
||||
a.indexed.every(
|
||||
((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]),
|
||||
);
|
||||
a.indexed
|
||||
.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
|
||||
}
|
||||
if (a is Map && b is Map) {
|
||||
if (a.length != b.length) {
|
||||
@@ -111,6 +106,7 @@ int _deepHash(Object? value) {
|
||||
return value.hashCode;
|
||||
}
|
||||
|
||||
|
||||
enum SingboxProxyProfileType {
|
||||
socks,
|
||||
http,
|
||||
@@ -129,7 +125,13 @@ enum SingboxProxyProfileType {
|
||||
customOutbound,
|
||||
}
|
||||
|
||||
enum SingboxProxyRuntimeStatus { stopped, starting, running, stopping, error }
|
||||
enum SingboxProxyRuntimeStatus {
|
||||
stopped,
|
||||
starting,
|
||||
running,
|
||||
stopping,
|
||||
error,
|
||||
}
|
||||
|
||||
class SingboxProxyProfile {
|
||||
SingboxProxyProfile({
|
||||
@@ -155,12 +157,17 @@ class SingboxProxyProfile {
|
||||
String? secretJson;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[id, name, type, configJson, secretJson];
|
||||
return <Object?>[
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
configJson,
|
||||
secretJson,
|
||||
];
|
||||
}
|
||||
|
||||
Object encode() {
|
||||
return _toList();
|
||||
}
|
||||
return _toList(); }
|
||||
|
||||
static SingboxProxyProfile decode(Object result) {
|
||||
result as List<Object?>;
|
||||
@@ -182,11 +189,7 @@ class SingboxProxyProfile {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(id, other.id) &&
|
||||
_deepEquals(name, other.name) &&
|
||||
_deepEquals(type, other.type) &&
|
||||
_deepEquals(configJson, other.configJson) &&
|
||||
_deepEquals(secretJson, other.secretJson);
|
||||
return _deepEquals(id, other.id) && _deepEquals(name, other.name) && _deepEquals(type, other.type) && _deepEquals(configJson, other.configJson) && _deepEquals(secretJson, other.secretJson);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -236,8 +239,7 @@ class SingboxProxyRuntimeOptions {
|
||||
}
|
||||
|
||||
Object encode() {
|
||||
return _toList();
|
||||
}
|
||||
return _toList(); }
|
||||
|
||||
static SingboxProxyRuntimeOptions decode(Object result) {
|
||||
result as List<Object?>;
|
||||
@@ -252,17 +254,13 @@ class SingboxProxyRuntimeOptions {
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
bool operator ==(Object other) {
|
||||
if (other is! SingboxProxyRuntimeOptions ||
|
||||
other.runtimeType != runtimeType) {
|
||||
if (other is! SingboxProxyRuntimeOptions || other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(preferredBasePort, other.preferredBasePort) &&
|
||||
_deepEquals(blockUnmatchedTraffic, other.blockUnmatchedTraffic) &&
|
||||
_deepEquals(dnsConfig, other.dnsConfig) &&
|
||||
_deepEquals(bootstrapDohUrl, other.bootstrapDohUrl);
|
||||
return _deepEquals(preferredBasePort, other.preferredBasePort) && _deepEquals(blockUnmatchedTraffic, other.blockUnmatchedTraffic) && _deepEquals(dnsConfig, other.dnsConfig) && _deepEquals(bootstrapDohUrl, other.bootstrapDohUrl);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -333,8 +331,7 @@ class SingboxProxyDnsServerConfig {
|
||||
}
|
||||
|
||||
Object encode() {
|
||||
return _toList();
|
||||
}
|
||||
return _toList(); }
|
||||
|
||||
static SingboxProxyDnsServerConfig decode(Object result) {
|
||||
result as List<Object?>;
|
||||
@@ -352,20 +349,13 @@ class SingboxProxyDnsServerConfig {
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
bool operator ==(Object other) {
|
||||
if (other is! SingboxProxyDnsServerConfig ||
|
||||
other.runtimeType != runtimeType) {
|
||||
if (other is! SingboxProxyDnsServerConfig || other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(tag, other.tag) &&
|
||||
_deepEquals(address, other.address) &&
|
||||
_deepEquals(detourTag, other.detourTag) &&
|
||||
_deepEquals(matchDomainSuffixes, other.matchDomainSuffixes) &&
|
||||
_deepEquals(matchGeosites, other.matchGeosites) &&
|
||||
_deepEquals(matchOutbounds, other.matchOutbounds) &&
|
||||
_deepEquals(matchInbounds, other.matchInbounds);
|
||||
return _deepEquals(tag, other.tag) && _deepEquals(address, other.address) && _deepEquals(detourTag, other.detourTag) && _deepEquals(matchDomainSuffixes, other.matchDomainSuffixes) && _deepEquals(matchGeosites, other.matchGeosites) && _deepEquals(matchOutbounds, other.matchOutbounds) && _deepEquals(matchInbounds, other.matchInbounds);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -395,18 +385,20 @@ class SingboxProxyDnsConfig {
|
||||
String domainStrategy;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[servers, finalServerTag, domainStrategy];
|
||||
return <Object?>[
|
||||
servers,
|
||||
finalServerTag,
|
||||
domainStrategy,
|
||||
];
|
||||
}
|
||||
|
||||
Object encode() {
|
||||
return _toList();
|
||||
}
|
||||
return _toList(); }
|
||||
|
||||
static SingboxProxyDnsConfig decode(Object result) {
|
||||
result as List<Object?>;
|
||||
return SingboxProxyDnsConfig(
|
||||
servers: (result[0]! as List<Object?>)
|
||||
.cast<SingboxProxyDnsServerConfig>(),
|
||||
servers: (result[0]! as List<Object?>).cast<SingboxProxyDnsServerConfig>(),
|
||||
finalServerTag: result[1] as String?,
|
||||
domainStrategy: result[2]! as String,
|
||||
);
|
||||
@@ -421,9 +413,7 @@ class SingboxProxyDnsConfig {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(servers, other.servers) &&
|
||||
_deepEquals(finalServerTag, other.finalServerTag) &&
|
||||
_deepEquals(domainStrategy, other.domainStrategy);
|
||||
return _deepEquals(servers, other.servers) && _deepEquals(finalServerTag, other.finalServerTag) && _deepEquals(domainStrategy, other.domainStrategy);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -456,12 +446,17 @@ class SingboxProxyRuntimeEndpoint {
|
||||
String password;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[profileId, host, port, username, password];
|
||||
return <Object?>[
|
||||
profileId,
|
||||
host,
|
||||
port,
|
||||
username,
|
||||
password,
|
||||
];
|
||||
}
|
||||
|
||||
Object encode() {
|
||||
return _toList();
|
||||
}
|
||||
return _toList(); }
|
||||
|
||||
static SingboxProxyRuntimeEndpoint decode(Object result) {
|
||||
result as List<Object?>;
|
||||
@@ -477,18 +472,13 @@ class SingboxProxyRuntimeEndpoint {
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
bool operator ==(Object other) {
|
||||
if (other is! SingboxProxyRuntimeEndpoint ||
|
||||
other.runtimeType != runtimeType) {
|
||||
if (other is! SingboxProxyRuntimeEndpoint || other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(profileId, other.profileId) &&
|
||||
_deepEquals(host, other.host) &&
|
||||
_deepEquals(port, other.port) &&
|
||||
_deepEquals(username, other.username) &&
|
||||
_deepEquals(password, other.password);
|
||||
return _deepEquals(profileId, other.profileId) && _deepEquals(host, other.host) && _deepEquals(port, other.port) && _deepEquals(username, other.username) && _deepEquals(password, other.password);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -515,19 +505,21 @@ class SingboxProxyRuntimeState {
|
||||
String? message;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[status, endpoints, message];
|
||||
return <Object?>[
|
||||
status,
|
||||
endpoints,
|
||||
message,
|
||||
];
|
||||
}
|
||||
|
||||
Object encode() {
|
||||
return _toList();
|
||||
}
|
||||
return _toList(); }
|
||||
|
||||
static SingboxProxyRuntimeState decode(Object result) {
|
||||
result as List<Object?>;
|
||||
return SingboxProxyRuntimeState(
|
||||
status: result[0]! as SingboxProxyRuntimeStatus,
|
||||
endpoints: (result[1]! as List<Object?>)
|
||||
.cast<SingboxProxyRuntimeEndpoint>(),
|
||||
endpoints: (result[1]! as List<Object?>).cast<SingboxProxyRuntimeEndpoint>(),
|
||||
message: result[2] as String?,
|
||||
);
|
||||
}
|
||||
@@ -535,16 +527,13 @@ class SingboxProxyRuntimeState {
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
bool operator ==(Object other) {
|
||||
if (other is! SingboxProxyRuntimeState ||
|
||||
other.runtimeType != runtimeType) {
|
||||
if (other is! SingboxProxyRuntimeState || other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(status, other.status) &&
|
||||
_deepEquals(endpoints, other.endpoints) &&
|
||||
_deepEquals(message, other.message);
|
||||
return _deepEquals(status, other.status) && _deepEquals(endpoints, other.endpoints) && _deepEquals(message, other.message);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -558,41 +547,43 @@ class SingboxProxyRuntimeState {
|
||||
}
|
||||
|
||||
class SingboxProxyConfigResult {
|
||||
SingboxProxyConfigResult({required this.configJson, required this.endpoints});
|
||||
SingboxProxyConfigResult({
|
||||
required this.configJson,
|
||||
required this.endpoints,
|
||||
});
|
||||
|
||||
String configJson;
|
||||
|
||||
List<SingboxProxyRuntimeEndpoint> endpoints;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[configJson, endpoints];
|
||||
return <Object?>[
|
||||
configJson,
|
||||
endpoints,
|
||||
];
|
||||
}
|
||||
|
||||
Object encode() {
|
||||
return _toList();
|
||||
}
|
||||
return _toList(); }
|
||||
|
||||
static SingboxProxyConfigResult decode(Object result) {
|
||||
result as List<Object?>;
|
||||
return SingboxProxyConfigResult(
|
||||
configJson: result[0]! as String,
|
||||
endpoints: (result[1]! as List<Object?>)
|
||||
.cast<SingboxProxyRuntimeEndpoint>(),
|
||||
endpoints: (result[1]! as List<Object?>).cast<SingboxProxyRuntimeEndpoint>(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
bool operator ==(Object other) {
|
||||
if (other is! SingboxProxyConfigResult ||
|
||||
other.runtimeType != runtimeType) {
|
||||
if (other is! SingboxProxyConfigResult || other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(configJson, other.configJson) &&
|
||||
_deepEquals(endpoints, other.endpoints);
|
||||
return _deepEquals(configJson, other.configJson) && _deepEquals(endpoints, other.endpoints);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -622,12 +613,16 @@ class SingboxProxyLogMessage {
|
||||
String? profileId;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[level, message, timestamp, profileId];
|
||||
return <Object?>[
|
||||
level,
|
||||
message,
|
||||
timestamp,
|
||||
profileId,
|
||||
];
|
||||
}
|
||||
|
||||
Object encode() {
|
||||
return _toList();
|
||||
}
|
||||
return _toList(); }
|
||||
|
||||
static SingboxProxyLogMessage decode(Object result) {
|
||||
result as List<Object?>;
|
||||
@@ -648,10 +643,7 @@ class SingboxProxyLogMessage {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(level, other.level) &&
|
||||
_deepEquals(message, other.message) &&
|
||||
_deepEquals(timestamp, other.timestamp) &&
|
||||
_deepEquals(profileId, other.profileId);
|
||||
return _deepEquals(level, other.level) && _deepEquals(message, other.message) && _deepEquals(timestamp, other.timestamp) && _deepEquals(profileId, other.profileId);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -664,6 +656,7 @@ class SingboxProxyLogMessage {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class _PigeonCodec extends StandardMessageCodec {
|
||||
const _PigeonCodec();
|
||||
@override
|
||||
@@ -671,34 +664,34 @@ class _PigeonCodec extends StandardMessageCodec {
|
||||
if (value is int) {
|
||||
buffer.putUint8(4);
|
||||
buffer.putInt64(value);
|
||||
} else if (value is SingboxProxyProfileType) {
|
||||
} else if (value is SingboxProxyProfileType) {
|
||||
buffer.putUint8(129);
|
||||
writeValue(buffer, value.index);
|
||||
} else if (value is SingboxProxyRuntimeStatus) {
|
||||
} else if (value is SingboxProxyRuntimeStatus) {
|
||||
buffer.putUint8(130);
|
||||
writeValue(buffer, value.index);
|
||||
} else if (value is SingboxProxyProfile) {
|
||||
} else if (value is SingboxProxyProfile) {
|
||||
buffer.putUint8(131);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is SingboxProxyRuntimeOptions) {
|
||||
} else if (value is SingboxProxyRuntimeOptions) {
|
||||
buffer.putUint8(132);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is SingboxProxyDnsServerConfig) {
|
||||
} else if (value is SingboxProxyDnsServerConfig) {
|
||||
buffer.putUint8(133);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is SingboxProxyDnsConfig) {
|
||||
} else if (value is SingboxProxyDnsConfig) {
|
||||
buffer.putUint8(134);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is SingboxProxyRuntimeEndpoint) {
|
||||
} else if (value is SingboxProxyRuntimeEndpoint) {
|
||||
buffer.putUint8(135);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is SingboxProxyRuntimeState) {
|
||||
} else if (value is SingboxProxyRuntimeState) {
|
||||
buffer.putUint8(136);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is SingboxProxyConfigResult) {
|
||||
} else if (value is SingboxProxyConfigResult) {
|
||||
buffer.putUint8(137);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is SingboxProxyLogMessage) {
|
||||
} else if (value is SingboxProxyLogMessage) {
|
||||
buffer.putUint8(138);
|
||||
writeValue(buffer, value.encode());
|
||||
} else {
|
||||
@@ -741,13 +734,9 @@ class SingboxProxyApi {
|
||||
/// Constructor for [SingboxProxyApi]. The [binaryMessenger] named argument is
|
||||
/// available for dependency injection. If it is left null, the default
|
||||
/// BinaryMessenger will be used which routes to the host platform.
|
||||
SingboxProxyApi({
|
||||
BinaryMessenger? binaryMessenger,
|
||||
String messageChannelSuffix = '',
|
||||
}) : pigeonVar_binaryMessenger = binaryMessenger,
|
||||
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty
|
||||
? '.$messageChannelSuffix'
|
||||
: '';
|
||||
SingboxProxyApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
|
||||
: pigeonVar_binaryMessenger = binaryMessenger,
|
||||
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||
final BinaryMessenger? pigeonVar_binaryMessenger;
|
||||
|
||||
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||
@@ -755,97 +744,82 @@ class SingboxProxyApi {
|
||||
final String pigeonVar_messageChannelSuffix;
|
||||
|
||||
Future<String?> validateProfile(SingboxProxyProfile profile) async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyApi.validateProfile$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyApi.validateProfile$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
|
||||
<Object?>[profile],
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[profile]);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
);
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
)
|
||||
;
|
||||
return pigeonVar_replyValue as String?;
|
||||
}
|
||||
|
||||
Future<SingboxProxyConfigResult> buildConfig(
|
||||
List<SingboxProxyProfile> profiles,
|
||||
SingboxProxyRuntimeOptions options,
|
||||
) async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyApi.buildConfig$pigeonVar_messageChannelSuffix';
|
||||
Future<SingboxProxyConfigResult> buildConfig(List<SingboxProxyProfile> profiles, SingboxProxyRuntimeOptions options) async {
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyApi.buildConfig$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
|
||||
<Object?>[profiles, options],
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[profiles, options]);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
);
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
)
|
||||
;
|
||||
return pigeonVar_replyValue! as SingboxProxyConfigResult;
|
||||
}
|
||||
|
||||
Future<SingboxProxyRuntimeState> start(
|
||||
List<SingboxProxyProfile> profiles,
|
||||
SingboxProxyRuntimeOptions options,
|
||||
) async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyApi.start$pigeonVar_messageChannelSuffix';
|
||||
Future<SingboxProxyRuntimeState> start(List<SingboxProxyProfile> profiles, SingboxProxyRuntimeOptions options) async {
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyApi.start$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
|
||||
<Object?>[profiles, options],
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[profiles, options]);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
);
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
)
|
||||
;
|
||||
return pigeonVar_replyValue! as SingboxProxyRuntimeState;
|
||||
}
|
||||
|
||||
Future<void> stop(List<String> profileIds) async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyApi.stop$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyApi.stop$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
|
||||
<Object?>[profileIds],
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[profileIds]);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
);
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
Future<void> stopAll() async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyApi.stopAll$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyApi.stopAll$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
@@ -855,15 +829,15 @@ class SingboxProxyApi {
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
);
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
Future<SingboxProxyRuntimeState> getState() async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyApi.getState$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyApi.getState$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
@@ -873,10 +847,11 @@ class SingboxProxyApi {
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
);
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
)
|
||||
;
|
||||
return pigeonVar_replyValue! as SingboxProxyRuntimeState;
|
||||
}
|
||||
}
|
||||
@@ -888,62 +863,46 @@ abstract class SingboxProxyEventsApi {
|
||||
|
||||
void onLogMessage(SingboxProxyLogMessage message);
|
||||
|
||||
static void setUp(
|
||||
SingboxProxyEventsApi? api, {
|
||||
BinaryMessenger? binaryMessenger,
|
||||
String messageChannelSuffix = '',
|
||||
}) {
|
||||
messageChannelSuffix = messageChannelSuffix.isNotEmpty
|
||||
? '.$messageChannelSuffix'
|
||||
: '';
|
||||
static void setUp(SingboxProxyEventsApi? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) {
|
||||
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||
{
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyEventsApi.onStateChanged$messageChannelSuffix',
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger,
|
||||
);
|
||||
'dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyEventsApi.onStateChanged$messageChannelSuffix', pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger);
|
||||
if (api == null) {
|
||||
pigeonVar_channel.setMessageHandler(null);
|
||||
} else {
|
||||
pigeonVar_channel.setMessageHandler((Object? message) async {
|
||||
final List<Object?> args = message! as List<Object?>;
|
||||
final SingboxProxyRuntimeState arg_state =
|
||||
args[0]! as SingboxProxyRuntimeState;
|
||||
final SingboxProxyRuntimeState arg_state = args[0]! as SingboxProxyRuntimeState;
|
||||
try {
|
||||
api.onStateChanged(arg_state);
|
||||
return wrapResponse(empty: true);
|
||||
} on PlatformException catch (e) {
|
||||
return wrapResponse(error: e);
|
||||
} catch (e) {
|
||||
return wrapResponse(
|
||||
error: PlatformException(code: 'error', message: e.toString()),
|
||||
);
|
||||
} catch (e) {
|
||||
return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
{
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyEventsApi.onLogMessage$messageChannelSuffix',
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger,
|
||||
);
|
||||
'dev.flutter.pigeon.flutter_singbox_proxy.SingboxProxyEventsApi.onLogMessage$messageChannelSuffix', pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger);
|
||||
if (api == null) {
|
||||
pigeonVar_channel.setMessageHandler(null);
|
||||
} else {
|
||||
pigeonVar_channel.setMessageHandler((Object? message) async {
|
||||
final List<Object?> args = message! as List<Object?>;
|
||||
final SingboxProxyLogMessage arg_message =
|
||||
args[0]! as SingboxProxyLogMessage;
|
||||
final SingboxProxyLogMessage arg_message = args[0]! as SingboxProxyLogMessage;
|
||||
try {
|
||||
api.onLogMessage(arg_message);
|
||||
return wrapResponse(empty: true);
|
||||
} on PlatformException catch (e) {
|
||||
return wrapResponse(error: e);
|
||||
} catch (e) {
|
||||
return wrapResponse(
|
||||
error: PlatformException(code: 'error', message: e.toString()),
|
||||
);
|
||||
} catch (e) {
|
||||
return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ import 'package:flutter/services.dart';
|
||||
import 'package:meta/meta.dart' show immutable, protected, visibleForTesting;
|
||||
|
||||
Object? _extractReplyValueOrThrow(
|
||||
List<Object?>? replyList,
|
||||
String channelName, {
|
||||
required bool isNullValid,
|
||||
List<Object?>? replyList,
|
||||
String channelName, {
|
||||
required bool isNullValid,
|
||||
}) {
|
||||
if (replyList == null) {
|
||||
throw PlatformException(
|
||||
@@ -34,11 +34,8 @@ Object? _extractReplyValueOrThrow(
|
||||
return replyList.firstOrNull;
|
||||
}
|
||||
|
||||
List<Object?> wrapResponse({
|
||||
Object? result,
|
||||
PlatformException? error,
|
||||
bool empty = false,
|
||||
}) {
|
||||
|
||||
List<Object?> wrapResponse({Object? result, PlatformException? error, bool empty = false}) {
|
||||
if (empty) {
|
||||
return <Object?>[];
|
||||
}
|
||||
@@ -47,7 +44,6 @@ List<Object?> wrapResponse({
|
||||
}
|
||||
return <Object?>[error.code, error.message, error.details];
|
||||
}
|
||||
|
||||
bool _deepEquals(Object? a, Object? b) {
|
||||
if (identical(a, b)) {
|
||||
return true;
|
||||
@@ -60,9 +56,8 @@ bool _deepEquals(Object? a, Object? b) {
|
||||
}
|
||||
if (a is List && b is List) {
|
||||
return a.length == b.length &&
|
||||
a.indexed.every(
|
||||
((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]),
|
||||
);
|
||||
a.indexed
|
||||
.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
|
||||
}
|
||||
if (a is Map && b is Map) {
|
||||
if (a.length != b.length) {
|
||||
@@ -111,29 +106,23 @@ int _deepHash(Object? value) {
|
||||
return value.hashCode;
|
||||
}
|
||||
|
||||
|
||||
/// Transport types for Tor connections
|
||||
enum TransportType {
|
||||
/// Direct Tor connection (no bridges)
|
||||
none,
|
||||
|
||||
/// obfs4 pluggable transport
|
||||
obfs4,
|
||||
|
||||
/// Snowflake pluggable transport (default broker)
|
||||
snowflake,
|
||||
|
||||
/// Snowflake via AMP cache
|
||||
snowflakeAmp,
|
||||
|
||||
/// Meek pluggable transport
|
||||
meek,
|
||||
|
||||
/// Meek via Azure CDN
|
||||
meekAzure,
|
||||
|
||||
/// WebTunnel pluggable transport
|
||||
webtunnel,
|
||||
|
||||
/// Custom bridge lines (passthrough)
|
||||
custom,
|
||||
}
|
||||
@@ -174,8 +163,7 @@ class TorConfiguration {
|
||||
}
|
||||
|
||||
Object encode() {
|
||||
return _toList();
|
||||
}
|
||||
return _toList(); }
|
||||
|
||||
static TorConfiguration decode(Object result) {
|
||||
result as List<Object?>;
|
||||
@@ -197,11 +185,7 @@ class TorConfiguration {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(transport, other.transport) &&
|
||||
_deepEquals(bridgeLines, other.bridgeLines) &&
|
||||
_deepEquals(entryNodeCountries, other.entryNodeCountries) &&
|
||||
_deepEquals(exitNodeCountries, other.exitNodeCountries) &&
|
||||
_deepEquals(strictNodes, other.strictNodes);
|
||||
return _deepEquals(transport, other.transport) && _deepEquals(bridgeLines, other.bridgeLines) && _deepEquals(entryNodeCountries, other.entryNodeCountries) && _deepEquals(exitNodeCountries, other.exitNodeCountries) && _deepEquals(strictNodes, other.strictNodes);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -250,8 +234,7 @@ class TorStatus {
|
||||
}
|
||||
|
||||
Object encode() {
|
||||
return _toList();
|
||||
}
|
||||
return _toList(); }
|
||||
|
||||
static TorStatus decode(Object result) {
|
||||
result as List<Object?>;
|
||||
@@ -273,11 +256,7 @@ class TorStatus {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(isRunning, other.isRunning) &&
|
||||
_deepEquals(socksPort, other.socksPort) &&
|
||||
_deepEquals(bootstrapProgress, other.bootstrapProgress) &&
|
||||
_deepEquals(currentCircuit, other.currentCircuit) &&
|
||||
_deepEquals(exitNodeCountry, other.exitNodeCountry);
|
||||
return _deepEquals(isRunning, other.isRunning) && _deepEquals(socksPort, other.socksPort) && _deepEquals(bootstrapProgress, other.bootstrapProgress) && _deepEquals(currentCircuit, other.currentCircuit) && _deepEquals(exitNodeCountry, other.exitNodeCountry);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -308,12 +287,15 @@ class TorLogMessage {
|
||||
int timestamp;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[severity, message, timestamp];
|
||||
return <Object?>[
|
||||
severity,
|
||||
message,
|
||||
timestamp,
|
||||
];
|
||||
}
|
||||
|
||||
Object encode() {
|
||||
return _toList();
|
||||
}
|
||||
return _toList(); }
|
||||
|
||||
static TorLogMessage decode(Object result) {
|
||||
result as List<Object?>;
|
||||
@@ -333,9 +315,7 @@ class TorLogMessage {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(severity, other.severity) &&
|
||||
_deepEquals(message, other.message) &&
|
||||
_deepEquals(timestamp, other.timestamp);
|
||||
return _deepEquals(severity, other.severity) && _deepEquals(message, other.message) && _deepEquals(timestamp, other.timestamp);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -348,6 +328,7 @@ class TorLogMessage {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class _PigeonCodec extends StandardMessageCodec {
|
||||
const _PigeonCodec();
|
||||
@override
|
||||
@@ -355,16 +336,16 @@ class _PigeonCodec extends StandardMessageCodec {
|
||||
if (value is int) {
|
||||
buffer.putUint8(4);
|
||||
buffer.putInt64(value);
|
||||
} else if (value is TransportType) {
|
||||
} else if (value is TransportType) {
|
||||
buffer.putUint8(129);
|
||||
writeValue(buffer, value.index);
|
||||
} else if (value is TorConfiguration) {
|
||||
} else if (value is TorConfiguration) {
|
||||
buffer.putUint8(130);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is TorStatus) {
|
||||
} else if (value is TorStatus) {
|
||||
buffer.putUint8(131);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is TorLogMessage) {
|
||||
} else if (value is TorLogMessage) {
|
||||
buffer.putUint8(132);
|
||||
writeValue(buffer, value.encode());
|
||||
} else {
|
||||
@@ -396,10 +377,8 @@ class TorApi {
|
||||
/// available for dependency injection. If it is left null, the default
|
||||
/// BinaryMessenger will be used which routes to the host platform.
|
||||
TorApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
|
||||
: pigeonVar_binaryMessenger = binaryMessenger,
|
||||
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty
|
||||
? '.$messageChannelSuffix'
|
||||
: '';
|
||||
: pigeonVar_binaryMessenger = binaryMessenger,
|
||||
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||
final BinaryMessenger? pigeonVar_binaryMessenger;
|
||||
|
||||
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||
@@ -409,30 +388,27 @@ class TorApi {
|
||||
/// Start Tor with the given configuration
|
||||
/// Returns a Future to avoid blocking the main thread
|
||||
Future<int> startTor(TorConfiguration config) async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.flutter_tor.TorApi.startTor$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tor.TorApi.startTor$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
|
||||
<Object?>[config],
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[config]);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
);
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
)
|
||||
;
|
||||
return pigeonVar_replyValue! as int;
|
||||
}
|
||||
|
||||
/// Stop Tor
|
||||
Future<void> stopTor() async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.flutter_tor.TorApi.stopTor$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tor.TorApi.stopTor$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
@@ -442,16 +418,16 @@ class TorApi {
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
);
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
/// Get current status
|
||||
Future<TorStatus> getStatus() async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.flutter_tor.TorApi.getStatus$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tor.TorApi.getStatus$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
@@ -461,17 +437,17 @@ class TorApi {
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
);
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
)
|
||||
;
|
||||
return pigeonVar_replyValue! as TorStatus;
|
||||
}
|
||||
|
||||
/// Request a new Tor identity (new circuit)
|
||||
Future<void> requestNewIdentity() async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.flutter_tor.TorApi.requestNewIdentity$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tor.TorApi.requestNewIdentity$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
@@ -481,10 +457,11 @@ class TorApi {
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
);
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,20 +475,12 @@ abstract class TorLogApi {
|
||||
/// Called when status changes
|
||||
void onStatusChanged(TorStatus status);
|
||||
|
||||
static void setUp(
|
||||
TorLogApi? api, {
|
||||
BinaryMessenger? binaryMessenger,
|
||||
String messageChannelSuffix = '',
|
||||
}) {
|
||||
messageChannelSuffix = messageChannelSuffix.isNotEmpty
|
||||
? '.$messageChannelSuffix'
|
||||
: '';
|
||||
static void setUp(TorLogApi? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) {
|
||||
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||
{
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.flutter_tor.TorLogApi.onLogMessage$messageChannelSuffix',
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger,
|
||||
);
|
||||
'dev.flutter.pigeon.flutter_tor.TorLogApi.onLogMessage$messageChannelSuffix', pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger);
|
||||
if (api == null) {
|
||||
pigeonVar_channel.setMessageHandler(null);
|
||||
} else {
|
||||
@@ -523,20 +492,16 @@ abstract class TorLogApi {
|
||||
return wrapResponse(empty: true);
|
||||
} on PlatformException catch (e) {
|
||||
return wrapResponse(error: e);
|
||||
} catch (e) {
|
||||
return wrapResponse(
|
||||
error: PlatformException(code: 'error', message: e.toString()),
|
||||
);
|
||||
} catch (e) {
|
||||
return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
{
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.flutter_tor.TorLogApi.onStatusChanged$messageChannelSuffix',
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger,
|
||||
);
|
||||
'dev.flutter.pigeon.flutter_tor.TorLogApi.onStatusChanged$messageChannelSuffix', pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger);
|
||||
if (api == null) {
|
||||
pigeonVar_channel.setMessageHandler(null);
|
||||
} else {
|
||||
@@ -548,10 +513,8 @@ abstract class TorLogApi {
|
||||
return wrapResponse(empty: true);
|
||||
} on PlatformException catch (e) {
|
||||
return wrapResponse(error: e);
|
||||
} catch (e) {
|
||||
return wrapResponse(
|
||||
error: PlatformException(code: 'error', message: e.toString()),
|
||||
);
|
||||
} catch (e) {
|
||||
return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -563,13 +526,9 @@ class IPtProxyController {
|
||||
/// Constructor for [IPtProxyController]. The [binaryMessenger] named argument is
|
||||
/// available for dependency injection. If it is left null, the default
|
||||
/// BinaryMessenger will be used which routes to the host platform.
|
||||
IPtProxyController({
|
||||
BinaryMessenger? binaryMessenger,
|
||||
String messageChannelSuffix = '',
|
||||
}) : pigeonVar_binaryMessenger = binaryMessenger,
|
||||
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty
|
||||
? '.$messageChannelSuffix'
|
||||
: '';
|
||||
IPtProxyController({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
|
||||
: pigeonVar_binaryMessenger = binaryMessenger,
|
||||
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||
final BinaryMessenger? pigeonVar_binaryMessenger;
|
||||
|
||||
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||
@@ -577,43 +536,39 @@ class IPtProxyController {
|
||||
final String pigeonVar_messageChannelSuffix;
|
||||
|
||||
Future<int> start(TransportType proxyType, String proxy) async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.flutter_tor.IPtProxyController.start$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tor.IPtProxyController.start$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
|
||||
<Object?>[proxyType, proxy],
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[proxyType, proxy]);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
);
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
)
|
||||
;
|
||||
return pigeonVar_replyValue! as int;
|
||||
}
|
||||
|
||||
Future<void> stop(TransportType proxyType) async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.flutter_tor.IPtProxyController.stop$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_tor.IPtProxyController.stop$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
|
||||
<Object?>[proxyType],
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[proxyType]);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
);
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ import 'package:flutter/services.dart';
|
||||
import 'package:meta/meta.dart' show immutable, protected, visibleForTesting;
|
||||
|
||||
Object? _extractReplyValueOrThrow(
|
||||
List<Object?>? replyList,
|
||||
String channelName, {
|
||||
required bool isNullValid,
|
||||
List<Object?>? replyList,
|
||||
String channelName, {
|
||||
required bool isNullValid,
|
||||
}) {
|
||||
if (replyList == null) {
|
||||
throw PlatformException(
|
||||
@@ -46,9 +46,8 @@ bool _deepEquals(Object? a, Object? b) {
|
||||
}
|
||||
if (a is List && b is List) {
|
||||
return a.length == b.length &&
|
||||
a.indexed.every(
|
||||
((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]),
|
||||
);
|
||||
a.indexed
|
||||
.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
|
||||
}
|
||||
if (a is Map && b is Map) {
|
||||
if (a.length != b.length) {
|
||||
@@ -97,20 +96,26 @@ int _deepHash(Object? value) {
|
||||
return value.hashCode;
|
||||
}
|
||||
|
||||
|
||||
class LocalizedResult {
|
||||
LocalizedResult({required this.languageName, this.countryName});
|
||||
LocalizedResult({
|
||||
required this.languageName,
|
||||
this.countryName,
|
||||
});
|
||||
|
||||
String languageName;
|
||||
|
||||
String? countryName;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[languageName, countryName];
|
||||
return <Object?>[
|
||||
languageName,
|
||||
countryName,
|
||||
];
|
||||
}
|
||||
|
||||
Object encode() {
|
||||
return _toList();
|
||||
}
|
||||
return _toList(); }
|
||||
|
||||
static LocalizedResult decode(Object result) {
|
||||
result as List<Object?>;
|
||||
@@ -129,8 +134,7 @@ class LocalizedResult {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(languageName, other.languageName) &&
|
||||
_deepEquals(countryName, other.countryName);
|
||||
return _deepEquals(languageName, other.languageName) && _deepEquals(countryName, other.countryName);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -143,6 +147,7 @@ class LocalizedResult {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class _PigeonCodec extends StandardMessageCodec {
|
||||
const _PigeonCodec();
|
||||
@override
|
||||
@@ -150,7 +155,7 @@ class _PigeonCodec extends StandardMessageCodec {
|
||||
if (value is int) {
|
||||
buffer.putUint8(4);
|
||||
buffer.putInt64(value);
|
||||
} else if (value is LocalizedResult) {
|
||||
} else if (value is LocalizedResult) {
|
||||
buffer.putUint8(129);
|
||||
writeValue(buffer, value.encode());
|
||||
} else {
|
||||
@@ -173,40 +178,31 @@ class LocaleResolver {
|
||||
/// Constructor for [LocaleResolver]. The [binaryMessenger] named argument is
|
||||
/// available for dependency injection. If it is left null, the default
|
||||
/// BinaryMessenger will be used which routes to the host platform.
|
||||
LocaleResolver({
|
||||
BinaryMessenger? binaryMessenger,
|
||||
String messageChannelSuffix = '',
|
||||
}) : pigeonVar_binaryMessenger = binaryMessenger,
|
||||
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty
|
||||
? '.$messageChannelSuffix'
|
||||
: '';
|
||||
LocaleResolver({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
|
||||
: pigeonVar_binaryMessenger = binaryMessenger,
|
||||
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||
final BinaryMessenger? pigeonVar_binaryMessenger;
|
||||
|
||||
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||
|
||||
final String pigeonVar_messageChannelSuffix;
|
||||
|
||||
Future<LocalizedResult> resolve(
|
||||
String languageTag,
|
||||
String targetLangouageTag,
|
||||
) async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.locale_resolver.LocaleResolver.resolve$pigeonVar_messageChannelSuffix';
|
||||
Future<LocalizedResult> resolve(String languageTag, String targetLangouageTag) async {
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.locale_resolver.LocaleResolver.resolve$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
|
||||
<Object?>[languageTag, targetLangouageTag],
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[languageTag, targetLangouageTag]);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
);
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
)
|
||||
;
|
||||
return pigeonVar_replyValue! as LocalizedResult;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ import 'package:flutter/services.dart';
|
||||
import 'package:meta/meta.dart' show immutable, protected, visibleForTesting;
|
||||
|
||||
Object? _extractReplyValueOrThrow(
|
||||
List<Object?>? replyList,
|
||||
String channelName, {
|
||||
required bool isNullValid,
|
||||
List<Object?>? replyList,
|
||||
String channelName, {
|
||||
required bool isNullValid,
|
||||
}) {
|
||||
if (replyList == null) {
|
||||
throw PlatformException(
|
||||
@@ -34,11 +34,8 @@ Object? _extractReplyValueOrThrow(
|
||||
return replyList.firstOrNull;
|
||||
}
|
||||
|
||||
List<Object?> wrapResponse({
|
||||
Object? result,
|
||||
PlatformException? error,
|
||||
bool empty = false,
|
||||
}) {
|
||||
|
||||
List<Object?> wrapResponse({Object? result, PlatformException? error, bool empty = false}) {
|
||||
if (empty) {
|
||||
return <Object?>[];
|
||||
}
|
||||
@@ -47,7 +44,6 @@ List<Object?> wrapResponse({
|
||||
}
|
||||
return <Object?>[error.code, error.message, error.details];
|
||||
}
|
||||
|
||||
bool _deepEquals(Object? a, Object? b) {
|
||||
if (identical(a, b)) {
|
||||
return true;
|
||||
@@ -60,9 +56,8 @@ bool _deepEquals(Object? a, Object? b) {
|
||||
}
|
||||
if (a is List && b is List) {
|
||||
return a.length == b.length &&
|
||||
a.indexed.every(
|
||||
((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]),
|
||||
);
|
||||
a.indexed
|
||||
.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
|
||||
}
|
||||
if (a is Map && b is Map) {
|
||||
if (a.length != b.length) {
|
||||
@@ -111,6 +106,7 @@ int _deepHash(Object? value) {
|
||||
return value.hashCode;
|
||||
}
|
||||
|
||||
|
||||
class Intent {
|
||||
Intent({
|
||||
this.fromPackageName,
|
||||
@@ -145,8 +141,7 @@ class Intent {
|
||||
}
|
||||
|
||||
Object encode() {
|
||||
return _toList();
|
||||
}
|
||||
return _toList(); }
|
||||
|
||||
static Intent decode(Object result) {
|
||||
result as List<Object?>;
|
||||
@@ -169,12 +164,7 @@ class Intent {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(fromPackageName, other.fromPackageName) &&
|
||||
_deepEquals(action, other.action) &&
|
||||
_deepEquals(data, other.data) &&
|
||||
_deepEquals(categories, other.categories) &&
|
||||
_deepEquals(mimeType, other.mimeType) &&
|
||||
_deepEquals(extra, other.extra);
|
||||
return _deepEquals(fromPackageName, other.fromPackageName) && _deepEquals(action, other.action) && _deepEquals(data, other.data) && _deepEquals(categories, other.categories) && _deepEquals(mimeType, other.mimeType) && _deepEquals(extra, other.extra);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -187,6 +177,7 @@ class Intent {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class _PigeonCodec extends StandardMessageCodec {
|
||||
const _PigeonCodec();
|
||||
@override
|
||||
@@ -194,7 +185,7 @@ class _PigeonCodec extends StandardMessageCodec {
|
||||
if (value is int) {
|
||||
buffer.putUint8(4);
|
||||
buffer.putInt64(value);
|
||||
} else if (value is Intent) {
|
||||
} else if (value is Intent) {
|
||||
buffer.putUint8(129);
|
||||
writeValue(buffer, value.encode());
|
||||
} else {
|
||||
@@ -217,13 +208,9 @@ class IntentHost {
|
||||
/// Constructor for [IntentHost]. The [binaryMessenger] named argument is
|
||||
/// available for dependency injection. If it is left null, the default
|
||||
/// BinaryMessenger will be used which routes to the host platform.
|
||||
IntentHost({
|
||||
BinaryMessenger? binaryMessenger,
|
||||
String messageChannelSuffix = '',
|
||||
}) : pigeonVar_binaryMessenger = binaryMessenger,
|
||||
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty
|
||||
? '.$messageChannelSuffix'
|
||||
: '';
|
||||
IntentHost({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
|
||||
: pigeonVar_binaryMessenger = binaryMessenger,
|
||||
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||
final BinaryMessenger? pigeonVar_binaryMessenger;
|
||||
|
||||
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||
@@ -235,8 +222,7 @@ class IntentHost {
|
||||
/// IntentEvents.setUp() was called (cold-start deep links).
|
||||
/// Returns null if no launch intent is pending.
|
||||
Future<Intent?> getInitialIntent() async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.simple_intent_receiver.IntentHost.getInitialIntent$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.simple_intent_receiver.IntentHost.getInitialIntent$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
@@ -246,10 +232,11 @@ class IntentHost {
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
);
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
)
|
||||
;
|
||||
return pigeonVar_replyValue as Intent?;
|
||||
}
|
||||
}
|
||||
@@ -259,20 +246,12 @@ abstract class IntentEvents {
|
||||
|
||||
void onIntentReceived(int sequence, Intent intent);
|
||||
|
||||
static void setUp(
|
||||
IntentEvents? api, {
|
||||
BinaryMessenger? binaryMessenger,
|
||||
String messageChannelSuffix = '',
|
||||
}) {
|
||||
messageChannelSuffix = messageChannelSuffix.isNotEmpty
|
||||
? '.$messageChannelSuffix'
|
||||
: '';
|
||||
static void setUp(IntentEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) {
|
||||
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||
{
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.simple_intent_receiver.IntentEvents.onIntentReceived$messageChannelSuffix',
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger,
|
||||
);
|
||||
'dev.flutter.pigeon.simple_intent_receiver.IntentEvents.onIntentReceived$messageChannelSuffix', pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger);
|
||||
if (api == null) {
|
||||
pigeonVar_channel.setMessageHandler(null);
|
||||
} else {
|
||||
@@ -285,10 +264,8 @@ abstract class IntentEvents {
|
||||
return wrapResponse(empty: true);
|
||||
} on PlatformException catch (e) {
|
||||
return wrapResponse(error: e);
|
||||
} catch (e) {
|
||||
return wrapResponse(
|
||||
error: PlatformException(code: 'error', message: e.toString()),
|
||||
);
|
||||
} catch (e) {
|
||||
return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -300,13 +277,9 @@ class IntentGatekeeperHostApi {
|
||||
/// Constructor for [IntentGatekeeperHostApi]. The [binaryMessenger] named argument is
|
||||
/// available for dependency injection. If it is left null, the default
|
||||
/// BinaryMessenger will be used which routes to the host platform.
|
||||
IntentGatekeeperHostApi({
|
||||
BinaryMessenger? binaryMessenger,
|
||||
String messageChannelSuffix = '',
|
||||
}) : pigeonVar_binaryMessenger = binaryMessenger,
|
||||
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty
|
||||
? '.$messageChannelSuffix'
|
||||
: '';
|
||||
IntentGatekeeperHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
|
||||
: pigeonVar_binaryMessenger = binaryMessenger,
|
||||
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||
final BinaryMessenger? pigeonVar_binaryMessenger;
|
||||
|
||||
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||
@@ -316,23 +289,21 @@ class IntentGatekeeperHostApi {
|
||||
/// Replicates the blocked-packages policy to the native side so the
|
||||
/// [IntentReceiverActivity] can reject intents without launching Flutter.
|
||||
Future<void> setConfig(bool enabled, List<String> blockedPackages) async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.simple_intent_receiver.IntentGatekeeperHostApi.setConfig$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.simple_intent_receiver.IntentGatekeeperHostApi.setConfig$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
|
||||
<Object?>[enabled, blockedPackages],
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[enabled, blockedPackages]);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
);
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
/// Replicates whether the Custom Tabs feature is enabled to the native side.
|
||||
@@ -340,46 +311,42 @@ class IntentGatekeeperHostApi {
|
||||
/// share-with-URL intents to the main browser instead of launching the
|
||||
/// stripped-down custom-tab activity. Defaults to enabled on the native side.
|
||||
Future<void> setCustomTabsEnabled(bool enabled) async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.simple_intent_receiver.IntentGatekeeperHostApi.setCustomTabsEnabled$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.simple_intent_receiver.IntentGatekeeperHostApi.setCustomTabsEnabled$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
|
||||
<Object?>[enabled],
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[enabled]);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
);
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
/// Resolves a package name to its user-visible application label via
|
||||
/// [PackageManager]. Returns `null` if the package is not installed or the
|
||||
/// label cannot be resolved.
|
||||
Future<String?> resolvePackageLabel(String packageName) async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.simple_intent_receiver.IntentGatekeeperHostApi.resolvePackageLabel$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.simple_intent_receiver.IntentGatekeeperHostApi.resolvePackageLabel$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
|
||||
<Object?>[packageName],
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[packageName]);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
);
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
)
|
||||
;
|
||||
return pigeonVar_replyValue as String?;
|
||||
}
|
||||
|
||||
@@ -389,8 +356,7 @@ class IntentGatekeeperHostApi {
|
||||
/// [ackPendingAlwaysAllows] after Flutter settings were updated
|
||||
/// successfully.
|
||||
Future<List<String>> getPendingAlwaysAllows() async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.simple_intent_receiver.IntentGatekeeperHostApi.getPendingAlwaysAllows$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.simple_intent_receiver.IntentGatekeeperHostApi.getPendingAlwaysAllows$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
@@ -400,32 +366,31 @@ class IntentGatekeeperHostApi {
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
);
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
)
|
||||
;
|
||||
return (pigeonVar_replyValue! as List<Object?>).cast<String>();
|
||||
}
|
||||
|
||||
/// Removes the given packages from the pending "Always allow" set after
|
||||
/// Flutter has successfully persisted them into its own policy store.
|
||||
Future<void> ackPendingAlwaysAllows(List<String> packageNames) async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.simple_intent_receiver.IntentGatekeeperHostApi.ackPendingAlwaysAllows$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.simple_intent_receiver.IntentGatekeeperHostApi.ackPendingAlwaysAllows$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
|
||||
<Object?>[packageNames],
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[packageNames]);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
_extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
);
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: true,
|
||||
)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ import 'package:flutter/services.dart';
|
||||
import 'package:meta/meta.dart' show immutable, protected, visibleForTesting;
|
||||
|
||||
Object? _extractReplyValueOrThrow(
|
||||
List<Object?>? replyList,
|
||||
String channelName, {
|
||||
required bool isNullValid,
|
||||
List<Object?>? replyList,
|
||||
String channelName, {
|
||||
required bool isNullValid,
|
||||
}) {
|
||||
if (replyList == null) {
|
||||
throw PlatformException(
|
||||
@@ -34,11 +34,8 @@ Object? _extractReplyValueOrThrow(
|
||||
return replyList.firstOrNull;
|
||||
}
|
||||
|
||||
List<Object?> wrapResponse({
|
||||
Object? result,
|
||||
PlatformException? error,
|
||||
bool empty = false,
|
||||
}) {
|
||||
|
||||
List<Object?> wrapResponse({Object? result, PlatformException? error, bool empty = false}) {
|
||||
if (empty) {
|
||||
return <Object?>[];
|
||||
}
|
||||
@@ -48,6 +45,7 @@ List<Object?> wrapResponse({
|
||||
return <Object?>[error.code, error.message, error.details];
|
||||
}
|
||||
|
||||
|
||||
class _PigeonCodec extends StandardMessageCodec {
|
||||
const _PigeonCodec();
|
||||
@override
|
||||
@@ -74,13 +72,9 @@ class SpeechToTextApi {
|
||||
/// Constructor for [SpeechToTextApi]. The [binaryMessenger] named argument is
|
||||
/// available for dependency injection. If it is left null, the default
|
||||
/// BinaryMessenger will be used which routes to the host platform.
|
||||
SpeechToTextApi({
|
||||
BinaryMessenger? binaryMessenger,
|
||||
String messageChannelSuffix = '',
|
||||
}) : pigeonVar_binaryMessenger = binaryMessenger,
|
||||
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty
|
||||
? '.$messageChannelSuffix'
|
||||
: '';
|
||||
SpeechToTextApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
|
||||
: pigeonVar_binaryMessenger = binaryMessenger,
|
||||
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||
final BinaryMessenger? pigeonVar_binaryMessenger;
|
||||
|
||||
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||
@@ -95,23 +89,21 @@ class SpeechToTextApi {
|
||||
/// The [locale] parameter specifies the language locale for recognition
|
||||
/// (e.g., 'en-US', 'de-DE'). If null, uses the device default.
|
||||
Future<bool> showDialog({String? locale}) async {
|
||||
final pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextApi.showDialog$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextApi.showDialog$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(
|
||||
<Object?>[locale],
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[locale]);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
|
||||
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
);
|
||||
pigeonVar_replyList,
|
||||
pigeonVar_channelName,
|
||||
isNullValid: false,
|
||||
)
|
||||
;
|
||||
return pigeonVar_replyValue! as bool;
|
||||
}
|
||||
}
|
||||
@@ -126,20 +118,12 @@ abstract class SpeechToTextEvents {
|
||||
/// recognition failed or was cancelled.
|
||||
void onTextReceived(String text);
|
||||
|
||||
static void setUp(
|
||||
SpeechToTextEvents? api, {
|
||||
BinaryMessenger? binaryMessenger,
|
||||
String messageChannelSuffix = '',
|
||||
}) {
|
||||
messageChannelSuffix = messageChannelSuffix.isNotEmpty
|
||||
? '.$messageChannelSuffix'
|
||||
: '';
|
||||
static void setUp(SpeechToTextEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) {
|
||||
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||
{
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived$messageChannelSuffix',
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger,
|
||||
);
|
||||
'dev.flutter.pigeon.speech_to_text_dialog.SpeechToTextEvents.onTextReceived$messageChannelSuffix', pigeonChannelCodec,
|
||||
binaryMessenger: binaryMessenger);
|
||||
if (api == null) {
|
||||
pigeonVar_channel.setMessageHandler(null);
|
||||
} else {
|
||||
@@ -151,10 +135,8 @@ abstract class SpeechToTextEvents {
|
||||
return wrapResponse(empty: true);
|
||||
} on PlatformException catch (e) {
|
||||
return wrapResponse(error: e);
|
||||
} catch (e) {
|
||||
return wrapResponse(
|
||||
error: PlatformException(code: 'error', message: e.toString()),
|
||||
);
|
||||
} catch (e) {
|
||||
return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user