Add container API: InAppWebViewSettings.containerId, ContainerController, per-container proxy#2825
Open
theoden8 wants to merge 11 commits into
Open
Conversation
…Settings for per-WebView data store partitioning Adds two opt-in, nullable fields to the source InAppWebViewSettings_ class and regenerates in_app_webview_settings.g.dart. Native bindings land in subsequent platform commits. * containerId: a free-form String identifier for a persistent, isolated data partition. Naming follows Firefox's "Multi-Account Containers" convention, which predates and reads more clearly than the underlying platform terms (Android calls them "profiles", Apple calls them "data store identifiers"). Tagged @SupportedPlatforms for Android (any version with WebViewFeature.MULTI_PROFILE), iOS 17+ and macOS 14+. * proxySettings: a ProxySettings (reuses the existing global-proxy type) scoped to a single WebView's data store. Tagged @SupportedPlatforms for iOS 17+ / macOS 14+ only — there is no per-WebView proxy API on the other platforms. Both fields are nullable and default to null; toMap/fromMap round-trips the new entries; existing callers see no behavior change.
…g and deleting per-WebView containers Defines a new abstract PlatformContainerController exposing three methods that map to the underlying native APIs: Future<List<String>> getAllContainerNames(); Future<bool> hasContainer(String containerId); Future<bool> deleteContainer(String containerId); @SupportedPlatforms is set for AndroidPlatform, IOSPlatform(17.0) and MacOSPlatform(14.0) — the same platforms that honor InAppWebViewSettings.containerId. Linux / Windows / Web stay UnimplementedError-by-default. The class follows the same factory + creation-params + .static() pattern as PlatformProxyController and PlatformWebStorageManager. Two new factory hooks on InAppWebViewPlatform (createPlatformContainerController and createPlatformContainerControllerStatic) let federated packages register their concrete implementations. The exchangeable codegen produces platform_container_controller.g.dart with the isClassSupported / isMethodSupported helpers. Naming note: "container" mirrors Firefox's Multi-Account Containers terminology, which predates and reads more clearly than the underlying platform terms (Android calls them "profiles", Apple calls them "data store identifiers"). Native API references in the implementation packages keep the platform names as Google / Apple publish them.
…ller wrapping androidx.webkit.ProfileStore
We map "container" (our public concept, naming follows Firefox) to
androidx.webkit.Profile (Android's underlying primitive). The native
API names — Profile, ProfileStore, WebViewFeature.MULTI_PROFILE,
WebViewCompat.setProfile/getProfile — stay as Google publishes them.
Pieces:
* InAppWebViewSettings.java parses/serializes the new "containerId"
key.
* InAppWebView.prepare() binds the WebView to the named container via
ProfileStore.getOrCreateProfile + WebViewCompat.setProfile at the
very top of prepare(), before addJavascriptInterface,
addDocumentStartJavaScript and CookieManager.setAcceptThirdPartyCookies
run — those calls lock the WebView to whichever profile is current,
and there is no API to rebind a live WebView. Guarded by
WebViewFeature.isFeatureSupported(MULTI_PROFILE) and try/catch — on
System WebView <110 the WebView falls back to the default profile.
* MyCookieManager gains a webViewId-scoped path
(cookieManagerForContainerOf) so cookie ops on a container-bound
WebView land in the container's cookie jar rather than the global
default. The store is keyed by container, not by WebView — two
WebViews sharing a containerId share their cookies. Falls back to
the global manager when the lookup misses.
* InAppWebView gains a small live-instance registry (weak refs in a
ConcurrentHashMap) so the cookie manager can find a WebView by its
platform-view id reliably even when the view is detached from the
visible tree.
* New native file container/ContainerManager.java handles a fresh
com.pichillilorenzo/flutter_inappwebview_containercontroller
MethodChannel. Methods:
- getAllContainerNames → ProfileStore.getInstance().getAllProfileNames()
- hasContainer(name) → name in getAllProfileNames()
- deleteContainer(name)→ ProfileStore.getInstance().deleteProfile(name)
Every method short-circuits to empty / false when MULTI_PROFILE is
unsupported, so callers get a consistent answer on System WebView
<110 instead of an exception. The default profile name is rejected
up front; IllegalStateException from "profile in use by a live
WebView" is surfaced as success(false) — the contract of
deleteContainer is "true when it actually deleted something".
* AndroidContainerController (lib/src/container_controller.dart) is
the concrete PlatformContainerController. Same shape as
AndroidProxyController.
* InAppWebViewFlutterPlugin.java instantiates ContainerManager in
onAttachedToEngine and disposes it on detach.
* AndroidInAppWebViewPlatform.createPlatformContainerController(Static)
factories return AndroidContainerController.
* Dart-side cookie_manager.dart forwards webViewController.id to the
native channel when a controller is supplied.
caeaad1 to
26606bf
Compare
… WKWebsiteDataStore + UserDefaults registry
iOS 17+ only. We map "container" (our public concept, naming follows
Firefox) to WKWebsiteDataStore identifier-based stores (Apple's
underlying primitive).
Apple's API stores data store identifiers as UUIDs, while our
containerId is a free-form String — and SHA-256 is one-way, so we
can't recover the original String from a UUID. ContainerManager
maintains an `containerId → uuidString` side-map in a UserDefaults
suite ("com.pichillilorenzo.flutter_inappwebview.containers"),
updated automatically when a containerId is bound (via
InAppWebView.preWKWebViewConfiguration's call to
ContainerManager.registerContainerBinding) and when a container is
deleted through this controller. The map survives app restarts;
uninstall wipes it together with the underlying WKWebsiteDataStore
data, so they stay consistent.
Pieces:
* InAppWebViewSettings.swift declares containerId and proxySettings.
proxySettings is carried as the raw Dart map and converted via the
existing ProxySettings.fromMap inside iOS-17 availability blocks.
* InAppWebView.preWKWebViewConfiguration sets
configuration.websiteDataStore = WKWebsiteDataStore(forIdentifier:)
when containerId is supplied and #available(iOS 17.0, *). The
identifier is derived via SHA-256 of the UTF-8 bytes (first 16
bytes formatted as a UUID). incognito always wins. The same block
attaches proxySettings to the data store the WebView ended up with,
so a container-bound WebView genuinely uses its own proxy.
* MyCookieManager gains a webViewId-scoped path
(httpCookieStore(forProfileOf:)) so cookie ops land in the bound
container's WKHTTPCookieStore. The store is keyed by container, not
by WebView; two WebViews sharing a containerId share their cookies.
* InAppWebView gains a live-instance registry (NSHashTable.weakObjects)
so the cookie manager can find a WebView by its platform-view id
reliably, including when detached from the visible hierarchy.
* New native file ContainerManager.swift handles a fresh
com.pichillilorenzo/flutter_inappwebview_containercontroller
MethodChannel. Methods:
- getAllContainerNames intersects the UserDefaults registry with
WKWebsiteDataStore.fetchAllDataStoreIdentifiers, so stale
registry entries (where the underlying store has been removed
out of band) are filtered out automatically.
- hasContainer derives the UUID via containerIdToUUID and checks
it against the live identifier list.
- deleteContainer calls WKWebsiteDataStore.remove(forIdentifier:),
which returns an error when no such store exists or when a live
WKWebView is using it; both are surfaced as success(false).
Removes the registry entry on success.
* IOSContainerController is the concrete PlatformContainerController.
* InAppWebViewFlutterPlugin.swift instantiates ContainerManager under
#available(iOS 17.0, *).
* IOSInAppWebViewPlatform.createPlatformContainerController(Static)
factories return IOSContainerController.
* Dart-side cookie_manager.dart forwards webViewController.id to the
native channel when a controller is supplied.
… via WKWebsiteDataStore + UserDefaults registry
Mirror of the iOS implementation, gated on macOS 14+ for the
identifier-based WKWebsiteDataStore APIs (Apple ships them
simultaneously across iOS 17 and macOS 14). The SHA-256 → UUID
derivation is byte-identical to the iOS sibling so the same
containerId maps to the same on-disk store on either platform —
useful when a user's data syncs across devices.
Files mirror the iOS commit:
- InAppWebViewSettings.swift: containerId, proxySettings declarations
- InAppWebView.swift: liveWebViews registry, containerIdToUUID helper,
websiteDataStore + proxyConfigurations bind in
preWKWebViewConfiguration(settings:), call to
ContainerManager.registerContainerBinding
- MyCookieManager.swift: httpCookieStore(forProfileOf:) helper plus
webViewId-scoped getCookies/deleteCookie overloads
- ContainerManager.swift: WKWebsiteDataStore.fetchAllDataStoreIdentifiers /
.remove(forIdentifier:) with the UserDefaults id ↔ UUID side-map
- lib/src/container_controller.dart: MacOSContainerController
- lib/src/inappwebview_platform.dart: factory hooks
- InAppWebViewFlutterPlugin.swift: instantiates ContainerManager under
#available(macOS 14.0, *)
- lib/src/cookie_manager.dart: forwards webViewController.id
…entries, container_isolation integration test
Public-facing wrapper class and tests that pull together the
per-platform PlatformContainerController implementations from prior
commits.
* lib/src/container_controller.dart wraps PlatformContainerController
with the same shape as ProxyController. ContainerController
delegates to the platform implementation registered by the chosen
federated package, exposes static isClassSupported /
isMethodSupported, and is reachable via ContainerController.instance().
* CHANGELOG entries land in:
- flutter_inappwebview_platform_interface (1.4.0-beta.3)
- flutter_inappwebview (6.2.0-beta.3, with per-platform sub-sections
matching the existing 6.2.0-beta.3 style)
* example/integration_test/in_app_webview/container_isolation.dart
is a new integration test that asserts:
- two WebViews bound to different containerIds do not share
localStorage state;
- a WebView re-bound to the same containerId reads back its
previously-written marker;
- ContainerController list/has/delete round-trips a probe
container (materialized via a HeadlessInAppWebView and disposed
before deleting, since deleteContainer fails while a live
WebView is using the container on every supported platform).
Skipped when InAppWebViewSettings.isPropertySupported(containerId)
or ContainerController.isClassSupported() returns false, so it
no-ops on platforms without per-container partitioning.
…nAppWebViewSettings.containerId LinuxPlatform() joins AndroidPlatform / IOSPlatform / MacOSPlatform on the @SupportedPlatforms blocks for InAppWebViewSettings.containerId and on the PlatformContainerController class plus its three methods (getAllContainerNames, hasContainer, deleteContainer). The Linux notes describe the filesystem-backed shape rather than referencing a native API class — WPE WebKit's WebKitNetworkSession takes data + cache directories directly, so the controller scans under <XDG_DATA_HOME>/flutter_inappwebview/containers/ for getAllContainerNames and removes both the data and cache subtrees on deleteContainer. While here, fixed three sed-mangled native API references in this file from the original rename pass: ProfileStore.getAllProfileNames, ProfileStore.deleteProfile, and the corresponding URL anchors should match the literal androidx.webkit names — those are Google's, not ours to rename. Regenerates platform_container_controller.g.dart and in_app_webview_settings.g.dart via build_runner; the generated diff adds TargetPlatform.linux to the support tables.
9723392 to
d1994d7
Compare
… WebKitNetworkSession
Adds WebView container support on Linux WPE WebKit, matching the API
shape already implemented for Android, iOS and macOS:
* InAppWebViewSettings.containerId is parsed from the Dart map and,
when non-empty (and incognito is false), causes InitWebView to
create or reuse a process-wide cached WebKitNetworkSession with
per-container data and cache directories under
<XDG_DATA_HOME>/flutter_inappwebview/containers/<id>/data
<XDG_CACHE_HOME>/flutter_inappwebview/containers/<id>/cache
Cached sessions hold an extra ref so they survive a WebView
teardown — without that, lazy-reloading a WebView into the same
container aborts with a thread-join error inside libwebkit. Both
HAVE_WPE_PLATFORM and HAVE_WPE_BACKEND_LEGACY backend branches
thread the session through g_object_new's "network-session"
property, mirroring the existing incognito path.
* New ContainerManager (linux/container_manager.{h,cc}) handles the
com.pichillilorenzo/flutter_inappwebview_containercontroller
channel:
- getAllContainerNames lists subdirectories of the data root
- hasContainer checks for one such subdirectory
- deleteContainer removes both the data and cache subtrees and
returns true iff at least one of them existed
Registered alongside ProxyManager in
flutter_inappwebview_linux_plugin.cc and exposed on PluginInstance
for symmetry with the other managers.
* Dart side: LinuxContainerController extends PlatformContainerController
and is wired through LinuxInAppWebViewPlatform.createPlatformContainerController(Static).
The container join requires WPE WebKit 2.40+ (where
webkit_network_session_new exists); on older runtimes the network-
session property is unavailable and the WebView falls back to the
shared default session, the same way it already falls back when
incognito is requested but the session creation fails.
CHANGELOG entry under 0.1.0-beta.2; pubspec version bumped to match.
…ry, bump linux dep to ^0.1.0-beta.2 Renames the lingering hasProfile/deleteProfile references in the Android, iOS, macOS and public CHANGELOG entries to hasContainer / deleteContainer to match the actual API. Adds a "Linux Platform" sub-section to the public 6.2.0-beta.3 entry covering PlatformContainerController and the WebKitNetworkSession-backed containerId join, and bumps the flutter_inappwebview_linux dep listed in the dependency-bump bullet to ^0.1.0-beta.2. No code changes — pure docs.
d1994d7 to
0abc7f0
Compare
…e across all platforms
Adds clearContainerData(containerId) to PlatformContainerController.
This is the API the consumer needed when deleteContainer's "must not
be in use" precondition can't be met — a "Clear Site Data" button on
a live WebView. Apple's WKWebsiteDataStore.remove(forIdentifier:)
silently no-ops if any WKWebView still references the store; Android's
ProfileStore.deleteProfile throws IllegalStateException; Linux's
deleteContainer just rms the directories without affecting the
in-memory session. None of those serve the "wipe but keep alive" case.
clearContainerData does. Per platform:
Apple (iOS 17+ / macOS 14+):
WKWebsiteDataStore(forIdentifier: uuid).removeData(
ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(),
modifiedSince: .distantPast,
completionHandler: …)
Works while a WKWebView is bound. The id ↔ UUID registry is left
intact — only the store's contents are gone.
Linux (WPE WebKit 2.40+):
webkit_website_data_manager_clear(manager, WEBKIT_WEBSITE_DATA_ALL,
0 /* since epoch */, …)
Routed through the container's cached WebKitNetworkSession (the
one InitWebView's get_or_create_container_session populates). If
the container has never been joined this process there's no live
session to clear — returns false. Callers that need to wipe a
never-joined container can use deleteContainer.
Android (androidx.webkit.Profile, MULTI_PROFILE 110+):
Profile has no single clear-all primitive, so this composes:
- profile.getCookieManager().removeAllCookies(null)
- profile.getWebStorage().deleteAllData()
- profile.getGeolocationPermissions().clearAll()
Documented as best-effort. The per-WebView HTTP cache (cleared
via WebView.clearCache) and the *global* ServiceWorkerController
Compat are NOT reached — these aren't scoped to a Profile.
Callers that need the HTTP cache wiped should also call
InAppWebViewController.clearCache on every live WebView in the
container.
Returns Future<bool>: true when the platform reported success on the
subsystems it touched; false when the container does not exist or
the underlying call errored. Per-platform notes are encoded in the
@SupportedPlatforms annotation on PlatformContainerController.clear
ContainerData so the generated supported-platforms docs are honest
about the asymmetry.
CHANGELOG entries added to the platform_interface, android, ios,
macos, linux and flutter_inappwebview packages.
Sits on top of the existing container API; doesn't change any
existing call signatures. Consumers (e.g. webspace_app) can use this
without touching their containerRev defense-in-depth workaround —
the workaround stays valuable for the case where they want a fresh
store regardless of Apple's release timing.
…g Linux's session cache
Apple's WKWebsiteDataStore(forIdentifier: uuid) hands out a fresh
ObjC wrapper on every call. All wrappers for the same UUID back the
same underlying on-disk store, but the wrappers themselves are
independent objects — each kept alive by whoever's holding it. When
multiple WebViews join the same container, each one's
configuration.websiteDataStore gets a different wrapper for the same
underlying store, and so does every ContainerController op
(clearContainerData, etc.). All of those wrapper instances together
constitute an "in-use" reference graph that WKWebsiteDataStore.
remove(forIdentifier:) has to wait on.
We can't fix the WebView-lifetime side of that race (the consumer's
containerRev defense-in-depth workaround handles that), but we can
stop being one of the wrapper duplicators. Mirrors the same shape
Linux's container_session_cache already uses for the same reason —
the Linux header documents the symptom as
"std::system_error during session teardown thread joins" and Apple
likely has the same wrapper-duplication problem even though the
symptom is the silent no-op of remove(forIdentifier:) rather than an
abort.
ContainerManager.swift (iOS + macOS):
- Adds `static var sharedStores: [UUID: WKWebsiteDataStore]` with
an NSLock.
- Adds `getOrCreateDataStore(forContainer:)` which returns the
cached wrapper (or creates+caches one) and updates the id ↔ UUID
registry in the same call.
- Adds `evictDataStore(forContainer:)` used by deleteContainer to
drop the cached wrapper before WKWebsiteDataStore.remove
(forIdentifier:) runs. The WebView-side wrapper is still
in-use; this just removes the plugin as a reason.
- clearContainerData now goes through the cache so it operates on
the *same* wrapper any live WebView is holding via its
configuration.websiteDataStore.
- registerContainerBinding kept for backwards compatibility; new
code paths use getOrCreateDataStore which does both jobs.
InAppWebView.swift (iOS + macOS):
- The inline WKWebsiteDataStore(forIdentifier: uuid) +
registerContainerBinding pair in preWKWebViewConfiguration is
replaced with a single ContainerManager.getOrCreateDataStore
(forContainer:) call. Sibling WebViews in the same container now
share one wrapper.
No Dart-side API change. CHANGELOGs updated under the in-progress
1.2.0-beta.3 entries for iOS and macOS.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Testing and Review Notes
What's new:
InAppWebViewSettings.containerIdlets a WebView join a named, persistent storage container (cookies + DOM storage + IndexedDB + ServiceWorkers + HTTP cache).InAppWebViewSettings.proxySettingsattaches a proxy to that container.ContainerControllerenumerates and deletes them. Naming follows Firefox's Multi-Account Containers; native APIs (androidx.webkit.Profile,WKWebsiteDataStore(forIdentifier:)) keep their platform names in the implementation packages.Platform support: Android (System WebView 110+), iOS 17+, macOS 14+, Linux. Older versions and Windows/Web silently ignore the new settings;
ContainerControllerreturns empty/false on unsupported platforms. Gate UI withInAppWebViewSettings.isPropertySupported(InAppWebViewSettingsProperty.containerId)andContainerController.isClassSupported().How to test:
The integration test asserts that two WebViews with different
containerIds don't sharelocalStorage, that re-joining a container restores its data, and thatContainerController.getAllContainerNames/hasContainer/deleteContainerround-trip. End-to-end validation against a multi-account browser consumer at theoden8/webspace_app#250, theoden8/webspace_app#255 and theoden8/webspace_app#256.Implementation notes:
addJavascriptInterface/setAcceptThirdPartyCookiesfirst run; Apple copiesWKWebViewConfiguration.websiteDataStoreby value atWKWebView.init. So the join is wired inprepare()(Android) andpreWKWebViewConfiguration(Apple).setSettingsafter construction is silently ignored — the WebView stays in whichever container it joined at birth.containerIdis hashed (SHA-256, first 16 bytes) into a stable UUID; anid ↔ UUIDside-map in aUserDefaultssuite letsgetAllContainerNamesreturn the original strings, intersected withfetchAllDataStoreIdentifiersso stale entries are filtered out.MyCookieManager(all three native platforms) now resolves the per-container cookie store via an optionalwebViewIdthat Dart'scookie_managerforwards when awebViewControlleris supplied. Unscoped callers stay on the default-jar path — byte-identical to current behaviour.Relation to #2671: that PR routed iOS proxy through the global
default()/nonPersistent()stores; this PR'sproxySettingsattaches to whichever store the WebView ended up with (typically the per-container one), so proxy isolation tracks container membership instead of being global. Similar on Linux as it uses webkit port also.CHANGELOGs updated in all five touched packages.
Screenshots or Videos
N/A — non-visual. Happy to record the integration test or a two-tab login demo on request.