Isolate extension WebView profiles and follow host proxy settings

This commit is contained in:
Levi committed 2026-09-14 00:30:52 +08:00
1 parent cee5f3336e
commit a0e84a75b1
556 files changed
+71097 -31

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
name: WebView host profiles
on:
push:
paths: ['internal/webview/**', 'pkg/download/extension_webview*', 'ui/flutter/third_party/**', 'ui/flutter/lib/app/rpc/webview*', 'ui/flutter/tool/webview_profile_check.dart', '.github/workflows/webview-profiles.yml']
pull_request:
paths: ['internal/webview/**', 'pkg/download/extension_webview*', 'ui/flutter/third_party/**', 'ui/flutter/lib/app/rpc/webview*', 'ui/flutter/tool/webview_profile_check.dart', '.github/workflows/webview-profiles.yml']
workflow_dispatch:
jobs:
android-plugin:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.41.6'
- run: flutter pub get
working-directory: ui/flutter
- run: ./gradlew :flutter_inappwebview_android:compileDebugJavaWithJavac
working-directory: ui/flutter/android
apple-plugin:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.25.4'
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.41.6'
- name: Build Go host
run: go build -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib ./bind/desktop
- name: Check native macOS profiles
working-directory: ui/flutter
run: |
flutter pub get
flutter build macos --debug -t tool/webview_profile_check.dart
build/macos/Build/Products/Debug/Gopeed.app/Contents/MacOS/Gopeed
- name: Compile iOS native plugin
working-directory: ui/flutter
run: |
flutter build ios --simulator --config-only
cd ios
pod install
xcodebuild -project Pods/Pods.xcodeproj -target flutter_inappwebview_ios -sdk iphonesimulator -configuration Debug CODE_SIGNING_ALLOWED=NO build
+1 -1
View File
@@ -3,7 +3,7 @@ module github.com/GopeedLab/gopeed
go 1.25.4
require (
github.com/GopeedLab/webview_go v0.0.0-20260423085439-7a2f88b6e9b5
github.com/GopeedLab/webview_go v0.0.0-20260913162656-5f4deb099005
github.com/anacrolix/torrent v1.60.1-0.20251217073903-486bcbe758e0
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/bodgit/sevenzip v1.6.1
+2
View File
@@ -25,6 +25,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/GopeedLab/webview_go v0.0.0-20260423085439-7a2f88b6e9b5 h1:86LVTaNDM/j/uTAD2HedkXNO9LIQlt642VAdbU/0D/M=
github.com/GopeedLab/webview_go v0.0.0-20260423085439-7a2f88b6e9b5/go.mod h1:FvV19lu9rQ9NcbczN5y8pbWecURRRbWa8tbfFARBDa0=
github.com/GopeedLab/webview_go v0.0.0-20260913162656-5f4deb099005 h1:4jyEJfmP5tpx+qaWqTF6Y04jy4xTTTGWo3l2alh9uZM=
github.com/GopeedLab/webview_go v0.0.0-20260913162656-5f4deb099005/go.mod h1:FvV19lu9rQ9NcbczN5y8pbWecURRRbWa8tbfFARBDa0=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
+42
View File
@@ -0,0 +1,42 @@
# Extension browser host configuration
The downloader wraps each provider with the extension's stable identity before
constructing the runtime. `OpenOptions.ProfileID`, `DataPath`, and `ProxyURL` are
host-only fields. `parseOpenOptions` never reads them from extension JavaScript.
The extension API is unchanged.
Profiles are persistent and shared across invocations/pages of the same extension.
Different identities use different native stores. Go providers use a SHA-256-based
path under `<StorageDir>/webview`; Apple RPC hosts use a stable UUID data store;
Android uses a named WebView profile. Cookie operations select that same store,
including `clearCookies`. Closing an execution closes its pages, not its profile.
Existing shared browser data is not copied into every extension's new profile.
The downloader lazily owns one loopback forward proxy with HTTP and SOCKS5
listeners. Windows/Linux/Android use HTTP; Apple uses SOCKS5 because its HTTP
CONNECT configuration may bypass plain HTTP. The forwarder reads the current
Gopeed proxy config for each new request/tunnel and supports direct, system,
HTTP(S) upstream proxies with Basic authentication, and authenticated SOCKS5.
Credentials remain in Go and are never sent over the WebView RPC.
Changes affect new requests/connections. Existing CONNECT/SOCKS tunnels keep their
route until closed; opening another page in the same profile does not reset it.
SOCKS carries host/port rather than a full URL: system proxy/PAC selection for
Apple is host-based, with port 80 treated as HTTP and other ports as HTTPS.
Path-dependent PAC rules cannot be reproduced from a SOCKS connection.
Apple requires macOS 14 / iOS 17. Android checks MULTI_PROFILE and PROXY_OVERRIDE.
Unsupported hosts fail explicitly instead of silently sharing browser storage or
ignoring the proxy.
## Validation
- `go test -race ./internal/webview/proxy ./pkg/download/engine/webview`
- `go test ./pkg/download -run 'TestWebViewProfileBound|TestDownloader_ExtensionRuntimeWebView'`
- In webview_go: `go run ./examples/profilecheck` (Linux: `xvfb-run -a ...`).
- In ui/flutter: `flutter run -d macos -t tool/webview_profile_check.dart`.
The native smoke tests cover routing and persistent profile isolation. The Flutter
smoke test also checks concurrent pages, HttpOnly cookies, LocalStorage, IndexedDB,
profile-scoped clearing, and reopening. The self-signed TLS fixture is trusted only
by that test's callback; production certificate validation is unchanged.
+9 -1
View File
@@ -105,7 +105,15 @@ func (p *pageWrapper) start() error {
defer runtimepkg.UnlockOSThread()
var w webview.WebView
if p.opts.Headless {
if p.opts.DataPath != "" {
var err error
w, err = webview.NewWithOptions(webview.Options{Debug: p.opts.Debug, Headless: p.opts.Headless, DataPath: p.opts.DataPath, ProxyURL: p.opts.ProxyURL})
if err != nil {
p.ready <- err
close(p.done)
return
}
} else if p.opts.Headless {
w = webview.NewHeadless(p.opts.Debug)
} else {
w = webview.New(p.opts.Debug)
+29 -1
View File
@@ -1,10 +1,14 @@
package integrationtest
import (
"crypto/sha256"
"fmt"
webviewproxy "github.com/GopeedLab/gopeed/internal/webview/proxy"
"net/http"
"net/http/httptest"
"net/url"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
@@ -219,7 +223,19 @@ func openTestPage(t *testing.T, provider enginewebview.Provider, opts enginewebv
}))
t.Cleanup(server.Close)
runtime := enginewebview.NewRuntime(provider, true)
proxy, err := webviewproxy.New(nil)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { proxy.Close() })
path := filepath.Join(t.TempDir(), "profile")
sum := sha256.Sum256([]byte(path))
profileID := fmt.Sprintf("%x-%x-%x-%x-%x", sum[:4], sum[4:6], sum[6:8], sum[8:10], sum[10:16])
proxyURL := proxy.URL()
if runtime.GOOS == "darwin" {
proxyURL = proxy.SOCKSURL()
}
runtime := enginewebview.NewRuntime(&configuredOpener{provider: provider, profileID: profileID, dataPath: path, proxyURL: proxyURL}, true)
page, err := runtime.Open(map[string]any{
"headless": opts.Headless,
"debug": opts.Debug,
@@ -380,3 +396,15 @@ func hasCookie(cookies []enginewebview.Cookie, name string, value string) bool {
}
return false
}
type configuredOpener struct {
provider enginewebview.Provider
profileID, dataPath, proxyURL string
}
func (o *configuredOpener) Open(opts enginewebview.OpenOptions) (enginewebview.Page, error) {
opts.ProfileID = o.profileID
opts.DataPath = o.dataPath
opts.ProxyURL = o.proxyURL
return o.provider.Open(opts)
}
+341
View File
@@ -0,0 +1,341 @@
// Package proxy provides the host-owned WebView forward proxy. Browser engines
// always use this endpoint; Gopeed decides the upstream route for each request.
package proxy
import (
"bufio"
"context"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/armon/go-socks5"
xproxy "golang.org/x/net/proxy"
)
type Selector func(*http.Request) (*url.URL, error)
type Server struct {
listener net.Listener
socksListener net.Listener
server *http.Server
transport *http.Transport
selectProxy Selector
mu sync.Mutex
connections map[io.Closer]struct{}
closed bool
}
func New(selectProxy Selector) (*Server, error) {
if selectProxy == nil {
selectProxy = func(*http.Request) (*url.URL, error) { return nil, nil }
}
listener, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
return nil, err
}
s := &Server{listener: listener, selectProxy: selectProxy, connections: make(map[io.Closer]struct{})}
s.transport = &http.Transport{Proxy: selectProxy, DisableKeepAlives: true, TLSHandshakeTimeout: 15 * time.Second, ResponseHeaderTimeout: 30 * time.Second}
s.server = &http.Server{Handler: s, ReadHeaderTimeout: 15 * time.Second}
s.socksListener, err = net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
listener.Close()
return nil, err
}
socks, err := socks5.New(&socks5.Config{Logger: log.New(io.Discard, "", 0), Resolver: unresolvedDNS{}, Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
_, port, _ := net.SplitHostPort(address)
scheme := "https"
if port == "80" {
scheme = "http"
}
request := &http.Request{URL: &url.URL{Scheme: scheme, Host: address}, Header: make(http.Header)}
upstream, err := selectProxy(request)
if err != nil {
return nil, err
}
timed, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
conn, err := dialTunnel(timed, address, upstream)
if err != nil {
return nil, err
}
return s.track(conn)
}})
if err != nil {
listener.Close()
s.socksListener.Close()
return nil, err
}
go s.server.Serve(listener)
go socks.Serve(&trackedListener{Listener: s.socksListener, server: s})
return s, nil
}
func (s *Server) URL() string { return "http://" + s.listener.Addr().String() }
func (s *Server) SOCKSURL() string { return "socks5://" + s.socksListener.Addr().String() }
func (s *Server) Close() error {
s.mu.Lock()
s.closed = true
connections := make([]io.Closer, 0, len(s.connections))
for c := range s.connections {
connections = append(connections, c)
}
s.mu.Unlock()
for _, c := range connections {
c.Close()
}
s.socksListener.Close()
s.transport.CloseIdleConnections()
return s.server.Close()
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodConnect {
s.connect(w, r)
return
}
if !r.URL.IsAbs() || (r.URL.Scheme != "http" && r.URL.Scheme != "https") {
http.Error(w, "absolute HTTP URL required", http.StatusBadRequest)
return
}
out := r.Clone(r.Context())
out.RequestURI = ""
upgrade := out.Header.Get("Upgrade")
stripHopHeaders(out.Header)
if upgrade != "" {
out.Header.Set("Connection", "Upgrade")
out.Header.Set("Upgrade", upgrade)
}
resp, err := s.transport.RoundTrip(out)
if err != nil {
http.Error(w, "upstream request failed", http.StatusBadGateway)
return
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusSwitchingProtocols {
remote, ok := resp.Body.(io.ReadWriteCloser)
if !ok {
http.Error(w, "invalid upstream upgrade", 502)
return
}
client, buffer, err := w.(http.Hijacker).Hijack()
if err != nil {
return
}
defer client.Close()
resp.Body = nil
if err := resp.Write(buffer); err != nil {
return
}
if err := buffer.Flush(); err != nil {
return
}
s.bridge(client, buffer, remote)
return
}
stripHopHeaders(resp.Header)
for k, v := range resp.Header {
w.Header()[k] = v
}
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
}
func stripHopHeaders(h http.Header) {
for _, key := range strings.Split(h.Get("Connection"), ",") {
h.Del(strings.TrimSpace(key))
}
for _, key := range []string{"Connection", "Proxy-Connection", "Proxy-Authorization", "Proxy-Authenticate", "Keep-Alive", "TE", "Trailer", "Transfer-Encoding", "Upgrade"} {
h.Del(key)
}
}
func (s *Server) connect(w http.ResponseWriter, r *http.Request) {
if _, _, err := net.SplitHostPort(r.Host); err != nil {
http.Error(w, "invalid tunnel destination", 400)
return
}
target := r.Clone(r.Context())
target.URL = &url.URL{Scheme: "https", Host: r.Host}
upstream, err := s.selectProxy(target)
if err != nil {
http.Error(w, "proxy selection failed", 502)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
remote, err := dialTunnel(ctx, r.Host, upstream)
if err != nil {
http.Error(w, "upstream connection failed", 502)
return
}
defer remote.Close()
client, buffer, err := w.(http.Hijacker).Hijack()
if err != nil {
return
}
defer client.Close()
if _, err := buffer.WriteString("HTTP/1.1 200 Connection Established\r\n\r\n"); err != nil {
return
}
if err := buffer.Flush(); err != nil {
return
}
s.bridge(client, buffer, remote)
}
func (s *Server) bridge(client net.Conn, buffer *bufio.ReadWriter, remote io.ReadWriteCloser) {
s.mu.Lock()
if s.closed {
s.mu.Unlock()
return
}
s.connections[client] = struct{}{}
s.connections[remote] = struct{}{}
s.mu.Unlock()
defer func() { s.mu.Lock(); delete(s.connections, client); delete(s.connections, remote); s.mu.Unlock() }()
done := make(chan struct{})
go func() { io.Copy(remote, buffer); remote.Close(); close(done) }()
io.Copy(client, remote)
client.Close()
<-done
}
type bufferedConn struct {
net.Conn
reader *bufio.Reader
}
func (c *bufferedConn) Read(p []byte) (int, error) { return c.reader.Read(p) }
func dialTunnel(ctx context.Context, target string, upstream *url.URL) (net.Conn, error) {
dialer := &net.Dialer{}
if upstream == nil {
return dialer.DialContext(ctx, "tcp", target)
}
if upstream.Scheme == "socks5" || upstream.Scheme == "socks5h" {
u := *upstream
u.Scheme = "socks5"
d, err := xproxy.FromURL(&u, dialer)
if err != nil {
return nil, err
}
return d.(xproxy.ContextDialer).DialContext(ctx, "tcp", target)
}
if upstream.Scheme != "http" && upstream.Scheme != "https" {
return nil, errors.New("unsupported proxy scheme")
}
host := upstream.Host
if upstream.Port() == "" {
port := "80"
if upstream.Scheme == "https" {
port = "443"
}
host = net.JoinHostPort(upstream.Hostname(), port)
}
conn, err := dialer.DialContext(ctx, "tcp", host)
if err != nil {
return nil, err
}
ok := false
defer func() {
if !ok {
conn.Close()
}
}()
if deadline, exists := ctx.Deadline(); exists {
conn.SetDeadline(deadline)
}
if upstream.Scheme == "https" {
secure := tls.Client(conn, &tls.Config{ServerName: upstream.Hostname(), MinVersion: tls.VersionTLS12})
if err := secure.HandshakeContext(ctx); err != nil {
return nil, err
}
conn = secure
}
request := &http.Request{Method: http.MethodConnect, URL: &url.URL{Opaque: target}, Host: target, Header: make(http.Header)}
if upstream.User != nil {
password, _ := upstream.User.Password()
request.Header.Set("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(upstream.User.Username()+":"+password)))
}
if err := request.Write(conn); err != nil {
return nil, err
}
reader := bufio.NewReader(conn)
response, err := http.ReadResponse(reader, request)
if err != nil {
return nil, err
}
if response.StatusCode != 200 {
response.Body.Close()
return nil, fmt.Errorf("upstream CONNECT status %d", response.StatusCode)
}
conn.SetDeadline(time.Time{})
ok = true
return &bufferedConn{Conn: conn, reader: reader}, nil
}
// Leave hostname resolution to the selected upstream (SOCKS remote DNS / CONNECT).
type unresolvedDNS struct{}
func (unresolvedDNS) Resolve(ctx context.Context, name string) (context.Context, net.IP, error) {
return ctx, nil, nil
}
type trackedListener struct {
net.Listener
server *Server
}
func (l *trackedListener) Accept() (net.Conn, error) {
c, err := l.Listener.Accept()
if err != nil {
return nil, err
}
return l.server.track(c)
}
type trackedConn struct {
net.Conn
server *Server
}
func (c *trackedConn) Close() error {
err := c.Conn.Close()
c.server.mu.Lock()
delete(c.server.connections, c)
c.server.mu.Unlock()
return err
}
func (s *Server) track(c net.Conn) (net.Conn, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.closed {
c.Close()
return nil, net.ErrClosed
}
tracked := &trackedConn{Conn: c, server: s}
s.connections[tracked] = struct{}{}
return tracked, nil
}
func (c *trackedConn) CloseWrite() error { return closeWrite(c.Conn) }
func (c *bufferedConn) CloseWrite() error { return closeWrite(c.Conn) }
func closeWrite(c net.Conn) error {
if writer, ok := c.(interface{ CloseWrite() error }); ok {
return writer.CloseWrite()
}
return c.Close()
}
+211
View File
@@ -0,0 +1,211 @@
package proxy
import (
"bufio"
"context"
"crypto/tls"
"io"
"net"
"net/http"
"net/http/httptest"
"net/url"
"sync/atomic"
"testing"
"time"
"github.com/armon/go-socks5"
)
func TestRoutesAndAuthentication(t *testing.T) {
origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Proxy-Authorization") != "" {
t.Error("proxy credentials leaked to origin")
}
w.Write([]byte("origin"))
}))
defer origin.Close()
secure := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("secure")) }))
defer secure.Close()
upstream, err := New(func(*http.Request) (*url.URL, error) { return nil, nil })
if err != nil {
t.Fatal(err)
}
defer upstream.Close()
var authenticated atomic.Int32
authProxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Proxy-Authorization") != "Basic dXNlcjpwYXNz" {
http.Error(w, "authentication required", 407)
return
}
authenticated.Add(1)
upstream.ServeHTTP(w, r)
}))
defer authProxy.Close()
socks, err := socks5.New(&socks5.Config{Credentials: socks5.StaticCredentials{"user": "pass"}})
if err != nil {
t.Fatal(err)
}
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
go socks.Serve(listener)
for _, route := range []string{"", authProxy.URL, "socks5://" + listener.Addr().String()} {
t.Run(route, func(t *testing.T) {
var selected *url.URL
if route != "" {
selected, _ = url.Parse(route)
selected.User = url.UserPassword("user", "pass")
}
local, err := New(func(r *http.Request) (*url.URL, error) { return selected, nil })
if err != nil {
t.Fatal(err)
}
defer local.Close()
proxyURL, _ := url.Parse(local.URL())
transport := &http.Transport{Proxy: http.ProxyURL(proxyURL), TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
defer transport.CloseIdleConnections()
client := &http.Client{Transport: transport, Timeout: 5 * time.Second}
for _, test := range []struct{ url, want string }{{origin.URL, "origin"}, {secure.URL, "secure"}} {
response, err := client.Get(test.url)
if err != nil {
t.Fatal(err)
}
body, _ := io.ReadAll(response.Body)
response.Body.Close()
if string(body) != test.want {
t.Fatalf("got status %d, body %q", response.StatusCode, body)
}
}
})
}
if authenticated.Load() != 2 {
t.Fatalf("expected authenticated HTTP and CONNECT, got %d", authenticated.Load())
}
}
func TestRoutingChangesAndFailureDoesNotBypassProxy(t *testing.T) {
var hits atomic.Int32
origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { hits.Add(1); w.Write([]byte("direct")) }))
defer origin.Close()
var useProxy atomic.Bool
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("proxied")) }))
upstreamURL, _ := url.Parse(upstream.URL)
local, err := New(func(*http.Request) (*url.URL, error) {
if useProxy.Load() {
return upstreamURL, nil
}
return nil, nil
})
if err != nil {
t.Fatal(err)
}
defer local.Close()
endpoint, _ := url.Parse(local.URL())
transport := &http.Transport{Proxy: http.ProxyURL(endpoint)}
defer transport.CloseIdleConnections()
client := &http.Client{Transport: transport, Timeout: 3 * time.Second}
for _, want := range []string{"direct", "proxied", "upstream request failed\n"} {
resp, err := client.Get(origin.URL)
if err != nil {
t.Fatal(err)
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if string(body) != want {
t.Fatalf("got %q want %q", body, want)
}
if want == "direct" {
useProxy.Store(true)
} else if want == "proxied" {
upstream.Close()
}
}
if hits.Load() != 1 {
t.Fatal("failed proxy must not fall back to direct")
}
}
func TestCloseTerminatesTunnels(t *testing.T) {
echo, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer echo.Close()
go func() {
c, e := echo.Accept()
if e == nil {
defer c.Close()
io.Copy(c, c)
}
}()
s, err := New(func(*http.Request) (*url.URL, error) { return nil, nil })
if err != nil {
t.Fatal(err)
}
endpoint, _ := url.Parse(s.URL())
c, err := dialTunnel(context.Background(), echo.Addr().String(), endpoint)
if err != nil {
t.Fatal(err)
}
defer c.Close()
c.SetDeadline(time.Now().Add(3 * time.Second))
c.Write([]byte("hello\n"))
line, err := bufio.NewReader(c).ReadString('\n')
if err != nil || line != "hello\n" {
t.Fatalf("echo %q %v", line, err)
}
s.Close()
if _, err := c.Read(make([]byte, 1)); err == nil {
t.Fatal("tunnel stayed open")
}
}
func TestSOCKSIngressPreservesRemoteDNS(t *testing.T) {
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "CONNECT" || r.Host != "unresolved.invalid:80" {
t.Errorf("lost destination: %s %s", r.Method, r.Host)
}
c, b, err := w.(http.Hijacker).Hijack()
if err != nil {
return
}
defer c.Close()
b.WriteString("HTTP/1.1 200 Connection Established\r\n\r\n")
b.Flush()
request, err := http.ReadRequest(b.Reader)
if err != nil {
t.Error(err)
return
}
request.Body.Close()
b.WriteString("HTTP/1.1 200 OK\r\nContent-Length: 7\r\nConnection: close\r\n\r\nproxied")
b.Flush()
}))
defer upstream.Close()
target, _ := url.Parse(upstream.URL)
local, err := New(func(r *http.Request) (*url.URL, error) {
if r.URL.Host != "unresolved.invalid:80" {
t.Errorf("selector lost hostname: %s", r.URL)
}
return target, nil
})
if err != nil {
t.Fatal(err)
}
defer local.Close()
endpoint, _ := url.Parse(local.SOCKSURL())
transport := &http.Transport{Proxy: http.ProxyURL(endpoint)}
defer transport.CloseIdleConnections()
client := &http.Client{Transport: transport, Timeout: 5 * time.Second}
response, err := client.Get("http://unresolved.invalid/")
if err != nil {
t.Fatal(err)
}
defer response.Body.Close()
body, _ := io.ReadAll(response.Body)
if string(body) != "proxied" {
t.Fatalf("unexpected body %q", body)
}
}
+10 -2
View File
@@ -21,6 +21,7 @@ import (
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/internal/logger"
webviewproxy "github.com/GopeedLab/gopeed/internal/webview/proxy"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/http"
"github.com/GopeedLab/gopeed/pkg/util"
@@ -97,8 +98,10 @@ type Progress struct {
}
type Downloader struct {
Logger *logger.Logger
ExtensionLogger *logger.Logger
webviewProxyLock sync.Mutex
webviewProxy *webviewproxy.Server
Logger *logger.Logger
ExtensionLogger *logger.Logger
cfg *DownloaderConfig
fetcherCache map[string]fetcher.Fetcher
@@ -1114,6 +1117,11 @@ func (d *Downloader) doDelete(task *Task, force bool) (err error) {
func (d *Downloader) Close() error {
d.closed.Store(true)
d.webviewProxyLock.Lock()
if d.webviewProxy != nil {
_ = d.webviewProxy.Close()
}
d.webviewProxyLock.Unlock()
closeArr := []func() error{
d.pauseAll,
+6
View File
@@ -68,6 +68,9 @@ type IsAvailableResult struct {
}
type PageOpenParams struct {
ProfileID string `json:"profileId,omitempty"`
DataPath string `json:"dataPath,omitempty"`
ProxyURL string `json:"proxyUrl,omitempty"`
Headless bool `json:"headless,omitempty"`
Debug bool `json:"debug,omitempty"`
Title string `json:"title,omitempty"`
@@ -78,6 +81,9 @@ type PageOpenParams struct {
func NewPageOpenParams(opts OpenOptions) PageOpenParams {
return PageOpenParams{
ProfileID: opts.ProfileID,
DataPath: opts.DataPath,
ProxyURL: opts.ProxyURL,
Headless: opts.Headless,
Debug: opts.Debug,
Title: opts.Title,
+4
View File
@@ -29,6 +29,10 @@ type Page interface {
}
type OpenOptions struct {
// Host-owned fields are never parsed from extension JavaScript options.
ProfileID string
DataPath string
ProxyURL string
Headless bool
Debug bool
Title string
@@ -336,3 +336,15 @@ func (p *fakePage) Close() error {
p.closed = true
return nil
}
func TestExtensionCannotSupplyHostProfileOrProxy(t *testing.T) {
opener := &fakeOpener{page: &fakePage{}}
runtime := NewRuntime(opener, true)
_, err := runtime.Open(map[string]any{"profileId": "other-extension", "dataPath": "/tmp/other-profile", "proxyUrl": "http://example.com:1234", "ProfileID": "other"})
if err != nil {
t.Fatal(err)
}
if opener.opts.ProfileID != "" || opener.opts.DataPath != "" || opener.opts.ProxyURL != "" {
t.Fatalf("extension controlled host settings: %+v", opener.opts)
}
}
+1 -1
View File
@@ -365,7 +365,7 @@ func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, c
engine, session := d.newExtensionEngine()
defer session.CloseIfIdle()
gopeed.Runtime = &InstanceRuntime{
WebView: d.newExtensionWebViewRuntime(session),
WebView: d.newExtensionWebViewRuntime(session, ext.buildIdentity()),
}
err = injectGopeed(engine.Runtime, gopeed)
if err != nil {
+1 -1
View File
@@ -30,7 +30,7 @@ func (d *Downloader) NewExtensionEngine(ext *Extension, settings map[string]any)
identity: ext.buildIdentity(),
},
Runtime: &InstanceRuntime{
WebView: d.newExtensionWebViewRuntime(session),
WebView: d.newExtensionWebViewRuntime(session, ext.buildIdentity()),
},
}
if err := injectGopeed(engine.Runtime, gopeed); err != nil {
+2 -2
View File
@@ -127,13 +127,13 @@ func (d *Downloader) newExtensionEngine() (*engine.Engine, *engineSession) {
return e, session
}
func (d *Downloader) newExtensionWebViewRuntime(session *engineSession) *enginewebview.Runtime {
func (d *Downloader) newExtensionWebViewRuntime(session *engineSession, identity string) *enginewebview.Runtime {
var (
opener enginewebview.Opener
available bool
)
if provider := d.cfg.WebViewProvider; provider != nil && provider.IsAvailable() {
opener = provider
opener = &extensionWebViewOpener{downloader: d, provider: provider, identity: identity}
available = true
}
runtime := enginewebview.NewRuntime(opener, available)
+74
View File
@@ -0,0 +1,74 @@
package download
import (
"crypto/sha256"
"fmt"
"github.com/GopeedLab/gopeed/pkg/base"
"net/http"
"net/url"
"path/filepath"
"runtime"
webviewproxy "github.com/GopeedLab/gopeed/internal/webview/proxy"
enginewebview "github.com/GopeedLab/gopeed/pkg/download/engine/webview"
)
// extensionWebViewOpener binds browser state to the owning extension. Neither
// identity, filesystem paths nor proxy settings are accepted from JavaScript.
type extensionWebViewOpener struct {
downloader *Downloader
provider enginewebview.Provider
identity string
}
func (o *extensionWebViewOpener) Open(opts enginewebview.OpenOptions) (enginewebview.Page, error) {
if o.identity == "" {
return nil, fmt.Errorf("webview extension identity is required")
}
sum := sha256.Sum256([]byte(o.identity))
opts.ProfileID = fmt.Sprintf("%x-%x-%x-%x-%x", sum[:4], sum[4:6], sum[6:8], sum[8:10], sum[10:16])
path, err := filepath.Abs(filepath.Join(o.downloader.cfg.StorageDir, "webview", fmt.Sprintf("%x", sum[:])))
if err != nil {
return nil, err
}
opts.DataPath = path
opts.ProxyURL, err = o.downloader.webviewProxyURL()
if err != nil {
return nil, err
}
return o.provider.Open(opts)
}
func (d *Downloader) webviewProxyURL() (string, error) {
d.webviewProxyLock.Lock()
defer d.webviewProxyLock.Unlock()
if d.closed.Load() {
return "", fmt.Errorf("downloader is closed")
}
if d.webviewProxy == nil {
server, err := webviewproxy.New(func(r *http.Request) (*url.URL, error) {
if d.blob != nil && d.blob.IsURL(r.URL.String()) {
return nil, nil
}
d.configLock.RLock()
var cfg base.DownloaderProxyConfig
if d.cfg.Proxy != nil {
cfg = *d.cfg.Proxy
}
d.configLock.RUnlock()
handler := cfg.ToHandler()
if handler == nil {
return nil, nil
}
return handler(r)
})
if err != nil {
return "", err
}
d.webviewProxy = server
}
if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
return d.webviewProxy.SOCKSURL(), nil
}
return d.webviewProxy.URL(), nil
}
@@ -0,0 +1,51 @@
package download
import (
"path/filepath"
"sync"
"testing"
"github.com/GopeedLab/gopeed/pkg/base"
enginewebview "github.com/GopeedLab/gopeed/pkg/download/engine/webview"
)
type profileCaptureProvider struct{ options []enginewebview.OpenOptions }
func (*profileCaptureProvider) IsAvailable() bool { return true }
func (p *profileCaptureProvider) Open(opts enginewebview.OpenOptions) (enginewebview.Page, error) {
p.options = append(p.options, opts)
return fakeRuntimeWebViewPage{}, nil
}
func TestWebViewProfileBoundToExtension(t *testing.T) {
root := t.TempDir()
d := &Downloader{cfg: &DownloaderConfig{StorageDir: root, DownloaderStoreConfig: &base.DownloaderStoreConfig{}}, configLock: &sync.RWMutex{}}
t.Cleanup(func() {
if d.webviewProxy != nil {
d.webviewProxy.Close()
}
})
provider := &profileCaptureProvider{}
for _, id := range []string{"extension-a", "extension-b", "extension-a", "../../escape"} {
opener := &extensionWebViewOpener{downloader: d, provider: provider, identity: id}
_, err := opener.Open(enginewebview.OpenOptions{ProfileID: "forged", DataPath: "/tmp/forged", ProxyURL: "http://wrong:80"})
if err != nil {
t.Fatal(err)
}
}
a, b, reopen := provider.options[0], provider.options[1], provider.options[2]
if a.ProfileID == b.ProfileID || a.DataPath == b.DataPath {
t.Fatal("different extensions shared browser storage")
}
if a.ProfileID != reopen.ProfileID || a.DataPath != reopen.DataPath {
t.Fatal("same extension lost profile identity")
}
for _, opts := range provider.options {
if filepath.Dir(opts.DataPath) != filepath.Join(root, "webview") {
t.Fatalf("profile escaped storage root: %s", opts.DataPath)
}
if opts.ProxyURL != d.webviewProxy.URL() && opts.ProxyURL != d.webviewProxy.SOCKSURL() {
t.Fatal("host proxy was overridden")
}
}
}
+4
View File
@@ -29,3 +29,7 @@ linter:
formatter:
page_width: 120
analyzer:
exclude:
- third_party/**
@@ -0,0 +1,62 @@
import 'package:flutter/services.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
// Private host/plugin bridge. These settings never originate from extension JS.
class WebViewProfileSettings extends InAppWebViewSettings {
WebViewProfileSettings({required this.profileId, required bool debug, super.userAgent})
: super(javaScriptEnabled: true, transparentBackground: true, isInspectable: debug);
final String profileId;
@override
Map<String, dynamic> toMap() => {...super.toMap(), 'gopeedProfileId': profileId};
}
class WebViewProfile {
WebViewProfile(this.id);
final String id;
static const _channel = MethodChannel('com.pichillilorenzo/flutter_inappwebview_cookiemanager');
Future<void> prepare(String proxyUrl) async {
await _call('gopeed.prepareProfile', {'proxyUrl': proxyUrl});
}
Future<dynamic> _call(String method, Map<String, dynamic> arguments) =>
_channel.invokeMethod(method, {...arguments, 'gopeedProfileId': id});
Future<List<Cookie>> getCookies({required WebUri url}) async {
final values = await _call('getCookies', {'url': url.toString()}) as List? ?? [];
return values.map((value) => Cookie.fromMap(Map<String, dynamic>.from(value as Map))!).toList();
}
Future<void> setCookie({
required WebUri url,
required String name,
required String value,
String? domain,
String path = '/',
int? expiresDate,
bool isSecure = false,
bool isHttpOnly = false,
}) async {
await _call('setCookie', {
'url': url.toString(),
'name': name,
'value': value,
'domain': domain,
'path': path,
'expiresDate': expiresDate?.toString(),
'isSecure': isSecure,
'isHttpOnly': isHttpOnly,
});
}
Future<void> deleteCookie({required WebUri url, required String name, String? domain, String path = '/'}) async {
await _call('deleteCookie', {'url': url.toString(), 'name': name, 'domain': domain, 'path': path});
}
Future<void> deleteAllCookies() async {
await _call('deleteAllCookies', {});
}
}
@@ -9,6 +9,7 @@ import '../../core/common/start_config.dart';
import '../../util/log_util.dart';
import '../../util/util.dart';
import 'server.dart';
import 'webview_profile.dart';
String buildWebViewExecuteScript({
required String channelName,
@@ -73,7 +74,6 @@ class WebViewRpcService {
final Map<String, WebViewRpcPageSession> _pagesById = {};
final Completer<void> _overlayReady = Completer<void>();
late final CookieManager _cookieManager = CookieManager.instance();
RpcServerHandle? _server;
int _pageSeq = 0;
@@ -162,10 +162,12 @@ class WebViewRpcService {
}
Future<Map<String, dynamic>> _openPage(Map<String, dynamic> params) async {
final profile = WebViewProfile(_string(params, 'profileId'));
await profile.prepare(_string(params, 'proxyUrl'));
final pageId = 'page-${++_pageSeq}';
final session = WebViewRpcPageSession(
pageId: pageId,
cookieManager: _cookieManager,
cookieManager: profile,
headless: params['headless'] as bool? ?? false,
debug: params['debug'] as bool? ?? false,
title: params['title'] as String? ?? '',
@@ -293,7 +295,7 @@ class WebViewRpcPageSession {
});
final String pageId;
final CookieManager cookieManager;
final WebViewProfile cookieManager;
final bool headless;
final bool debug;
final String title;
@@ -523,10 +525,9 @@ class WebViewRpcPageSession {
);
}
InAppWebViewSettings get _settings => InAppWebViewSettings(
javaScriptEnabled: true,
transparentBackground: true,
isInspectable: debug,
InAppWebViewSettings get _settings => WebViewProfileSettings(
profileId: cookieManager.id,
debug: debug,
userAgent: userAgent.isNotEmpty ? userAgent : null,
);
+5
View File
@@ -2,6 +2,11 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
</dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
+12 -15
View File
@@ -463,12 +463,11 @@ packages:
source: hosted
version: "6.1.5"
flutter_inappwebview_android:
dependency: transitive
dependency: "direct overridden"
description:
name: flutter_inappwebview_android
sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba"
url: "https://pub.dev"
source: hosted
path: "third_party/flutter_inappwebview_android"
relative: true
source: path
version: "1.1.3"
flutter_inappwebview_internal_annotations:
dependency: transitive
@@ -479,20 +478,18 @@ packages:
source: hosted
version: "1.3.0"
flutter_inappwebview_ios:
dependency: transitive
dependency: "direct overridden"
description:
name: flutter_inappwebview_ios
sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d"
url: "https://pub.dev"
source: hosted
path: "third_party/flutter_inappwebview_ios"
relative: true
source: path
version: "1.1.2"
flutter_inappwebview_macos:
dependency: transitive
dependency: "direct overridden"
description:
name: flutter_inappwebview_macos
sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1
url: "https://pub.dev"
source: hosted
path: "third_party/flutter_inappwebview_macos"
relative: true
source: path
version: "1.1.2"
flutter_inappwebview_platform_interface:
dependency: transitive
+7
View File
@@ -86,6 +86,13 @@ dependencies:
go_router: ^16.2.1
dependency_overrides:
# Host-only native WebView profile/proxy bridge.
flutter_inappwebview_macos:
path: third_party/flutter_inappwebview_macos
flutter_inappwebview_ios:
path: third_party/flutter_inappwebview_ios
flutter_inappwebview_android:
path: third_party/flutter_inappwebview_android
# Prevent a native Windows crash when the push notification service is unavailable.
# Keep this fork until https://github.com/MaikuB/flutter_local_notifications/pull/2813 is merged and released.
flutter_local_notifications:
+31
View File
@@ -0,0 +1,31 @@
# Native WebView host bridge
Vendored unmodified Dart facades and native sources from pub.dev:
- flutter_inappwebview_macos 1.1.2
- flutter_inappwebview_ios 1.1.2
- flutter_inappwebview_android 1.1.3
Each package retains its upstream license. Versions and original archive hashes
are available in the parent commit's pubspec.lock.
Local native changes are limited to:
- `GopeedProfiles`: prepare a persistent profile and apply the internal loopback
proxy before creating a WebView. Apple uses SOCKS5 to cover HTTP as well as HTTPS;
Android uses the application-wide HTTP proxy override completion callback.
- `InAppWebViewSettings` and `InAppWebView`: read the host-only `gopeedProfileId`
setting and attach the profile before navigation.
- `MyCookieManager`: route cookie operations to the requested profile, capturing
the store per operation so asynchronous calls cannot cross profiles.
`lib/app/rpc/webview_profile.dart` uses the existing plugin method channel and
serializes these internal settings. No extension JavaScript API is added.
Apple requires macOS 14 / iOS 17 for named persistent stores and proxy settings.
Android requires `MULTI_PROFILE` and `PROXY_OVERRIDE`; unsupported runtimes return
an explicit unavailable error rather than opening a shared, unproxied browser.
When updating upstream, refresh these three packages, reapply the above changes,
and run `tool/webview_profile_check.dart` on a native Apple host. Do not patch the
user's pub cache: dependency_overrides selects these tracked sources reproducibly.
@@ -0,0 +1,103 @@
## 1.1.3
- Updated flutter_inappwebview_platform_interface version to ^1.3.0
## 1.1.2
- Removed webview/plugin_scripts_js/ConsoleLogJS.java file, use native WebChromeClient.onConsoleMessage instead
## 1.1.1
- Updated flutter_inappwebview_platform_interface version to ^1.2.0
## 1.1.0+4
- Updated flutter_inappwebview_platform_interface version
## 1.1.0+3
- Fixed compilation error
## 1.1.0+2
- Updated pubspec.yaml
## 1.1.0+1
- Downgraded androidx.appcompat:appcompat:1.7.0 to androidx.appcompat:appcompat:1.6.1
- Added `-dontwarn android.window.BackEvent` proguard rule
## 1.1.0
- Updated androidx.webkit:webkit:1.8.0 to androidx.webkit:webkit:1.12.0
- Updated androidx.browser:browser:1.6.0 to androidx.browser:browser:1.8.0
- Updates minimum supported SDK version to Flutter 3.24/Dart 3.5.
- Removed unsupported WebViewFeature.SUPPRESS_ERROR_PAGE
- Merged "Remove references to deprecated v1 Android embedding" [#2176](https://github.com/pichillilorenzo/flutter_inappwebview/pull/2176) (thanks to [gmackall](https://github.com/gmackall))
## 1.0.13
- Fixed "Android emulator using API 34 fails to draw on resume sometimes" [#1981](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1981)
## 1.0.12
- Updated `flutter_inappwebview_platform_interface` version dependency to `^1.0.10`
## 1.0.11
- Updated `flutter_inappwebview_platform_interface` version dependency to `^1.0.9`
- Fix typos (thanks to [michalsrutek](https://github.com/michalsrutek))
## 1.0.10
- Updated `flutter_inappwebview_platform_interface` version dependency to `^1.0.8`
- Implemented `PlatformCustomPathHandler` class
## 1.0.9
- Updated `flutter_inappwebview_platform_interface` version dependency to `^1.0.7`
- Fixed "Cloudflare Turnstile failure" [#1738](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1738)
- Fixed `InAppWebViewController.callAsyncJavaScript` issue when the last line of the `functionBody` parameter includes a code comment
## 1.0.8
- Implemented `InAppWebViewSettings.interceptOnlyAsyncAjaxRequests`
- Implemented `PlatformInAppWebViewController.clearFormData` method
- Implemented `PlatformCookieManager.removeSessionCookies` method
- Updated `useShouldInterceptAjaxRequest` automatic infer logic
- Updated `CookieManager` methods return value
## 1.0.7
- Merged "Fixed error in InterceptAjaxRequestJS 'Failed to set responseType property'" [#1904](https://github.com/pichillilorenzo/flutter_inappwebview/pull/1904) (thanks to [EArminjon](https://github.com/EArminjon))
- Fixed shouldInterceptAjaxRequest javascript code when overriding XMLHttpRequest.open method parameters
## 1.0.6
- Fixed "getFavicons: _TypeError: type '_Map<String, dynamic>' is not a subtype of type 'Iterable<dynamic>'" [#1897](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1897)
- Fixed "onClosed not considering back navigation or up button / close button in ChromeSafariBrowser when using noHistory: true" [#1882](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1882)
## 1.0.5
- Call `super.dispose();` on `InAppBrowser` and `ChromeSafari` implementations
## 1.0.4
- Throw platform exception when ProcessGlobalConfig.apply throws an error on the native side to be able to catch it on Flutter side
## 1.0.3
- Updated `ContentBlockerHandler` CSS_DISPLAY_NONE action type and `JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_JS_SOURCE` javascript implementation code
## 1.0.2
- Updated `flutter_inappwebview_platform_interface` version dependency to `1.0.2`
- Fixed "Crash when starting ChromeSafariBrowser on Android java.lang.NoSuchMethodError: No virtual method isEngagementSignalsApiAvailable" [#1881](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1881)
## 1.0.1
- Updated README
## 1.0.0
Initial release.
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2023 Lorenzo Pichilli
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,13 @@
# flutter\_inappwebview\_android
The Android WebView implementation of [`flutter_inappwebview`](https://pub.dev/packages/flutter_inappwebview).
## Usage
This package is [endorsed](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin),
which means you can simply use `flutter_inappwebview`
normally. This package will be automatically included in your app when you do,
so you do not need to add it to your `pubspec.yaml`.
However, if you `import` this package to use any of its APIs directly, you
should add it to your `pubspec.yaml` as usual.
@@ -0,0 +1,57 @@
group 'com.pichillilorenzo.flutter_inappwebview_android'
version '1.0-SNAPSHOT'
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.0.1'
}
}
rootProject.allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: 'com.android.library'
android {
// Conditional for compatibility with AGP <4.2.
if (project.android.hasProperty("namespace")) {
namespace 'com.pichillilorenzo.flutter_inappwebview_android'
}
compileSdk 34
defaultConfig {
minSdkVersion 19
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
consumerProguardFiles 'proguard-rules.pro'
}
lintOptions {
disable 'InvalidPackage'
}
buildTypes {
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
dependencies {
implementation 'androidx.webkit:webkit:1.12.0'
implementation 'androidx.browser:browser:1.8.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
}
}
@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
@@ -0,0 +1,19 @@
# WebView
-keepattributes *JavascriptInterface*
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
-keepclassmembers class * extends android.webkit.WebViewClient {
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
public boolean *(android.webkit.WebView, java.lang.String);
public void *(android.webkit.webView, jav.lang.String);
}
-keepclassmembers class com.pichillilorenzo.flutter_inappwebview_android$JavaScriptBridgeInterface {
<fields>;
<methods>;
public *;
private *;
}
-keep class com.pichillilorenzo.flutter_inappwebview_android.** { *; }
-dontwarn android.window.BackEvent
@@ -0,0 +1 @@
rootProject.name = 'flutter_inappwebview_android'
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.pichillilorenzo.flutter_inappwebview_android">
<application>
<activity
android:theme="@style/AppTheme"
android:exported="false"
android:name="com.pichillilorenzo.flutter_inappwebview_android.in_app_browser.InAppBrowserActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density" />
<activity
android:theme="@style/ThemeTransparent"
android:exported="false"
android:name="com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs.ChromeCustomTabsActivity" />
<activity
android:theme="@style/ThemeTransparent"
android:exported="false"
android:name="com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs.TrustedWebActivity" />
<activity
android:theme="@style/ThemeTransparent"
android:exported="false"
android:name="com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs.ChromeCustomTabsActivitySingleInstance"
android:launchMode="singleInstance"/>
<activity
android:theme="@style/ThemeTransparent"
android:exported="false"
android:name="com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs.TrustedWebActivitySingleInstance"
android:launchMode="singleInstance"/>
<receiver
android:name="com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs.ActionBroadcastReceiver"
android:enabled="true"
android:exported="false" />
<meta-data
android:name="io.flutter.embedded_views_preview"
android:value="true" />
</application>
<queries>
<intent>
<action android:name=
"android.support.customtabs.action.CustomTabsService" />
</intent>
</queries>
</manifest>
@@ -0,0 +1,33 @@
package com.pichillilorenzo.flutter_inappwebview_android;
import android.net.Uri;
import androidx.webkit.ProfileStore;
import androidx.webkit.ProxyConfig;
import androidx.webkit.ProxyController;
import androidx.webkit.WebViewFeature;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
// Internal host bridge: profile creation and proxy completion precede navigation.
final class GopeedProfiles {
static void prepare(MethodCall call, MethodChannel.Result result) {
if (!WebViewFeature.isFeatureSupported(WebViewFeature.MULTI_PROFILE) ||
!WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) {
result.error("UNAVAILABLE", "Android WebView does not support isolated profiles and proxy override", null);
return;
}
String id = call.argument("gopeedProfileId");
String proxy = call.argument("proxyUrl");
try {
java.util.UUID.fromString(id);
Uri uri = Uri.parse(proxy);
if (!"http".equals(uri.getScheme()) || !"127.0.0.1".equals(uri.getHost()) || uri.getPort() <= 0)
throw new IllegalArgumentException("Invalid host proxy configuration");
ProfileStore.getInstance().getOrCreateProfile(id);
ProxyConfig config = new ProxyConfig.Builder().addProxyRule(proxy).removeImplicitRules().build();
ProxyController.getInstance().setProxyOverride(config, Runnable::run, () -> result.success(true));
} catch (RuntimeException e) {
result.error("UNAVAILABLE", "Unable to configure WebView profile and proxy", null);
}
}
}
@@ -0,0 +1,11 @@
package com.pichillilorenzo.flutter_inappwebview_android;
import androidx.annotation.NonNull;
import java.util.Map;
public interface ISettings<T> {
@NonNull ISettings<T> parse(@NonNull Map<String, Object> settings);
@NonNull Map<String, Object> toMap();
@NonNull Map<String, Object> getRealSettings(@NonNull T obj);
}
@@ -0,0 +1,9 @@
package com.pichillilorenzo.flutter_inappwebview_android;
import androidx.core.content.FileProvider;
public class InAppWebViewFileProvider extends FileProvider {
public static final String fileProviderAuthorityExtension = "flutter_inappwebview_android.fileprovider";
}
@@ -0,0 +1,224 @@
package com.pichillilorenzo.flutter_inappwebview_android;
import android.app.Activity;
import android.content.Context;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs.ChromeSafariBrowserManager;
import com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs.NoHistoryCustomTabsActivityCallbacks;
import com.pichillilorenzo.flutter_inappwebview_android.credential_database.CredentialDatabaseHandler;
import com.pichillilorenzo.flutter_inappwebview_android.headless_in_app_webview.HeadlessInAppWebViewManager;
import com.pichillilorenzo.flutter_inappwebview_android.in_app_browser.InAppBrowserManager;
import com.pichillilorenzo.flutter_inappwebview_android.print_job.PrintJobManager;
import com.pichillilorenzo.flutter_inappwebview_android.process_global_config.ProcessGlobalConfigManager;
import com.pichillilorenzo.flutter_inappwebview_android.proxy.ProxyManager;
import com.pichillilorenzo.flutter_inappwebview_android.service_worker.ServiceWorkerManager;
import com.pichillilorenzo.flutter_inappwebview_android.tracing.TracingControllerManager;
import com.pichillilorenzo.flutter_inappwebview_android.webview.FlutterWebViewFactory;
import com.pichillilorenzo.flutter_inappwebview_android.webview.InAppWebViewManager;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.platform.PlatformViewRegistry;
import io.flutter.embedding.android.FlutterView;
public class InAppWebViewFlutterPlugin implements FlutterPlugin, ActivityAware {
protected static final String LOG_TAG = "InAppWebViewFlutterPL";
@Nullable
public PlatformUtil platformUtil;
@Nullable
public InAppBrowserManager inAppBrowserManager;
@Nullable
public HeadlessInAppWebViewManager headlessInAppWebViewManager;
@Nullable
public ChromeSafariBrowserManager chromeSafariBrowserManager;
@Nullable
public NoHistoryCustomTabsActivityCallbacks noHistoryCustomTabsActivityCallbacks;
@Nullable
public InAppWebViewManager inAppWebViewManager;
@Nullable
public MyCookieManager myCookieManager;
@Nullable
public CredentialDatabaseHandler credentialDatabaseHandler;
@Nullable
public MyWebStorage myWebStorage;
@Nullable
public ServiceWorkerManager serviceWorkerManager;
@Nullable
public WebViewFeatureManager webViewFeatureManager;
@Nullable
public ProxyManager proxyManager;
@Nullable
public PrintJobManager printJobManager;
@Nullable
public TracingControllerManager tracingControllerManager;
@Nullable
public ProcessGlobalConfigManager processGlobalConfigManager;
public FlutterWebViewFactory flutterWebViewFactory;
public Context applicationContext;
public BinaryMessenger messenger;
public FlutterPlugin.FlutterAssets flutterAssets;
@Nullable
public ActivityPluginBinding activityPluginBinding;
@Nullable
public Activity activity;
public FlutterView flutterView;
public InAppWebViewFlutterPlugin() {}
@Override
public void onAttachedToEngine(FlutterPluginBinding binding) {
this.flutterAssets = binding.getFlutterAssets();
// Shared.activity could be null or not.
// It depends on who is called first between onAttachedToEngine event and onAttachedToActivity event.
//
// See https://github.com/pichillilorenzo/flutter_inappwebview/issues/390#issuecomment-647039084
onAttachedToEngine(
binding.getApplicationContext(), binding.getBinaryMessenger(), this.activity, binding.getPlatformViewRegistry(), null);
}
private void onAttachedToEngine(Context applicationContext, BinaryMessenger messenger, Activity activity, PlatformViewRegistry platformViewRegistry, FlutterView flutterView) {
this.applicationContext = applicationContext;
this.activity = activity;
this.messenger = messenger;
this.flutterView = flutterView;
inAppBrowserManager = new InAppBrowserManager(this);
headlessInAppWebViewManager = new HeadlessInAppWebViewManager(this);
chromeSafariBrowserManager = new ChromeSafariBrowserManager(this);
noHistoryCustomTabsActivityCallbacks = new NoHistoryCustomTabsActivityCallbacks(this);
flutterWebViewFactory = new FlutterWebViewFactory(this);
platformViewRegistry.registerViewFactory(
FlutterWebViewFactory.VIEW_TYPE_ID, flutterWebViewFactory);
platformUtil = new PlatformUtil(this);
inAppWebViewManager = new InAppWebViewManager(this);
myCookieManager = new MyCookieManager(this);
myWebStorage = new MyWebStorage(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
serviceWorkerManager = new ServiceWorkerManager(this);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
credentialDatabaseHandler = new CredentialDatabaseHandler(this);
}
webViewFeatureManager = new WebViewFeatureManager(this);
proxyManager = new ProxyManager(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
printJobManager = new PrintJobManager(this);
}
tracingControllerManager = new TracingControllerManager(this);
processGlobalConfigManager = new ProcessGlobalConfigManager(this);
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
if (platformUtil != null) {
platformUtil.dispose();
platformUtil = null;
}
if (inAppBrowserManager != null) {
inAppBrowserManager.dispose();
inAppBrowserManager = null;
}
if (headlessInAppWebViewManager != null) {
headlessInAppWebViewManager.dispose();
headlessInAppWebViewManager = null;
}
if (chromeSafariBrowserManager != null) {
chromeSafariBrowserManager.dispose();
chromeSafariBrowserManager = null;
}
if (noHistoryCustomTabsActivityCallbacks != null) {
noHistoryCustomTabsActivityCallbacks.dispose();
noHistoryCustomTabsActivityCallbacks = null;
}
if (myCookieManager != null) {
myCookieManager.dispose();
myCookieManager = null;
}
if (myWebStorage != null) {
myWebStorage.dispose();
myWebStorage = null;
}
if (credentialDatabaseHandler != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
credentialDatabaseHandler.dispose();
credentialDatabaseHandler = null;
}
if (inAppWebViewManager != null) {
inAppWebViewManager.dispose();
inAppWebViewManager = null;
}
if (serviceWorkerManager != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
serviceWorkerManager.dispose();
serviceWorkerManager = null;
}
if (webViewFeatureManager != null) {
webViewFeatureManager.dispose();
webViewFeatureManager = null;
}
if (proxyManager != null) {
proxyManager.dispose();
proxyManager = null;
}
if (printJobManager != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
printJobManager.dispose();
printJobManager = null;
}
if (tracingControllerManager != null) {
tracingControllerManager.dispose();
tracingControllerManager = null;
}
if (processGlobalConfigManager != null) {
processGlobalConfigManager.dispose();
processGlobalConfigManager = null;
}
}
@Override
public void onAttachedToActivity(ActivityPluginBinding activityPluginBinding) {
this.activityPluginBinding = activityPluginBinding;
this.activity = activityPluginBinding.getActivity();
if (noHistoryCustomTabsActivityCallbacks != null) {
this.activity.getApplication().registerActivityLifecycleCallbacks(noHistoryCustomTabsActivityCallbacks.activityLifecycleCallbacks);
}
}
@Override
public void onDetachedFromActivityForConfigChanges() {
if (activity != null && noHistoryCustomTabsActivityCallbacks != null) {
this.activity.getApplication().unregisterActivityLifecycleCallbacks(noHistoryCustomTabsActivityCallbacks.activityLifecycleCallbacks);
}
activityPluginBinding = null;
activity = null;
}
@Override
public void onReattachedToActivityForConfigChanges(ActivityPluginBinding activityPluginBinding) {
this.activityPluginBinding = activityPluginBinding;
this.activity = activityPluginBinding.getActivity();
if (noHistoryCustomTabsActivityCallbacks != null) {
this.activity.getApplication().registerActivityLifecycleCallbacks(noHistoryCustomTabsActivityCallbacks.activityLifecycleCallbacks);
}
}
@Override
public void onDetachedFromActivity() {
if (activity != null && noHistoryCustomTabsActivityCallbacks != null) {
this.activity.getApplication().unregisterActivityLifecycleCallbacks(noHistoryCustomTabsActivityCallbacks.activityLifecycleCallbacks);
}
activityPluginBinding = null;
activity = null;
}
}
@@ -0,0 +1,452 @@
package com.pichillilorenzo.flutter_inappwebview_android;
import android.os.Build;
import android.util.Log;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
import android.webkit.ValueCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.webkit.CookieManagerCompat;
import androidx.webkit.WebViewFeature;
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
public class MyCookieManager extends ChannelDelegateImpl {
protected static final String LOG_TAG = "MyCookieManager";
public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_cookiemanager";
@Nullable
public static CookieManager cookieManager;
private CookieManager selectedCookieManager;
@Nullable
public InAppWebViewFlutterPlugin plugin;
public MyCookieManager(@NonNull final InAppWebViewFlutterPlugin plugin) {
super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME));
this.plugin = plugin;
}
public static void init() {
if (cookieManager == null) {
cookieManager = getCookieManager();
}
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
if (call.method.equals("gopeed.prepareProfile")) {
GopeedProfiles.prepare(call, result);
return;
}
String profileID = call.argument("gopeedProfileId");
if (profileID != null && !profileID.isEmpty()) {
if (!androidx.webkit.WebViewFeature.isFeatureSupported(androidx.webkit.WebViewFeature.MULTI_PROFILE) ||
androidx.webkit.ProfileStore.getInstance().getProfile(profileID) == null) {
result.error("UNAVAILABLE", "WebView profile is not initialized", null);
return;
}
selectedCookieManager = androidx.webkit.ProfileStore.getInstance().getProfile(profileID).getCookieManager();
}
init();
try {
switch (call.method) {
case "setCookie":
{
String url = (String) call.argument("url");
String name = (String) call.argument("name");
String value = (String) call.argument("value");
String domain = (String) call.argument("domain");
String path = (String) call.argument("path");
String expiresDateString = (String) call.argument("expiresDate");
Long expiresDate = (expiresDateString != null ? new Long(expiresDateString) : null);
Integer maxAge = (Integer) call.argument("maxAge");
Boolean isSecure = (Boolean) call.argument("isSecure");
Boolean isHttpOnly = (Boolean) call.argument("isHttpOnly");
String sameSite = (String) call.argument("sameSite");
setCookie(url,
name,
value,
domain,
path,
expiresDate,
maxAge,
isSecure,
isHttpOnly,
sameSite,
result);
}
break;
case "getCookies":
result.success(getCookies((String) call.argument("url")));
break;
case "deleteCookie":
{
String url = (String) call.argument("url");
String name = (String) call.argument("name");
String domain = (String) call.argument("domain");
String path = (String) call.argument("path");
deleteCookie(url, name, domain, path, result);
}
break;
case "deleteCookies":
{
String url = (String) call.argument("url");
String domain = (String) call.argument("domain");
String path = (String) call.argument("path");
deleteCookies(url, domain, path, result);
}
break;
case "deleteAllCookies":
deleteAllCookies(result);
break;
case "removeSessionCookies":
removeSessionCookies(result);
break;
default:
result.notImplemented();
}
} finally { selectedCookieManager = null; }
}
/**
* Instantiating CookieManager will load the Chromium task taking a 100ish ms so we do it lazily
* to make sure it's done on a background thread as needed.
*
* https://github.com/facebook/react-native/blob/1903f6680d9750e244d97c3cd4a9f755a9a47c61/ReactAndroid/src/main/java/com/facebook/react/modules/network/ForwardingCookieHandler.java#L132
*/
static private @Nullable CookieManager getCookieManager() {
if (cookieManager == null) {
try {
cookieManager = CookieManager.getInstance();
} catch (IllegalArgumentException ex) {
// https://bugs.chromium.org/p/chromium/issues/detail?id=559720
return null;
} catch (Exception exception) {
String message = exception.getMessage();
// We cannot catch MissingWebViewPackageException as it is in a private / system API
// class. This validates the exception's message to ensure we are only handling this
// specific exception.
// https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/webkit/WebViewFactory.java#348
if (message != null
&& exception
.getClass()
.getCanonicalName()
.equals("android.webkit.WebViewFactory.MissingWebViewPackageException")) {
return null;
} else {
throw exception;
}
}
}
return cookieManager;
}
public void setCookie(String url,
String name,
String value,
String domain,
String path,
Long expiresDate,
Integer maxAge,
Boolean isSecure,
Boolean isHttpOnly,
String sameSite,
final MethodChannel.Result result) {
final CookieManager cookieManager = selectedCookieManager != null ? selectedCookieManager : getCookieManager();
if (cookieManager == null) {
result.success(false);
return;
}
String cookieValue = name + "=" + value + "; Path=" + path;
if (domain != null)
cookieValue += "; Domain=" + domain;
if (expiresDate != null)
cookieValue += "; Expires=" + getCookieExpirationDate(expiresDate);
if (maxAge != null)
cookieValue += "; Max-Age=" + maxAge.toString();
if (isSecure != null && isSecure)
cookieValue += "; Secure";
if (isHttpOnly != null && isHttpOnly)
cookieValue += "; HttpOnly";
if (sameSite != null)
cookieValue += "; SameSite=" + sameSite;
cookieValue += ";";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cookieManager.setCookie(url, cookieValue, new ValueCallback<Boolean>() {
@Override
public void onReceiveValue(Boolean successful) {
result.success(successful);
}
});
cookieManager.flush();
}
else if (plugin != null) {
CookieSyncManager cookieSyncMngr = CookieSyncManager.createInstance(plugin.applicationContext);
cookieSyncMngr.startSync();
cookieManager.setCookie(url, cookieValue);
cookieSyncMngr.stopSync();
cookieSyncMngr.sync();
result.success(true);
} else {
cookieManager.setCookie(url, cookieValue);
result.success(true);
}
}
public List<Map<String, Object>> getCookies(final String url) {
final List<Map<String, Object>> cookieListMap = new ArrayList<>();
final CookieManager cookieManager = selectedCookieManager != null ? selectedCookieManager : getCookieManager();
if (cookieManager == null) return cookieListMap;
List<String> cookies = new ArrayList<>();
if (WebViewFeature.isFeatureSupported(WebViewFeature.GET_COOKIE_INFO)) {
cookies = CookieManagerCompat.getCookieInfo(cookieManager, url);
} else {
String cookiesString = cookieManager.getCookie(url);
if (cookiesString != null) {
cookies = Arrays.asList(cookiesString.split(";"));
}
}
for (String cookie : cookies) {
String[] cookieParams = cookie.split(";");
if (cookieParams.length == 0) continue;
String[] nameValue = cookieParams[0].split("=", 2);
String name = nameValue[0].trim();
String value = (nameValue.length > 1) ? nameValue[1].trim() : "";
Map<String, Object> cookieMap = new HashMap<>();
cookieMap.put("name", name);
cookieMap.put("value", value);
cookieMap.put("expiresDate", null);
cookieMap.put("isSessionOnly", null);
cookieMap.put("domain", null);
cookieMap.put("sameSite", null);
cookieMap.put("isSecure", null);
cookieMap.put("isHttpOnly", null);
cookieMap.put("path", null);
if (WebViewFeature.isFeatureSupported(WebViewFeature.GET_COOKIE_INFO)) {
cookieMap.put("isSecure", false);
cookieMap.put("isHttpOnly", false);
for (int i = 1; i < cookieParams.length; i++) {
String[] cookieParamNameValue = cookieParams[i].split("=", 2);
String cookieParamName = cookieParamNameValue[0].trim();
String cookieParamValue = (cookieParamNameValue.length > 1) ? cookieParamNameValue[1].trim() : "";
if (cookieParamName.equalsIgnoreCase("Expires")) {
try {
final SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy hh:mm:ss z", Locale.US);
Date expiryDate = sdf.parse(cookieParamValue);
if (expiryDate != null) {
cookieMap.put("expiresDate", expiryDate.getTime());
}
} catch (ParseException e) {
Log.e(LOG_TAG, "", e);
}
} else if (cookieParamName.equalsIgnoreCase("Max-Age")) {
try {
long maxAge = Long.parseLong(cookieParamValue);
cookieMap.put("expiresDate", System.currentTimeMillis() + maxAge);
} catch (NumberFormatException e) {
Log.e(LOG_TAG, "", e);
}
} else if (cookieParamName.equalsIgnoreCase("Domain")) {
cookieMap.put("domain", cookieParamValue);
} else if (cookieParamName.equalsIgnoreCase("SameSite")) {
cookieMap.put("sameSite", cookieParamValue);
} else if (cookieParamName.equalsIgnoreCase("Secure")) {
cookieMap.put("isSecure", true);
} else if (cookieParamName.equalsIgnoreCase("HttpOnly")) {
cookieMap.put("isHttpOnly", true);
} else if (cookieParamName.equalsIgnoreCase("Path")) {
cookieMap.put("path", cookieParamValue);
}
}
}
cookieListMap.add(cookieMap);
}
return cookieListMap;
}
public void deleteCookie(String url, String name, String domain, String path, final MethodChannel.Result result) {
final CookieManager cookieManager = selectedCookieManager != null ? selectedCookieManager : getCookieManager();
if (cookieManager == null) {
result.success(false);
return;
}
String cookieValue = name + "=; Path=" + path + "; Max-Age=-1";
if (domain != null)
cookieValue += "; Domain=" + domain;
cookieValue += ";";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cookieManager.setCookie(url, cookieValue, new ValueCallback<Boolean>() {
@Override
public void onReceiveValue(Boolean successful) {
result.success(successful);
}
});
cookieManager.flush();
}
else if (plugin != null) {
CookieSyncManager cookieSyncMngr = CookieSyncManager.createInstance(plugin.applicationContext);
cookieSyncMngr.startSync();
cookieManager.setCookie(url, cookieValue);
cookieSyncMngr.stopSync();
cookieSyncMngr.sync();
result.success(true);
} else {
cookieManager.setCookie(url, cookieValue);
result.success(true);
}
}
public void deleteCookies(String url, String domain, String path, final MethodChannel.Result result) {
final CookieManager cookieManager = selectedCookieManager != null ? selectedCookieManager : getCookieManager();
if (cookieManager == null) {
result.success(false);
return;
}
CookieSyncManager cookieSyncMngr = null;
String cookiesString = cookieManager.getCookie(url);
if (cookiesString != null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP && plugin != null) {
cookieSyncMngr = CookieSyncManager.createInstance(plugin.applicationContext);
cookieSyncMngr.startSync();
}
String[] cookies = cookiesString.split(";");
for (String cookie : cookies) {
String[] nameValue = cookie.split("=", 2);
String name = nameValue[0].trim();
String cookieValue = name + "=; Path=" + path + "; Max-Age=-1";
if (domain != null)
cookieValue += "; Domain=" + domain;
cookieValue += ";";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
cookieManager.setCookie(url, cookieValue, null);
else
cookieManager.setCookie(url, cookieValue);
}
if (cookieSyncMngr != null) {
cookieSyncMngr.stopSync();
cookieSyncMngr.sync();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
cookieManager.flush();
}
result.success(true);
}
public void deleteAllCookies(final MethodChannel.Result result) {
final CookieManager cookieManager = selectedCookieManager != null ? selectedCookieManager : getCookieManager();
if (cookieManager == null) {
result.success(false);
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cookieManager.removeAllCookies(new ValueCallback<Boolean>() {
@Override
public void onReceiveValue(Boolean successful) {
result.success(successful);
}
});
cookieManager.flush();
}
else if (plugin != null) {
CookieSyncManager cookieSyncMngr = CookieSyncManager.createInstance(plugin.applicationContext);
cookieSyncMngr.startSync();
cookieManager.removeAllCookie();
cookieSyncMngr.stopSync();
cookieSyncMngr.sync();
result.success(true);
} else {
cookieManager.removeAllCookie();
result.success(true);
}
}
public void removeSessionCookies(final MethodChannel.Result result) {
final CookieManager cookieManager = selectedCookieManager != null ? selectedCookieManager : getCookieManager();
if (cookieManager == null) {
result.success(false);
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
cookieManager.removeSessionCookies(new ValueCallback<Boolean>() {
@Override
public void onReceiveValue(Boolean successful) {
result.success(successful);
}
});
cookieManager.flush();
}
else if (plugin != null) {
CookieSyncManager cookieSyncMngr = CookieSyncManager.createInstance(plugin.applicationContext);
cookieSyncMngr.startSync();
cookieManager.removeSessionCookie();
cookieSyncMngr.stopSync();
cookieSyncMngr.sync();
result.success(true);
} else {
cookieManager.removeSessionCookie();
result.success(true);
}
}
public static String getCookieExpirationDate(Long timestamp) {
final SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy hh:mm:ss z", Locale.US);
sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
return sdf.format(new Date(timestamp));
}
@Override
public void dispose() {
super.dispose();
plugin = null;
}
}
@@ -0,0 +1,138 @@
package com.pichillilorenzo.flutter_inappwebview_android;
import android.webkit.ValueCallback;
import android.webkit.WebStorage;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
public class MyWebStorage extends ChannelDelegateImpl {
protected static final String LOG_TAG = "MyWebStorage";
public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_webstoragemanager";
@Nullable
public static WebStorage webStorageManager;
@Nullable
public InAppWebViewFlutterPlugin plugin;
public MyWebStorage(@NonNull final InAppWebViewFlutterPlugin plugin) {
super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME));
this.plugin = plugin;
}
public static void init() {
if (webStorageManager == null) {
webStorageManager = WebStorage.getInstance();
}
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
init();
switch (call.method) {
case "getOrigins":
getOrigins(result);
break;
case "deleteAllData":
if (webStorageManager != null) {
webStorageManager.deleteAllData();
result.success(true);
} else {
result.success(false);
}
break;
case "deleteOrigin":
{
if (webStorageManager != null) {
String origin = (String) call.argument("origin");
webStorageManager.deleteOrigin(origin);
result.success(true);
} else {
result.success(false);
}
}
break;
case "getQuotaForOrigin":
{
String origin = (String) call.argument("origin");
getQuotaForOrigin(origin, result);
}
break;
case "getUsageForOrigin":
{
String origin = (String) call.argument("origin");
getUsageForOrigin(origin, result);
}
break;
default:
result.notImplemented();
}
}
public void getOrigins(final MethodChannel.Result result) {
if (webStorageManager == null) {
result.success(new ArrayList<>());
return;
}
webStorageManager.getOrigins(new ValueCallback<Map>() {
@Override
public void onReceiveValue(Map value) {
List<Map<String, Object>> origins = new ArrayList<>();
for(Object key : value.keySet()) {
WebStorage.Origin originObj = (WebStorage.Origin) value.get(key);
Map<String, Object> originInfo = new HashMap<>();
originInfo.put("origin", originObj.getOrigin());
originInfo.put("quota", originObj.getQuota());
originInfo.put("usage", originObj.getUsage());
origins.add(originInfo);
}
result.success(origins);
}
});
}
public void getQuotaForOrigin(String origin, final MethodChannel.Result result) {
if (webStorageManager == null) {
result.success(0);
return;
}
webStorageManager.getQuotaForOrigin(origin, new ValueCallback<Long>() {
@Override
public void onReceiveValue(Long value) {
result.success(value);
}
});
}
public void getUsageForOrigin(String origin, final MethodChannel.Result result) {
if (webStorageManager == null) {
result.success(0);
return;
}
webStorageManager.getUsageForOrigin(origin, new ValueCallback<Long>() {
@Override
public void onReceiveValue(Long value) {
result.success(value);
}
});
}
@Override
public void dispose() {
super.dispose();
plugin = null;
}
}
@@ -0,0 +1,73 @@
package com.pichillilorenzo.flutter_inappwebview_android;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
public class PlatformUtil extends ChannelDelegateImpl {
protected static final String LOG_TAG = "PlatformUtil";
public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_platformutil";
@Nullable
public InAppWebViewFlutterPlugin plugin;
public PlatformUtil(final InAppWebViewFlutterPlugin plugin) {
super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME));
this.plugin = plugin;
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull final MethodChannel.Result result) {
switch (call.method) {
case "getSystemVersion":
result.success(String.valueOf(Build.VERSION.SDK_INT));
break;
case "formatDate":
long date = (long) call.argument("date");
String format = (String) call.argument("format");
Locale locale = PlatformUtil.getLocaleFromString((String) call.argument("locale"));
String timezone = (String) call.argument("timezone");
if (timezone == null) {
timezone = "UTC";
}
result.success(PlatformUtil.formatDate(date, format, locale, TimeZone.getTimeZone(timezone)));
break;
default:
result.notImplemented();
}
}
public static Locale getLocaleFromString(@Nullable String locale) {
if (locale == null) {
return Locale.US;
}
String[] localeSplit = locale.split("_");
String language = localeSplit[0];
String country = localeSplit.length > 1 ? localeSplit[1] : "";
String variant = localeSplit.length > 2 ? localeSplit[2] : "";
return new Locale(language, country, variant);
}
public static String formatDate(long date, String format, Locale locale, TimeZone timezone) {
final SimpleDateFormat sdf = new SimpleDateFormat(format, locale);
sdf.setTimeZone(timezone);
return sdf.format(new Date(date));
}
@Override
public void dispose() {
super.dispose();
plugin = null;
}
}
@@ -0,0 +1,386 @@
package com.pichillilorenzo.flutter_inappwebview_android;
import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.BitmapFactory;
import android.graphics.Insets;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.http.SslCertificate;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.pichillilorenzo.flutter_inappwebview_android.types.Size2D;
import com.pichillilorenzo.flutter_inappwebview_android.types.SyncBaseCallbackResultImpl;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.URL;
import java.net.UnknownHostException;
import java.security.Key;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;
import javax.net.ssl.SSLHandshakeException;
import io.flutter.plugin.common.MethodChannel;
public class Util {
static final String LOG_TAG = "Util";
public static final String ANDROID_ASSET_URL = "file:///android_asset/";
private Util() {}
public static String getUrlAsset(InAppWebViewFlutterPlugin plugin, String assetFilePath) throws IOException {
String key = plugin.flutterAssets.getAssetFilePathByName(assetFilePath);
InputStream is = null;
IOException e = null;
try {
is = getFileAsset(plugin, assetFilePath);
} catch (IOException ex) {
e = ex;
} finally {
if (is != null) {
try {
is.close();
} catch (IOException ex) {
e = ex;
}
}
}
if (e != null) {
throw e;
}
return ANDROID_ASSET_URL + key;
}
public static InputStream getFileAsset(InAppWebViewFlutterPlugin plugin, String assetFilePath) throws IOException {
String key = plugin.flutterAssets.getAssetFilePathByName(assetFilePath);
AssetManager mg = plugin.applicationContext.getResources().getAssets();
return mg.open(key);
}
public static <T> T invokeMethodAndWaitResult(final @NonNull MethodChannel channel,
final @NonNull String method, final @Nullable Object arguments,
final @NonNull SyncBaseCallbackResultImpl<T> callback) throws InterruptedException {
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
channel.invokeMethod(method, arguments, callback);
}
});
callback.latch.await();
return callback.result;
}
@Nullable
public static PrivateKeyAndCertificates loadPrivateKeyAndCertificate(@NonNull InAppWebViewFlutterPlugin plugin,
@NonNull String certificatePath,
@Nullable String certificatePassword,
@NonNull String keyStoreType) {
PrivateKeyAndCertificates privateKeyAndCertificates = null;
InputStream certificateFileStream = null;
try {
certificateFileStream = getFileAsset(plugin, certificatePath);
} catch (IOException ignored) {}
try {
if (certificateFileStream == null) {
certificateFileStream = new FileInputStream(certificatePath);
}
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(certificateFileStream, (certificatePassword != null ? certificatePassword : "").toCharArray());
Enumeration<String> aliases = keyStore.aliases();
String alias = aliases.nextElement();
Key key = keyStore.getKey(alias, (certificatePassword != null ? certificatePassword : "").toCharArray());
if (key instanceof PrivateKey) {
PrivateKey privateKey = (PrivateKey)key;
Certificate cert = keyStore.getCertificate(alias);
X509Certificate[] certificates = new X509Certificate[1];
certificates[0] = (X509Certificate)cert;
privateKeyAndCertificates = new PrivateKeyAndCertificates(privateKey, certificates);
}
certificateFileStream.close();
} catch (Exception e) {
Log.e(LOG_TAG, "", e);
} finally {
if (certificateFileStream != null) {
try {
certificateFileStream.close();
} catch (IOException ex) {
Log.e(LOG_TAG, "", ex);
}
}
}
return privateKeyAndCertificates;
}
public static class PrivateKeyAndCertificates {
public X509Certificate[] certificates;
public PrivateKey privateKey;
public PrivateKeyAndCertificates(PrivateKey privateKey, X509Certificate[] certificates) {
this.privateKey = privateKey;
this.certificates = certificates;
}
}
@Nullable
public static HttpURLConnection makeHttpRequest(String urlString, String method, @Nullable Map<String, String> headers) {
HttpURLConnection urlConnection = null;
try {
URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod(method);
if (headers != null) {
for (Map.Entry<String, String> header : headers.entrySet()) {
urlConnection.setRequestProperty(header.getKey(), header.getValue());
}
}
urlConnection.setConnectTimeout(15000); // 15 seconds
urlConnection.setReadTimeout(15000); // 15 seconds
urlConnection.setDoInput(true);
urlConnection.setInstanceFollowRedirects(true);
if ("GET".equalsIgnoreCase(method)) {
urlConnection.setDoOutput(false);
}
urlConnection.connect();
return urlConnection;
}
catch (Exception e) {
if (!(e instanceof SSLHandshakeException)) {
Log.e(LOG_TAG, "", e);
}
if (urlConnection != null) {
urlConnection.disconnect();
}
}
return null;
}
/**
* SslCertificate class does not has a public getter for the underlying
* X509Certificate, we can only do this by hack. This only works for Android 4.0+
* https://groups.google.com/forum/#!topic/android-developers/eAPJ6b7mrmg
*/
public static X509Certificate getX509CertFromSslCertHack(SslCertificate sslCert) {
X509Certificate x509Certificate = null;
Bundle bundle = SslCertificate.saveState(sslCert);
byte[] bytes = bundle.getByteArray("x509-certificate");
if (bytes == null) {
x509Certificate = null;
} else {
try {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
Certificate cert = certFactory.generateCertificate(new ByteArrayInputStream(bytes));
x509Certificate = (X509Certificate) cert;
} catch (CertificateException e) {
x509Certificate = null;
}
}
return x509Certificate;
}
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public static String JSONStringify(@Nullable Object value) {
if (value == null) {
return "null";
}
if (value instanceof Map) {
return new JSONObject((Map<String, Object>) value).toString();
} else if (value instanceof List) {
return new JSONArray((List<Object>) value).toString();
} else if (value instanceof String) {
return JSONObject.quote((String) value);
} else {
return JSONObject.wrap(value).toString();
}
}
public static boolean objEquals(@Nullable Object a, @Nullable Object b) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return Objects.equals(a, b);
}
return (a == b) || (a != null && a.equals(b));
}
public static String replaceAll(String s, String oldString, String newString) {
return TextUtils.join(newString, s.split(Pattern.quote(oldString)));
}
public static void log(String tag, String message) {
// Split by line, then ensure each line can fit into Log's maximum length.
for (int i = 0, length = message.length(); i < length; i++) {
int newline = message.indexOf('\n', i);
newline = newline != -1 ? newline : length;
do {
int end = Math.min(newline, i + 4000);
Log.d(tag, message.substring(i, end));
i = end;
} while (i < newline);
}
}
public static float getPixelDensity(Context context) {
return context.getResources().getDisplayMetrics().density;
}
public static Size2D getFullscreenSize(Context context) {
Size2D fullscreenSize = new Size2D(-1, -1);
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
if (wm != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
final WindowMetrics metrics = wm.getCurrentWindowMetrics();
// Gets all excluding insets
final WindowInsets windowInsets = metrics.getWindowInsets();
Insets insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars()
| WindowInsets.Type.displayCutout());
int insetsWidth = insets.right + insets.left;
int insetsHeight = insets.top + insets.bottom;
final Rect bounds = metrics.getBounds();
fullscreenSize.setWidth(bounds.width() - insetsWidth);
fullscreenSize.setHeight(bounds.height() - insetsHeight);
} else {
DisplayMetrics displayMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(displayMetrics);
fullscreenSize.setWidth(displayMetrics.widthPixels);
fullscreenSize.setHeight(displayMetrics.heightPixels);
}
}
return fullscreenSize;
}
public static boolean isClass(String className) {
try {
Class.forName(className);
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
public static boolean isIPv6(String address) {
try {
Inet6Address.getByName(address);
} catch (UnknownHostException e) {
return false;
}
return true;
}
public static String normalizeIPv6(String address) throws Exception {
if (!Util.isIPv6(address)) {
throw new Exception("Invalid address: " + address);
}
return InetAddress.getByName(address).getCanonicalHostName();
}
public static <T> T getOrDefault(Map<String, Object> map, String key, T defaultValue) {
return map.containsKey(key) ? (T) map.get(key) : defaultValue;
}
@Nullable
public static byte[] readAllBytes(@Nullable InputStream inputStream) {
if (inputStream == null) {
return null;
}
final int bufLen = 4 * 0x400; // 4KB
byte[] buf = new byte[bufLen];
int readLen;
IOException exception = null;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] data = null;
try {
while ((readLen = inputStream.read(buf, 0, bufLen)) != -1)
outputStream.write(buf, 0, readLen);
data = outputStream.toByteArray();
} catch (IOException e) {
exception = e;
} finally {
try {
inputStream.close();
} catch (IOException e) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && exception != null) {
exception.addSuppressed(e);
}
}
try {
outputStream.close();
} catch (IOException e) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && exception != null) {
exception.addSuppressed(e);
}
}
}
return data;
}
@Nullable
public static <O> Object invokeMethodIfExists(final O o, final String methodName, Object... args) {
Method[] methods = o.getClass().getMethods();
for (Method method : methods) {
if (method.getName().equals(methodName)) {
try {
return method.invoke(o, args);
} catch (IllegalAccessException e) {
return null;
} catch (InvocationTargetException e) {
return null;
}
}
}
return null;
}
public static Drawable drawableFromBytes(Context context, byte[] data) {
return new BitmapDrawable(context.getResources(), BitmapFactory.decodeByteArray(data, 0, data.length));
}
}
@@ -0,0 +1,47 @@
package com.pichillilorenzo.flutter_inappwebview_android;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.webkit.WebViewFeature;
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
public class WebViewFeatureManager extends ChannelDelegateImpl {
protected static final String LOG_TAG = "WebViewFeatureManager";
public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_webviewfeature";
@Nullable
public InAppWebViewFlutterPlugin plugin;
public WebViewFeatureManager(@NonNull final InAppWebViewFlutterPlugin plugin) {
super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME));
this.plugin = plugin;
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
switch (call.method) {
case "isFeatureSupported":
String feature = (String) call.argument("feature");
result.success(WebViewFeature.isFeatureSupported(feature));
break;
case "isStartupFeatureSupported":
if (plugin != null && plugin.activity != null) {
String startupFeature = (String) call.argument("startupFeature");
result.success(WebViewFeature.isStartupFeatureSupported(plugin.activity, startupFeature));
}
break;
default:
result.notImplemented();
}
}
@Override
public void dispose() {
super.dispose();
plugin = null;
}
}
@@ -0,0 +1,47 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.browser.customtabs.CustomTabsIntent;
public class ActionBroadcastReceiver extends BroadcastReceiver {
protected static final String LOG_TAG = "ActionBroadcastReceiver";
public static final String KEY_ACTION_ID = "com.pichillilorenzo.flutter_inappwebview.ChromeCustomTabs.ACTION_ID";
public static final String KEY_ACTION_VIEW_ID = "com.pichillilorenzo.flutter_inappwebview.ChromeCustomTabs.ACTION_VIEW_ID";
public static final String KEY_ACTION_MANAGER_ID = "com.pichillilorenzo.flutter_inappwebview.ChromeCustomTabs.ACTION_MANAGER_ID";
public static final String KEY_URL_TITLE = "android.intent.extra.SUBJECT";
@Override
public void onReceive(Context context, Intent intent) {
int clickedId = intent.getIntExtra(CustomTabsIntent.EXTRA_REMOTEVIEWS_CLICKED_ID, -1);
String url = intent.getDataString();
if (url != null) {
Bundle b = intent.getExtras();
String viewId = b.getString(KEY_ACTION_VIEW_ID);
String managerId = b.getString(KEY_ACTION_MANAGER_ID);
if (managerId != null) {
ChromeSafariBrowserManager chromeSafariBrowserManager = ChromeSafariBrowserManager.shared.get(managerId);
if (chromeSafariBrowserManager != null) {
if (clickedId == -1) {
int id = b.getInt(KEY_ACTION_ID);
String title = b.getString(KEY_URL_TITLE);
ChromeCustomTabsActivity browser = chromeSafariBrowserManager.browsers.get(viewId);
if (browser != null && browser.channelDelegate != null) {
browser.channelDelegate.onItemActionPerform(id, url, title);
}
} else {
ChromeCustomTabsActivity browser = chromeSafariBrowserManager.browsers.get(viewId);
if (browser != null && browser.channelDelegate != null) {
browser.channelDelegate.onSecondaryItemActionPerform(browser.getResources().getResourceName(clickedId), url);
}
}
}
}
}
}
}
@@ -0,0 +1,462 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.widget.RemoteViews;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabColorSchemeParams;
import androidx.browser.customtabs.CustomTabsCallback;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.browser.customtabs.CustomTabsService;
import androidx.browser.customtabs.CustomTabsSession;
import androidx.browser.customtabs.EngagementSignalsCallback;
import com.pichillilorenzo.flutter_inappwebview_android.R;
import com.pichillilorenzo.flutter_inappwebview_android.types.AndroidResource;
import com.pichillilorenzo.flutter_inappwebview_android.types.CustomTabsActionButton;
import com.pichillilorenzo.flutter_inappwebview_android.types.CustomTabsMenuItem;
import com.pichillilorenzo.flutter_inappwebview_android.types.CustomTabsSecondaryToolbar;
import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.flutter.plugin.common.MethodChannel;
public class ChromeCustomTabsActivity extends Activity implements Disposable {
protected static final String LOG_TAG = "CustomTabsActivity";
public static final String METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_chromesafaribrowser_";
public String id;
@Nullable
public CustomTabsIntent.Builder builder;
public ChromeCustomTabsSettings customSettings = new ChromeCustomTabsSettings();
public CustomTabActivityHelper customTabActivityHelper = new CustomTabActivityHelper();
@Nullable
public CustomTabsSession customTabsSession;
public final static int CHROME_CUSTOM_TAB_REQUEST_CODE = 100;
public final static int NO_HISTORY_CHROME_CUSTOM_TAB_REQUEST_CODE = 101;
protected boolean onOpened = false;
protected boolean onCompletedInitialLoad = false;
protected boolean isBindSuccess = false;
@Nullable
public ChromeSafariBrowserManager manager;
@Nullable
public String initialUrl;
@Nullable
public List<String> initialOtherLikelyURLs;
@Nullable
public Map<String, String> initialHeaders;
@Nullable
public String initialReferrer;
public List<CustomTabsMenuItem> menuItems = new ArrayList<>();
@Nullable
public CustomTabsActionButton actionButton;
@Nullable
public CustomTabsSecondaryToolbar secondaryToolbar;
@Nullable
public ChromeCustomTabsChannelDelegate channelDelegate;
@CallSuper
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.chrome_custom_tabs_layout);
Bundle b = getIntent().getExtras();
if (b == null) return;
id = b.getString("id");
String managerId = b.getString("managerId");
manager = ChromeSafariBrowserManager.shared.get(managerId);
if (manager == null || manager.plugin == null || manager.plugin.messenger == null) return;
manager.browsers.put(id, this);
MethodChannel channel = new MethodChannel(manager.plugin.messenger, METHOD_CHANNEL_NAME_PREFIX + id);
channelDelegate = new ChromeCustomTabsChannelDelegate(this, channel);
initialUrl = b.getString("url");
initialHeaders = (Map<String, String>) b.getSerializable("headers");
initialReferrer = b.getString("referrer");
initialOtherLikelyURLs = b.getStringArrayList("otherLikelyURLs");
customSettings = new ChromeCustomTabsSettings();
customSettings.parse((HashMap<String, Object>) b.getSerializable("settings"));
actionButton = CustomTabsActionButton.fromMap((Map<String, Object>) b.getSerializable("actionButton"));
secondaryToolbar = CustomTabsSecondaryToolbar.fromMap((Map<String, Object>) b.getSerializable("secondaryToolbar"));
List<Map<String, Object>> menuItemList = (List<Map<String, Object>>) b.getSerializable("menuItemList");
for (Map<String, Object> menuItem : menuItemList) {
menuItems.add(CustomTabsMenuItem.fromMap(menuItem));
}
if (customSettings.noHistory && manager.plugin.noHistoryCustomTabsActivityCallbacks != null) {
manager.plugin.noHistoryCustomTabsActivityCallbacks.noHistoryBrowserIDs.put(id, id);
}
final ChromeCustomTabsActivity chromeCustomTabsActivity = this;
customTabActivityHelper.setConnectionCallback(new CustomTabActivityHelper.ConnectionCallback() {
@Override
public void onCustomTabsConnected() {
customTabsConnected();
if (channelDelegate != null) {
channelDelegate.onServiceConnected();
}
}
@Override
public void onCustomTabsDisconnected() {
chromeCustomTabsActivity.close();
dispose();
}
});
customTabActivityHelper.setCustomTabsCallback(new CustomTabsCallback() {
@Override
public void onNavigationEvent(int navigationEvent, Bundle extras) {
if (navigationEvent == TAB_SHOWN && !onOpened) {
onOpened = true;
if (channelDelegate != null) {
channelDelegate.onOpened();
}
}
if (navigationEvent == NAVIGATION_FINISHED && !onCompletedInitialLoad) {
onCompletedInitialLoad = true;
if (channelDelegate != null) {
channelDelegate.onCompletedInitialLoad();
}
}
if (channelDelegate != null) {
channelDelegate.onNavigationEvent(navigationEvent);
}
}
@Override
public void extraCallback(@NonNull String callbackName, Bundle args) {
}
@Override
public void onMessageChannelReady(Bundle extras) {
if (channelDelegate != null) {
channelDelegate.onMessageChannelReady();
}
}
@Override
public void onPostMessage(@NonNull String message, Bundle extras) {
if (channelDelegate != null) {
channelDelegate.onPostMessage(message);
}
}
@Override
public void onRelationshipValidationResult(@CustomTabsService.Relation int relation,
@NonNull Uri requestedOrigin,
boolean result, Bundle extras) {
if (channelDelegate != null) {
channelDelegate.onRelationshipValidationResult(relation, requestedOrigin, result);
}
}
});
}
public void launchUrl(@NonNull String url,
@Nullable Map<String, String> headers,
@Nullable String referrer,
@Nullable List<String> otherLikelyURLs) {
launchUrlWithSession(customTabsSession, url, headers, referrer, otherLikelyURLs);
}
public void launchUrlWithSession(@Nullable CustomTabsSession session,
@NonNull String url,
@Nullable Map<String, String> headers,
@Nullable String referrer,
@Nullable List<String> otherLikelyURLs) {
mayLaunchUrl(url, otherLikelyURLs);
builder = new CustomTabsIntent.Builder(session);
prepareCustomTabs();
CustomTabsIntent customTabsIntent = builder.build();
prepareCustomTabsIntent(customTabsIntent);
CustomTabActivityHelper.openCustomTab(this, customTabsIntent, Uri.parse(url), headers,
referrer != null ? Uri.parse(referrer) : null, CHROME_CUSTOM_TAB_REQUEST_CODE);
}
public boolean mayLaunchUrl(@Nullable String url, @Nullable List<String> otherLikelyURLs) {
Uri uri = url != null ? Uri.parse(url) : null;
List<Bundle> bundleOtherLikelyURLs = new ArrayList<>();
if (otherLikelyURLs != null) {
Bundle bundleOtherLikelyURL = new Bundle();
for (String otherLikelyURL : otherLikelyURLs) {
bundleOtherLikelyURL.putString(CustomTabsService.KEY_URL, otherLikelyURL);
}
}
return customTabActivityHelper.mayLaunchUrl(uri, null, bundleOtherLikelyURLs);
}
@CallSuper
public void customTabsConnected() {
customTabsSession = customTabActivityHelper.getSession();
if (customTabsSession != null) {
try {
Bundle bundle = new Bundle();
if (customTabsSession.isEngagementSignalsApiAvailable(bundle)) {
customTabsSession.setEngagementSignalsCallback(new EngagementSignalsCallback() {
@Override
public void onVerticalScrollEvent(boolean isDirectionUp, @NonNull Bundle extras) {
if (channelDelegate != null) {
channelDelegate.onVerticalScrollEvent(isDirectionUp);
}
}
@Override
public void onGreatestScrollPercentageIncreased(int scrollPercentage, @NonNull Bundle extras) {
if (channelDelegate != null) {
channelDelegate.onGreatestScrollPercentageIncreased(scrollPercentage);
}
}
@Override
public void onSessionEnded(boolean didUserInteract, @NonNull Bundle extras) {
if (channelDelegate != null) {
channelDelegate.onSessionEnded(didUserInteract);
}
}
}, bundle);
}
} catch (Throwable e) {
Log.d(LOG_TAG, "Custom Tabs Engagement Signals API not supported", e);
}
}
// avoid webpage reopen if isBindSuccess is false: onServiceConnected->launchUrl
if (isBindSuccess && initialUrl != null) {
launchUrl(initialUrl, initialHeaders, initialReferrer, initialOtherLikelyURLs);
}
}
private void prepareCustomTabs() {
if (builder == null) {
return;
}
if (customSettings.addDefaultShareMenuItem != null) {
builder.setShareState(customSettings.addDefaultShareMenuItem ?
CustomTabsIntent.SHARE_STATE_ON : CustomTabsIntent.SHARE_STATE_OFF);
} else {
builder.setShareState(customSettings.shareState);
}
CustomTabColorSchemeParams.Builder defaultColorSchemeBuilder = new CustomTabColorSchemeParams.Builder();
if (customSettings.toolbarBackgroundColor != null && !customSettings.toolbarBackgroundColor.isEmpty()) {
defaultColorSchemeBuilder.setToolbarColor(Color.parseColor(customSettings.toolbarBackgroundColor));
}
if (customSettings.navigationBarColor != null && !customSettings.navigationBarColor.isEmpty()) {
defaultColorSchemeBuilder.setNavigationBarColor(Color.parseColor(customSettings.navigationBarColor));
}
if (customSettings.navigationBarDividerColor != null && !customSettings.navigationBarDividerColor.isEmpty()) {
defaultColorSchemeBuilder.setNavigationBarDividerColor(Color.parseColor(customSettings.navigationBarDividerColor));
}
if (customSettings.secondaryToolbarColor != null && !customSettings.secondaryToolbarColor.isEmpty()) {
defaultColorSchemeBuilder.setSecondaryToolbarColor(Color.parseColor(customSettings.secondaryToolbarColor));
}
builder.setDefaultColorSchemeParams(defaultColorSchemeBuilder.build());
builder.setShowTitle(customSettings.showTitle);
builder.setUrlBarHidingEnabled(customSettings.enableUrlBarHiding);
builder.setInstantAppsEnabled(customSettings.instantAppsEnabled);
if (customSettings.startAnimations.size() == 2) {
builder.setStartAnimations(this,
customSettings.startAnimations.get(0).getIdentifier(this),
customSettings.startAnimations.get(1).getIdentifier(this));
}
if (customSettings.exitAnimations.size() == 2) {
builder.setExitAnimations(this,
customSettings.exitAnimations.get(0).getIdentifier(this),
customSettings.exitAnimations.get(1).getIdentifier(this));
}
for (CustomTabsMenuItem menuItem : menuItems) {
builder.addMenuItem(menuItem.getLabel(),
createPendingIntent(menuItem.getId()));
}
if (actionButton != null) {
byte[] data = actionButton.getIcon();
BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
bitmapOptions.inMutable = true;
Bitmap bmp = BitmapFactory.decodeByteArray(
data, 0, data.length, bitmapOptions
);
builder.setActionButton(bmp, actionButton.getDescription(),
createPendingIntent(actionButton.getId()),
actionButton.isShouldTint());
}
if (secondaryToolbar != null) {
AndroidResource layout = secondaryToolbar.getLayout();
RemoteViews remoteViews = new RemoteViews(layout.getDefPackage(), layout.getIdentifier(this));
int[] clickableIDs = new int[secondaryToolbar.getClickableIDs().size()];
for (int i = 0, length = secondaryToolbar.getClickableIDs().size(); i < length; i++) {
AndroidResource clickableID = secondaryToolbar.getClickableIDs().get(i);
clickableIDs[i] = clickableID.getIdentifier(this);
}
builder.setSecondaryToolbarViews(remoteViews, clickableIDs, getSecondaryToolbarOnClickPendingIntent());
}
}
public PendingIntent getSecondaryToolbarOnClickPendingIntent() {
Intent broadcastIntent = new Intent(this, ActionBroadcastReceiver.class);
Bundle extras = new Bundle();
extras.putString(ActionBroadcastReceiver.KEY_ACTION_VIEW_ID, id);
extras.putString(ActionBroadcastReceiver.KEY_ACTION_MANAGER_ID, manager != null ? manager.id : null);
broadcastIntent.putExtras(extras);
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
return PendingIntent.getBroadcast(
this, 0, broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
} else {
return PendingIntent.getBroadcast(
this, 0, broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT);
}
}
private void prepareCustomTabsIntent(CustomTabsIntent customTabsIntent) {
if (customSettings.packageName != null)
customTabsIntent.intent.setPackage(customSettings.packageName);
else
customTabsIntent.intent.setPackage(CustomTabsHelper.getPackageNameToUse(this));
if (customSettings.keepAliveEnabled)
CustomTabsHelper.addKeepAliveExtra(this, customTabsIntent.intent);
if (customSettings.alwaysUseBrowserUI)
CustomTabsIntent.setAlwaysUseBrowserUI(customTabsIntent.intent);
}
public void updateActionButton(@NonNull byte[] icon, @NonNull String description) {
if (customTabsSession == null || actionButton == null) {
return;
}
BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
bitmapOptions.inMutable = true;
Bitmap bmp = BitmapFactory.decodeByteArray(
icon, 0, icon.length, bitmapOptions
);
customTabsSession.setActionButton(bmp, description);
actionButton.setIcon(icon);
actionButton.setDescription(description);
}
public void updateSecondaryToolbar(CustomTabsSecondaryToolbar secondaryToolbar) {
if (customTabsSession == null) {
return;
}
AndroidResource layout = secondaryToolbar.getLayout();
RemoteViews remoteViews = new RemoteViews(layout.getDefPackage(), layout.getIdentifier(this));
int[] clickableIDs = new int[secondaryToolbar.getClickableIDs().size()];
for (int i = 0, length = secondaryToolbar.getClickableIDs().size(); i < length; i++) {
AndroidResource clickableID = secondaryToolbar.getClickableIDs().get(i);
clickableIDs[i] = clickableID.getIdentifier(this);
}
customTabsSession.setSecondaryToolbarViews(remoteViews, clickableIDs, getSecondaryToolbarOnClickPendingIntent());
this.secondaryToolbar = secondaryToolbar;
}
@Override
protected void onStart() {
super.onStart();
isBindSuccess = customTabActivityHelper.bindCustomTabsService(this);
if (!isBindSuccess && initialUrl != null) {
// chrome process not running, start tab directly
launchUrlWithSession(null, initialUrl, initialHeaders, initialReferrer, initialOtherLikelyURLs);
}
}
@Override
protected void onStop() {
super.onStop();
customTabActivityHelper.unbindCustomTabsService(this);
isBindSuccess = false;
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == CHROME_CUSTOM_TAB_REQUEST_CODE) {
close();
dispose();
}
}
private PendingIntent createPendingIntent(int actionSourceId) {
Intent actionIntent = new Intent(this, ActionBroadcastReceiver.class);
Bundle extras = new Bundle();
extras.putInt(ActionBroadcastReceiver.KEY_ACTION_ID, actionSourceId);
extras.putString(ActionBroadcastReceiver.KEY_ACTION_VIEW_ID, id);
extras.putString(ActionBroadcastReceiver.KEY_ACTION_MANAGER_ID, manager != null ? manager.id : null);
actionIntent.putExtras(extras);
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
return PendingIntent.getBroadcast(
this, actionSourceId, actionIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
} else {
return PendingIntent.getBroadcast(
this, actionSourceId, actionIntent, PendingIntent.FLAG_UPDATE_CURRENT);
}
}
@Override
public void dispose() {
onStop();
onDestroy();
if (channelDelegate != null) {
channelDelegate.dispose();
channelDelegate = null;
}
if (manager != null) {
if (manager.browsers.containsKey(id)) {
manager.browsers.put(id, null);
}
}
manager = null;
}
public void close() {
onStop();
onDestroy();
customTabsSession = null;
finish();
if (channelDelegate != null) {
channelDelegate.onClosed();
}
}
}
@@ -0,0 +1,7 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
public class ChromeCustomTabsActivitySingleInstance extends ChromeCustomTabsActivity {
protected static final String LOG_TAG = "ChromeCustomTabsActivitySingleInstance";
}
@@ -0,0 +1,251 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabsService;
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
import com.pichillilorenzo.flutter_inappwebview_android.types.CustomTabsSecondaryToolbar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
public class ChromeCustomTabsChannelDelegate extends ChannelDelegateImpl {
@Nullable
private ChromeCustomTabsActivity chromeCustomTabsActivity;
public ChromeCustomTabsChannelDelegate(@NonNull ChromeCustomTabsActivity chromeCustomTabsActivity, @NonNull MethodChannel channel) {
super(channel);
this.chromeCustomTabsActivity = chromeCustomTabsActivity;
}
@Override
public void onMethodCall(@NonNull final MethodCall call, @NonNull final MethodChannel.Result result) {
switch (call.method) {
case "launchUrl":
if (chromeCustomTabsActivity != null) {
String url = (String) call.argument("url");
if (url != null) {
Map<String, String> headers = (Map<String, String>) call.argument("headers");
String referrer = (String) call.argument("referrer");
List<String> otherLikelyURLs = (List<String>) call.argument("otherLikelyURLs");
chromeCustomTabsActivity.launchUrl(url, headers, referrer, otherLikelyURLs);
result.success(true);
} else {
result.success(false);
}
} else {
result.success(false);
}
break;
case "mayLaunchUrl":
if (chromeCustomTabsActivity != null) {
String url = (String) call.argument("url");
List<String> otherLikelyURLs = (List<String>) call.argument("otherLikelyURLs");
result.success(chromeCustomTabsActivity.mayLaunchUrl(url, otherLikelyURLs));
} else {
result.success(false);
}
break;
case "updateActionButton":
if (chromeCustomTabsActivity != null) {
byte[] icon = (byte[]) call.argument("icon");
String description = (String) call.argument("description");
chromeCustomTabsActivity.updateActionButton(icon, description);
result.success(true);
} else {
result.success(false);
}
break;
case "validateRelationship":
if (chromeCustomTabsActivity != null && chromeCustomTabsActivity.customTabsSession != null) {
Integer relation = (Integer) call.argument("relation");
String origin = (String) call.argument("origin");
result.success(chromeCustomTabsActivity.customTabsSession.validateRelationship(relation, Uri.parse(origin), null));
} else {
result.success(false);
}
break;
case "updateSecondaryToolbar":
if (chromeCustomTabsActivity != null) {
CustomTabsSecondaryToolbar secondaryToolbar = CustomTabsSecondaryToolbar.fromMap((Map<String, Object>) call.argument("secondaryToolbar"));
chromeCustomTabsActivity.updateSecondaryToolbar(secondaryToolbar);
result.success(true);
} else {
result.success(false);
}
break;
case "requestPostMessageChannel":
if (chromeCustomTabsActivity != null && chromeCustomTabsActivity.customTabsSession != null) {
String sourceOrigin = (String) call.argument("sourceOrigin");
String targetOrigin = (String) call.argument("targetOrigin");
result.success(chromeCustomTabsActivity.customTabsSession.requestPostMessageChannel(Uri.parse(sourceOrigin),
targetOrigin != null ? Uri.parse(targetOrigin) : null, new Bundle()));
} else {
result.success(false);
}
break;
case "postMessage":
if (chromeCustomTabsActivity != null && chromeCustomTabsActivity.customTabsSession != null) {
String message = (String) call.argument("message");
result.success(chromeCustomTabsActivity.customTabsSession.postMessage(message, new Bundle()));
} else {
result.success(CustomTabsService.RESULT_FAILURE_MESSAGING_ERROR);
}
break;
case "isEngagementSignalsApiAvailable":
if (chromeCustomTabsActivity != null && chromeCustomTabsActivity.customTabsSession != null) {
try {
result.success(chromeCustomTabsActivity.customTabsSession.isEngagementSignalsApiAvailable(new Bundle()));
} catch (Throwable e) {
result.success(false);
}
} else {
result.success(false);
}
break;
case "close":
if (chromeCustomTabsActivity != null) {
chromeCustomTabsActivity.onStop();
chromeCustomTabsActivity.onDestroy();
chromeCustomTabsActivity.close();
if (chromeCustomTabsActivity.manager != null && chromeCustomTabsActivity.manager.plugin != null &&
chromeCustomTabsActivity.manager.plugin.activity != null) {
Activity activity = chromeCustomTabsActivity.manager.plugin.activity;
// https://stackoverflow.com/a/41596629/4637638
Intent myIntent = new Intent(activity, activity.getClass());
myIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
myIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
activity.startActivity(myIntent);
}
chromeCustomTabsActivity.dispose();
result.success(true);
} else {
result.success(false);
}
break;
default:
result.notImplemented();
}
}
public void onServiceConnected() {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
channel.invokeMethod("onServiceConnected", obj);
}
public void onOpened() {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
channel.invokeMethod("onOpened", obj);
}
public void onCompletedInitialLoad() {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
channel.invokeMethod("onCompletedInitialLoad", obj);
}
public void onNavigationEvent(int navigationEvent) {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
obj.put("navigationEvent", navigationEvent);
channel.invokeMethod("onNavigationEvent", obj);
}
public void onClosed() {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
channel.invokeMethod("onClosed", obj);
}
public void onItemActionPerform(int id, String url, String title) {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
obj.put("id", id);
obj.put("url", url);
obj.put("title", title);
channel.invokeMethod("onItemActionPerform", obj);
}
public void onSecondaryItemActionPerform(String name, String url) {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
obj.put("name", name);
obj.put("url", url);
channel.invokeMethod("onSecondaryItemActionPerform", obj);
}
public void onRelationshipValidationResult(int relation, @NonNull Uri requestedOrigin, boolean result) {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
obj.put("relation", relation);
obj.put("requestedOrigin", requestedOrigin.toString());
obj.put("result", result);
channel.invokeMethod("onRelationshipValidationResult", obj);
}
public void onMessageChannelReady() {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
channel.invokeMethod("onMessageChannelReady", obj);
}
public void onPostMessage(@NonNull String message) {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
obj.put("message", message);
channel.invokeMethod("onPostMessage", obj);
}
public void onVerticalScrollEvent(boolean isDirectionUp) {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
obj.put("isDirectionUp", isDirectionUp);
channel.invokeMethod("onVerticalScrollEvent", obj);
}
public void onGreatestScrollPercentageIncreased(int scrollPercentage) {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
obj.put("scrollPercentage", scrollPercentage);
channel.invokeMethod("onGreatestScrollPercentageIncreased", obj);
}
public void onSessionEnded(boolean didUserInteract) {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
obj.put("didUserInteract", didUserInteract);
channel.invokeMethod("onSessionEnded", obj);
}
@Override
public void dispose() {
super.dispose();
chromeCustomTabsActivity = null;
}
}
@@ -0,0 +1,187 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.browser.trusted.ScreenOrientation;
import androidx.browser.trusted.TrustedWebActivityDisplayMode;
import com.pichillilorenzo.flutter_inappwebview_android.ISettings;
import com.pichillilorenzo.flutter_inappwebview_android.types.AndroidResource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ChromeCustomTabsSettings implements ISettings<ChromeCustomTabsActivity> {
final static String LOG_TAG = "ChromeCustomTabsSettings";
@Deprecated
public Boolean addDefaultShareMenuItem;
public Integer shareState = CustomTabsIntent.SHARE_STATE_DEFAULT;
public Boolean showTitle = true;
@Nullable
public String toolbarBackgroundColor;
@Nullable
public String navigationBarColor;
@Nullable
public String navigationBarDividerColor;
@Nullable
public String secondaryToolbarColor;
public Boolean enableUrlBarHiding = false;
public Boolean instantAppsEnabled = false;
public String packageName;
public Boolean keepAliveEnabled = false;
public Boolean isSingleInstance = false;
public Boolean noHistory = false;
public Boolean isTrustedWebActivity = false;
public List<String> additionalTrustedOrigins = new ArrayList<>();
public TrustedWebActivityDisplayMode displayMode = null;
public Integer screenOrientation = ScreenOrientation.DEFAULT;
public List<AndroidResource> startAnimations = new ArrayList<>();
public List<AndroidResource> exitAnimations = new ArrayList<>();
public Boolean alwaysUseBrowserUI = false;
@NonNull
@Override
public ChromeCustomTabsSettings parse(@NonNull Map<String, Object> options) {
for (Map.Entry<String, Object> pair : options.entrySet()) {
String key = pair.getKey();
Object value = pair.getValue();
if (value == null) {
continue;
}
switch (key) {
case "addDefaultShareMenuItem":
addDefaultShareMenuItem = (Boolean) value;
break;
case "shareState":
shareState = (Integer) value;
break;
case "showTitle":
showTitle = (Boolean) value;
break;
case "toolbarBackgroundColor":
toolbarBackgroundColor = (String) value;
break;
case "navigationBarColor":
navigationBarColor = (String) value;
break;
case "navigationBarDividerColor":
navigationBarDividerColor = (String) value;
break;
case "secondaryToolbarColor":
secondaryToolbarColor = (String) value;
break;
case "enableUrlBarHiding":
enableUrlBarHiding = (Boolean) value;
break;
case "instantAppsEnabled":
instantAppsEnabled = (Boolean) value;
break;
case "packageName":
packageName = (String) value;
break;
case "keepAliveEnabled":
keepAliveEnabled = (Boolean) value;
break;
case "isSingleInstance":
isSingleInstance = (Boolean) value;
break;
case "noHistory":
noHistory = (Boolean) value;
break;
case "isTrustedWebActivity":
isTrustedWebActivity = (Boolean) value;
break;
case "additionalTrustedOrigins":
additionalTrustedOrigins = (List<String>) value;
break;
case "displayMode":
Map<String, Object> displayModeMap = (Map<String, Object>) value;
String displayModeType = (String) displayModeMap.get("type");
if (displayModeType != null) {
switch (displayModeType) {
case "IMMERSIVE_MODE":
boolean isSticky = (boolean) displayModeMap.get("isSticky");
int layoutInDisplayCutoutMode = (int) displayModeMap.get("displayCutoutMode");
displayMode = new TrustedWebActivityDisplayMode.ImmersiveMode(isSticky, layoutInDisplayCutoutMode);
break;
case "DEFAULT_MODE":
displayMode = new TrustedWebActivityDisplayMode.DefaultMode();
break;
}
}
break;
case "screenOrientation":
screenOrientation = (Integer) value;
break;
case "startAnimations":
List<Map<String, Object>> startAnimationsList = (List<Map<String, Object>>) value;
for (Map<String, Object> startAnimation : startAnimationsList) {
AndroidResource androidResource = AndroidResource.fromMap(startAnimation);
if (androidResource != null) {
startAnimations.add(AndroidResource.fromMap(startAnimation));
}
}
break;
case "exitAnimations":
List<Map<String, Object>> exitAnimationsList = (List<Map<String, Object>>) value;
for (Map<String, Object> exitAnimation : exitAnimationsList) {
AndroidResource androidResource = AndroidResource.fromMap(exitAnimation);
if (androidResource != null) {
exitAnimations.add(AndroidResource.fromMap(exitAnimation));
}
}
break;
case "alwaysUseBrowserUI":
alwaysUseBrowserUI = (Boolean) value;
break;
}
}
return this;
}
@NonNull
@Override
public Map<String, Object> toMap() {
Map<String, Object> options = new HashMap<>();
options.put("addDefaultShareMenuItem", addDefaultShareMenuItem);
options.put("showTitle", showTitle);
options.put("toolbarBackgroundColor", toolbarBackgroundColor);
options.put("navigationBarColor", navigationBarColor);
options.put("navigationBarDividerColor", navigationBarDividerColor);
options.put("secondaryToolbarColor", secondaryToolbarColor);
options.put("enableUrlBarHiding", enableUrlBarHiding);
options.put("instantAppsEnabled", instantAppsEnabled);
options.put("packageName", packageName);
options.put("keepAliveEnabled", keepAliveEnabled);
options.put("isSingleInstance", isSingleInstance);
options.put("noHistory", noHistory);
options.put("isTrustedWebActivity", isTrustedWebActivity);
options.put("additionalTrustedOrigins", additionalTrustedOrigins);
options.put("screenOrientation", screenOrientation);
options.put("alwaysUseBrowserUI", alwaysUseBrowserUI);
return options;
}
@NonNull
@Override
public Map<String, Object> getRealSettings(@NonNull ChromeCustomTabsActivity chromeCustomTabsActivity) {
Map<String, Object> realOptions = toMap();
if (chromeCustomTabsActivity != null) {
Intent intent = chromeCustomTabsActivity.getIntent();
if (intent != null) {
realOptions.put("packageName", intent.getPackage());
realOptions.put("keepAliveEnabled", intent.hasExtra(CustomTabsHelper.EXTRA_CUSTOM_TABS_KEEP_ALIVE));
}
}
return realOptions;
}
}
@@ -0,0 +1,139 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabsClient;
import androidx.browser.customtabs.CustomTabsIntent;
import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin;
import com.pichillilorenzo.flutter_inappwebview_android.Util;
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
public class ChromeSafariBrowserManager extends ChannelDelegateImpl {
protected static final String LOG_TAG = "ChromeBrowserManager";
public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_chromesafaribrowser";
@Nullable
public InAppWebViewFlutterPlugin plugin;
public String id;
public static final Map<String, ChromeSafariBrowserManager> shared = new HashMap<>();
public final Map<String, ChromeCustomTabsActivity> browsers = new HashMap<>();
public ChromeSafariBrowserManager(final InAppWebViewFlutterPlugin plugin) {
super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME));
this.id = UUID.randomUUID().toString();
this.plugin = plugin;
shared.put(this.id, this);
}
@Override
public void onMethodCall(final MethodCall call, final MethodChannel.Result result) {
final String viewId = (String) call.argument("id");
switch (call.method) {
case "open":
if (plugin != null && plugin.activity != null) {
String url = (String) call.argument("url");
HashMap<String, Object> headers = (HashMap<String, Object>) call.argument("headers");
String referrer = (String) call.argument("referrer");
ArrayList<String> otherLikelyURLs = (ArrayList<String>) call.argument("otherLikelyURLs");
HashMap<String, Object> settings = (HashMap<String, Object>) call.argument("settings");
HashMap<String, Object> actionButton = (HashMap<String, Object>) call.argument("actionButton");
HashMap<String, Object> secondaryToolbar = (HashMap<String, Object>) call.argument("secondaryToolbar");
List<HashMap<String, Object>> menuItemList = (List<HashMap<String, Object>>) call.argument("menuItemList");
open(plugin.activity, viewId, url, headers, referrer, otherLikelyURLs, settings, actionButton, secondaryToolbar, menuItemList, result);
} else {
result.success(false);
}
break;
case "isAvailable":
if (plugin != null && plugin.activity != null) {
result.success(CustomTabActivityHelper.isAvailable(plugin.activity));
} else {
result.success(false);
}
break;
case "getMaxToolbarItems":
result.success(CustomTabsIntent.getMaxToolbarItems());
break;
case "getPackageName":
if (plugin != null && plugin.activity != null) {
ArrayList<String> packages = (ArrayList<String>) call.argument("packages");
Boolean ignoreDefault = (Boolean) call.argument("ignoreDefault");
result.success(CustomTabsClient.getPackageName(plugin.activity, packages, ignoreDefault));
} else {
result.success(null);
}
break;
default:
result.notImplemented();
}
}
public void open(Activity activity, String viewId, @Nullable String url, @Nullable HashMap<String, Object> headers,
@Nullable String referrer, @Nullable ArrayList<String> otherLikelyURLs,
HashMap<String, Object> settings, HashMap<String, Object> actionButton,
HashMap<String, Object> secondaryToolbar,
List<HashMap<String, Object>> menuItemList, MethodChannel.Result result) {
Intent intent = null;
Bundle extras = new Bundle();
extras.putString("url", url);
extras.putString("id", viewId);
extras.putString("managerId", this.id);
extras.putSerializable("headers", headers);
extras.putString("referrer", referrer);
extras.putSerializable("otherLikelyURLs", otherLikelyURLs);
extras.putSerializable("settings", settings);
extras.putSerializable("actionButton", (Serializable) actionButton);
extras.putSerializable("secondaryToolbar", (Serializable) secondaryToolbar);
extras.putSerializable("menuItemList", (Serializable) menuItemList);
Boolean isSingleInstance = Util.<Boolean>getOrDefault(settings, "isSingleInstance", false);
Boolean isTrustedWebActivity = Util.<Boolean>getOrDefault(settings, "isTrustedWebActivity", false);
if (CustomTabActivityHelper.isAvailable(activity)) {
intent = new Intent(activity, !isSingleInstance ?
(!isTrustedWebActivity ? ChromeCustomTabsActivity.class : TrustedWebActivity.class) :
(!isTrustedWebActivity ? ChromeCustomTabsActivitySingleInstance.class : TrustedWebActivitySingleInstance.class));
intent.putExtras(extras);
Boolean noHistory = Util.<Boolean>getOrDefault(settings, "noHistory", false);
if (noHistory) {
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
}
activity.startActivity(intent);
result.success(true);
return;
}
result.error(LOG_TAG, "ChromeCustomTabs is not available!", null);
}
@Override
public void dispose() {
super.dispose();
Collection<ChromeCustomTabsActivity> browserList = browsers.values();
for (ChromeCustomTabsActivity browser : browserList) {
if (browser != null) {
browser.close();
browser.dispose();
}
}
browsers.clear();
shared.remove(this.id);
plugin = null;
}
}
@@ -0,0 +1,177 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Browser;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabsCallback;
import androidx.browser.customtabs.CustomTabsClient;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.browser.customtabs.CustomTabsServiceConnection;
import androidx.browser.customtabs.CustomTabsSession;
import androidx.browser.trusted.TrustedWebActivityIntent;
import java.util.List;
import java.util.Map;
/**
* This is a helper class to manage the connection to the Custom Tabs Service.
*/
public class CustomTabActivityHelper implements ServiceConnectionCallback {
private CustomTabsSession mCustomTabsSession;
private CustomTabsClient mClient;
private CustomTabsServiceConnection mConnection;
private ConnectionCallback mConnectionCallback;
private CustomTabsCallback mCustomTabsCallback;
/**
* Opens the URL on a Custom Tab if possible.
*
* @param activity The host activity.
* @param intent a intent to be used if Custom Tabs is available.
* @param uri the Uri to be opened.
*/
public static void openCustomTab(Activity activity,
Intent intent,
Uri uri,
@Nullable Map<String, String> headers,
@Nullable Uri referrer,
int requestCode) {
intent.setData(uri);
if (headers != null) {
Bundle bundleHeaders = new Bundle();
for (Map.Entry<String, String> header : headers.entrySet()) {
bundleHeaders.putString(header.getKey(), header.getValue());
}
intent.putExtra(Browser.EXTRA_HEADERS, bundleHeaders);
}
if (referrer != null) {
intent.putExtra(Intent.EXTRA_REFERRER, referrer);
}
activity.startActivityForResult(intent, requestCode);
}
public static void openCustomTab(Activity activity,
CustomTabsIntent customTabsIntent,
Uri uri,
@Nullable Map<String, String> headers,
@Nullable Uri referrer,
int requestCode) {
CustomTabActivityHelper.openCustomTab(activity, customTabsIntent.intent, uri,
headers, referrer, requestCode);
}
public static void openTrustedWebActivity(Activity activity,
TrustedWebActivityIntent trustedWebActivityIntent,
Uri uri,
@Nullable Map<String, String> headers,
@Nullable Uri referrer,
int requestCode) {
CustomTabActivityHelper.openCustomTab(activity, trustedWebActivityIntent.getIntent(), uri,
headers, referrer, requestCode);
}
public static boolean isAvailable(Activity activity) {
return CustomTabsHelper.getPackageNameToUse(activity) != null;
}
/**
* Unbinds the Activity from the Custom Tabs Service.
* @param activity the activity that is connected to the service.
*/
public void unbindCustomTabsService(Activity activity) {
if (mConnection == null) return;
activity.unbindService(mConnection);
mClient = null;
mCustomTabsSession = null;
mConnection = null;
}
/**
* Creates or retrieves an exiting CustomTabsSession.
*
* @return a CustomTabsSession.
*/
@Nullable
public CustomTabsSession getSession() {
if (mClient == null) {
mCustomTabsSession = null;
} else if (mCustomTabsSession == null) {
mCustomTabsSession = mClient.newSession(mCustomTabsCallback);
}
return mCustomTabsSession;
}
/**
* Register a Callback to be called when connected or disconnected from the Custom Tabs Service.
* @param connectionCallback
*/
public void setConnectionCallback(ConnectionCallback connectionCallback) {
this.mConnectionCallback = connectionCallback;
}
public void setCustomTabsCallback(CustomTabsCallback customTabsCallback) {
this.mCustomTabsCallback = customTabsCallback;
}
/**
* Binds the Activity to the Custom Tabs Service.
* @param activity the activity to be binded to the service.
*/
public boolean bindCustomTabsService(Activity activity) {
if (mClient != null) return true;
String packageName = CustomTabsHelper.getPackageNameToUse(activity);
if (packageName == null) return false;
mConnection = new ServiceConnection(this);
return CustomTabsClient.bindCustomTabsService(activity, packageName, mConnection);
}
/**
* @see {@link CustomTabsSession#mayLaunchUrl(Uri, Bundle, List)}.
* @return true if call to mayLaunchUrl was accepted.
*/
public boolean mayLaunchUrl(Uri uri, Bundle extras, List<Bundle> otherLikelyBundles) {
if (mClient == null) return false;
CustomTabsSession session = getSession();
if (session == null) return false;
return session.mayLaunchUrl(uri, extras, otherLikelyBundles);
}
@Override
public void onServiceConnected(CustomTabsClient client) {
mClient = client;
mClient.warmup(0L);
if (mConnectionCallback != null) mConnectionCallback.onCustomTabsConnected();
}
@Override
public void onServiceDisconnected() {
mClient = null;
mCustomTabsSession = null;
if (mConnectionCallback != null) mConnectionCallback.onCustomTabsDisconnected();
}
/**
* A Callback for when the service is connected or disconnected. Use those callbacks to
* handle UI changes when the service is connected or disconnected.
*/
public interface ConnectionCallback {
/**
* Called when the service is connected.
*/
void onCustomTabsConnected();
/**
* Called when the service is disconnected.
*/
void onCustomTabsDisconnected();
}
}
@@ -0,0 +1,134 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import androidx.browser.customtabs.CustomTabsService;
import java.util.ArrayList;
import java.util.List;
/**
* Helper class for Custom Tabs.
*/
public class CustomTabsHelper {
protected static final String TAG = "CustomTabsHelper";
static final String STABLE_PACKAGE = "com.android.chrome";
static final String BETA_PACKAGE = "com.chrome.beta";
static final String DEV_PACKAGE = "com.chrome.dev";
static final String LOCAL_PACKAGE = "com.google.android.apps.chrome";
protected static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE =
"android.support.customtabs.extra.KEEP_ALIVE";
private static String sPackageNameToUse;
private CustomTabsHelper() {}
public static void addKeepAliveExtra(Context context, Intent intent) {
Intent keepAliveIntent = new Intent().setClassName(
context.getPackageName(), KeepAliveService.class.getCanonicalName());
intent.putExtra(EXTRA_CUSTOM_TABS_KEEP_ALIVE, keepAliveIntent);
}
/**
* Goes through all apps that handle VIEW intents and have a warmup service. Picks
* the one chosen by the user if there is one, otherwise makes a best effort to return a
* valid package name.
*
* This is <strong>not</strong> threadsafe.
*
* @param context {@link Context} to use for accessing {@link PackageManager}.
* @return The package name recommended to use for connecting to custom tabs related components.
*/
public static String getPackageNameToUse(Context context) {
if (sPackageNameToUse != null) return sPackageNameToUse;
PackageManager pm = context.getPackageManager();
// Get default VIEW intent handler.
Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com"));
activityIntent.addCategory(Intent.CATEGORY_BROWSABLE);
ResolveInfo defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0);
String defaultViewHandlerPackageName = null;
if (defaultViewHandlerInfo != null) {
defaultViewHandlerPackageName = defaultViewHandlerInfo.activityInfo.packageName;
}
// Get all apps that can handle VIEW intents.
int flags = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
flags |= PackageManager.MATCH_ALL;
}
List<ResolveInfo> resolvedActivityList = pm.queryIntentActivities(activityIntent, flags);
List<String> packagesSupportingCustomTabs = new ArrayList<>();
for (ResolveInfo info : resolvedActivityList) {
Intent serviceIntent = new Intent();
serviceIntent.setAction(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION);
serviceIntent.setPackage(info.activityInfo.packageName);
if (pm.resolveService(serviceIntent, 0) != null) {
packagesSupportingCustomTabs.add(info.activityInfo.packageName);
}
}
// Now packagesSupportingCustomTabs contains all apps that can handle both VIEW intents
// and service calls.
if (packagesSupportingCustomTabs.isEmpty()) {
sPackageNameToUse = null;
} else if (packagesSupportingCustomTabs.size() == 1) {
sPackageNameToUse = packagesSupportingCustomTabs.get(0);
} else if (!TextUtils.isEmpty(defaultViewHandlerPackageName)
&& !hasSpecializedHandlerIntents(context, activityIntent)
&& packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName)) {
sPackageNameToUse = defaultViewHandlerPackageName;
} else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) {
sPackageNameToUse = STABLE_PACKAGE;
} else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) {
sPackageNameToUse = BETA_PACKAGE;
} else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) {
sPackageNameToUse = DEV_PACKAGE;
} else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) {
sPackageNameToUse = LOCAL_PACKAGE;
}
return sPackageNameToUse;
}
/**
* Used to check whether there is a specialized handler for a given intent.
* @param intent The intent to check with.
* @return Whether there is a specialized handler for the given intent.
*/
private static boolean hasSpecializedHandlerIntents(Context context, Intent intent) {
try {
PackageManager pm = context.getPackageManager();
List<ResolveInfo> handlers = pm.queryIntentActivities(
intent,
PackageManager.GET_RESOLVED_FILTER);
if (handlers == null || handlers.size() == 0) {
return false;
}
for (ResolveInfo resolveInfo : handlers) {
IntentFilter filter = resolveInfo.filter;
if (filter == null) continue;
if (filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0) continue;
if (resolveInfo.activityInfo == null) continue;
return true;
}
} catch (RuntimeException e) {
Log.e(TAG, "Runtime exception while getting specialized handlers");
}
return false;
}
/**
* @return All possible chrome package names that provide custom tabs feature.
*/
public static String[] getPackages() {
return new String[]{"", STABLE_PACKAGE, BETA_PACKAGE, DEV_PACKAGE, LOCAL_PACKAGE};
}
}
@@ -0,0 +1,18 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
/**
* Empty service used by the custom tab to bind to, raising the application's importance.
*/
public class KeepAliveService extends Service {
private static final Binder sBinder = new Binder();
@Override
public IBinder onBind(Intent intent) {
return sBinder;
}
}
@@ -0,0 +1,77 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin;
import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import io.flutter.embedding.android.FlutterActivity;
public class NoHistoryCustomTabsActivityCallbacks implements Disposable {
@Nullable
public InAppWebViewFlutterPlugin plugin;
public final Map<String, String> noHistoryBrowserIDs = new HashMap<>();
public NoHistoryCustomTabsActivityCallbacks(@NonNull final InAppWebViewFlutterPlugin plugin) {
this.plugin = plugin;
}
public Application.ActivityLifecycleCallbacks activityLifecycleCallbacks = new Application.ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
}
@Override
public void onActivityStarted(@NonNull Activity activity) {
}
@Override
public void onActivityResumed(@NonNull Activity activity) {
if (activity instanceof FlutterActivity && plugin != null && plugin.chromeSafariBrowserManager != null) {
Collection<String> browserIds = noHistoryBrowserIDs.values();
for (String browserId : browserIds) {
if (browserId != null) {
noHistoryBrowserIDs.put(browserId, null);
ChromeCustomTabsActivity browser = plugin.chromeSafariBrowserManager.browsers.get(browserId);
if (browser != null) {
browser.close();
browser.dispose();
}
}
}
}
}
@Override
public void onActivityPaused(@NonNull Activity activity) {
}
@Override
public void onActivityStopped(@NonNull Activity activity) {
}
@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
}
};
@Override
public void dispose() {
noHistoryBrowserIDs.clear();
plugin = null;
}
}
@@ -0,0 +1,32 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
import android.content.ComponentName;
import androidx.browser.customtabs.CustomTabsClient;
import androidx.browser.customtabs.CustomTabsServiceConnection;
import java.lang.ref.WeakReference;
/**
* Implementation for the CustomTabsServiceConnection that avoids leaking the
* ServiceConnectionCallback
*/
public class ServiceConnection extends CustomTabsServiceConnection {
// A weak reference to the ServiceConnectionCallback to avoid leaking it.
private WeakReference<ServiceConnectionCallback> mConnectionCallback;
public ServiceConnection(ServiceConnectionCallback connectionCallback) {
mConnectionCallback = new WeakReference<>(connectionCallback);
}
@Override
public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) {
ServiceConnectionCallback connectionCallback = mConnectionCallback.get();
if (connectionCallback != null) connectionCallback.onServiceConnected(client);
}
@Override
public void onServiceDisconnected(ComponentName name) {
ServiceConnectionCallback connectionCallback = mConnectionCallback.get();
if (connectionCallback != null) connectionCallback.onServiceDisconnected();
}
}
@@ -0,0 +1,19 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
import androidx.browser.customtabs.CustomTabsClient;
/**
* Callback for events when connecting and disconnecting from Custom Tabs Service.
*/
public interface ServiceConnectionCallback {
/**
* Called when the service is connected.
* @param client a CustomTabsClient
*/
void onServiceConnected(CustomTabsClient client);
/**
* Called when the service is disconnected.
*/
void onServiceDisconnected();
}
@@ -0,0 +1,84 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
import android.content.Intent;
import android.graphics.Color;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabColorSchemeParams;
import androidx.browser.customtabs.CustomTabsIntent;
import androidx.browser.trusted.TrustedWebActivityIntent;
import androidx.browser.trusted.TrustedWebActivityIntentBuilder;
import java.util.List;
import java.util.Map;
public class TrustedWebActivity extends ChromeCustomTabsActivity {
protected static final String LOG_TAG = "TrustedWebActivity";
public TrustedWebActivityIntentBuilder builder;
@Override
public void launchUrl(@NonNull String url,
@Nullable Map<String, String> headers,
@Nullable String referrer,
@Nullable List<String> otherLikelyURLs) {
if (customTabsSession == null) {
return;
}
Uri uri = Uri.parse(url);
mayLaunchUrl(url, otherLikelyURLs);
builder = new TrustedWebActivityIntentBuilder(uri);
prepareCustomTabs();
TrustedWebActivityIntent trustedWebActivityIntent = builder.build(customTabsSession);
prepareCustomTabsIntent(trustedWebActivityIntent);
CustomTabActivityHelper.openTrustedWebActivity(this, trustedWebActivityIntent, uri, headers,
referrer != null ? Uri.parse(referrer) : null, CHROME_CUSTOM_TAB_REQUEST_CODE);
}
private void prepareCustomTabs() {
CustomTabColorSchemeParams.Builder defaultColorSchemeBuilder = new CustomTabColorSchemeParams.Builder();
if (customSettings.toolbarBackgroundColor != null && !customSettings.toolbarBackgroundColor.isEmpty()) {
defaultColorSchemeBuilder.setToolbarColor(Color.parseColor(customSettings.toolbarBackgroundColor));
}
if (customSettings.navigationBarColor != null && !customSettings.navigationBarColor.isEmpty()) {
defaultColorSchemeBuilder.setNavigationBarColor(Color.parseColor(customSettings.navigationBarColor));
}
if (customSettings.navigationBarDividerColor != null && !customSettings.navigationBarDividerColor.isEmpty()) {
defaultColorSchemeBuilder.setNavigationBarDividerColor(Color.parseColor(customSettings.navigationBarDividerColor));
}
if (customSettings.secondaryToolbarColor != null && !customSettings.secondaryToolbarColor.isEmpty()) {
defaultColorSchemeBuilder.setSecondaryToolbarColor(Color.parseColor(customSettings.secondaryToolbarColor));
}
builder.setDefaultColorSchemeParams(defaultColorSchemeBuilder.build());
if (customSettings.additionalTrustedOrigins != null && !customSettings.additionalTrustedOrigins.isEmpty()) {
builder.setAdditionalTrustedOrigins(customSettings.additionalTrustedOrigins);
}
if (customSettings.displayMode != null) {
builder.setDisplayMode(customSettings.displayMode);
}
builder.setScreenOrientation(customSettings.screenOrientation);
}
private void prepareCustomTabsIntent(TrustedWebActivityIntent trustedWebActivityIntent) {
Intent intent = trustedWebActivityIntent.getIntent();
if (customSettings.packageName != null)
intent.setPackage(customSettings.packageName);
else
intent.setPackage(CustomTabsHelper.getPackageNameToUse(this));
if (customSettings.keepAliveEnabled)
CustomTabsHelper.addKeepAliveExtra(this, intent);
if (customSettings.alwaysUseBrowserUI)
CustomTabsIntent.setAlwaysUseBrowserUI(intent);
}
}
@@ -0,0 +1,7 @@
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
public class TrustedWebActivitySingleInstance extends TrustedWebActivity {
protected static final String LOG_TAG = "TrustedWebActivitySingleInstance";
}
@@ -0,0 +1,59 @@
package com.pichillilorenzo.flutter_inappwebview_android.content_blocker;
import androidx.annotation.NonNull;
public class ContentBlocker {
@NonNull
private ContentBlockerTrigger trigger;
@NonNull
private ContentBlockerAction action;
public ContentBlocker (@NonNull ContentBlockerTrigger trigger, @NonNull ContentBlockerAction action) {
this.trigger = trigger;
this.action = action;
}
@NonNull
public ContentBlockerTrigger getTrigger() {
return trigger;
}
public void setTrigger(@NonNull ContentBlockerTrigger trigger) {
this.trigger = trigger;
}
@NonNull
public ContentBlockerAction getAction() {
return action;
}
public void setAction(@NonNull ContentBlockerAction action) {
this.action = action;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ContentBlocker that = (ContentBlocker) o;
if (!trigger.equals(that.trigger)) return false;
return action.equals(that.action);
}
@Override
public int hashCode() {
int result = trigger.hashCode();
result = 31 * result + action.hashCode();
return result;
}
@Override
public String toString() {
return "ContentBlocker{" +
"trigger=" + trigger +
", action=" + action +
'}';
}
}
@@ -0,0 +1,71 @@
package com.pichillilorenzo.flutter_inappwebview_android.content_blocker;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Map;
public class ContentBlockerAction {
@NonNull
private ContentBlockerActionType type;
@Nullable
private String selector;
ContentBlockerAction(@NonNull ContentBlockerActionType type, @Nullable String selector) {
this.type = type;
if (this.type.equals(ContentBlockerActionType.CSS_DISPLAY_NONE)) {
assert(selector != null);
}
this.selector = selector;
}
public static ContentBlockerAction fromMap(Map<String, Object> map) {
ContentBlockerActionType type = ContentBlockerActionType.fromValue((String) map.get("type"));
String selector = (String) map.get("selector");
return new ContentBlockerAction(type, selector);
}
@NonNull
public ContentBlockerActionType getType() {
return type;
}
public void setType(@NonNull ContentBlockerActionType type) {
this.type = type;
}
public String getSelector() {
return selector;
}
public void setSelector(String selector) {
this.selector = selector;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ContentBlockerAction that = (ContentBlockerAction) o;
if (type != that.type) return false;
return selector != null ? selector.equals(that.selector) : that.selector == null;
}
@Override
public int hashCode() {
int result = type.hashCode();
result = 31 * result + (selector != null ? selector.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "ContentBlockerAction{" +
"type=" + type +
", selector='" + selector + '\'' +
'}';
}
}
@@ -0,0 +1,30 @@
package com.pichillilorenzo.flutter_inappwebview_android.content_blocker;
public enum ContentBlockerActionType {
BLOCK ("block"),
CSS_DISPLAY_NONE ("css-display-none"),
MAKE_HTTPS ("make-https");
private final String value;
private ContentBlockerActionType(String value) {
this.value = value;
}
public boolean equalsValue(String otherValue) {
return value.equals(otherValue);
}
public static ContentBlockerActionType fromValue(String value) {
for( ContentBlockerActionType type : ContentBlockerActionType.values()) {
if(value.equals(type.value))
return type;
}
throw new IllegalArgumentException("No enum constant: " + value);
}
@Override
public String toString() {
return this.value;
}
}
@@ -0,0 +1,336 @@
package com.pichillilorenzo.flutter_inappwebview_android.content_blocker;
import android.os.Build;
import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;
import android.webkit.WebResourceResponse;
import androidx.annotation.Nullable;
import com.pichillilorenzo.flutter_inappwebview_android.Util;
import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.JavaScriptBridgeJS;
import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceRequestExt;
import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebView;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.regex.Matcher;
import javax.net.ssl.SSLHandshakeException;
public class ContentBlockerHandler {
protected static final String LOG_TAG = "ContentBlockerHandler";
protected List<ContentBlocker> ruleList = new ArrayList<>();
public ContentBlockerHandler() {}
public ContentBlockerHandler(List<ContentBlocker> ruleList) {
this.ruleList = ruleList;
}
public List<ContentBlocker> getRuleList() {
return this.ruleList;
}
public void setRuleList(List<ContentBlocker> newRuleList) {
this.ruleList = newRuleList;
}
@Nullable
public WebResourceResponse checkUrl(final InAppWebView webView, WebResourceRequestExt request,
ContentBlockerTriggerResourceType responseResourceType)
throws URISyntaxException, InterruptedException, MalformedURLException {
if (webView.customSettings.contentBlockers == null)
return null;
String url = request.getUrl();
URI u;
try {
u = new URI(url);
} catch (URISyntaxException e) {
String[] urlSplit = url.split(":");
String scheme = urlSplit[0];
URL tempUrl = new URL(url.replace(scheme, "https"));
u = new URI(scheme, tempUrl.getUserInfo(), tempUrl.getHost(), tempUrl.getPort(), tempUrl.getPath(), tempUrl.getQuery(), tempUrl.getRef());
}
String host = u.getHost();
int port = u.getPort();
String scheme = u.getScheme();
// thread safe copy list
List<ContentBlocker> ruleListCopy = new CopyOnWriteArrayList<ContentBlocker>(ruleList);
for (ContentBlocker contentBlocker : ruleListCopy) {
ContentBlockerTrigger trigger = contentBlocker.getTrigger();
List<ContentBlockerTriggerResourceType> resourceTypes = trigger.getResourceType();
if (resourceTypes.contains(ContentBlockerTriggerResourceType.IMAGE) && !resourceTypes.contains(ContentBlockerTriggerResourceType.SVG_DOCUMENT)) {
resourceTypes.add(ContentBlockerTriggerResourceType.SVG_DOCUMENT);
}
ContentBlockerAction action = contentBlocker.getAction();
Matcher m = trigger.getUrlFilterPatternCompiled().matcher(url);
if (m.matches()) {
if (!resourceTypes.isEmpty() && !resourceTypes.contains(responseResourceType)) {
return null;
}
if (!trigger.getIfDomain().isEmpty()) {
boolean matchFound = false;
for (String domain : trigger.getIfDomain()) {
if ((domain.startsWith("*") && host.endsWith(domain.replace("*", ""))) || domain.equals(host)) {
matchFound = true;
break;
}
}
if (!matchFound)
return null;
}
if (!trigger.getUnlessDomain().isEmpty()) {
for (String domain : trigger.getUnlessDomain())
if ((domain.startsWith("*") && host.endsWith(domain.replace("*", ""))) || domain.equals(host))
return null;
}
final String[] webViewUrl = new String[1];
if (!trigger.getLoadType().isEmpty() || !trigger.getIfTopUrl().isEmpty() || !trigger.getUnlessTopUrl().isEmpty()) {
final CountDownLatch latch = new CountDownLatch(1);
Handler handler = new Handler(webView.getWebViewLooper());
handler.post(new Runnable() {
@Override
public void run() {
webViewUrl[0] = webView.getUrl();
latch.countDown();
}
});
latch.await();
}
if (webViewUrl[0] != null) {
if (!trigger.getLoadType().isEmpty()) {
URI cUrl = new URI(webViewUrl[0]);
String cHost = cUrl.getHost();
int cPort = cUrl.getPort();
String cScheme = cUrl.getScheme();
if ( (trigger.getLoadType().contains("first-party") && cHost != null && !(cScheme.equals(scheme) && cHost.equals(host) && cPort == port)) ||
(trigger.getLoadType().contains("third-party") && cHost != null && cHost.equals(host)) )
return null;
}
if (!trigger.getIfTopUrl().isEmpty()) {
boolean matchFound = false;
for (String topUrl : trigger.getIfTopUrl()) {
if (webViewUrl[0].startsWith(topUrl)) {
matchFound = true;
break;
}
}
if (!matchFound)
return null;
}
if (!trigger.getUnlessTopUrl().isEmpty()) {
for (String topUrl : trigger.getUnlessTopUrl())
if (webViewUrl[0].startsWith(topUrl))
return null;
}
}
switch (action.getType()) {
case BLOCK:
return new WebResourceResponse("", "", null);
case CSS_DISPLAY_NONE:
final String cssSelector = action.getSelector();
final String jsScript = "(function(d) { " +
" function hide () { " +
" if (d.body != null && !d.getElementById('" + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "-css-display-none-style')) { " +
" var c = d.createElement('style'); " +
" c.id = '" + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "-css-display-none-style'; " +
" c.innerHTML = '" + cssSelector + " { display: none !important; }'; " +
" d.body.appendChild(c); " +
" }" +
" d.querySelectorAll('" + cssSelector + "').forEach(function (item, index) { " +
" item.setAttribute('style', 'display: none !important;'); " +
" }); " +
" }; " +
" hide(); " +
" d.addEventListener('DOMContentLoaded', function(event) { hide(); }); " +
"})(document);";
final Handler handler = new Handler(webView.getWebViewLooper());
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
webView.evaluateJavascript(jsScript, null);
} else {
webView.loadUrl("javascript:" + jsScript);
}
}
}, 800);
break;
case MAKE_HTTPS:
if (scheme.equals("http") && (port == -1 || port == 80)) {
String urlHttps = url.replace("http://", "https://");
HttpURLConnection urlConnection = Util.makeHttpRequest(urlHttps, request.getMethod(), request.getHeaders());
if (urlConnection != null) {
try {
byte[] dataBytes = Util.readAllBytes(urlConnection.getInputStream());
if (dataBytes == null) {
return null;
}
InputStream dataStream = new ByteArrayInputStream(dataBytes);
String encoding = urlConnection.getContentEncoding();
String contentType = urlConnection.getContentType();
if (contentType == null) {
contentType = "text/plain";
} else {
String[] contentTypeSplit = contentType.split(";");
contentType = contentTypeSplit[0].trim();
if (encoding == null) {
encoding = (contentTypeSplit.length > 1 && contentTypeSplit[1].contains("charset="))
? contentTypeSplit[1].replace("charset=", "").trim()
: "utf-8";
}
}
String reasonPhrase = urlConnection.getResponseMessage();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && reasonPhrase != null) {
Map<String, String> responseHeaders = new HashMap<>();
for (Map.Entry<String, List<String>> responseHeader : urlConnection.getHeaderFields().entrySet()) {
responseHeaders.put(responseHeader.getKey(), TextUtils.join(",", responseHeader.getValue()));
}
return new WebResourceResponse(contentType,
encoding,
urlConnection.getResponseCode(),
reasonPhrase,
responseHeaders,
dataStream);
} else {
return new WebResourceResponse(contentType,
encoding,
dataStream);
}
} catch (Exception e) {
if (!(e instanceof SSLHandshakeException)) {
Log.e(LOG_TAG, "", e);
}
} finally {
urlConnection.disconnect();
}
}
// Request mRequest = new Request.Builder().url(urlHttps).build();
// Response response = null;
//
// try {
// response = Util.getBasicOkHttpClient().newCall(mRequest).execute();
// byte[] dataBytes = response.body().bytes();
// InputStream dataStream = new ByteArrayInputStream(dataBytes);
//
// String[] contentTypeSplit = response.header("content-type", "text/plain").split(";");
//
// String contentType = contentTypeSplit[0].trim();
// String encoding = (contentTypeSplit.length > 1 && contentTypeSplit[1].contains("charset="))
// ? contentTypeSplit[1].replace("charset=", "").trim()
// : "utf-8";
//
// response.body().close();
// response.close();
//
// return new WebResourceResponse(contentType, encoding, dataStream);
//
// } catch (Exception e) {
// if (response != null) {
// response.body().close();
// response.close();
// }
// if (!(e instanceof SSLHandshakeException)) {
// Log.e(LOG_TAG, "", e);
// }
// }
}
break;
}
}
}
return null;
}
@Nullable
public WebResourceResponse checkUrl(final InAppWebView webView, WebResourceRequestExt request) throws URISyntaxException, InterruptedException, MalformedURLException {
ContentBlockerTriggerResourceType responseResourceType = getResourceTypeFromUrl(request);
return checkUrl(webView, request, responseResourceType);
}
@Nullable
public WebResourceResponse checkUrl(final InAppWebView webView, WebResourceRequestExt request, String contentType) throws URISyntaxException, InterruptedException, MalformedURLException {
ContentBlockerTriggerResourceType responseResourceType = getResourceTypeFromContentType(contentType);
return checkUrl(webView, request, responseResourceType);
}
public ContentBlockerTriggerResourceType getResourceTypeFromUrl(WebResourceRequestExt request) {
ContentBlockerTriggerResourceType responseResourceType = ContentBlockerTriggerResourceType.RAW;
String url = request.getUrl();
if (url.startsWith("http://") || url.startsWith("https://")) {
// make an HTTP "HEAD" request to the server for that URL. This will not return the full content of the URL.
HttpURLConnection urlConnection = Util.makeHttpRequest(url, "HEAD", request.getHeaders());
if (urlConnection != null) {
try {
String contentType = urlConnection.getContentType();
if (contentType != null) {
String[] contentTypeSplit = contentType.split(";");
contentType = contentTypeSplit[0].trim();
responseResourceType = getResourceTypeFromContentType(contentType);
}
} catch (Exception e) {
Log.e(LOG_TAG, "", e);
} finally {
urlConnection.disconnect();
}
}
}
return responseResourceType;
}
public ContentBlockerTriggerResourceType getResourceTypeFromContentType(String contentType) {
ContentBlockerTriggerResourceType responseResourceType = ContentBlockerTriggerResourceType.RAW;
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
if (contentType.equals("text/css")) {
responseResourceType = ContentBlockerTriggerResourceType.STYLE_SHEET;
} else if (contentType.equals("image/svg+xml")) {
responseResourceType = ContentBlockerTriggerResourceType.SVG_DOCUMENT;
} else if (contentType.startsWith("image/")) {
responseResourceType = ContentBlockerTriggerResourceType.IMAGE;
} else if (contentType.startsWith("font/")) {
responseResourceType = ContentBlockerTriggerResourceType.FONT;
} else if (contentType.startsWith("audio/") || contentType.startsWith("video/") || contentType.equals("application/ogg")) {
responseResourceType = ContentBlockerTriggerResourceType.MEDIA;
} else if (contentType.endsWith("javascript")) {
responseResourceType = ContentBlockerTriggerResourceType.SCRIPT;
} else if (contentType.startsWith("text/")) {
responseResourceType = ContentBlockerTriggerResourceType.DOCUMENT;
}
return responseResourceType;
}
}
@@ -0,0 +1,185 @@
package com.pichillilorenzo.flutter_inappwebview_android.content_blocker;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
public class ContentBlockerTrigger {
@NonNull
private String urlFilter;
private Pattern urlFilterPatternCompiled;
private Boolean urlFilterIsCaseSensitive;
private List<ContentBlockerTriggerResourceType> resourceType = new ArrayList<>();
private List<String> ifDomain = new ArrayList<>();
private List<String> unlessDomain = new ArrayList<>();
private List<String> loadType = new ArrayList<>();
private List<String> ifTopUrl = new ArrayList<>();
private List<String> unlessTopUrl = new ArrayList<>();
public ContentBlockerTrigger(@NonNull String urlFilter, @Nullable Boolean urlFilterIsCaseSensitive, @Nullable List<ContentBlockerTriggerResourceType> resourceType,
@Nullable List<String> ifDomain, @Nullable List<String> unlessDomain, @Nullable List<String> loadType,
@Nullable List<String> ifTopUrl, @Nullable List<String> unlessTopUrl) {
this.urlFilterIsCaseSensitive = urlFilterIsCaseSensitive != null ? urlFilterIsCaseSensitive : false;
this.urlFilter = urlFilter;
this.urlFilterPatternCompiled = Pattern.compile(this.urlFilter, this.urlFilterIsCaseSensitive ? 0 : Pattern.CASE_INSENSITIVE);
this.resourceType = resourceType != null ? resourceType : this.resourceType;
this.ifDomain = ifDomain != null ? ifDomain : this.ifDomain;
this.unlessDomain = unlessDomain != null ? unlessDomain : this.unlessDomain;
if ((!(this.ifDomain.isEmpty() || this.unlessDomain.isEmpty()) != false))
throw new AssertionError();
this.loadType = loadType != null ? loadType : this.loadType;
if ((this.loadType.size() > 2)) throw new AssertionError();
this.ifTopUrl = ifTopUrl != null ? ifTopUrl : this.ifTopUrl;
this.unlessTopUrl = unlessTopUrl != null ? unlessTopUrl : this.unlessTopUrl;
if ((!(this.ifTopUrl.isEmpty() || this.unlessTopUrl.isEmpty()) != false))
throw new AssertionError();
}
public static ContentBlockerTrigger fromMap(Map<String, Object> map) {
String urlFilter = (String) map.get("url-filter");
Boolean urlFilterIsCaseSensitive = (Boolean) map.get("url-filter-is-case-sensitive");
List<String> resourceTypeStringList = (List<String>) map.get("resource-type");
List<ContentBlockerTriggerResourceType> resourceType = new ArrayList<>();
if (resourceTypeStringList != null) {
for (String type : resourceTypeStringList) {
resourceType.add(ContentBlockerTriggerResourceType.fromValue(type));
}
} else {
resourceType.addAll(Arrays.asList(ContentBlockerTriggerResourceType.values()));
}
List<String> ifDomain = (List<String>) map.get("if-domain");
List<String> unlessDomain = (List<String>) map.get("unless-domain");
List<String> loadType = (List<String>) map.get("load-type");
List<String> ifTopUrl = (List<String>) map.get("if-top-url");
List<String> unlessTopUrl = (List<String>) map.get("unless-top-url");
return new ContentBlockerTrigger(urlFilter, urlFilterIsCaseSensitive, resourceType, ifDomain, unlessDomain, loadType, ifTopUrl, unlessTopUrl);
}
@NonNull
public String getUrlFilter() {
return urlFilter;
}
public void setUrlFilter(@NonNull String urlFilter) {
this.urlFilter = urlFilter;
}
public Pattern getUrlFilterPatternCompiled() {
return urlFilterPatternCompiled;
}
public void setUrlFilterPatternCompiled(Pattern urlFilterPatternCompiled) {
this.urlFilterPatternCompiled = urlFilterPatternCompiled;
}
public Boolean getUrlFilterIsCaseSensitive() {
return urlFilterIsCaseSensitive;
}
public void setUrlFilterIsCaseSensitive(Boolean urlFilterIsCaseSensitive) {
this.urlFilterIsCaseSensitive = urlFilterIsCaseSensitive;
}
public List<ContentBlockerTriggerResourceType> getResourceType() {
return resourceType;
}
public void setResourceType(List<ContentBlockerTriggerResourceType> resourceType) {
this.resourceType = resourceType;
}
public List<String> getIfDomain() {
return ifDomain;
}
public void setIfDomain(List<String> ifDomain) {
this.ifDomain = ifDomain;
}
public List<String> getUnlessDomain() {
return unlessDomain;
}
public void setUnlessDomain(List<String> unlessDomain) {
this.unlessDomain = unlessDomain;
}
public List<String> getLoadType() {
return loadType;
}
public void setLoadType(List<String> loadType) {
this.loadType = loadType;
}
public List<String> getIfTopUrl() {
return ifTopUrl;
}
public void setIfTopUrl(List<String> ifTopUrl) {
this.ifTopUrl = ifTopUrl;
}
public List<String> getUnlessTopUrl() {
return unlessTopUrl;
}
public void setUnlessTopUrl(List<String> unlessTopUrl) {
this.unlessTopUrl = unlessTopUrl;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ContentBlockerTrigger that = (ContentBlockerTrigger) o;
if (!urlFilter.equals(that.urlFilter)) return false;
if (!urlFilterPatternCompiled.equals(that.urlFilterPatternCompiled)) return false;
if (!urlFilterIsCaseSensitive.equals(that.urlFilterIsCaseSensitive)) return false;
if (!resourceType.equals(that.resourceType)) return false;
if (!ifDomain.equals(that.ifDomain)) return false;
if (!unlessDomain.equals(that.unlessDomain)) return false;
if (!loadType.equals(that.loadType)) return false;
if (!ifTopUrl.equals(that.ifTopUrl)) return false;
return unlessTopUrl.equals(that.unlessTopUrl);
}
@Override
public int hashCode() {
int result = urlFilter.hashCode();
result = 31 * result + urlFilterPatternCompiled.hashCode();
result = 31 * result + urlFilterIsCaseSensitive.hashCode();
result = 31 * result + resourceType.hashCode();
result = 31 * result + ifDomain.hashCode();
result = 31 * result + unlessDomain.hashCode();
result = 31 * result + loadType.hashCode();
result = 31 * result + ifTopUrl.hashCode();
result = 31 * result + unlessTopUrl.hashCode();
return result;
}
@Override
public String toString() {
return "ContentBlockerTrigger{" +
"urlFilter='" + urlFilter + '\'' +
", urlFilterPatternCompiled=" + urlFilterPatternCompiled +
", urlFilterIsCaseSensitive=" + urlFilterIsCaseSensitive +
", resourceType=" + resourceType +
", ifDomain=" + ifDomain +
", unlessDomain=" + unlessDomain +
", loadType=" + loadType +
", ifTopUrl=" + ifTopUrl +
", unlessTopUrl=" + unlessTopUrl +
'}';
}
}
@@ -0,0 +1,36 @@
package com.pichillilorenzo.flutter_inappwebview_android.content_blocker;
public enum ContentBlockerTriggerResourceType {
DOCUMENT ("document"),
IMAGE ("image"),
STYLE_SHEET ("style-sheet"),
SCRIPT ("script"),
FONT ("font"),
SVG_DOCUMENT ("svg-document"),
MEDIA ("media"),
POPUP ("popup"),
RAW ("raw");
private final String value;
private ContentBlockerTriggerResourceType(String value) {
this.value = value;
}
public boolean equalsValue(String otherValue) {
return value.equals(otherValue);
}
public static ContentBlockerTriggerResourceType fromValue(String value) {
for( ContentBlockerTriggerResourceType type : ContentBlockerTriggerResourceType.values()) {
if(value.equals(type.value))
return type;
}
throw new IllegalArgumentException("No enum constant: " + value);
}
@Override
public String toString() {
return this.value;
}
}
@@ -0,0 +1,95 @@
package com.pichillilorenzo.flutter_inappwebview_android.credential_database;
import android.content.Context;
import com.pichillilorenzo.flutter_inappwebview_android.types.URLCredential;
import com.pichillilorenzo.flutter_inappwebview_android.types.URLProtectionSpace;
import java.util.ArrayList;
import java.util.List;
public class CredentialDatabase {
private static CredentialDatabase instance;
static final String LOG_TAG = "CredentialDatabase";
// If you change the database schema, you must increment the database version.
public static final int DATABASE_VERSION = 2;
public static final String DATABASE_NAME = "CredentialDatabase.db";
public URLProtectionSpaceDao protectionSpaceDao;
public URLCredentialDao credentialDao;
public CredentialDatabaseHelper db;
private CredentialDatabase() {}
private CredentialDatabase(CredentialDatabaseHelper db, URLProtectionSpaceDao protectionSpaceDao, URLCredentialDao credentialDao) {
this.db = db;
this.protectionSpaceDao = protectionSpaceDao;
this.credentialDao = credentialDao;
}
public static CredentialDatabase getInstance(Context context) {
if (instance != null)
return instance;
CredentialDatabaseHelper db = new CredentialDatabaseHelper(context);
instance = new CredentialDatabase(db, new URLProtectionSpaceDao(db), new URLCredentialDao(db));
return instance;
}
public List<URLCredential> getHttpAuthCredentials(String host, String protocol, String realm, Integer port) {
List<URLCredential> credentials = new ArrayList<>();
URLProtectionSpace protectionSpace = protectionSpaceDao.find(host, protocol, realm, port);
if (protectionSpace != null) {
credentials = credentialDao.getAllByProtectionSpaceId(protectionSpace.getId());
}
return credentials;
}
public void clearAllAuthCredentials() {
db.clearAllTables(db.getWritableDatabase());
}
public void removeHttpAuthCredentials(String host, String protocol, String realm, Integer port) {
URLProtectionSpace URLProtectionSpace = protectionSpaceDao.find(host, protocol, realm, port);
if (URLProtectionSpace != null) {
protectionSpaceDao.delete(URLProtectionSpace);
}
}
public void removeHttpAuthCredential(String host, String protocol, String realm, Integer port, String username, String password) {
URLProtectionSpace protectionSpace = protectionSpaceDao.find(host, protocol, realm, port);
if (protectionSpace != null) {
URLCredential credential = credentialDao.find(username, password, protectionSpace.getId());
credentialDao.delete(credential);
}
}
public void setHttpAuthCredential(String host, String protocol, String realm, Integer port, String username, String password) {
URLProtectionSpace protectionSpace = protectionSpaceDao.find(host, protocol, realm, port);
Long protectionSpaceId;
if (protectionSpace == null) {
protectionSpaceId = protectionSpaceDao.insert(new URLProtectionSpace(null, host, protocol, realm, port));
} else {
protectionSpaceId = protectionSpace.getId();
}
URLCredential credential = credentialDao.find(username, password, protectionSpaceId);
if (credential != null) {
boolean needUpdate = false;
if (!credential.getUsername().equals(username)) {
credential.setUsername(username);
needUpdate = true;
}
if (!credential.getPassword().equals(password)) {
credential.setPassword(password);
needUpdate = true;
}
if (needUpdate)
credentialDao.update(credential);
} else {
credential = new URLCredential(null, username, password, protectionSpaceId);
credential.setId(credentialDao.insert(credential));
}
}
}
@@ -0,0 +1,157 @@
package com.pichillilorenzo.flutter_inappwebview_android.credential_database;
import android.os.Build;
import android.webkit.WebViewDatabase;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin;
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
import com.pichillilorenzo.flutter_inappwebview_android.types.URLCredential;
import com.pichillilorenzo.flutter_inappwebview_android.types.URLProtectionSpace;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
@RequiresApi(api = Build.VERSION_CODES.O)
public class CredentialDatabaseHandler extends ChannelDelegateImpl {
protected static final String LOG_TAG = "CredentialDatabaseHandler";
public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_credential_database";
@Nullable
public static CredentialDatabase credentialDatabase;
@Nullable
public InAppWebViewFlutterPlugin plugin;
public CredentialDatabaseHandler(@NonNull final InAppWebViewFlutterPlugin plugin) {
super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME));
this.plugin = plugin;
}
public static void init(@NonNull InAppWebViewFlutterPlugin plugin) {
if (credentialDatabase == null) {
credentialDatabase = CredentialDatabase.getInstance(plugin.applicationContext);
}
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
if (plugin != null) {
init(plugin);
}
switch (call.method) {
case "getAllAuthCredentials":
{
List<Map<String, Object>> allCredentials = new ArrayList<>();
if (credentialDatabase != null) {
List<URLProtectionSpace> protectionSpaces = credentialDatabase.protectionSpaceDao.getAll();
for (URLProtectionSpace protectionSpace : protectionSpaces) {
List<Map<String, Object>> credentials = new ArrayList<>();
for (URLCredential credential : credentialDatabase.credentialDao.getAllByProtectionSpaceId(protectionSpace.getId())) {
credentials.add(credential.toMap());
}
Map<String, Object> obj = new HashMap<>();
obj.put("protectionSpace", protectionSpace.toMap());
obj.put("credentials", credentials);
allCredentials.add(obj);
}
}
result.success(allCredentials);
}
break;
case "getHttpAuthCredentials":
{
List<Map<String, Object>> credentials = new ArrayList<>();
if (credentialDatabase != null) {
String host = (String) call.argument("host");
String protocol = (String) call.argument("protocol");
String realm = (String) call.argument("realm");
Integer port = (Integer) call.argument("port");
for (URLCredential credential : credentialDatabase.getHttpAuthCredentials(host, protocol, realm, port)) {
credentials.add(credential.toMap());
}
}
result.success(credentials);
}
break;
case "setHttpAuthCredential":
{
if (credentialDatabase != null) {
String host = (String) call.argument("host");
String protocol = (String) call.argument("protocol");
String realm = (String) call.argument("realm");
Integer port = (Integer) call.argument("port");
String username = (String) call.argument("username");
String password = (String) call.argument("password");
credentialDatabase.setHttpAuthCredential(host, protocol, realm, port, username, password);
result.success(true);
} else {
result.success(false);
}
}
break;
case "removeHttpAuthCredential":
{
if (credentialDatabase != null) {
String host = (String) call.argument("host");
String protocol = (String) call.argument("protocol");
String realm = (String) call.argument("realm");
Integer port = (Integer) call.argument("port");
String username = (String) call.argument("username");
String password = (String) call.argument("password");
credentialDatabase.removeHttpAuthCredential(host, protocol, realm, port, username, password);
result.success(true);
} else {
result.success(false);
}
}
break;
case "removeHttpAuthCredentials":
{
if (credentialDatabase != null) {
String host = (String) call.argument("host");
String protocol = (String) call.argument("protocol");
String realm = (String) call.argument("realm");
Integer port = (Integer) call.argument("port");
credentialDatabase.removeHttpAuthCredentials(host, protocol, realm, port);
result.success(true);
} else {
result.success(false);
}
}
break;
case "clearAllAuthCredentials":
if (credentialDatabase != null) {
credentialDatabase.clearAllAuthCredentials();
if (plugin != null && plugin.applicationContext != null) {
WebViewDatabase.getInstance(plugin.applicationContext).clearHttpAuthUsernamePassword();
}
result.success(true);
} else {
result.success(false);
}
break;
default:
result.notImplemented();
}
}
@Override
public void dispose() {
super.dispose();
plugin = null;
credentialDatabase = null;
}
}
@@ -0,0 +1,66 @@
package com.pichillilorenzo.flutter_inappwebview_android.credential_database;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public class CredentialDatabaseHelper extends SQLiteOpenHelper {
private static final String SQL_CREATE_PROTECTION_SPACE_TABLE =
"CREATE TABLE " + URLProtectionSpaceContract.FeedEntry.TABLE_NAME + " (" +
URLProtectionSpaceContract.FeedEntry._ID + " INTEGER PRIMARY KEY," +
URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST + " TEXT NOT NULL," +
URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL + " TEXT," +
URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM + " TEXT," +
URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT + " INTEGER," +
"UNIQUE(" + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST + ", " + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL + ", " +
URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM + ", " + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT +
")" +
");";
private static final String SQL_CREATE_CREDENTIAL_TABLE =
"CREATE TABLE " + URLCredentialContract.FeedEntry.TABLE_NAME + " (" +
URLCredentialContract.FeedEntry._ID + " INTEGER PRIMARY KEY," +
URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME + " TEXT NOT NULL," +
URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD + " TEXT NOT NULL," +
URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + " INTEGER NOT NULL," +
"UNIQUE(" + URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME + ", " + URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD + ", " +
URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID +
")," +
"FOREIGN KEY (" + URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + ") REFERENCES " +
URLProtectionSpaceContract.FeedEntry.TABLE_NAME + " (" + URLProtectionSpaceContract.FeedEntry._ID + ") ON DELETE CASCADE" +
");";
private static final String SQL_DELETE_PROTECTION_SPACE_TABLE =
"DROP TABLE IF EXISTS " + URLProtectionSpaceContract.FeedEntry.TABLE_NAME;
private static final String SQL_DELETE_CREDENTIAL_TABLE =
"DROP TABLE IF EXISTS " + URLCredentialContract.FeedEntry.TABLE_NAME;
public CredentialDatabaseHelper(Context context) {
super(context, CredentialDatabase.DATABASE_NAME, null, CredentialDatabase.DATABASE_VERSION);
}
public void onCreate(SQLiteDatabase db) {
db.execSQL(SQL_CREATE_PROTECTION_SPACE_TABLE);
db.execSQL(SQL_CREATE_CREDENTIAL_TABLE);
}
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// This database is only a cache for online data, so its upgrade policy is
// to simply to discard the data and start over
db.execSQL(SQL_DELETE_PROTECTION_SPACE_TABLE);
db.execSQL(SQL_DELETE_CREDENTIAL_TABLE);
onCreate(db);
}
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
onUpgrade(db, oldVersion, newVersion);
}
public void clearAllTables(SQLiteDatabase db) {
db.execSQL(SQL_DELETE_PROTECTION_SPACE_TABLE);
db.execSQL(SQL_DELETE_CREDENTIAL_TABLE);
onCreate(db);
}
}
@@ -0,0 +1,15 @@
package com.pichillilorenzo.flutter_inappwebview_android.credential_database;
import android.provider.BaseColumns;
public class URLCredentialContract {
private URLCredentialContract() {}
/* Inner class that defines the table contents */
public static class FeedEntry implements BaseColumns {
public static final String TABLE_NAME = "credential";
public static final String COLUMN_NAME_USERNAME = "username";
public static final String COLUMN_NAME_PASSWORD = "password";
public static final String COLUMN_NAME_PROTECTION_SPACE_ID = "protection_space_id";
}
}
@@ -0,0 +1,106 @@
package com.pichillilorenzo.flutter_inappwebview_android.credential_database;
import android.content.ContentValues;
import android.database.Cursor;
import com.pichillilorenzo.flutter_inappwebview_android.types.URLCredential;
import java.util.ArrayList;
import java.util.List;
public class URLCredentialDao {
CredentialDatabaseHelper credentialDatabaseHelper;
String[] projection = {
URLCredentialContract.FeedEntry._ID,
URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME,
URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD,
URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID
};
public URLCredentialDao(CredentialDatabaseHelper credentialDatabaseHelper) {
this.credentialDatabaseHelper = credentialDatabaseHelper;
}
public List<URLCredential> getAllByProtectionSpaceId(Long protectionSpaceId) {
String selection = URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + " = ?";
String[] selectionArgs = {protectionSpaceId.toString()};
Cursor cursor = credentialDatabaseHelper.getReadableDatabase().query(
URLCredentialContract.FeedEntry.TABLE_NAME,
projection,
selection,
selectionArgs,
null,
null,
null
);
List<URLCredential> URLCredentials = new ArrayList<>();
while (cursor.moveToNext()) {
Long id = cursor.getLong(cursor.getColumnIndexOrThrow(URLCredentialContract.FeedEntry._ID));
String username = cursor.getString(cursor.getColumnIndexOrThrow(URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME));
String password = cursor.getString(cursor.getColumnIndexOrThrow(URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD));
URLCredentials.add(new URLCredential(id, username, password, protectionSpaceId));
}
cursor.close();
return URLCredentials;
}
public URLCredential find(String username, String password, Long protectionSpaceId) {
String selection = URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME + " = ? AND " +
URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD + " = ? AND " +
URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + " = ?";
String[] selectionArgs = {username, password, protectionSpaceId.toString()};
Cursor cursor = credentialDatabaseHelper.getReadableDatabase().query(
URLCredentialContract.FeedEntry.TABLE_NAME,
projection,
selection,
selectionArgs,
null,
null,
null
);
URLCredential URLCredential = null;
if (cursor.moveToNext()) {
Long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(URLCredentialContract.FeedEntry._ID));
String rowUsername = cursor.getString(cursor.getColumnIndexOrThrow(URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME));
String rowPassword = cursor.getString(cursor.getColumnIndexOrThrow(URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD));
URLCredential = new URLCredential(rowId, rowUsername, rowPassword, protectionSpaceId);
}
cursor.close();
return URLCredential;
}
public long insert(URLCredential urlCredential) {
ContentValues credentialValues = new ContentValues();
credentialValues.put(URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME, urlCredential.getUsername());
credentialValues.put(URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD, urlCredential.getPassword());
credentialValues.put(URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID, urlCredential.getProtectionSpaceId());
return credentialDatabaseHelper.getWritableDatabase().insert(URLCredentialContract.FeedEntry.TABLE_NAME, null, credentialValues);
}
public long update(URLCredential urlCredential) {
ContentValues credentialValues = new ContentValues();
credentialValues.put(URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME, urlCredential.getUsername());
credentialValues.put(URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD, urlCredential.getPassword());
String whereClause = URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + " = ?";
String[] whereArgs = {urlCredential.getProtectionSpaceId().toString()};
return credentialDatabaseHelper.getWritableDatabase().update(URLCredentialContract.FeedEntry.TABLE_NAME, credentialValues, whereClause, whereArgs);
}
public long delete(URLCredential urlCredential) {
String whereClause = URLCredentialContract.FeedEntry._ID + " = ?";
String[] whereArgs = {urlCredential.getId().toString()};
return credentialDatabaseHelper.getWritableDatabase().delete(URLCredentialContract.FeedEntry.TABLE_NAME, whereClause, whereArgs);
}
}
@@ -0,0 +1,16 @@
package com.pichillilorenzo.flutter_inappwebview_android.credential_database;
import android.provider.BaseColumns;
public class URLProtectionSpaceContract {
private URLProtectionSpaceContract() {}
/* Inner class that defines the table contents */
public static class FeedEntry implements BaseColumns {
public static final String TABLE_NAME = "protection_space";
public static final String COLUMN_NAME_HOST = "host";
public static final String COLUMN_NAME_PROTOCOL = "protocol";
public static final String COLUMN_NAME_REALM = "realm";
public static final String COLUMN_NAME_PORT = "port";
}
}
@@ -0,0 +1,100 @@
package com.pichillilorenzo.flutter_inappwebview_android.credential_database;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import com.pichillilorenzo.flutter_inappwebview_android.types.URLProtectionSpace;
import java.util.ArrayList;
import java.util.List;
public class URLProtectionSpaceDao {
CredentialDatabaseHelper credentialDatabaseHelper;
String[] projection = {
URLProtectionSpaceContract.FeedEntry._ID,
URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST,
URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL,
URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM,
URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT
};
public URLProtectionSpaceDao(CredentialDatabaseHelper credentialDatabaseHelper) {
this.credentialDatabaseHelper = credentialDatabaseHelper;
}
public List<URLProtectionSpace> getAll() {
SQLiteDatabase readableDatabase = credentialDatabaseHelper.getReadableDatabase();
Cursor cursor = readableDatabase.query(
URLProtectionSpaceContract.FeedEntry.TABLE_NAME,
projection,
null,
null,
null,
null,
null
);
List<URLProtectionSpace> URLProtectionSpaces = new ArrayList<>();
while (cursor.moveToNext()) {
Long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry._ID));
String rowHost = cursor.getString(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST));
String rowProtocol = cursor.getString(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL));
String rowRealm = cursor.getString(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM));
Integer rowPort = cursor.getInt(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT));
URLProtectionSpaces.add(new URLProtectionSpace(rowId, rowHost, rowProtocol, rowRealm, rowPort));
}
cursor.close();
return URLProtectionSpaces;
}
public URLProtectionSpace find(String host, String protocol, String realm, Integer port) {
SQLiteDatabase readableDatabase = credentialDatabaseHelper.getReadableDatabase();
String selection = URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST + " = ? AND " + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL + " = ? AND " +
URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM + " = ? AND " + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT + " = ?";
String[] selectionArgs = {host, protocol, realm, port.toString()};
Cursor cursor = readableDatabase.query(
URLProtectionSpaceContract.FeedEntry.TABLE_NAME,
projection,
selection,
selectionArgs,
null,
null,
null
);
URLProtectionSpace URLProtectionSpace = null;
if (cursor.moveToNext()) {
Long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry._ID));
String rowHost = cursor.getString(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST));
String rowProtocol = cursor.getString(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL));
String rowRealm = cursor.getString(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM));
Integer rowPort = cursor.getInt(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT));
URLProtectionSpace = new URLProtectionSpace(rowId, rowHost, rowProtocol, rowRealm, rowPort);
}
cursor.close();
return URLProtectionSpace;
}
public long insert(URLProtectionSpace URLProtectionSpace) {
ContentValues protectionSpaceValues = new ContentValues();
protectionSpaceValues.put(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST, URLProtectionSpace.getHost());
protectionSpaceValues.put(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL, URLProtectionSpace.getProtocol());
protectionSpaceValues.put(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM, URLProtectionSpace.getRealm());
protectionSpaceValues.put(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT, URLProtectionSpace.getPort());
return credentialDatabaseHelper.getWritableDatabase().insert(URLProtectionSpaceContract.FeedEntry.TABLE_NAME, null, protectionSpaceValues);
};
public long delete(URLProtectionSpace URLProtectionSpace) {
String whereClause = URLProtectionSpaceContract.FeedEntry._ID + " = ?";
String[] whereArgs = {URLProtectionSpace.getId().toString()};
return credentialDatabaseHelper.getWritableDatabase().delete(URLProtectionSpaceContract.FeedEntry.TABLE_NAME, whereClause, whereArgs);
}
}
@@ -0,0 +1,94 @@
package com.pichillilorenzo.flutter_inappwebview_android.find_interaction;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
import com.pichillilorenzo.flutter_inappwebview_android.types.FindSession;
import java.util.HashMap;
import java.util.Map;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
public class FindInteractionChannelDelegate extends ChannelDelegateImpl {
@Nullable
private FindInteractionController findInteractionController;
public FindInteractionChannelDelegate(@NonNull FindInteractionController findInteractionController, @NonNull MethodChannel channel) {
super(channel);
this.findInteractionController = findInteractionController;
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull final MethodChannel.Result result) {
switch (call.method) {
case "findAll":
if (findInteractionController != null) {
String find = (String) call.argument("find");
findInteractionController.findAll(find);
}
result.success(true);
break;
case "findNext":
if (findInteractionController != null) {
Boolean forward = (Boolean) call.argument("forward");
findInteractionController.findNext(forward);
}
result.success(true);
break;
case "clearMatches":
if (findInteractionController != null) {
findInteractionController.clearMatches();
}
result.success(true);
break;
case "setSearchText":
if (findInteractionController != null) {
findInteractionController.searchText = (String) call.argument("searchText");
result.success(true);
} else {
result.success(false);
}
break;
case "getSearchText":
if (findInteractionController != null) {
result.success(findInteractionController.searchText);
} else {
result.success(false);
}
break;
case "getActiveFindSession":
if (findInteractionController != null && findInteractionController.activeFindSession != null) {
result.success(findInteractionController.activeFindSession.toMap());
} else {
result.success(null);
}
break;
default:
result.notImplemented();
}
}
public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) {
MethodChannel channel = getChannel();
if (channel == null) return;
if (isDoneCounting && findInteractionController != null && findInteractionController.webView != null) {
findInteractionController.activeFindSession = new FindSession(numberOfMatches, activeMatchOrdinal);
}
Map<String, Object> obj = new HashMap<>();
obj.put("activeMatchOrdinal", activeMatchOrdinal);
obj.put("numberOfMatches", numberOfMatches);
obj.put("isDoneCounting", isDoneCounting);
channel.invokeMethod("onFindResultReceived", obj);
}
@Override
public void dispose() {
super.dispose();
findInteractionController = null;
}
}
@@ -0,0 +1,73 @@
package com.pichillilorenzo.flutter_inappwebview_android.find_interaction;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin;
import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable;
import com.pichillilorenzo.flutter_inappwebview_android.types.FindSession;
import com.pichillilorenzo.flutter_inappwebview_android.webview.InAppWebViewInterface;
import io.flutter.plugin.common.MethodChannel;
public class FindInteractionController implements Disposable {
static final String LOG_TAG = "FindInteractionController";
public static final String METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_find_interaction_";
@Nullable
public InAppWebViewInterface webView;
@Nullable
public FindSession activeFindSession;
@Nullable
public FindInteractionChannelDelegate channelDelegate;
@Nullable
public FindInteractionSettings settings;
@Nullable
public String searchText;
public FindInteractionController(@NonNull InAppWebViewInterface webView, @NonNull InAppWebViewFlutterPlugin plugin,
@NonNull Object id, @Nullable FindInteractionSettings settings) {
this.webView = webView;
this.settings = settings;
final MethodChannel channel = new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME_PREFIX + id);
this.channelDelegate = new FindInteractionChannelDelegate(this, channel);
}
public void prepare() {
}
public void findAll(@Nullable String find) {
if (find == null) {
find = searchText;
} else {
// updated searchText
searchText = find;
}
if (webView != null && find != null) {
webView.findAllAsync(find);
}
}
public void findNext(boolean forward) {
if (webView != null) {
webView.findNext(forward);
}
}
public void clearMatches() {
if (webView != null) {
webView.clearMatches();
}
}
public void dispose() {
if (channelDelegate != null) {
channelDelegate.dispose();
channelDelegate = null;
}
webView = null;
activeFindSession = null;
searchText = null;
}
}
@@ -0,0 +1,45 @@
package com.pichillilorenzo.flutter_inappwebview_android.find_interaction;
import androidx.annotation.NonNull;
import com.pichillilorenzo.flutter_inappwebview_android.ISettings;
import java.util.HashMap;
import java.util.Map;
public class FindInteractionSettings implements ISettings<FindInteractionController> {
public static final String LOG_TAG = "FindInteractionSettings";
@NonNull
@Override
public FindInteractionSettings parse(@NonNull Map<String, Object> settings) {
// for (Map.Entry<String, Object> pair : settings.entrySet()) {
// String key = pair.getKey();
// Object value = pair.getValue();
// if (value == null) {
// continue;
// }
//
// switch (key) {
//
// }
// }
return this;
}
@NonNull
public Map<String, Object> toMap() {
Map<String, Object> settings = new HashMap<>();
return settings;
}
@NonNull
@Override
public Map<String, Object> getRealSettings(@NonNull FindInteractionController findInteractionController) {
Map<String, Object> realSettings = toMap();
return realSettings;
}
}
@@ -0,0 +1,159 @@
package com.pichillilorenzo.flutter_inappwebview_android.headless_in_app_webview;
import android.app.Activity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin;
import com.pichillilorenzo.flutter_inappwebview_android.Util;
import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable;
import com.pichillilorenzo.flutter_inappwebview_android.types.Size2D;
import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.FlutterWebView;
import java.util.Map;
import io.flutter.plugin.common.MethodChannel;
public class HeadlessInAppWebView implements Disposable {
protected static final String LOG_TAG = "HeadlessInAppWebView";
public static final String METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_headless_inappwebview_";
@NonNull
public final String id;
@Nullable
public HeadlessWebViewChannelDelegate channelDelegate;
@Nullable
public FlutterWebView flutterWebView;
@Nullable
public InAppWebViewFlutterPlugin plugin;
public HeadlessInAppWebView(@NonNull final InAppWebViewFlutterPlugin plugin, @NonNull String id, @NonNull FlutterWebView flutterWebView) {
this.id = id;
this.plugin = plugin;
this.flutterWebView = flutterWebView;
final MethodChannel channel = new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME_PREFIX + id);
this.channelDelegate = new HeadlessWebViewChannelDelegate(this, channel);
}
public void onWebViewCreated() {
if (channelDelegate != null) {
channelDelegate.onWebViewCreated();
}
}
public void prepare(Map<String, Object> params) {
if (flutterWebView != null) {
View view = flutterWebView.getView();
if (view != null) {
final Map<String, Object> initialSize = (Map<String, Object>) params.get("initialSize");
Size2D size = Size2D.fromMap(initialSize);
if (size == null) {
size = new Size2D(-1, -1);
}
setSize(size);
view.setVisibility(View.INVISIBLE);
}
}
if (plugin != null && plugin.activity != null) {
// Add the headless WebView to the view hierarchy.
// This way is also possible to take screenshots.
ViewGroup contentView = (ViewGroup) plugin.activity.findViewById(android.R.id.content);
if (contentView != null) {
ViewGroup mainView = (ViewGroup) (contentView).getChildAt(0);
if (mainView != null && flutterWebView != null) {
View view = flutterWebView.getView();
if (view != null) {
mainView.addView(view, 0);
}
}
}
}
}
public void setSize(@NonNull Size2D size) {
if (flutterWebView != null && flutterWebView.webView != null) {
View view = flutterWebView.getView();
if (view != null) {
float scale = Util.getPixelDensity(view.getContext());
Size2D fullscreenSize = Util.getFullscreenSize(view.getContext());
int width = (int) (size.getWidth() == -1 ? fullscreenSize.getWidth() : (size.getWidth() * scale));
int height = (int) (size.getWidth() == -1 ? fullscreenSize.getHeight() : (size.getHeight() * scale));
view.setLayoutParams(new FrameLayout.LayoutParams(width, height));
}
}
}
@Nullable
public Size2D getSize() {
if (flutterWebView != null && flutterWebView.webView != null) {
View view = flutterWebView.getView();
if (view != null) {
float scale = Util.getPixelDensity(view.getContext());
Size2D fullscreenSize = Util.getFullscreenSize(view.getContext());
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
return new Size2D(
fullscreenSize.getWidth() == layoutParams.width ? layoutParams.width : (layoutParams.width / scale),
fullscreenSize.getHeight() == layoutParams.height ? layoutParams.height : (layoutParams.height / scale)
);
}
}
return null;
}
@Nullable
public FlutterWebView disposeAndGetFlutterWebView() {
FlutterWebView newFlutterWebView = flutterWebView;
if (flutterWebView != null) {
View view = flutterWebView.getView();
if (view != null) {
// restore WebView layout params and visibility
view.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
view.setVisibility(View.VISIBLE);
// remove from parent
ViewGroup parent = (ViewGroup) view.getParent();
if (parent != null) {
parent.removeView(view);
}
}
// set to null to avoid to be disposed before calling "dispose()"
flutterWebView = null;
dispose();
}
return newFlutterWebView;
}
public void dispose() {
if (channelDelegate != null) {
channelDelegate.dispose();
channelDelegate = null;
}
if (plugin != null) {
HeadlessInAppWebViewManager headlessInAppWebViewManager = plugin.headlessInAppWebViewManager;
if (headlessInAppWebViewManager != null && headlessInAppWebViewManager.webViews.containsKey(id)) {
headlessInAppWebViewManager.webViews.put(id, null);
}
Activity activity = plugin.activity;
if (activity != null) {
ViewGroup contentView = plugin.activity.findViewById(android.R.id.content);
if (contentView != null) {
ViewGroup mainView = (ViewGroup) (contentView).getChildAt(0);
if (mainView != null && flutterWebView != null) {
View view = flutterWebView.getView();
if (view != null) {
mainView.removeView(flutterWebView.getView());
}
}
}
}
}
if (flutterWebView != null) {
flutterWebView.dispose();
}
flutterWebView = null;
plugin = null;
}
}
@@ -0,0 +1,98 @@
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
package com.pichillilorenzo.flutter_inappwebview_android.headless_in_app_webview;
import android.content.Context;
import androidx.annotation.Nullable;
import androidx.annotation.NonNull;
import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin;
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.FlutterWebView;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.Result;
public class HeadlessInAppWebViewManager extends ChannelDelegateImpl {
protected static final String LOG_TAG = "HeadlessInAppWebViewManager";
public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_headless_inappwebview";
public final Map<String, HeadlessInAppWebView> webViews = new HashMap<>();
@Nullable
public InAppWebViewFlutterPlugin plugin;
public HeadlessInAppWebViewManager(final InAppWebViewFlutterPlugin plugin) {
super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME));
this.plugin = plugin;
}
@Override
public void onMethodCall(final MethodCall call, @NonNull final Result result) {
final String id = (String) call.argument("id");
switch (call.method) {
case "run":
{
HashMap<String, Object> params = (HashMap<String, Object>) call.argument("params");
run(id, params);
}
result.success(true);
break;
default:
result.notImplemented();
}
}
public void run(String id, HashMap<String, Object> params) {
if (plugin == null || (plugin.activity == null && plugin.applicationContext == null)) return;
Context context = plugin.activity;
if (context == null) {
context = plugin.applicationContext;
}
FlutterWebView flutterWebView = new FlutterWebView(plugin, context, id, params);
HeadlessInAppWebView headlessInAppWebView = new HeadlessInAppWebView(plugin, id, flutterWebView);
webViews.put(id, headlessInAppWebView);
headlessInAppWebView.prepare(params);
headlessInAppWebView.onWebViewCreated();
flutterWebView.makeInitialLoad(params);
}
@Override
public void dispose() {
super.dispose();
Collection<HeadlessInAppWebView> headlessInAppWebViews = webViews.values();
for (HeadlessInAppWebView headlessInAppWebView : headlessInAppWebViews) {
if (headlessInAppWebView != null) {
headlessInAppWebView.dispose();
}
}
webViews.clear();
plugin = null;
}
}
@@ -0,0 +1,71 @@
package com.pichillilorenzo.flutter_inappwebview_android.headless_in_app_webview;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
import com.pichillilorenzo.flutter_inappwebview_android.types.Size2D;
import java.util.HashMap;
import java.util.Map;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
public class HeadlessWebViewChannelDelegate extends ChannelDelegateImpl {
@Nullable
private HeadlessInAppWebView headlessWebView;
public HeadlessWebViewChannelDelegate(@NonNull HeadlessInAppWebView headlessWebView, @NonNull MethodChannel channel) {
super(channel);
this.headlessWebView = headlessWebView;
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
switch (call.method) {
case "dispose":
if (headlessWebView != null) {
headlessWebView.dispose();
result.success(true);
} else {
result.success(false);
}
break;
case "setSize":
if (headlessWebView != null) {
Map<String, Object> sizeMap = (Map<String, Object>) call.argument("size");
Size2D size = Size2D.fromMap(sizeMap);
if (size != null)
headlessWebView.setSize(size);
result.success(true);
} else {
result.success(false);
}
break;
case "getSize":
if (headlessWebView != null) {
Size2D size = headlessWebView.getSize();
result.success(size != null ? size.toMap() : null);
} else {
result.success(null);
}
break;
default:
result.notImplemented();
}
}
public void onWebViewCreated() {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
channel.invokeMethod("onWebViewCreated", obj);
}
@Override
public void dispose() {
super.dispose();
headlessWebView = null;
}
}
@@ -0,0 +1,8 @@
package com.pichillilorenzo.flutter_inappwebview_android.in_app_browser;
import android.content.Intent;
public interface ActivityResultListener {
/** @return true if the result has been handled. */
boolean onActivityResult(int requestCode, int resultCode, Intent data);
}
@@ -0,0 +1,643 @@
package com.pichillilorenzo.flutter_inappwebview_android.in_app_browser;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Message;
import android.util.Log;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.webkit.WebView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.SearchView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.menu.MenuBuilder;
import com.pichillilorenzo.flutter_inappwebview_android.R;
import com.pichillilorenzo.flutter_inappwebview_android.Util;
import com.pichillilorenzo.flutter_inappwebview_android.find_interaction.FindInteractionController;
import com.pichillilorenzo.flutter_inappwebview_android.pull_to_refresh.PullToRefreshChannelDelegate;
import com.pichillilorenzo.flutter_inappwebview_android.pull_to_refresh.PullToRefreshLayout;
import com.pichillilorenzo.flutter_inappwebview_android.pull_to_refresh.PullToRefreshSettings;
import com.pichillilorenzo.flutter_inappwebview_android.types.AndroidResource;
import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable;
import com.pichillilorenzo.flutter_inappwebview_android.types.InAppBrowserMenuItem;
import com.pichillilorenzo.flutter_inappwebview_android.types.URLRequest;
import com.pichillilorenzo.flutter_inappwebview_android.types.UserScript;
import com.pichillilorenzo.flutter_inappwebview_android.webview.WebViewChannelDelegate;
import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebView;
import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebViewSettings;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import io.flutter.plugin.common.MethodChannel;
public class InAppBrowserActivity extends AppCompatActivity implements InAppBrowserDelegate, Disposable {
protected static final String LOG_TAG = "InAppBrowserActivity";
public static final String METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappbrowser_";
@Nullable
public Integer windowId;
public String id;
@Nullable
public InAppWebView webView;
@Nullable
public PullToRefreshLayout pullToRefreshLayout;
@Nullable
public ActionBar actionBar;
@Nullable
public Menu menu;
@Nullable
public SearchView searchView;
public InAppBrowserSettings customSettings = new InAppBrowserSettings();
@Nullable
public ProgressBar progressBar;
public boolean isHidden = false;
@Nullable
public String fromActivity;
private List<ActivityResultListener> activityResultListeners = new ArrayList<>();
@Nullable
public InAppBrowserManager manager;
@Nullable
public InAppBrowserChannelDelegate channelDelegate;
public List<InAppBrowserMenuItem> menuItems = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle b = getIntent().getExtras();
if (b == null) return;
id = b.getString("id");
String managerId = b.getString("managerId");
manager = InAppBrowserManager.shared.get(managerId);
if (manager == null || manager.plugin == null|| manager.plugin.messenger == null) return;
Map<String, Object> settingsMap = (Map<String, Object>) b.getSerializable("settings");
customSettings.parse(settingsMap);
windowId = b.getInt("windowId");
setContentView(R.layout.activity_web_view);
Map<String, Object> pullToRefreshInitialSettings = (Map<String, Object>) b.getSerializable("pullToRefreshInitialSettings");
MethodChannel pullToRefreshLayoutChannel = new MethodChannel(manager.plugin.messenger, PullToRefreshLayout.METHOD_CHANNEL_NAME_PREFIX + id);
PullToRefreshSettings pullToRefreshSettings = new PullToRefreshSettings();
pullToRefreshSettings.parse(pullToRefreshInitialSettings);
pullToRefreshLayout = findViewById(R.id.pullToRefresh);
pullToRefreshLayout.channelDelegate = new PullToRefreshChannelDelegate(pullToRefreshLayout, pullToRefreshLayoutChannel);
pullToRefreshLayout.settings = pullToRefreshSettings;
pullToRefreshLayout.prepare();
webView = findViewById(R.id.webView);
webView.id = id;
webView.windowId = windowId;
webView.inAppBrowserDelegate = this;
webView.plugin = manager.plugin;
FindInteractionController findInteractionController = new FindInteractionController(webView, manager.plugin, id, null);
webView.findInteractionController = findInteractionController;
findInteractionController.prepare();
final MethodChannel channel = new MethodChannel(manager.plugin.messenger, METHOD_CHANNEL_NAME_PREFIX + id);
channelDelegate = new InAppBrowserChannelDelegate(channel);
webView.channelDelegate = new WebViewChannelDelegate(webView, channel);
fromActivity = b.getString("fromActivity");
Map<String, Object> contextMenu = (Map<String, Object>) b.getSerializable("contextMenu");
List<Map<String, Object>> initialUserScripts = (List<Map<String, Object>>) b.getSerializable("initialUserScripts");
List<Map<String, Object>> menuItemList = (List<Map<String, Object>>) b.getSerializable("menuItems");
for (Map<String, Object> menuItem : menuItemList) {
menuItems.add(InAppBrowserMenuItem.fromMap(menuItem));
}
InAppWebViewSettings webViewSettings = new InAppWebViewSettings();
webViewSettings.parse(settingsMap);
webView.customSettings = webViewSettings;
webView.contextMenu = contextMenu;
List<UserScript> userScripts = new ArrayList<>();
if (initialUserScripts != null) {
for (Map<String, Object> initialUserScript : initialUserScripts) {
userScripts.add(UserScript.fromMap(initialUserScript));
}
}
webView.userContentController.addUserOnlyScripts(userScripts);
actionBar = getSupportActionBar();
prepareView();
if (windowId != -1) {
if (webView.plugin != null && webView.plugin.inAppWebViewManager != null) {
Message resultMsg = webView.plugin.inAppWebViewManager.windowWebViewMessages.get(windowId);
if (resultMsg != null) {
((WebView.WebViewTransport) resultMsg.obj).setWebView(webView);
resultMsg.sendToTarget();
}
}
} else {
String initialFile = b.getString("initialFile");
Map<String, Object> initialUrlRequest = (Map<String, Object>) b.getSerializable("initialUrlRequest");
String initialData = b.getString("initialData");
if (initialFile != null) {
try {
webView.loadFile(initialFile);
} catch (IOException e) {
Log.e(LOG_TAG, initialFile + " asset file cannot be found!", e);
return;
}
}
else if (initialData != null) {
String mimeType = b.getString("initialMimeType");
String encoding = b.getString("initialEncoding");
String baseUrl = b.getString("initialBaseUrl");
String historyUrl = b.getString("initialHistoryUrl");
webView.loadDataWithBaseURL(baseUrl, initialData, mimeType, encoding, historyUrl);
}
else if (initialUrlRequest != null) {
URLRequest urlRequest = URLRequest.fromMap(initialUrlRequest);
if (urlRequest != null) {
webView.loadUrl(urlRequest);
}
}
}
if (channelDelegate != null) {
channelDelegate.onBrowserCreated();
}
}
private void prepareView() {
if (webView != null) {
webView.prepare();
}
if (customSettings.hidden)
hide();
else
show();
progressBar = findViewById(R.id.progressBar);
if (progressBar != null) {
if (customSettings.hideProgressBar)
progressBar.setMax(0);
else
progressBar.setMax(100);
}
if (actionBar != null) {
actionBar.setDisplayShowTitleEnabled(!customSettings.hideTitleBar);
if (customSettings.hideToolbarTop)
actionBar.hide();
if (customSettings.toolbarTopBackgroundColor != null && !customSettings.toolbarTopBackgroundColor.isEmpty())
actionBar.setBackgroundDrawable(new ColorDrawable(Color.parseColor(customSettings.toolbarTopBackgroundColor)));
if (customSettings.toolbarTopFixedTitle != null && !customSettings.toolbarTopFixedTitle.isEmpty())
actionBar.setTitle(customSettings.toolbarTopFixedTitle);
}
}
@SuppressLint("RestrictedApi")
@Override
public boolean onCreateOptionsMenu(Menu m) {
menu = m;
if (actionBar != null && (customSettings.toolbarTopFixedTitle == null || customSettings.toolbarTopFixedTitle.isEmpty()))
actionBar.setTitle(webView != null ? webView.getTitle() : "");
if (menu == null)
return super.onCreateOptionsMenu(m);
if (menu instanceof MenuBuilder) {
((MenuBuilder) menu).setOptionalIconsVisible(true);
}
MenuInflater inflater = getMenuInflater();
// Inflate menu to add items to action bar if it is present.
inflater.inflate(R.menu.menu_main, menu);
MenuItem menuSearchItem = menu.findItem(R.id.menu_search);
if (menuSearchItem != null) {
if (customSettings.hideUrlBar)
menuSearchItem.setVisible(false);
searchView = (SearchView) menuSearchItem.getActionView();
if (searchView != null) {
searchView.setFocusable(true);
searchView.setQuery(webView != null ? webView.getUrl() : "", false);
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
if (!query.isEmpty()) {
if (webView != null)
webView.loadUrl(query);
if (searchView != null) {
searchView.setQuery("", false);
searchView.setIconified(true);
}
return true;
}
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
return false;
}
});
searchView.setOnCloseListener(new SearchView.OnCloseListener() {
@Override
public boolean onClose() {
if (searchView != null && searchView.getQuery().toString().isEmpty())
searchView.setQuery(webView != null ? webView.getUrl() : "", false);
return false;
}
});
searchView.setOnQueryTextFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View view, boolean b) {
if (!b && searchView != null) {
searchView.setQuery("", false);
searchView.setIconified(true);
}
}
});
}
}
if (customSettings.hideDefaultMenuItems) {
MenuItem actionClose = menu.findItem(R.id.action_close);
if (actionClose != null) {
actionClose.setVisible(false);
}
MenuItem actionGoBack = menu.findItem(R.id.action_go_back);
if (actionGoBack != null) {
actionGoBack.setVisible(false);
}
MenuItem actionReload = menu.findItem(R.id.action_reload);
if (actionReload != null) {
actionReload.setVisible(false);
}
MenuItem actionGoForward = menu.findItem(R.id.action_go_forward);
if (actionGoForward != null) {
actionGoForward.setVisible(false);
}
MenuItem actionShare = menu.findItem(R.id.action_share);
if (actionShare != null) {
actionShare.setVisible(false);
}
}
for (final InAppBrowserMenuItem menuItem : menuItems) {
int order = menuItem.getOrder() != null ? menuItem.getOrder() : Menu.NONE;
MenuItem item = menu.add(Menu.NONE, menuItem.getId(), order, menuItem.getTitle());
if (menuItem.isShowAsAction()) {
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
}
Object icon = menuItem.getIcon();
if (icon != null) {
if (icon instanceof AndroidResource) {
item.setIcon(((AndroidResource) icon).getIdentifier(this));
} else {
item.setIcon(Util.drawableFromBytes(this, (byte[]) icon));
}
String iconColor = menuItem.getIconColor();
if (iconColor != null && !iconColor.isEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
item.getIcon().setTint(Color.parseColor(iconColor));
}
}
item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
@Override
public boolean onMenuItemClick(@NonNull MenuItem item) {
if (channelDelegate != null) {
channelDelegate.onMenuItemClicked(menuItem);
}
return true;
}
});
}
return true;
}
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (customSettings.shouldCloseOnBackButtonPressed) {
close(null);
return true;
}
if (customSettings.allowGoBackWithBackButton) {
if (canGoBack())
goBack();
else if (customSettings.closeOnCannotGoBack)
close(null);
return true;
}
if (!customSettings.shouldCloseOnBackButtonPressed) {
return true;
}
}
return super.onKeyDown(keyCode, event);
}
public void close(final MethodChannel.Result result) {
if (channelDelegate != null) {
channelDelegate.onExit();
}
dispose();
if (result != null) {
result.success(true);
}
}
public void reload() {
if (webView != null)
webView.reload();
}
public void goBack() {
if (webView != null && canGoBack())
webView.goBack();
}
public boolean canGoBack() {
if (webView != null)
return webView.canGoBack();
return false;
}
public void goForward() {
if (webView != null && canGoForward())
webView.goForward();
}
public boolean canGoForward() {
if (webView != null)
return webView.canGoForward();
return false;
}
public void hide() {
if (fromActivity != null) {
try {
isHidden = true;
Intent openActivity = new Intent(this, Class.forName(fromActivity));
openActivity.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
startActivityIfNeeded(openActivity, 0);
} catch (ClassNotFoundException e) {
Log.d(LOG_TAG, "", e);
}
}
}
public void show() {
isHidden = false;
Intent openActivity = new Intent(this, InAppBrowserActivity.class);
openActivity.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
startActivityIfNeeded(openActivity, 0);
}
public void goBackButtonClicked(MenuItem item) {
goBack();
}
public void goForwardButtonClicked(MenuItem item) {
goForward();
}
public void shareButtonClicked(MenuItem item) {
Intent share = new Intent(Intent.ACTION_SEND);
share.setType("text/plain");
share.putExtra(Intent.EXTRA_TEXT, webView != null ? webView.getUrl() : "");
startActivity(Intent.createChooser(share, "Share"));
}
public void reloadButtonClicked(MenuItem item) {
reload();
}
public void closeButtonClicked(MenuItem item) {
close(null);
}
public void setSettings(InAppBrowserSettings newSettings, HashMap<String, Object> newSettingsMap) {
InAppWebViewSettings newInAppWebViewSettings = new InAppWebViewSettings();
newInAppWebViewSettings.parse(newSettingsMap);
if (webView != null) {
webView.setSettings(newInAppWebViewSettings, newSettingsMap);
}
if (newSettingsMap.get("hidden") != null && customSettings.hidden != newSettings.hidden) {
if (newSettings.hidden)
hide();
else
show();
}
if (newSettingsMap.get("hideProgressBar") != null && customSettings.hideProgressBar != newSettings.hideProgressBar && progressBar != null) {
if (newSettings.hideProgressBar)
progressBar.setMax(0);
else
progressBar.setMax(100);
}
if (actionBar != null && newSettingsMap.get("hideTitleBar") != null && customSettings.hideTitleBar != newSettings.hideTitleBar)
actionBar.setDisplayShowTitleEnabled(!newSettings.hideTitleBar);
if (actionBar != null && newSettingsMap.get("hideToolbarTop") != null && customSettings.hideToolbarTop != newSettings.hideToolbarTop) {
if (newSettings.hideToolbarTop)
actionBar.hide();
else
actionBar.show();
}
if (actionBar != null && newSettingsMap.get("toolbarTopBackgroundColor") != null &&
!Util.objEquals(customSettings.toolbarTopBackgroundColor, newSettings.toolbarTopBackgroundColor) &&
newSettings.toolbarTopBackgroundColor != null && !newSettings.toolbarTopBackgroundColor.isEmpty())
actionBar.setBackgroundDrawable(new ColorDrawable(Color.parseColor(newSettings.toolbarTopBackgroundColor)));
if (actionBar != null && newSettingsMap.get("toolbarTopFixedTitle") != null &&
!Util.objEquals(customSettings.toolbarTopFixedTitle, newSettings.toolbarTopFixedTitle) &&
newSettings.toolbarTopFixedTitle != null && !newSettings.toolbarTopFixedTitle.isEmpty())
actionBar.setTitle(newSettings.toolbarTopFixedTitle);
if (menu != null && newSettingsMap.get("hideUrlBar") != null && customSettings.hideUrlBar != newSettings.hideUrlBar) {
MenuItem menuSearchItem = menu.findItem(R.id.menu_search);
if (menuSearchItem != null) {
menuSearchItem.setVisible(!newSettings.hideUrlBar);
}
}
if (menu != null && newSettingsMap.get("hideDefaultMenuItems") != null && customSettings.hideDefaultMenuItems != newSettings.hideDefaultMenuItems) {
MenuItem actionClose = menu.findItem(R.id.action_close);
if (actionClose != null) {
actionClose.setVisible(!newSettings.hideDefaultMenuItems);
}
MenuItem actionGoBack = menu.findItem(R.id.action_go_back);
if (actionGoBack != null) {
actionGoBack.setVisible(!newSettings.hideDefaultMenuItems);
}
MenuItem actionReload = menu.findItem(R.id.action_reload);
if (actionReload != null) {
actionReload.setVisible(!newSettings.hideDefaultMenuItems);
}
MenuItem actionGoForward = menu.findItem(R.id.action_go_forward);
if (actionGoForward != null) {
actionGoForward.setVisible(!newSettings.hideDefaultMenuItems);
}
MenuItem actionShare = menu.findItem(R.id.action_share);
if (actionShare != null) {
actionShare.setVisible(!newSettings.hideDefaultMenuItems);
}
}
customSettings = newSettings;
}
public Map<String, Object> getCustomSettings() {
Map<String, Object> webViewSettingsMap = webView != null ? webView.getCustomSettings() : null;
if (customSettings == null || webViewSettingsMap == null)
return null;
Map<String, Object> settingsMap = customSettings.getRealSettings(this);
settingsMap.putAll(webViewSettingsMap);
return settingsMap;
}
@Override
public Activity getActivity() {
return this;
}
@Override
public void didChangeTitle(String title) {
if (actionBar != null && (customSettings.toolbarTopFixedTitle == null || customSettings.toolbarTopFixedTitle.isEmpty())) {
actionBar.setTitle(title);
}
}
@Override
public void didStartNavigation(String url) {
if (progressBar != null) {
progressBar.setProgress(0);
}
if (searchView != null) {
searchView.setQuery(url, false);
}
}
@Override
public void didUpdateVisitedHistory(String url) {
if (searchView != null) {
searchView.setQuery(url, false);
}
}
@Override
public void didFinishNavigation(String url) {
if (searchView != null) {
searchView.setQuery(url, false);
}
if (progressBar != null) {
progressBar.setProgress(0);
}
}
@Override
public void didFailNavigation(String url, int errorCode, String description) {
if (progressBar != null) {
progressBar.setProgress(0);
}
}
@Override
public void didChangeProgress(int progress) {
if (progressBar != null) {
progressBar.setVisibility(View.VISIBLE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
progressBar.setProgress(progress, true);
} else {
progressBar.setProgress(progress);
}
if (progress == 100) {
progressBar.setVisibility(View.GONE);
}
}
}
public List<ActivityResultListener> getActivityResultListeners() {
return activityResultListeners;
}
@Override
protected void onActivityResult (int requestCode,
int resultCode,
Intent data) {
for (ActivityResultListener listener : activityResultListeners) {
if (listener.onActivityResult(requestCode, resultCode, data)) {
return;
}
}
super.onActivityResult(requestCode, resultCode, data);
}
@Override
public void dispose() {
if (channelDelegate != null) {
channelDelegate.dispose();
channelDelegate = null;
}
activityResultListeners.clear();
if (webView != null) {
if (manager != null && manager.plugin != null &&
manager.plugin.activityPluginBinding != null && webView.inAppWebViewChromeClient != null) {
manager.plugin.activityPluginBinding.removeActivityResultListener(webView.inAppWebViewChromeClient);
}
RelativeLayout containerView = (RelativeLayout) findViewById(R.id.container);
if (containerView != null) {
containerView.removeAllViews();
}
webView.dispose();
webView = null;
finish();
}
}
@Override
public void onDestroy() {
dispose();
super.onDestroy();
}
}
@@ -0,0 +1,39 @@
package com.pichillilorenzo.flutter_inappwebview_android.in_app_browser;
import androidx.annotation.NonNull;
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
import com.pichillilorenzo.flutter_inappwebview_android.types.InAppBrowserMenuItem;
import java.util.HashMap;
import java.util.Map;
import io.flutter.plugin.common.MethodChannel;
public class InAppBrowserChannelDelegate extends ChannelDelegateImpl {
public InAppBrowserChannelDelegate(@NonNull MethodChannel channel) {
super(channel);
}
public void onBrowserCreated() {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
channel.invokeMethod("onBrowserCreated", obj);
}
public void onMenuItemClicked(InAppBrowserMenuItem menuItem) {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
obj.put("id", menuItem.getId());
channel.invokeMethod("onMenuItemClicked", obj);
}
public void onExit() {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
channel.invokeMethod("onExit", obj);
}
}
@@ -0,0 +1,16 @@
package com.pichillilorenzo.flutter_inappwebview_android.in_app_browser;
import android.app.Activity;
import java.util.List;
public interface InAppBrowserDelegate {
Activity getActivity();
List<ActivityResultListener> getActivityResultListeners();
void didChangeTitle(String title);
void didStartNavigation(String url);
void didUpdateVisitedHistory(String url);
void didFinishNavigation(String url);
void didFailNavigation(String url, int errorCode, String description);
void didChangeProgress(int progress);
}
@@ -0,0 +1,219 @@
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
package com.pichillilorenzo.flutter_inappwebview_android.in_app_browser;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.provider.Browser;
import android.util.Log;
import android.webkit.MimeTypeMap;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin;
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.Result;
/**
* InAppBrowserManager
*/
public class InAppBrowserManager extends ChannelDelegateImpl {
protected static final String LOG_TAG = "InAppBrowserManager";
public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappbrowser";
@Nullable
public InAppWebViewFlutterPlugin plugin;
public String id;
public static final Map<String, InAppBrowserManager> shared = new HashMap<>();
public InAppBrowserManager(final InAppWebViewFlutterPlugin plugin) {
super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME));
this.id = UUID.randomUUID().toString();
this.plugin = plugin;
shared.put(this.id, this);
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
switch (call.method) {
case "open":
if (plugin != null && plugin.activity != null) {
open(plugin.activity, (Map<String, Object>) call.arguments());
result.success(true);
} else {
result.success(false);
}
break;
case "openWithSystemBrowser":
if (plugin != null && plugin.activity != null) {
String url = (String) call.argument("url");
openWithSystemBrowser(plugin.activity, url, result);
} else {
result.success(false);
}
break;
default:
result.notImplemented();
}
}
public static String getMimeType(String url) {
String type = null;
String extension = MimeTypeMap.getFileExtensionFromUrl(url);
if (extension != null) {
type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
}
return type;
}
/**
* Display a new browser with the specified URL.
*
* @param url the url to load.
* @param result
* @return "" if ok, or error message.
*/
public void openWithSystemBrowser(Activity activity, String url, Result result) {
try {
Intent intent;
intent = new Intent(Intent.ACTION_VIEW);
// Omitting the MIME type for file: URLs causes "No Activity found to handle Intent".
// Adding the MIME type to http: URLs causes them to not be handled by the downloader.
Uri uri = Uri.parse(url);
if ("file".equals(uri.getScheme())) {
intent.setDataAndType(uri, getMimeType(url));
} else {
intent.setData(uri);
}
intent.putExtra(Browser.EXTRA_APPLICATION_ID, activity.getPackageName());
// CB-10795: Avoid circular loops by preventing it from opening in the current app
this.openExternalExcludeCurrentApp(activity, intent);
result.success(true);
// not catching FileUriExposedException explicitly because buildtools<24 doesn't know about it
} catch (java.lang.RuntimeException e) {
Log.d(LOG_TAG, url + " cannot be opened: " + e.toString());
result.error(LOG_TAG, url + " cannot be opened!", null);
}
}
/**
* Opens the intent, providing a chooser that excludes the current app to avoid
* circular loops.
*/
public void openExternalExcludeCurrentApp(Activity activity, Intent intent) {
String currentPackage = activity.getPackageName();
boolean hasCurrentPackage = false;
PackageManager pm = activity.getPackageManager();
List<ResolveInfo> activities = pm.queryIntentActivities(intent, 0);
ArrayList<Intent> targetIntents = new ArrayList<Intent>();
for (ResolveInfo ri : activities) {
if (!currentPackage.equals(ri.activityInfo.packageName)) {
Intent targetIntent = (Intent) intent.clone();
targetIntent.setPackage(ri.activityInfo.packageName);
targetIntents.add(targetIntent);
} else {
hasCurrentPackage = true;
}
}
// If the current app package isn't a target for this URL, then use
// the normal launch behavior
if (!hasCurrentPackage || targetIntents.size() == 0) {
activity.startActivity(intent);
}
// If there's only one possible intent, launch it directly
else if (targetIntents.size() == 1) {
activity.startActivity(targetIntents.get(0));
}
// Otherwise, show a custom chooser without the current app listed
else if (targetIntents.size() > 0) {
Intent chooser = Intent.createChooser(targetIntents.remove(targetIntents.size() - 1), null);
chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetIntents.toArray(new Parcelable[]{}));
activity.startActivity(chooser);
}
}
public void open(Activity activity, Map<String, Object> arguments) {
String id = (String) arguments.get("id");
Map<String, Object> urlRequest = (Map<String, Object>) arguments.get("urlRequest");
String assetFilePath = (String) arguments.get("assetFilePath");
String data = (String) arguments.get("data");
String mimeType = (String) arguments.get("mimeType");
String encoding = (String) arguments.get("encoding");
String baseUrl = (String) arguments.get("baseUrl");
String historyUrl = (String) arguments.get("historyUrl");
Map<String, Object> settings = (Map<String, Object>) arguments.get("settings");
Map<String, Object> contextMenu = (Map<String, Object>) arguments.get("contextMenu");
Integer windowId = (Integer) arguments.get("windowId");
List<Map<String, Object>> initialUserScripts = (List<Map<String, Object>>) arguments.get("initialUserScripts");
Map<String, Object> pullToRefreshInitialSettings = (Map<String, Object>) arguments.get("pullToRefreshSettings");
List<Map<String, Object>> menuItems = (List<Map<String, Object>>) arguments.get("menuItems");
Bundle extras = new Bundle();
extras.putString("fromActivity", activity.getClass().getName());
extras.putSerializable("initialUrlRequest", (Serializable) urlRequest);
extras.putString("initialFile", assetFilePath);
extras.putString("initialData", data);
extras.putString("initialMimeType", mimeType);
extras.putString("initialEncoding", encoding);
extras.putString("initialBaseUrl", baseUrl);
extras.putString("initialHistoryUrl", historyUrl);
extras.putString("id", id);
extras.putString("managerId", this.id);
extras.putSerializable("settings", (Serializable) settings);
extras.putSerializable("contextMenu", (Serializable) contextMenu);
extras.putInt("windowId", windowId != null ? windowId : -1);
extras.putSerializable("initialUserScripts", (Serializable) initialUserScripts);
extras.putSerializable("pullToRefreshInitialSettings", (Serializable) pullToRefreshInitialSettings);
extras.putSerializable("menuItems", (Serializable) menuItems);
startInAppBrowserActivity(activity, extras);
}
public void startInAppBrowserActivity(Activity activity, Bundle extras) {
Intent intent = new Intent(activity, InAppBrowserActivity.class);
if (extras != null)
intent.putExtras(extras);
activity.startActivity(intent);
}
@Override
public void dispose() {
super.dispose();
shared.remove(this.id);
plugin = null;
}
}
@@ -0,0 +1,109 @@
package com.pichillilorenzo.flutter_inappwebview_android.in_app_browser;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.pichillilorenzo.flutter_inappwebview_android.ISettings;
import com.pichillilorenzo.flutter_inappwebview_android.R;
import java.util.HashMap;
import java.util.Map;
public class InAppBrowserSettings implements ISettings<InAppBrowserActivity> {
public static final String LOG_TAG = "InAppBrowserSettings";
public Boolean hidden = false;
public Boolean hideToolbarTop = false;
@Nullable
public String toolbarTopBackgroundColor;
@Nullable
public String toolbarTopFixedTitle;
public Boolean hideUrlBar = false;
public Boolean hideProgressBar = false;
public Boolean hideTitleBar = false;
public Boolean closeOnCannotGoBack = true;
public Boolean allowGoBackWithBackButton = true;
public Boolean shouldCloseOnBackButtonPressed = false;
public Boolean hideDefaultMenuItems = false;
@NonNull
@Override
public InAppBrowserSettings parse(@NonNull Map<String, Object> settings) {
for (Map.Entry<String, Object> pair : settings.entrySet()) {
String key = pair.getKey();
Object value = pair.getValue();
if (value == null) {
continue;
}
switch (key) {
case "hidden":
hidden = (Boolean) value;
break;
case "hideToolbarTop":
hideToolbarTop = (Boolean) value;
break;
case "toolbarTopBackgroundColor":
toolbarTopBackgroundColor = (String) value;
break;
case "toolbarTopFixedTitle":
toolbarTopFixedTitle = (String) value;
break;
case "hideUrlBar":
hideUrlBar = (Boolean) value;
break;
case "hideTitleBar":
hideTitleBar = (Boolean) value;
break;
case "closeOnCannotGoBack":
closeOnCannotGoBack = (Boolean) value;
break;
case "hideProgressBar":
hideProgressBar = (Boolean) value;
break;
case "allowGoBackWithBackButton":
allowGoBackWithBackButton = (Boolean) value;
break;
case "shouldCloseOnBackButtonPressed":
shouldCloseOnBackButtonPressed = (Boolean) value;
break;
case "hideDefaultMenuItems":
hideDefaultMenuItems = (Boolean) value;
break;
}
}
return this;
}
@NonNull
@Override
public Map<String, Object> toMap() {
Map<String, Object> settings = new HashMap<>();
settings.put("hidden", hidden);
settings.put("hideToolbarTop", hideToolbarTop);
settings.put("toolbarTopBackgroundColor", toolbarTopBackgroundColor);
settings.put("toolbarTopFixedTitle", toolbarTopFixedTitle);
settings.put("hideUrlBar", hideUrlBar);
settings.put("hideTitleBar", hideTitleBar);
settings.put("closeOnCannotGoBack", closeOnCannotGoBack);
settings.put("hideProgressBar", hideProgressBar);
settings.put("allowGoBackWithBackButton", allowGoBackWithBackButton);
settings.put("shouldCloseOnBackButtonPressed", shouldCloseOnBackButtonPressed);
settings.put("hideDefaultMenuItems", hideDefaultMenuItems);
return settings;
}
@NonNull
@Override
public Map<String, Object> getRealSettings(@NonNull InAppBrowserActivity inAppBrowserActivity) {
Map<String, Object> realSettings = toMap();
realSettings.put("hidden", inAppBrowserActivity.isHidden);
realSettings.put("hideToolbarTop", inAppBrowserActivity.actionBar == null || !inAppBrowserActivity.actionBar.isShowing());
realSettings.put("hideUrlBar", inAppBrowserActivity.menu == null || !inAppBrowserActivity.menu.findItem(R.id.menu_search).isVisible());
realSettings.put("hideProgressBar", inAppBrowserActivity.progressBar == null || inAppBrowserActivity.progressBar.getMax() == 0);
return realSettings;
}
}
@@ -0,0 +1,266 @@
package com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js;
import com.pichillilorenzo.flutter_inappwebview_android.types.PluginScript;
import com.pichillilorenzo.flutter_inappwebview_android.types.UserScriptInjectionTime;
public class InterceptAjaxRequestJS {
public static final String INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT";
public static final String FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE = JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._useShouldInterceptAjaxRequest";
public static final String FLAG_VARIABLE_FOR_INTERCEPT_ONLY_ASYNC_AJAX_REQUESTS_JS_SOURCE = JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._interceptOnlyAsyncAjaxRequests";
public static final PluginScript INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT = new PluginScript(
InterceptAjaxRequestJS.INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME,
InterceptAjaxRequestJS.INTERCEPT_AJAX_REQUEST_JS_SOURCE,
UserScriptInjectionTime.AT_DOCUMENT_START,
null,
true,
null
);
public static PluginScript createInterceptOnlyAsyncAjaxRequestsPluginScript(boolean onlyAsync) {
return new PluginScript(
InterceptAjaxRequestJS.INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME,
"window." + FLAG_VARIABLE_FOR_INTERCEPT_ONLY_ASYNC_AJAX_REQUESTS_JS_SOURCE + " = " + onlyAsync +";",
UserScriptInjectionTime.AT_DOCUMENT_START,
null,
true,
null
);
}
public static final String INTERCEPT_AJAX_REQUEST_JS_SOURCE = "(function(ajax) {" +
" var w = (window.top == null || window.top === window) ? window : window.top;" +
" w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE + " = true;" +
" var send = ajax.prototype.send;" +
" var open = ajax.prototype.open;" +
" var setRequestHeader = ajax.prototype.setRequestHeader;" +
" ajax.prototype._flutter_inappwebview_url = null;" +
" ajax.prototype._flutter_inappwebview_method = null;" +
" ajax.prototype._flutter_inappwebview_isAsync = null;" +
" ajax.prototype._flutter_inappwebview_user = null;" +
" ajax.prototype._flutter_inappwebview_password = null;" +
" ajax.prototype._flutter_inappwebview_password = null;" +
" ajax.prototype._flutter_inappwebview_already_onreadystatechange_wrapped = false;" +
" ajax.prototype._flutter_inappwebview_request_headers = {};" +
" function convertRequestResponse(request, callback) {" +
" if (request.response != null && request.responseType != null) {" +
" switch (request.responseType) {" +
" case 'arraybuffer':" +
" callback(new Uint8Array(request.response));" +
" return;" +
" case 'blob':" +
" const reader = new FileReader();" +
" reader.addEventListener('loadend', function() { " +
" callback(new Uint8Array(reader.result));" +
" });" +
" reader.readAsArrayBuffer(blob);" +
" return;" +
" case 'document':" +
" callback(request.response.documentElement.outerHTML);" +
" return;" +
" case 'json':" +
" callback(request.response);" +
" return;" +
" };" +
" }" +
" callback(null);" +
" };" +
" ajax.prototype.open = function(method, url, isAsync, user, password) {" +
" isAsync = (isAsync != null) ? isAsync : true;" +
" this._flutter_inappwebview_url = url;" +
" this._flutter_inappwebview_method = method;" +
" this._flutter_inappwebview_isAsync = isAsync;" +
" this._flutter_inappwebview_user = user;" +
" this._flutter_inappwebview_password = password;" +
" this._flutter_inappwebview_request_headers = {};" +
" open.call(this, method, url, isAsync, user, password);" +
" };" +
" ajax.prototype.setRequestHeader = function(header, value) {" +
" this._flutter_inappwebview_request_headers[header] = value;" +
" setRequestHeader.call(this, header, value);" +
" };" +
" function handleEvent(e) {" +
" var self = this;" +
" var w = (window.top == null || window.top === window) ? window : window.top;" +
" if (w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE + " == null || w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE + " == true) {" +
" var headers = this.getAllResponseHeaders();" +
" var responseHeaders = {};" +
" if (headers != null) {" +
" var arr = headers.trim().split(/[\\r\\n]+/);" +
" arr.forEach(function (line) {" +
" var parts = line.split(': ');" +
" var header = parts.shift();" +
" var value = parts.join(': ');" +
" responseHeaders[header] = value;" +
" });" +
" }" +
" convertRequestResponse(this, function(response) {" +
" var ajaxRequest = {" +
" method: self._flutter_inappwebview_method," +
" url: self._flutter_inappwebview_url," +
" isAsync: self._flutter_inappwebview_isAsync," +
" user: self._flutter_inappwebview_user," +
" password: self._flutter_inappwebview_password," +
" withCredentials: self.withCredentials," +
" headers: self._flutter_inappwebview_request_headers," +
" readyState: self.readyState," +
" status: self.status," +
" responseURL: self.responseURL," +
" responseType: self.responseType," +
" response: response," +
" responseText: (self.responseType == 'text' || self.responseType == '') ? self.responseText : null," +
" responseXML: (self.responseType == 'document' && self.responseXML != null) ? self.responseXML.documentElement.outerHTML : null," +
" statusText: self.statusText," +
" responseHeaders, responseHeaders," +
" event: {" +
" type: e.type," +
" loaded: e.loaded," +
" lengthComputable: e.lengthComputable," +
" total: e.total" +
" }" +
" };" +
" window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('onAjaxProgress', ajaxRequest).then(function(result) {" +
" if (result != null) {" +
" switch (result) {" +
" case 0:" +
" self.abort();" +
" return;" +
" };" +
" }" +
" });" +
" });" +
" }" +
" };" +
" ajax.prototype.send = function(data) {" +
" var self = this;" +
" var w = (window.top == null || window.top === window) ? window : window.top;" +
" var canBeIntercepted = self._flutter_inappwebview_isAsync || w." + FLAG_VARIABLE_FOR_INTERCEPT_ONLY_ASYNC_AJAX_REQUESTS_JS_SOURCE + " === false;" +
" if (canBeIntercepted && (w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE + " == null || w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE + " == true)) {" +
" if (!this._flutter_inappwebview_already_onreadystatechange_wrapped) {" +
" this._flutter_inappwebview_already_onreadystatechange_wrapped = true;" +
" var onreadystatechange = this.onreadystatechange;" +
" this.onreadystatechange = function() {" +
" var w = (window.top == null || window.top === window) ? window : window.top;" +
" if (w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE + " == null || w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE + " == true) {" +
" var headers = this.getAllResponseHeaders();" +
" var responseHeaders = {};" +
" if (headers != null) {" +
" var arr = headers.trim().split(/[\\r\\n]+/);" +
" arr.forEach(function (line) {" +
" var parts = line.split(': ');" +
" var header = parts.shift();" +
" var value = parts.join(': ');" +
" responseHeaders[header] = value;" +
" });" +
" }" +
" convertRequestResponse(this, function(response) {" +
" var ajaxRequest = {" +
" method: self._flutter_inappwebview_method," +
" url: self._flutter_inappwebview_url," +
" isAsync: self._flutter_inappwebview_isAsync," +
" user: self._flutter_inappwebview_user," +
" password: self._flutter_inappwebview_password," +
" withCredentials: self.withCredentials," +
" headers: self._flutter_inappwebview_request_headers," +
" readyState: self.readyState," +
" status: self.status," +
" responseURL: self.responseURL," +
" responseType: self.responseType," +
" response: response," +
" responseText: (self.responseType == 'text' || self.responseType == '') ? self.responseText : null," +
" responseXML: (self.responseType == 'document' && self.responseXML != null) ? self.responseXML.documentElement.outerHTML : null," +
" statusText: self.statusText," +
" responseHeaders: responseHeaders" +
" };" +
" window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('onAjaxReadyStateChange', ajaxRequest).then(function(result) {" +
" if (result != null) {" +
" switch (result) {" +
" case 0:" +
" self.abort();" +
" return;" +
" };" +
" }" +
" if (onreadystatechange != null) {" +
" onreadystatechange();" +
" }" +
" });" +
" });" +
" } else if (onreadystatechange != null) {" +
" onreadystatechange();" +
" }" +
" };" +
" }" +
" this.addEventListener('loadstart', handleEvent);" +
" this.addEventListener('load', handleEvent);" +
" this.addEventListener('loadend', handleEvent);" +
" this.addEventListener('progress', handleEvent);" +
" this.addEventListener('error', handleEvent);" +
" this.addEventListener('abort', handleEvent);" +
" this.addEventListener('timeout', handleEvent);" +
" " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".convertBodyRequest(data).then(function(data) {" +
" var ajaxRequest = {" +
" data: data," +
" method: self._flutter_inappwebview_method," +
" url: self._flutter_inappwebview_url," +
" isAsync: self._flutter_inappwebview_isAsync," +
" user: self._flutter_inappwebview_user," +
" password: self._flutter_inappwebview_password," +
" withCredentials: self.withCredentials," +
" headers: self._flutter_inappwebview_request_headers," +
" responseType: self.responseType" +
" };" +
" window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('shouldInterceptAjaxRequest', ajaxRequest).then(function(result) {" +
" if (result != null) {" +
" switch (result) {" +
" case 0:" +
" self.abort();" +
" return;" +
" };" +
" if (result.data != null && !" + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".isString(result.data) && result.data.length > 0) {" +
" var bodyString = " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".arrayBufferToString(result.data);" +
" if (" + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".isBodyFormData(bodyString)) {" +
" var formDataContentType = " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".getFormDataContentType(bodyString);" +
" if (result.headers != null) {" +
" result.headers['Content-Type'] = result.headers['Content-Type'] == null ? formDataContentType : result.headers['Content-Type'];" +
" } else {" +
" result.headers = { 'Content-Type': formDataContentType };" +
" }" +
" }" +
" }" +
" if (" + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".isString(result.data) || result.data == null) {" +
" data = result.data;" +
" } else if (result.data.length > 0) {" +
" data = new Uint8Array(result.data);" +
" }" +
" self.withCredentials = result.withCredentials;" +
" if (result.responseType != null && self._flutter_inappwebview_isAsync) {" +
" self.responseType = result.responseType;" +
" };" +
" for (var header in result.headers) {" +
" var value = result.headers[header];" +
" var flutter_inappwebview_value = self._flutter_inappwebview_request_headers[header];" +
" if (flutter_inappwebview_value == null) {" +
" self._flutter_inappwebview_request_headers[header] = value;" +
" } else {" +
" self._flutter_inappwebview_request_headers[header] += ', ' + value;" +
" }" +
" setRequestHeader.call(self, header, value);" +
" };" +
" if ((self._flutter_inappwebview_method != result.method && result.method != null) ||" +
" (self._flutter_inappwebview_url != result.url && result.url != null) ||" +
" (self._flutter_inappwebview_isAsync != result.isAsync && result.isAsync != null) ||" +
" (self._flutter_inappwebview_user != result.user && result.user != null) ||" +
" (self._flutter_inappwebview_password != result.password && result.password != null)) {" +
" self.abort();" +
" self.open(result.method, result.url, result.isAsync, result.user, result.password);" +
" }" +
" }" +
" send.call(self, data);" +
" });" +
" });" +
" } else {" +
" send.call(this, data);" +
" }" +
" };" +
"})(window.XMLHttpRequest);";
}
@@ -0,0 +1,152 @@
package com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js;
import com.pichillilorenzo.flutter_inappwebview_android.types.PluginScript;
import com.pichillilorenzo.flutter_inappwebview_android.types.UserScriptInjectionTime;
public class InterceptFetchRequestJS {
public static final String INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT";
public static final String FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE = JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._useShouldInterceptFetchRequest";
public static final PluginScript INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT = new PluginScript(
InterceptFetchRequestJS.INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME,
InterceptFetchRequestJS.INTERCEPT_FETCH_REQUEST_JS_SOURCE,
UserScriptInjectionTime.AT_DOCUMENT_START,
null,
true,
null
);
public static final String INTERCEPT_FETCH_REQUEST_JS_SOURCE = "(function(fetch) {" +
" var w = (window.top == null || window.top === window) ? window : window.top;" +
" w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE + " = true;" +
" if (fetch == null) {" +
" return;" +
" }" +
" window.fetch = async function(resource, init) {" +
" var w = (window.top == null || window.top === window) ? window : window.top;" +
" if (w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE + " == null || w." + FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE + " == true) {" +
" var fetchRequest = {" +
" url: null," +
" method: null," +
" headers: null," +
" body: null," +
" mode: null," +
" credentials: null," +
" cache: null," +
" redirect: null," +
" referrer: null," +
" referrerPolicy: null," +
" integrity: null," +
" keepalive: null" +
" };" +
" if (resource instanceof Request) {" +
" fetchRequest.url = resource.url;" +
" fetchRequest.method = resource.method;" +
" fetchRequest.headers = resource.headers;" +
" fetchRequest.body = resource.body;" +
" fetchRequest.mode = resource.mode;" +
" fetchRequest.credentials = resource.credentials;" +
" fetchRequest.cache = resource.cache;" +
" fetchRequest.redirect = resource.redirect;" +
" fetchRequest.referrer = resource.referrer;" +
" fetchRequest.referrerPolicy = resource.referrerPolicy;" +
" fetchRequest.integrity = resource.integrity;" +
" fetchRequest.keepalive = resource.keepalive;" +
" } else {" +
" fetchRequest.url = resource != null ? resource.toString() : null;" +
" if (init != null) {" +
" fetchRequest.method = init.method;" +
" fetchRequest.headers = init.headers;" +
" fetchRequest.body = init.body;" +
" fetchRequest.mode = init.mode;" +
" fetchRequest.credentials = init.credentials;" +
" fetchRequest.cache = init.cache;" +
" fetchRequest.redirect = init.redirect;" +
" fetchRequest.referrer = init.referrer;" +
" fetchRequest.referrerPolicy = init.referrerPolicy;" +
" fetchRequest.integrity = init.integrity;" +
" fetchRequest.keepalive = init.keepalive;" +
" }" +
" }" +
" if (fetchRequest.headers instanceof Headers) {" +
" fetchRequest.headers = " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".convertHeadersToJson(fetchRequest.headers);" +
" }" +
" fetchRequest.credentials = " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".convertCredentialsToJson(fetchRequest.credentials);" +
" return " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".convertBodyRequest(fetchRequest.body).then(function(body) {" +
" fetchRequest.body = body;" +
" return window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('shouldInterceptFetchRequest', fetchRequest).then(function(result) {" +
" if (result != null) {" +
" switch (result.action) {" +
" case 0:" +
" var controller = new AbortController();" +
" if (init != null) {" +
" init.signal = controller.signal;" +
" } else {" +
" init = {" +
" signal: controller.signal" +
" };" +
" }" +
" controller.abort();" +
" break;" +
" }" +
" if (result.body != null && !" + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".isString(result.body) && result.body.length > 0) {" +
" var bodyString = " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".arrayBufferToString(result.body);" +
" if (" + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".isBodyFormData(bodyString)) {" +
" var formDataContentType = " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".getFormDataContentType(bodyString);" +
" if (result.headers != null) {" +
" result.headers['Content-Type'] = result.headers['Content-Type'] == null ? formDataContentType : result.headers['Content-Type'];" +
" } else {" +
" result.headers = { 'Content-Type': formDataContentType };" +
" }" +
" }" +
" }" +
" resource = result.url;" +
" if (init == null) {" +
" init = {};" +
" }" +
" if (result.method != null && result.method.length > 0) {" +
" init.method = result.method;" +
" }" +
" if (result.headers != null && Object.keys(result.headers).length > 0) {" +
" init.headers = " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".convertJsonToHeaders(result.headers);" +
" }" +
" if (" + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".isString(result.body) || result.body == null) {" +
" init.body = result.body;" +
" } else if (result.body.length > 0) {" +
" init.body = new Uint8Array(result.body);" +
" }" +
" if (result.mode != null && result.mode.length > 0) {" +
" init.mode = result.mode;" +
" }" +
" if (result.credentials != null) {" +
" init.credentials = " + JavaScriptBridgeJS.JAVASCRIPT_UTIL_VAR_NAME + ".convertJsonToCredential(result.credentials);" +
" }" +
" if (result.cache != null && result.cache.length > 0) {" +
" init.cache = result.cache;" +
" }" +
" if (result.redirect != null && result.redirect.length > 0) {" +
" init.redirect = result.redirect;" +
" }" +
" if (result.referrer != null && result.referrer.length > 0) {" +
" init.referrer = result.referrer;" +
" }" +
" if (result.referrerPolicy != null && result.referrerPolicy.length > 0) {" +
" init.referrerPolicy = result.referrerPolicy;" +
" }" +
" if (result.integrity != null && result.integrity.length > 0) {" +
" init.integrity = result.integrity;" +
" }" +
" if (result.keepalive != null) {" +
" init.keepalive = result.keepalive;" +
" }" +
" return fetch(resource, init);" +
" }" +
" return fetch(resource, init);" +
" });" +
" });" +
" } else {" +
" return fetch(resource, init);" +
" }" +
" };" +
"})(window.fetch);";
}
@@ -0,0 +1,250 @@
package com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js;
import com.pichillilorenzo.flutter_inappwebview_android.types.PluginScript;
import com.pichillilorenzo.flutter_inappwebview_android.types.UserScriptInjectionTime;
public class JavaScriptBridgeJS {
public static final String JAVASCRIPT_BRIDGE_NAME = "flutter_inappwebview";
public static final String JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT";
public static final PluginScript JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT = new PluginScript(
JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT_GROUP_NAME,
JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_JS_SOURCE,
UserScriptInjectionTime.AT_DOCUMENT_START,
null,
true,
null
);
public static final String JAVASCRIPT_UTIL_VAR_NAME = "window." + JAVASCRIPT_BRIDGE_NAME + "._Util";
public static final String WEB_MESSAGE_CHANNELS_VARIABLE_NAME = "window." + JAVASCRIPT_BRIDGE_NAME + "._webMessageChannels";
public static final String UTIL_JS_SOURCE = JAVASCRIPT_UTIL_VAR_NAME + " = {" +
" support: {" +
" searchParams: 'URLSearchParams' in window," +
" iterable: 'Symbol' in window && 'iterator' in Symbol," +
" blob:" +
" 'FileReader' in window &&" +
" 'Blob' in window &&" +
" (function() {" +
" try {" +
" new Blob();" +
" return true;" +
" } catch (e) {" +
" return false;" +
" }" +
" })()," +
" formData: 'FormData' in window," +
" arrayBuffer: 'ArrayBuffer' in window" +
" }," +
" isDataView: function(obj) {" +
" return obj && DataView.prototype.isPrototypeOf(obj);" +
" }," +
" fileReaderReady: function(reader) {" +
" return new Promise(function(resolve, reject) {" +
" reader.onload = function() {" +
" resolve(reader.result);" +
" };" +
" reader.onerror = function() {" +
" reject(reader.error);" +
" };" +
" });" +
" }," +
" readBlobAsArrayBuffer: function(blob) {" +
" var reader = new FileReader();" +
" var promise = " + JAVASCRIPT_UTIL_VAR_NAME + ".fileReaderReady(reader);" +
" reader.readAsArrayBuffer(blob);" +
" return promise;" +
" }," +
" convertBodyToArrayBuffer: function(body) {" +
" var viewClasses = [" +
" '[object Int8Array]'," +
" '[object Uint8Array]'," +
" '[object Uint8ClampedArray]'," +
" '[object Int16Array]'," +
" '[object Uint16Array]'," +
" '[object Int32Array]'," +
" '[object Uint32Array]'," +
" '[object Float32Array]'," +
" '[object Float64Array]'" +
" ];" +
" var isArrayBufferView = null;" +
" if (" + JAVASCRIPT_UTIL_VAR_NAME + ".support.arrayBuffer) {" +
" isArrayBufferView =" +
" ArrayBuffer.isView ||" +
" function(obj) {" +
" return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1;" +
" };" +
" }" +
" var bodyUsed = false;" +
" this._bodyInit = body;" +
" if (!body) {" +
" this._bodyText = '';" +
" } else if (typeof body === 'string') {" +
" this._bodyText = body;" +
" } else if (" + JAVASCRIPT_UTIL_VAR_NAME + ".support.blob && Blob.prototype.isPrototypeOf(body)) {" +
" this._bodyBlob = body;" +
" } else if (" + JAVASCRIPT_UTIL_VAR_NAME + ".support.formData && FormData.prototype.isPrototypeOf(body)) {" +
" this._bodyFormData = body;" +
" } else if (" + JAVASCRIPT_UTIL_VAR_NAME + ".support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) {" +
" this._bodyText = body.toString();" +
" } else if (" + JAVASCRIPT_UTIL_VAR_NAME + ".support.arrayBuffer && " + JAVASCRIPT_UTIL_VAR_NAME + ".support.blob && " + JAVASCRIPT_UTIL_VAR_NAME + ".isDataView(body)) {" +
" this._bodyArrayBuffer = bufferClone(body.buffer);" +
" this._bodyInit = new Blob([this._bodyArrayBuffer]);" +
" } else if (" + JAVASCRIPT_UTIL_VAR_NAME + ".support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) {" +
" this._bodyArrayBuffer = bufferClone(body);" +
" } else {" +
" this._bodyText = body = Object.prototype.toString.call(body);" +
" }" +
" this.blob = function () {" +
" if (bodyUsed) {" +
" return Promise.reject(new TypeError('Already read'));" +
" }" +
" bodyUsed = true;" +
" if (this._bodyBlob) {" +
" return Promise.resolve(this._bodyBlob);" +
" } else if (this._bodyArrayBuffer) {" +
" return Promise.resolve(new Blob([this._bodyArrayBuffer]));" +
" } else if (this._bodyFormData) {" +
" throw new Error('could not read FormData body as blob');" +
" } else {" +
" return Promise.resolve(new Blob([this._bodyText]));" +
" }" +
" };" +
" if (this._bodyArrayBuffer) {" +
" if (bodyUsed) {" +
" return Promise.reject(new TypeError('Already read'));" +
" }" +
" bodyUsed = true;" +
" if (ArrayBuffer.isView(this._bodyArrayBuffer)) {" +
" return Promise.resolve(" +
" this._bodyArrayBuffer.buffer.slice(" +
" this._bodyArrayBuffer.byteOffset," +
" this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength" +
" )" +
" );" +
" } else {" +
" return Promise.resolve(this._bodyArrayBuffer);" +
" }" +
" }" +
" return this.blob().then(" + JAVASCRIPT_UTIL_VAR_NAME + ".readBlobAsArrayBuffer);" +
" }," +
" isString: function(variable) {" +
" return typeof variable === 'string' || variable instanceof String;" +
" }," +
" convertBodyRequest: function(body) {" +
" if (body == null) {" +
" return new Promise(function(resolve, reject) { resolve(null); });" +
" }" +
" if (" + JAVASCRIPT_UTIL_VAR_NAME + ".isString(body) || (" + JAVASCRIPT_UTIL_VAR_NAME + ".support.searchParams && body instanceof URLSearchParams)) {" +
" return new Promise(function(resolve, reject) { resolve(body.toString()); });" +
" }" +
" if (window.Response != null) {" +
" return new Response(body).arrayBuffer().then(function(arrayBuffer) {" +
" return Array.from(new Uint8Array(arrayBuffer));" +
" });" +
" }" +
" return " + JAVASCRIPT_UTIL_VAR_NAME + ".convertBodyToArrayBuffer(body).then(function(arrayBuffer) {" +
" return Array.from(new Uint8Array(arrayBuffer));" +
" });" +
" }," +
" arrayBufferToString: function(arrayBuffer) {" +
" var uint8Array = new Uint8Array(arrayBuffer);" +
" return uint8Array.reduce(function(acc, i) { return acc += String.fromCharCode.apply(null, [i]); }, '');" +
" }," +
" isBodyFormData: function(bodyString) {" +
" return bodyString.indexOf('------WebKitFormBoundary') >= 0;" +
" }," +
" getFormDataContentType: function(bodyString) {" +
" var boundary = bodyString.substr(2, 40);" +
" return 'multipart/form-data; boundary=' + boundary;" +
" }," +
" convertHeadersToJson: function(headers) {" +
" var headersObj = {};" +
" for (var header of headers.keys()) {" +
" var value = headers.get(header);" +
" headersObj[header] = value;" +
" }" +
" return headersObj;" +
" }," +
" convertJsonToHeaders: function(headersJson) {" +
" return new Headers(headersJson);" +
" }," +
" convertCredentialsToJson: function(credentials) {" +
" var credentialsObj = {};" +
" if (window.FederatedCredential != null && credentials instanceof FederatedCredential) {" +
" credentialsObj.type = credentials.type;" +
" credentialsObj.id = credentials.id;" +
" credentialsObj.name = credentials.name;" +
" credentialsObj.protocol = credentials.protocol;" +
" credentialsObj.provider = credentials.provider;" +
" credentialsObj.iconURL = credentials.iconURL;" +
" } else if (window.PasswordCredential != null && credentials instanceof PasswordCredential) {" +
" credentialsObj.type = credentials.type;" +
" credentialsObj.id = credentials.id;" +
" credentialsObj.name = credentials.name;" +
" credentialsObj.password = credentials.password;" +
" credentialsObj.iconURL = credentials.iconURL;" +
" } else {" +
" credentialsObj.type = 'default';" +
" credentialsObj.value = credentials;" +
" }" +
" return credentialsObj;" +
" }," +
" convertJsonToCredential: function(credentialsJson) {" +
" var credentials;" +
" if (window.FederatedCredential != null && credentialsJson.type === 'federated') {" +
" credentials = new FederatedCredential({" +
" id: credentialsJson.id," +
" name: credentialsJson.name," +
" protocol: credentialsJson.protocol," +
" provider: credentialsJson.provider," +
" iconURL: credentialsJson.iconURL" +
" });" +
" } else if (window.PasswordCredential != null && credentialsJson.type === 'password') {" +
" credentials = new PasswordCredential({" +
" id: credentialsJson.id," +
" name: credentialsJson.name," +
" password: credentialsJson.password," +
" iconURL: credentialsJson.iconURL" +
" });" +
" } else {" +
" credentials = credentialsJson.value == null ? undefined : credentialsJson.value;" +
" }" +
" return credentials;" +
" }" +
"};";
public static final String JAVASCRIPT_BRIDGE_JS_SOURCE = "if (window." + JAVASCRIPT_BRIDGE_NAME + " != null) {" +
" window." + JAVASCRIPT_BRIDGE_NAME + ".callHandler = function() {" +
" var _callHandlerID = setTimeout(function(){});" +
" window." + JAVASCRIPT_BRIDGE_NAME + "._callHandler(arguments[0], _callHandlerID, JSON.stringify(Array.prototype.slice.call(arguments, 1)));" +
" return new Promise(function(resolve, reject) {" +
" window." + JAVASCRIPT_BRIDGE_NAME + "[_callHandlerID] = {resolve: resolve, reject: reject};" +
" });" +
" };" +
"}"+
"if (window.top != null && window.top !== window && window." + JAVASCRIPT_BRIDGE_NAME + " == null) {" +
" window." + JAVASCRIPT_BRIDGE_NAME + " = {};" +
" window." + JAVASCRIPT_BRIDGE_NAME + ".callHandler = function() {" +
" var _callHandlerID = setTimeout(function(){});" +
" try {" +
" window.top." + JAVASCRIPT_BRIDGE_NAME + "._callHandler(arguments[0], _callHandlerID, JSON.stringify(Array.prototype.slice.call(arguments, 1)));" +
" return new Promise(function(resolve, reject) {" +
" window.top." + JAVASCRIPT_BRIDGE_NAME + "[_callHandlerID] = {resolve: resolve, reject: reject};" +
" });" +
" } catch (error) {" +
" return new Promise(function(resolve, reject) { reject(error); });" +
" }" +
" };" +
"}" +
"if (window." + JAVASCRIPT_BRIDGE_NAME + " != null) {" +
" " + UTIL_JS_SOURCE +
"}";
public static final String PLATFORM_READY_JS_SOURCE = "(function() {" +
" if ((window.top == null || window.top === window) && window." + JAVASCRIPT_BRIDGE_NAME + " != null && window." + JAVASCRIPT_BRIDGE_NAME + "._platformReady == null) {" +
" window.dispatchEvent(new Event('flutterInAppWebViewPlatformReady'));" +
" window." + JAVASCRIPT_BRIDGE_NAME + "._platformReady = true;" +
" }" +
"})();";
}
@@ -0,0 +1,35 @@
package com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js;
import com.pichillilorenzo.flutter_inappwebview_android.types.PluginScript;
import com.pichillilorenzo.flutter_inappwebview_android.types.UserScriptInjectionTime;
public class OnLoadResourceJS {
public static final String ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT";
public static final String FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE = JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._useOnLoadResource";
public static final PluginScript ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT = new PluginScript(
OnLoadResourceJS.ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT_GROUP_NAME,
OnLoadResourceJS.ON_LOAD_RESOURCE_JS_SOURCE,
UserScriptInjectionTime.AT_DOCUMENT_START,
null,
false,
null
);
public static final String ON_LOAD_RESOURCE_JS_SOURCE = "window." + FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE + " = true;" +
"(function() {" +
" var observer = new PerformanceObserver(function(list) {" +
" list.getEntries().forEach(function(entry) {" +
" if (" + FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE + " == null || " + FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE + " == true) {" +
" var resource = {" +
" 'url': entry.name," +
" 'initiatorType': entry.initiatorType," +
" 'startTime': entry.startTime," +
" 'duration': entry.duration" +
" };" +
" window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('onLoadResource', resource);" +
" }" +
" });" +
" });" +
" observer.observe({entryTypes: ['resource']});" +
"})();";
}
@@ -0,0 +1,22 @@
package com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js;
import com.pichillilorenzo.flutter_inappwebview_android.types.PluginScript;
import com.pichillilorenzo.flutter_inappwebview_android.types.UserScriptInjectionTime;
public class OnWindowBlurEventJS {
public static final String ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT";
public static final PluginScript ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT = new PluginScript(
OnWindowBlurEventJS.ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME,
OnWindowBlurEventJS.ON_WINDOW_BLUR_EVENT_JS_SOURCE,
UserScriptInjectionTime.AT_DOCUMENT_START,
null,
false,
null
);
public static final String ON_WINDOW_BLUR_EVENT_JS_SOURCE = "(function(){" +
" window.addEventListener('blur', function(e) {" +
" window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('onWindowBlur');" +
" });" +
"})();";
}
@@ -0,0 +1,22 @@
package com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js;
import com.pichillilorenzo.flutter_inappwebview_android.types.PluginScript;
import com.pichillilorenzo.flutter_inappwebview_android.types.UserScriptInjectionTime;
public class OnWindowFocusEventJS {
public static final String ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT";
public static final PluginScript ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT = new PluginScript(
OnWindowFocusEventJS.ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME,
OnWindowFocusEventJS.ON_WINDOW_FOCUS_EVENT_JS_SOURCE,
UserScriptInjectionTime.AT_DOCUMENT_START,
null,
false,
null
);
public static final String ON_WINDOW_FOCUS_EVENT_JS_SOURCE = "(function(){" +
" window.addEventListener('focus', function(e) {" +
" window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('onWindowFocus');" +
" });" +
"})();";
}
@@ -0,0 +1,89 @@
package com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js;
import com.pichillilorenzo.flutter_inappwebview_android.types.PluginScript;
import com.pichillilorenzo.flutter_inappwebview_android.types.UserScriptInjectionTime;
public class PluginScriptsUtil {
public static final String VAR_PLACEHOLDER_VALUE = "$IN_APP_WEBVIEW_PLACEHOLDER_VALUE";
public static final String VAR_CONTENT_WORLD_NAME_ARRAY = "$IN_APP_WEBVIEW_CONTENT_WORLD_NAME_ARRAY";
public static final String VAR_CONTENT_WORLD_NAME = "$IN_APP_WEBVIEW_CONTENT_WORLD_NAME";
public static final String VAR_JSON_SOURCE_ENCODED = "$IN_APP_WEBVIEW_JSON_SOURCE_ENCODED";
public static final String VAR_FUNCTION_ARGUMENT_NAMES = "$IN_APP_WEBVIEW_FUNCTION_ARGUMENT_NAMES";
public static final String VAR_FUNCTION_ARGUMENT_VALUES = "$IN_APP_WEBVIEW_FUNCTION_ARGUMENT_VALUES";
public static final String VAR_FUNCTION_ARGUMENTS_OBJ = "$IN_APP_WEBVIEW_FUNCTION_ARGUMENTS_OBJ";
public static final String VAR_FUNCTION_BODY = "$IN_APP_WEBVIEW_FUNCTION_BODY";
public static final String VAR_RESULT_UUID = "$IN_APP_WEBVIEW_RESULT_UUID";
public static final String VAR_RANDOM_NAME = "$IN_APP_WEBVIEW_VARIABLE_RANDOM_NAME";
public static final String CALL_ASYNC_JAVA_SCRIPT_WRAPPER_JS_SOURCE = "(function(obj) {" +
" (async function(" + VAR_FUNCTION_ARGUMENT_NAMES + ") {" +
" \n" + VAR_FUNCTION_BODY + "\n" +
" })(" + VAR_FUNCTION_ARGUMENT_VALUES + ").then(function(value) {" +
" window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('callAsyncJavaScript', {'value': value, 'error': null, 'resultUuid': '" + VAR_RESULT_UUID + "'});" +
" }).catch(function(error) {" +
" window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('callAsyncJavaScript', {'value': null, 'error': error + '', 'resultUuid': '" + VAR_RESULT_UUID + "'});" +
" });" +
" return null;" +
"})(" + VAR_FUNCTION_ARGUMENTS_OBJ + ");";
public static final String EVALUATE_JAVASCRIPT_WITH_CONTENT_WORLD_WRAPPER_JS_SOURCE = "var $IN_APP_WEBVIEW_VARIABLE_RANDOM_NAME = null;" +
"try {" +
" $IN_APP_WEBVIEW_VARIABLE_RANDOM_NAME = eval(" + VAR_PLACEHOLDER_VALUE + ");" +
"} catch(e) {" +
" console.error(e);" +
"}" +
"window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('evaluateJavaScriptWithContentWorld', {'value': $IN_APP_WEBVIEW_VARIABLE_RANDOM_NAME, 'resultUuid': '" + VAR_RESULT_UUID + "'});";
public static final String IS_ACTIVE_ELEMENT_INPUT_EDITABLE_JS_SOURCE =
"var activeEl = document.activeElement;" +
"var nodeName = (activeEl != null) ? activeEl.nodeName.toLowerCase() : '';" +
"var isActiveElementInputEditable = activeEl != null && " +
"(activeEl.nodeType == 1 && (nodeName == 'textarea' || (nodeName == 'input' && /^(?:text|email|number|search|tel|url|password)$/i.test(activeEl.type != null ? activeEl.type : 'text')))) && " +
"!activeEl.disabled && !activeEl.readOnly;" +
"var isActiveElementEditable = isActiveElementInputEditable || (activeEl != null && activeEl.isContentEditable) || document.designMode === 'on';";
// android Workaround to hide context menu when selected text is empty
// and the document active element is not an input element.
public static final String CHECK_CONTEXT_MENU_SHOULD_BE_HIDDEN_JS_SOURCE = "(function(){" +
" var txt;" +
" if (window.getSelection) {" +
" txt = window.getSelection().toString();" +
" } else if (window.document.getSelection) {" +
" txt = window.document.getSelection().toString();" +
" } else if (window.document.selection) {" +
" txt = window.document.selection.createRange().text;" +
" }" +
IS_ACTIVE_ELEMENT_INPUT_EDITABLE_JS_SOURCE +
" return txt === '' && !isActiveElementEditable;" +
"})();";
public static final String GET_SELECTED_TEXT_JS_SOURCE = "(function(){" +
" var txt;" +
" if (window.getSelection) {" +
" txt = window.getSelection().toString();" +
" } else if (window.document.getSelection) {" +
" txt = window.document.getSelection().toString();" +
" } else if (window.document.selection) {" +
" txt = window.document.selection.createRange().text;" +
" }" +
" return txt;" +
"})();";
public static final String CHECK_GLOBAL_KEY_DOWN_EVENT_TO_HIDE_CONTEXT_MENU_JS_PLUGIN_SCRIPT_GROUP_NAME = "CHECK_GLOBAL_KEY_DOWN_EVENT_TO_HIDE_CONTEXT_MENU_JS_PLUGIN_SCRIPT";
public static final PluginScript CHECK_GLOBAL_KEY_DOWN_EVENT_TO_HIDE_CONTEXT_MENU_JS_PLUGIN_SCRIPT = new PluginScript(
PluginScriptsUtil.CHECK_GLOBAL_KEY_DOWN_EVENT_TO_HIDE_CONTEXT_MENU_JS_PLUGIN_SCRIPT_GROUP_NAME,
PluginScriptsUtil.CHECK_GLOBAL_KEY_DOWN_EVENT_TO_HIDE_CONTEXT_MENU_JS_SOURCE,
UserScriptInjectionTime.AT_DOCUMENT_START,
null,
false,
null
);
// android Workaround to hide context menu when user emit a keydown event
public static final String CHECK_GLOBAL_KEY_DOWN_EVENT_TO_HIDE_CONTEXT_MENU_JS_SOURCE = "(function(){" +
" document.addEventListener('keydown', function(e) {" +
" window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._hideContextMenu();" +
" });" +
"})();";
}
@@ -0,0 +1,24 @@
package com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js;
import com.pichillilorenzo.flutter_inappwebview_android.types.PluginScript;
import com.pichillilorenzo.flutter_inappwebview_android.types.UserScriptInjectionTime;
public class PrintJS {
public static final String PRINT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_PRINT_JS_PLUGIN_SCRIPT";
public static final PluginScript PRINT_JS_PLUGIN_SCRIPT = new PluginScript(
PrintJS.PRINT_JS_PLUGIN_SCRIPT_GROUP_NAME,
PrintJS.PRINT_JS_SOURCE,
UserScriptInjectionTime.AT_DOCUMENT_START,
null,
false,
null
);
public static final String PRINT_JS_SOURCE = "window.print = function() {" +
" if (window.top == null || window.top === window) {" +
" window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('onPrintRequest', window.location.href);" +
" } else {" +
" window.top.print();" +
" }" +
"};";
}
@@ -0,0 +1,21 @@
package com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js;
import com.pichillilorenzo.flutter_inappwebview_android.types.PluginScript;
import com.pichillilorenzo.flutter_inappwebview_android.types.UserScriptInjectionTime;
public class PromisePolyfillJS {
public static final String PROMISE_POLYFILL_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_PROMISE_POLYFILL_JS_PLUGIN_SCRIPT";
public static final PluginScript PROMISE_POLYFILL_JS_PLUGIN_SCRIPT = new PluginScript(
PromisePolyfillJS.PROMISE_POLYFILL_JS_PLUGIN_SCRIPT_GROUP_NAME,
PromisePolyfillJS.PROMISE_POLYFILL_JS_SOURCE,
UserScriptInjectionTime.AT_DOCUMENT_START,
null,
true,
null
);
// https://github.com/tildeio/rsvp.js
public static final String PROMISE_POLYFILL_JS_SOURCE = "if (window.Promise == null) {" +
" !function(t,e){\"object\"==typeof exports&&\"undefined\"!=typeof module?e(exports):\"function\"==typeof define&&define.amd?define([\"exports\"],e):e(t.RSVP={})}(this,function(t){\"use strict\";function e(t){var e=t._promiseCallbacks;return e||(e=t._promiseCallbacks={}),e}var r={mixin:function(t){return t.on=this.on,t.off=this.off,t.trigger=this.trigger,t._promiseCallbacks=void 0,t},on:function(t,r){if(\"function\"!=typeof r)throw new TypeError(\"Callback must be a function\");var n=e(this),o=n[t];o||(o=n[t]=[]),-1===o.indexOf(r)&&o.push(r)},off:function(t,r){var n=e(this);if(r){var o=n[t],i=o.indexOf(r);-1!==i&&o.splice(i,1)}else n[t]=[]},trigger:function(t,r,n){var o=e(this)[t];if(o)for(var i=0;i<o.length;i++)(0,o[i])(r,n)}},n={instrument:!1};function o(t,e){if(2!==arguments.length)return n[t];n[t]=e}r.mixin(n);var i=[];function s(t,e,r){1===i.push({name:t,payload:{key:e._guidKey,id:e._id,eventName:t,detail:e._result,childId:r&&r._id,label:e._label,timeStamp:Date.now(),error:n[\"instrument-with-stack\"]?new Error(e._label):null}})&&setTimeout(function(){for(var t=0;t<i.length;t++){var e=i[t],r=e.payload;r.guid=r.key+r.id,r.childGuid=r.key+r.childId,r.error&&(r.stack=r.error.stack),n.trigger(e.name,e.payload)}i.length=0},50)}function u(t,e){if(t&&\"object\"==typeof t&&t.constructor===this)return t;var r=new this(c,e);return m(r,t),r}function c(){}var a=void 0,f=1,l=2,h={error:null};function p(t){try{return t.then}catch(t){return h.error=t,h}}var y=void 0;function _(){try{var t=y;return y=null,t.apply(this,arguments)}catch(t){return h.error=t,h}}function v(t){return y=t,_}function d(t,e,r){if(e.constructor===t.constructor&&r===A&&t.constructor.resolve===u)!function(t,e){e._state===f?b(t,e._result):e._state===l?(e._onError=null,g(t,e._result)):j(e,void 0,function(r){e===r?b(t,r):m(t,r)},function(e){return g(t,e)})}(t,e);else if(r===h){var o=h.error;h.error=null,g(t,o)}else\"function\"==typeof r?function(t,e,r){n.async(function(t){var n=!1,o=v(r).call(e,function(r){n||(n=!0,e===r?b(t,r):m(t,r))},function(e){n||(n=!0,g(t,e))},\"Settle: \"+(t._label||\" unknown promise\"));if(!n&&o===h){n=!0;var i=h.error;h.error=null,g(t,i)}},t)}(t,e,r):b(t,e)}function m(t,e){var r,n;t===e?b(t,e):(n=typeof(r=e),null===r||\"object\"!==n&&\"function\"!==n?b(t,e):d(t,e,p(e)))}function w(t){t._onError&&t._onError(t._result),O(t)}function b(t,e){t._state===a&&(t._result=e,t._state=f,0===t._subscribers.length?n.instrument&&s(\"fulfilled\",t):n.async(O,t))}function g(t,e){t._state===a&&(t._state=l,t._result=e,n.async(w,t))}function j(t,e,r,o){var i=t._subscribers,s=i.length;t._onError=null,i[s]=e,i[s+f]=r,i[s+l]=o,0===s&&t._state&&n.async(O,t)}function O(t){var e=t._subscribers,r=t._state;if(n.instrument&&s(r===f?\"fulfilled\":\"rejected\",t),0!==e.length){for(var o=void 0,i=void 0,u=t._result,c=0;c<e.length;c+=3)o=e[c],i=e[c+r],o?E(r,o,i,u):i(u);t._subscribers.length=0}}function E(t,e,r,n){var o=\"function\"==typeof r,i=void 0;if(i=o?v(r)(n):n,e._state!==a);else if(i===e)g(e,new TypeError(\"A promises callback cannot return that same promise.\"));else if(i===h){var s=h.error;h.error=null,g(e,s)}else o?m(e,i):t===f?b(e,i):t===l&&g(e,i)}function A(t,e,r){var o=this._state;if(o===f&&!t||o===l&&!e)return n.instrument&&s(\"chained\",this,this),this;this._onError=null;var i=new this.constructor(c,r),u=this._result;if(n.instrument&&s(\"chained\",this,i),o===a)j(this,i,t,e);else{var h=o===f?t:e;n.async(function(){return E(o,i,h,u)})}return i}var T=function(){function t(t,e,r,n){this._instanceConstructor=t,this.promise=new t(c,n),this._abortOnReject=r,this._isUsingOwnPromise=t===k,this._isUsingOwnResolve=t.resolve===u,this._init.apply(this,arguments)}return t.prototype._init=function(t,e){var r=e.length||0;this.length=r,this._remaining=r,this._result=new Array(r),this._enumerate(e)},t.prototype._enumerate=function(t){for(var e=this.length,r=this.promise,n=0;r._state===a&&n<e;n++)this._eachEntry(t[n],n,!0);this._checkFullfillment()},t.prototype._checkFullfillment=function(){if(0===this._remaining){var t=this._result;b(this.promise,t),this._result=null}},t.prototype._settleMaybeThenable=function(t,e,r){var n=this._instanceConstructor;if(this._isUsingOwnResolve){var o=p(t);if(o===A&&t._state!==a)t._onError=null,this._settledAt(t._state,e,t._result,r);else if(\"function\"!=typeof o)this._settledAt(f,e,t,r);else if(this._isUsingOwnPromise){var i=new n(c);d(i,t,o),this._willSettleAt(i,e,r)}else this._willSettleAt(new n(function(e){return e(t)}),e,r)}else this._willSettleAt(n.resolve(t),e,r)},t.prototype._eachEntry=function(t,e,r){null!==t&&\"object\"==typeof t?this._settleMaybeThenable(t,e,r):this._setResultAt(f,e,t,r)},t.prototype._settledAt=function(t,e,r,n){var o=this.promise;o._state===a&&(this._abortOnReject&&t===l?g(o,r):(this._setResultAt(t,e,r,n),this._checkFullfillment()))},t.prototype._setResultAt=function(t,e,r,n){this._remaining--,this._result[e]=r},t.prototype._willSettleAt=function(t,e,r){var n=this;j(t,void 0,function(tLine truncated
"}";
}
@@ -0,0 +1,70 @@
package com.pichillilorenzo.flutter_inappwebview_android.print_job;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
import com.pichillilorenzo.flutter_inappwebview_android.types.PrintJobInfoExt;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public class PrintJobChannelDelegate extends ChannelDelegateImpl {
@Nullable
private PrintJobController printJobController;
public PrintJobChannelDelegate(@NonNull PrintJobController printJobController, @NonNull MethodChannel channel) {
super(channel);
this.printJobController = printJobController;
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
switch (call.method) {
case "cancel":
if (printJobController != null) {
printJobController.cancel();
result.success(true);
} else {
result.success(false);
}
break;
case "restart":
if (printJobController != null) {
printJobController.restart();
result.success(true);
} else {
result.success(false);
}
break;
case "getInfo":
if (printJobController != null) {
PrintJobInfoExt info = printJobController.getInfo();
result.success(info != null ? info.toMap() : null);
} else {
result.success(null);
}
break;
case "dispose":
if (printJobController != null) {
printJobController.dispose();
result.success(true);
} else {
result.success(false);
}
break;
default:
result.notImplemented();
}
}
@Override
public void dispose() {
super.dispose();
printJobController = null;
}
}
@@ -0,0 +1,96 @@
package com.pichillilorenzo.flutter_inappwebview_android.print_job;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin;
import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable;
import com.pichillilorenzo.flutter_inappwebview_android.types.PrintJobInfoExt;
import io.flutter.plugin.common.MethodChannel;
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public class PrintJobController implements Disposable {
protected static final String LOG_TAG = "PrintJob";
public static final String METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_printjobcontroller_";
@NonNull
public String id;
@Nullable
public InAppWebViewFlutterPlugin plugin;
@Nullable
public PrintJobChannelDelegate channelDelegate;
@Nullable
public android.print.PrintJob job;
@Nullable
public PrintJobSettings settings;
public PrintJobController(@NonNull String id, @NonNull android.print.PrintJob job,
@Nullable PrintJobSettings settings, @NonNull InAppWebViewFlutterPlugin plugin) {
this.id = id;
this.plugin = plugin;
this.job = job;
this.settings = settings;
final MethodChannel channel = new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME_PREFIX + id);
this.channelDelegate = new PrintJobChannelDelegate(this, channel);
}
public void cancel() {
if (this.job != null) {
this.job.cancel();
}
}
public void restart() {
if (this.job != null) {
this.job.restart();
}
}
@Nullable
public PrintJobInfoExt getInfo() {
if (this.job != null) {
return PrintJobInfoExt.fromPrintJobInfo(this.job.getInfo());
}
return null;
}
public void disposeNoCancel() {
if (channelDelegate != null) {
channelDelegate.dispose();
channelDelegate = null;
}
if (plugin != null) {
PrintJobManager printJobManager = plugin.printJobManager;
if (printJobManager != null && printJobManager.jobs.containsKey(id)) {
printJobManager.jobs.put(id, null);
}
}
if (job != null) {
job = null;
}
plugin = null;
}
@Override
public void dispose() {
if (channelDelegate != null) {
channelDelegate.dispose();
channelDelegate = null;
}
if (plugin != null) {
PrintJobManager printJobManager = plugin.printJobManager;
if (printJobManager != null && printJobManager.jobs.containsKey(id)) {
printJobManager.jobs.put(id, null);
}
}
if (job != null) {
job.cancel();
job = null;
}
plugin = null;
}
}
@@ -0,0 +1,59 @@
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
package com.pichillilorenzo.flutter_inappwebview_android.print_job;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin;
import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public class PrintJobManager implements Disposable {
protected static final String LOG_TAG = "PrintJobManager";
@Nullable
public InAppWebViewFlutterPlugin plugin;
public final Map<String, PrintJobController> jobs = new HashMap<>();
public PrintJobManager(@NonNull final InAppWebViewFlutterPlugin plugin) {
super();
this.plugin = plugin;
}
public void dispose() {
Collection<PrintJobController> printJobControllers = jobs.values();
for (PrintJobController job : printJobControllers) {
if (job != null) {
job.dispose();
}
}
jobs.clear();
plugin = null;
}
}
@@ -0,0 +1,99 @@
package com.pichillilorenzo.flutter_inappwebview_android.print_job;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.pichillilorenzo.flutter_inappwebview_android.ISettings;
import com.pichillilorenzo.flutter_inappwebview_android.types.MediaSizeExt;
import com.pichillilorenzo.flutter_inappwebview_android.types.ResolutionExt;
import java.util.HashMap;
import java.util.Map;
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
public class PrintJobSettings implements ISettings<PrintJobController> {
public static final String LOG_TAG = "PrintJobSettings";
public Boolean handledByClient = false;
@Nullable
public String jobName;
@Nullable
public Integer orientation;
// @Nullable
// public MarginsExt margins;
@Nullable
public MediaSizeExt mediaSize;
@Nullable
public Integer colorMode;
@Nullable
public Integer duplexMode;
@Nullable
public ResolutionExt resolution;
@NonNull
@Override
public PrintJobSettings parse(@NonNull Map<String, Object> settings) {
for (Map.Entry<String, Object> pair : settings.entrySet()) {
String key = pair.getKey();
Object value = pair.getValue();
if (value == null) {
continue;
}
switch (key) {
case "handledByClient":
handledByClient = (Boolean) value;
break;
case "jobName":
jobName = (String) value;
break;
case "orientation":
orientation = (Integer) value;
break;
// case "margins":
// margins = MarginsExt.fromMap((Map<String, Object>) value);
// break;
case "mediaSize":
mediaSize = MediaSizeExt.fromMap((Map<String, Object>) value);
break;
case "colorMode":
colorMode = (Integer) value;
break;
case "duplexMode":
duplexMode = (Integer) value;
break;
case "resolution":
resolution = ResolutionExt.fromMap((Map<String, Object>) value);
break;
}
}
return this;
}
@NonNull
@Override
public Map<String, Object> toMap() {
Map<String, Object> settings = new HashMap<>();
settings.put("handledByClient", handledByClient);
settings.put("jobName", jobName);
settings.put("orientation", orientation);
// settings.put("margins", margins != null ? margins.toMap() : null);
settings.put("mediaSize", mediaSize != null ? mediaSize.toMap() : null);
settings.put("colorMode", colorMode);
settings.put("duplexMode", duplexMode);
settings.put("resolution", resolution != null ? resolution.toMap() : null);
return settings;
}
@NonNull
@Override
public Map<String, Object> getRealSettings(@NonNull PrintJobController printJobController) {
Map<String, Object> realSettings = toMap();
return realSettings;
}
}
@@ -0,0 +1,56 @@
package com.pichillilorenzo.flutter_inappwebview_android.process_global_config;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.webkit.ProcessGlobalConfig;
import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin;
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
import java.util.Map;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
public class ProcessGlobalConfigManager extends ChannelDelegateImpl {
protected static final String LOG_TAG = "ProcessGlobalConfigM";
public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_processglobalconfig";
@Nullable
public InAppWebViewFlutterPlugin plugin;
public ProcessGlobalConfigManager(@NonNull final InAppWebViewFlutterPlugin plugin) {
super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME));
this.plugin = plugin;
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
switch (call.method) {
case "apply":
if (plugin != null && plugin.activity != null) {
ProcessGlobalConfigSettings settings = (new ProcessGlobalConfigSettings())
.parse((Map<String, Object>) call.argument("settings"));
try {
ProcessGlobalConfig.apply(settings.toProcessGlobalConfig(plugin.activity));
result.success(true);
} catch (Exception e) {
result.error(LOG_TAG, "", e);
}
} else {
result.success(false);
}
break;
default:
result.notImplemented();
}
}
@Override
public void dispose() {
super.dispose();
plugin = null;
}
}
@@ -0,0 +1,119 @@
package com.pichillilorenzo.flutter_inappwebview_android.process_global_config;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.webkit.ProcessGlobalConfig;
import androidx.webkit.WebViewFeature;
import com.pichillilorenzo.flutter_inappwebview_android.ISettings;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
public class ProcessGlobalConfigSettings implements ISettings<ProcessGlobalConfig> {
public static final String LOG_TAG = "ProcessGlobalConfigSettings";
@Nullable
public String dataDirectorySuffix;
@Nullable
public DirectoryBasePaths directoryBasePaths;
@NonNull
@Override
public ProcessGlobalConfigSettings parse(@NonNull Map<String, Object> settings) {
for (Map.Entry<String, Object> pair : settings.entrySet()) {
String key = pair.getKey();
Object value = pair.getValue();
if (value == null) {
continue;
}
switch (key) {
case "dataDirectorySuffix":
dataDirectorySuffix = (String) value;
break;
case "directoryBasePaths":
directoryBasePaths = (new DirectoryBasePaths()).parse((Map<String, Object>) value);
break;
}
}
return this;
}
public ProcessGlobalConfig toProcessGlobalConfig(@NonNull Context context) {
ProcessGlobalConfig config = new ProcessGlobalConfig();
if (dataDirectorySuffix != null &&
WebViewFeature.isStartupFeatureSupported(context, WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX)) {
config.setDataDirectorySuffix(context, dataDirectorySuffix);
}
if (directoryBasePaths != null &&
WebViewFeature.isStartupFeatureSupported(context, WebViewFeature.STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS)) {
config.setDirectoryBasePaths(context,
new File(directoryBasePaths.dataDirectoryBasePath),
new File(directoryBasePaths.cacheDirectoryBasePath));
}
return config;
}
@NonNull
public Map<String, Object> toMap() {
Map<String, Object> settings = new HashMap<>();
settings.put("dataDirectorySuffix", dataDirectorySuffix);
return settings;
}
@NonNull
@Override
public Map<String, Object> getRealSettings(@NonNull ProcessGlobalConfig processGlobalConfig) {
Map<String, Object> realSettings = toMap();
return realSettings;
}
static class DirectoryBasePaths implements ISettings<Object> {
public static final String LOG_TAG = "ProcessGlobalConfigSettings";
public String cacheDirectoryBasePath;
public String dataDirectoryBasePath;
@NonNull
@Override
public DirectoryBasePaths parse(@NonNull Map<String, Object> settings) {
for (Map.Entry<String, Object> pair : settings.entrySet()) {
String key = pair.getKey();
Object value = pair.getValue();
if (value == null) {
continue;
}
switch (key) {
case "cacheDirectoryBasePath":
cacheDirectoryBasePath = (String) value;
break;
case "dataDirectoryBasePath":
dataDirectoryBasePath = (String) value;
break;
}
}
return this;
}
@NonNull
public Map<String, Object> toMap() {
Map<String, Object> settings = new HashMap<>();
settings.put("cacheDirectoryBasePath", cacheDirectoryBasePath);
settings.put("dataDirectoryBasePath", dataDirectoryBasePath);
return settings;
}
@NonNull
@Override
public Map<String, Object> getRealSettings(@NonNull Object obj) {
Map<String, Object> realSettings = toMap();
return realSettings;
}
}
}
@@ -0,0 +1,129 @@
package com.pichillilorenzo.flutter_inappwebview_android.proxy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.webkit.ProxyConfig;
import androidx.webkit.ProxyController;
import androidx.webkit.WebViewFeature;
import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin;
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
import com.pichillilorenzo.flutter_inappwebview_android.types.ProxyRuleExt;
import java.util.HashMap;
import java.util.concurrent.Executor;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
public class ProxyManager extends ChannelDelegateImpl {
protected static final String LOG_TAG = "ProxyManager";
public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_proxycontroller";
@Nullable
public static ProxyController proxyController;
@Nullable
public InAppWebViewFlutterPlugin plugin;
public ProxyManager(@NonNull final InAppWebViewFlutterPlugin plugin) {
super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME));
this.plugin = plugin;
}
public static void init() {
if (proxyController == null &&
WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) {
proxyController = ProxyController.getInstance();
}
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
init();
switch (call.method) {
case "setProxyOverride":
if (proxyController != null) {
HashMap<String, Object> settingsMap = (HashMap<String, Object>) call.argument("settings");
ProxySettings settings = new ProxySettings();
if (settingsMap != null) {
settings.parse(settingsMap);
}
setProxyOverride(settings, result);
} else {
result.success(false);
}
break;
case "clearProxyOverride":
if (proxyController != null ) {
clearProxyOverride(result);
} else {
result.success(false);
}
break;
default:
result.notImplemented();
}
}
private void setProxyOverride(ProxySettings settings, final MethodChannel.Result result) {
if (proxyController != null) {
ProxyConfig.Builder proxyConfigBuilder = new ProxyConfig.Builder();
for (String bypassRule : settings.bypassRules) {
proxyConfigBuilder.addBypassRule(bypassRule);
}
for (String direct : settings.directs) {
proxyConfigBuilder.addDirect(direct);
}
for (ProxyRuleExt proxyRule : settings.proxyRules) {
if (proxyRule.getSchemeFilter() != null) {
proxyConfigBuilder.addProxyRule(proxyRule.getUrl(), proxyRule.getSchemeFilter());
} else {
proxyConfigBuilder.addProxyRule(proxyRule.getUrl());
}
}
if (settings.bypassSimpleHostnames != null && settings.bypassSimpleHostnames) {
proxyConfigBuilder.bypassSimpleHostnames();
}
if (settings.removeImplicitRules != null && settings.removeImplicitRules) {
proxyConfigBuilder.removeImplicitRules();
}
if (settings.reverseBypassEnabled != null && WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE_REVERSE_BYPASS)) {
proxyConfigBuilder.setReverseBypassEnabled(settings.reverseBypassEnabled);
}
proxyController.setProxyOverride(proxyConfigBuilder.build(), new Executor() {
@Override
public void execute(Runnable command) {
command.run();
}
}, new Runnable() {
@Override
public void run() {
result.success(true);
}
});
}
}
private void clearProxyOverride(final MethodChannel.Result result) {
if (proxyController != null) {
proxyController.clearProxyOverride(new Executor() {
@Override
public void execute(Runnable command) {
command.run();
}
}, new Runnable() {
@Override
public void run() {
result.success(true);
}
});
}
}
@Override
public void dispose() {
super.dispose();
plugin = null;
}
}
@@ -0,0 +1,98 @@
package com.pichillilorenzo.flutter_inappwebview_android.proxy;
import androidx.annotation.NonNull;
import androidx.webkit.ProxyConfig;
import com.pichillilorenzo.flutter_inappwebview_android.ISettings;
import com.pichillilorenzo.flutter_inappwebview_android.types.ProxyRuleExt;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ProxySettings implements ISettings<ProxyConfig> {
List<String> bypassRules = new ArrayList<>();
List<String> directs = new ArrayList<>();
List<ProxyRuleExt> proxyRules = new ArrayList<>();
Boolean bypassSimpleHostnames = null;
Boolean removeImplicitRules = null;
Boolean reverseBypassEnabled = false;
@NonNull
@Override
public ProxySettings parse(@NonNull Map<String, Object> settings) {
for (Map.Entry<String, Object> pair : settings.entrySet()) {
String key = pair.getKey();
Object value = pair.getValue();
if (value == null) {
continue;
}
switch (key) {
case "bypassRules":
bypassRules = (List<String>) value;
break;
case "directs":
directs = (List<String>) value;
break;
case "proxyRules":
proxyRules = new ArrayList<>();
List<Map<String, String>> proxyRuleMapList = (List<Map<String, String>>) value;
for (Map<String, String> proxyRuleMap : proxyRuleMapList) {
ProxyRuleExt proxyRuleExt = ProxyRuleExt.fromMap(proxyRuleMap);
if (proxyRuleExt != null) {
proxyRules.add(proxyRuleExt);
}
}
break;
case "bypassSimpleHostnames":
bypassSimpleHostnames = (Boolean) value;
break;
case "removeImplicitRules":
removeImplicitRules = (Boolean) value;
break;
case "reverseBypassEnabled":
reverseBypassEnabled = (Boolean) value;
break;
}
}
return this;
}
@NonNull
@Override
public Map<String, Object> toMap() {
List<Map<String, String>> proxyRuleMapList = new ArrayList<>();
for (ProxyRuleExt proxyRuleExt : proxyRules) {
proxyRuleMapList.add(proxyRuleExt.toMap());
}
Map<String, Object> settings = new HashMap<>();
settings.put("bypassRules", bypassRules);
settings.put("directs", directs);
settings.put("proxyRules", proxyRuleMapList);
settings.put("bypassSimpleHostnames", bypassSimpleHostnames);
settings.put("removeImplicitRules", removeImplicitRules);
settings.put("reverseBypassEnabled", reverseBypassEnabled);
return settings;
}
@NonNull
@Override
public Map<String, Object> getRealSettings(@NonNull ProxyConfig proxyConfig) {
Map<String, Object> realSettings = toMap();
List<Map<String, String>> proxyRuleMapList = new ArrayList<>();
List<ProxyConfig.ProxyRule> proxyRules = proxyConfig.getProxyRules();
for (ProxyConfig.ProxyRule proxyRule : proxyRules) {
Map<String, String> proxyRuleMap = new HashMap<>();
proxyRuleMap.put("url", proxyRule.getUrl());
proxyRuleMap.put("schemeFilter", proxyRule.getSchemeFilter());
proxyRuleMapList.add(proxyRuleMap);
}
realSettings.put("bypassRules", proxyConfig.getBypassRules());
realSettings.put("proxyRules", proxyRuleMapList);
realSettings.put("reverseBypassEnabled", proxyConfig.isReverseBypassEnabled());
return realSettings;
}
}
@@ -0,0 +1,123 @@
package com.pichillilorenzo.flutter_inappwebview_android.pull_to_refresh;
import android.graphics.Color;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
import java.util.HashMap;
import java.util.Map;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
public class PullToRefreshChannelDelegate extends ChannelDelegateImpl {
@Nullable
private PullToRefreshLayout pullToRefreshView;
public PullToRefreshChannelDelegate(@NonNull PullToRefreshLayout pullToRefreshView, @NonNull MethodChannel channel) {
super(channel);
this.pullToRefreshView = pullToRefreshView;
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull final MethodChannel.Result result) {
switch (call.method) {
case "setEnabled":
if (pullToRefreshView != null) {
Boolean enabled = (Boolean) call.argument("enabled");
pullToRefreshView.settings.enabled = enabled; // used by InAppWebView.onOverScrolled
pullToRefreshView.setEnabled(enabled);
result.success(true);
} else {
result.success(false);
}
break;
case "isEnabled":
if (pullToRefreshView != null) {
result.success(pullToRefreshView.isEnabled());
} else {
result.success(false);
}
break;
case "setRefreshing":
if (pullToRefreshView != null) {
Boolean refreshing = (Boolean) call.argument("refreshing");
pullToRefreshView.setRefreshing(refreshing);
result.success(true);
} else {
result.success(false);
}
break;
case "isRefreshing":
result.success(pullToRefreshView != null && pullToRefreshView.isRefreshing());
break;
case "setColor":
if (pullToRefreshView != null) {
String color = (String) call.argument("color");
pullToRefreshView.setColorSchemeColors(Color.parseColor(color));
result.success(true);
} else {
result.success(false);
}
break;
case "setBackgroundColor":
if (pullToRefreshView != null) {
String color = (String) call.argument("color");
pullToRefreshView.setProgressBackgroundColorSchemeColor(Color.parseColor(color));
result.success(true);
} else {
result.success(false);
}
break;
case "setDistanceToTriggerSync":
if (pullToRefreshView != null) {
Integer distanceToTriggerSync = (Integer) call.argument("distanceToTriggerSync");
pullToRefreshView.setDistanceToTriggerSync(distanceToTriggerSync);
result.success(true);
} else {
result.success(false);
}
break;
case "setSlingshotDistance":
if (pullToRefreshView != null) {
Integer slingshotDistance = (Integer) call.argument("slingshotDistance");
pullToRefreshView.setSlingshotDistance(slingshotDistance);
result.success(true);
} else {
result.success(false);
}
break;
case "getDefaultSlingshotDistance":
result.success(SwipeRefreshLayout.DEFAULT_SLINGSHOT_DISTANCE);
break;
case "setSize":
if (pullToRefreshView != null) {
Integer size = (Integer) call.argument("size");
pullToRefreshView.setSize(size);
result.success(true);
} else {
result.success(false);
}
break;
default:
result.notImplemented();
}
}
public void onRefresh() {
MethodChannel channel = getChannel();
if (channel == null) return;
Map<String, Object> obj = new HashMap<>();
channel.invokeMethod("onRefresh", obj);
}
@Override
public void dispose() {
super.dispose();
pullToRefreshView = null;
}
}
@@ -0,0 +1,86 @@
package com.pichillilorenzo.flutter_inappwebview_android.pull_to_refresh;
import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin;
import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable;
import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebView;
import io.flutter.plugin.common.MethodChannel;
public class PullToRefreshLayout extends SwipeRefreshLayout implements Disposable {
static final String LOG_TAG = "PullToRefreshLayout";
public static final String METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_pull_to_refresh_";
@Nullable
public PullToRefreshChannelDelegate channelDelegate;
public PullToRefreshSettings settings = new PullToRefreshSettings();
public PullToRefreshLayout(@NonNull Context context, @NonNull InAppWebViewFlutterPlugin plugin,
@NonNull Object id, @NonNull PullToRefreshSettings settings) {
super(context);
this.settings = settings;
final MethodChannel channel = new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME_PREFIX + id);
this.channelDelegate = new PullToRefreshChannelDelegate(this, channel);
}
public PullToRefreshLayout(@NonNull Context context) {
super(context);
}
public PullToRefreshLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public void prepare() {
final PullToRefreshLayout self = this;
setEnabled(settings.enabled);
setOnChildScrollUpCallback(new OnChildScrollUpCallback() {
@Override
public boolean canChildScrollUp(@NonNull SwipeRefreshLayout parent, @Nullable View child) {
if (child instanceof InAppWebView) {
InAppWebView inAppWebView = (InAppWebView) child;
return (inAppWebView.canScrollVertically() && inAppWebView.getScrollY() > 0) ||
(!inAppWebView.canScrollVertically() && inAppWebView.getScrollY() == 0);
}
return true;
}
});
setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
if (channelDelegate == null) {
self.setRefreshing(false);
return;
}
channelDelegate.onRefresh();
}
});
if (settings.color != null)
setColorSchemeColors(Color.parseColor(settings.color));
if (settings.backgroundColor != null)
setProgressBackgroundColorSchemeColor(Color.parseColor(settings.backgroundColor));
if (settings.distanceToTriggerSync != null)
setDistanceToTriggerSync(settings.distanceToTriggerSync);
if (settings.slingshotDistance != null)
setSlingshotDistance(settings.slingshotDistance);
if (settings.size != null)
setSize(settings.size);
}
public void dispose() {
if (channelDelegate != null) {
channelDelegate.dispose();
channelDelegate = null;
}
removeAllViews();
}
}
@@ -0,0 +1,80 @@
package com.pichillilorenzo.flutter_inappwebview_android.pull_to_refresh;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.pichillilorenzo.flutter_inappwebview_android.ISettings;
import java.util.HashMap;
import java.util.Map;
public class PullToRefreshSettings implements ISettings<PullToRefreshLayout> {
public static final String LOG_TAG = "PullToRefreshSettings";
public Boolean enabled = true;
@Nullable
public String color;
@Nullable
public String backgroundColor;
@Nullable
public Integer distanceToTriggerSync;
@Nullable
public Integer slingshotDistance;
@Nullable
public Integer size;
@NonNull
@Override
public PullToRefreshSettings parse(@NonNull Map<String, Object> settings) {
for (Map.Entry<String, Object> pair : settings.entrySet()) {
String key = pair.getKey();
Object value = pair.getValue();
if (value == null) {
continue;
}
switch (key) {
case "enabled":
enabled = (Boolean) value;
break;
case "color":
color = (String) value;
break;
case "backgroundColor":
backgroundColor = (String) value;
break;
case "distanceToTriggerSync":
distanceToTriggerSync = (Integer) value;
break;
case "slingshotDistance":
slingshotDistance = (Integer) value;
break;
case "size":
size = (Integer) value;
break;
}
}
return this;
}
@NonNull
public Map<String, Object> toMap() {
Map<String, Object> settings = new HashMap<>();
settings.put("enabled", enabled);
settings.put("color", color);
settings.put("backgroundColor", backgroundColor);
settings.put("distanceToTriggerSync", distanceToTriggerSync);
settings.put("slingshotDistance", slingshotDistance);
settings.put("size", size);
return settings;
}
@NonNull
@Override
public Map<String, Object> getRealSettings(@NonNull PullToRefreshLayout pullToRefreshLayout) {
Map<String, Object> realSettings = toMap();
return realSettings;
}
}
Loaded 100 of 556 files, more files were not shown because too many files have changed in this diff. Show more