diff --git a/.github/workflows/webview-profiles.yml b/.github/workflows/webview-profiles.yml new file mode 100644 index 00000000..e0fb62de --- /dev/null +++ b/.github/workflows/webview-profiles.yml @@ -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 diff --git a/go.mod b/go.mod index 13db5774..7bfd675d 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 9c4680f4..f7bb99cd 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/webview/README.md b/internal/webview/README.md new file mode 100644 index 00000000..9b7e3e18 --- /dev/null +++ b/internal/webview/README.md @@ -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 `/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. diff --git a/internal/webview/goprovider/provider.go b/internal/webview/goprovider/provider.go index a81c20df..d58d2422 100644 --- a/internal/webview/goprovider/provider.go +++ b/internal/webview/goprovider/provider.go @@ -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) diff --git a/internal/webview/integrationtest/contract.go b/internal/webview/integrationtest/contract.go index 1a508e35..4116a211 100644 --- a/internal/webview/integrationtest/contract.go +++ b/internal/webview/integrationtest/contract.go @@ -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) +} diff --git a/internal/webview/proxy/server.go b/internal/webview/proxy/server.go new file mode 100644 index 00000000..17697d40 --- /dev/null +++ b/internal/webview/proxy/server.go @@ -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() +} diff --git a/internal/webview/proxy/server_test.go b/internal/webview/proxy/server_test.go new file mode 100644 index 00000000..9822b5bc --- /dev/null +++ b/internal/webview/proxy/server_test.go @@ -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) + } +} diff --git a/pkg/download/downloader.go b/pkg/download/downloader.go index 72953a8e..6eb98058 100644 --- a/pkg/download/downloader.go +++ b/pkg/download/downloader.go @@ -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, diff --git a/pkg/download/engine/webview/rpc.go b/pkg/download/engine/webview/rpc.go index 5dab17ef..d3fbf64b 100644 --- a/pkg/download/engine/webview/rpc.go +++ b/pkg/download/engine/webview/rpc.go @@ -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, diff --git a/pkg/download/engine/webview/runtime.go b/pkg/download/engine/webview/runtime.go index 64a236e4..763e87ef 100644 --- a/pkg/download/engine/webview/runtime.go +++ b/pkg/download/engine/webview/runtime.go @@ -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 diff --git a/pkg/download/engine/webview/runtime_test.go b/pkg/download/engine/webview/runtime_test.go index 066dea2e..6f963055 100644 --- a/pkg/download/engine/webview/runtime_test.go +++ b/pkg/download/engine/webview/runtime_test.go @@ -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) + } +} diff --git a/pkg/download/extension.go b/pkg/download/extension.go index c9bb7832..8733072c 100644 --- a/pkg/download/extension.go +++ b/pkg/download/extension.go @@ -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 { diff --git a/pkg/download/extension_engine.go b/pkg/download/extension_engine.go index 39b832f1..4ef05d23 100644 --- a/pkg/download/extension_engine.go +++ b/pkg/download/extension_engine.go @@ -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 { diff --git a/pkg/download/extension_engine_session.go b/pkg/download/extension_engine_session.go index 451a2764..92379550 100644 --- a/pkg/download/extension_engine_session.go +++ b/pkg/download/extension_engine_session.go @@ -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) diff --git a/pkg/download/extension_webview_config.go b/pkg/download/extension_webview_config.go new file mode 100644 index 00000000..2eb02d17 --- /dev/null +++ b/pkg/download/extension_webview_config.go @@ -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 +} diff --git a/pkg/download/extension_webview_config_test.go b/pkg/download/extension_webview_config_test.go new file mode 100644 index 00000000..2c972970 --- /dev/null +++ b/pkg/download/extension_webview_config_test.go @@ -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") + } + } +} diff --git a/ui/flutter/analysis_options.yaml b/ui/flutter/analysis_options.yaml index 89f7f921..a1a8bbfc 100644 --- a/ui/flutter/analysis_options.yaml +++ b/ui/flutter/analysis_options.yaml @@ -29,3 +29,7 @@ linter: formatter: page_width: 120 + +analyzer: + exclude: + - third_party/** diff --git a/ui/flutter/lib/app/rpc/webview_profile.dart b/ui/flutter/lib/app/rpc/webview_profile.dart new file mode 100644 index 00000000..0b172930 --- /dev/null +++ b/ui/flutter/lib/app/rpc/webview_profile.dart @@ -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 toMap() => {...super.toMap(), 'gopeedProfileId': profileId}; +} + +class WebViewProfile { + WebViewProfile(this.id); + + final String id; + static const _channel = MethodChannel('com.pichillilorenzo/flutter_inappwebview_cookiemanager'); + + Future prepare(String proxyUrl) async { + await _call('gopeed.prepareProfile', {'proxyUrl': proxyUrl}); + } + + Future _call(String method, Map arguments) => + _channel.invokeMethod(method, {...arguments, 'gopeedProfileId': id}); + + Future> getCookies({required WebUri url}) async { + final values = await _call('getCookies', {'url': url.toString()}) as List? ?? []; + return values.map((value) => Cookie.fromMap(Map.from(value as Map))!).toList(); + } + + Future 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 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 deleteAllCookies() async { + await _call('deleteAllCookies', {}); + } +} diff --git a/ui/flutter/lib/app/rpc/webview_rpc_service.dart b/ui/flutter/lib/app/rpc/webview_rpc_service.dart index 653e78e6..6fb4b161 100644 --- a/ui/flutter/lib/app/rpc/webview_rpc_service.dart +++ b/ui/flutter/lib/app/rpc/webview_rpc_service.dart @@ -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 _pagesById = {}; final Completer _overlayReady = Completer(); - late final CookieManager _cookieManager = CookieManager.instance(); RpcServerHandle? _server; int _pageSeq = 0; @@ -162,10 +162,12 @@ class WebViewRpcService { } Future> _openPage(Map 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, ); diff --git a/ui/flutter/macos/Runner/Info.plist b/ui/flutter/macos/Runner/Info.plist index cee06fe7..48add6d5 100644 --- a/ui/flutter/macos/Runner/Info.plist +++ b/ui/flutter/macos/Runner/Info.plist @@ -2,6 +2,11 @@ + NSAppTransportSecurity + + NSAllowsArbitraryLoadsInWebContent + + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable diff --git a/ui/flutter/pubspec.lock b/ui/flutter/pubspec.lock index e3384198..0ad35006 100644 --- a/ui/flutter/pubspec.lock +++ b/ui/flutter/pubspec.lock @@ -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 diff --git a/ui/flutter/pubspec.yaml b/ui/flutter/pubspec.yaml index cb27bf83..74d102c0 100644 --- a/ui/flutter/pubspec.yaml +++ b/ui/flutter/pubspec.yaml @@ -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: diff --git a/ui/flutter/third_party/README.md b/ui/flutter/third_party/README.md new file mode 100644 index 00000000..09448d82 --- /dev/null +++ b/ui/flutter/third_party/README.md @@ -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. diff --git a/ui/flutter/third_party/flutter_inappwebview_android/CHANGELOG.md b/ui/flutter/third_party/flutter_inappwebview_android/CHANGELOG.md new file mode 100644 index 00000000..3170f86a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/CHANGELOG.md @@ -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' is not a subtype of type 'Iterable'" [#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. diff --git a/ui/flutter/third_party/flutter_inappwebview_android/LICENSE b/ui/flutter/third_party/flutter_inappwebview_android/LICENSE new file mode 100644 index 00000000..6ccd8da4 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/LICENSE @@ -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. diff --git a/ui/flutter/third_party/flutter_inappwebview_android/README.md b/ui/flutter/third_party/flutter_inappwebview_android/README.md new file mode 100644 index 00000000..77d0be6d --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/README.md @@ -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. \ No newline at end of file diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/build.gradle b/ui/flutter/third_party/flutter_inappwebview_android/android/build.gradle new file mode 100755 index 00000000..ddb812ca --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/build.gradle @@ -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' + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/gradle.properties b/ui/flutter/third_party/flutter_inappwebview_android/android/gradle.properties new file mode 100644 index 00000000..94adc3a3 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/proguard-rules.pro b/ui/flutter/third_party/flutter_inappwebview_android/android/proguard-rules.pro new file mode 100755 index 00000000..a806bdcc --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/proguard-rules.pro @@ -0,0 +1,19 @@ +# WebView +-keepattributes *JavascriptInterface* +-keepclassmembers class * { + @android.webkit.JavascriptInterface ; +} +-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 { + ; + ; + public *; + private *; +} +-keep class com.pichillilorenzo.flutter_inappwebview_android.** { *; } + +-dontwarn android.window.BackEvent \ No newline at end of file diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/settings.gradle b/ui/flutter/third_party/flutter_inappwebview_android/android/settings.gradle new file mode 100755 index 00000000..5841fff0 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'flutter_inappwebview_android' diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/AndroidManifest.xml b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/AndroidManifest.xml new file mode 100755 index 00000000..c8036b21 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/AndroidManifest.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/GopeedProfiles.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/GopeedProfiles.java new file mode 100644 index 00000000..da00ca3a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/GopeedProfiles.java @@ -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); + } + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/ISettings.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/ISettings.java new file mode 100755 index 00000000..c005c2ed --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/ISettings.java @@ -0,0 +1,11 @@ +package com.pichillilorenzo.flutter_inappwebview_android; + +import androidx.annotation.NonNull; + +import java.util.Map; + +public interface ISettings { + @NonNull ISettings parse(@NonNull Map settings); + @NonNull Map toMap(); + @NonNull Map getRealSettings(@NonNull T obj); +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/InAppWebViewFileProvider.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/InAppWebViewFileProvider.java new file mode 100644 index 00000000..3db86362 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/InAppWebViewFileProvider.java @@ -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"; + +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/InAppWebViewFlutterPlugin.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/InAppWebViewFlutterPlugin.java new file mode 100755 index 00000000..21ce254a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/InAppWebViewFlutterPlugin.java @@ -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; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/MyCookieManager.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/MyCookieManager.java new file mode 100755 index 00000000..345725e6 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/MyCookieManager.java @@ -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() { + @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> getCookies(final String url) { + + final List> cookieListMap = new ArrayList<>(); + + final CookieManager cookieManager = selectedCookieManager != null ? selectedCookieManager : getCookieManager(); + if (cookieManager == null) return cookieListMap; + + List 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 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() { + @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() { + @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() { + @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; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/MyWebStorage.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/MyWebStorage.java new file mode 100755 index 00000000..d826b583 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/MyWebStorage.java @@ -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() { + @Override + public void onReceiveValue(Map value) { + List> origins = new ArrayList<>(); + for(Object key : value.keySet()) { + WebStorage.Origin originObj = (WebStorage.Origin) value.get(key); + + Map 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() { + @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() { + @Override + public void onReceiveValue(Long value) { + result.success(value); + } + }); + } + + @Override + public void dispose() { + super.dispose(); + plugin = null; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/PlatformUtil.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/PlatformUtil.java new file mode 100755 index 00000000..7a2459fc --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/PlatformUtil.java @@ -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; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/Util.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/Util.java new file mode 100755 index 00000000..15315621 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/Util.java @@ -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 invokeMethodAndWaitResult(final @NonNull MethodChannel channel, + final @NonNull String method, final @Nullable Object arguments, + final @NonNull SyncBaseCallbackResultImpl 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 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 headers) { + HttpURLConnection urlConnection = null; + try { + URL url = new URL(urlString); + urlConnection = (HttpURLConnection) url.openConnection(); + urlConnection.setRequestMethod(method); + if (headers != null) { + for (Map.Entry 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) value).toString(); + } else if (value instanceof List) { + return new JSONArray((List) 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 getOrDefault(Map 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 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)); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/WebViewFeatureManager.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/WebViewFeatureManager.java new file mode 100755 index 00000000..f72c03c0 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/WebViewFeatureManager.java @@ -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; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ActionBroadcastReceiver.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ActionBroadcastReceiver.java new file mode 100644 index 00000000..cf160c71 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ActionBroadcastReceiver.java @@ -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); + } + } + } + } + } + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsActivity.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsActivity.java new file mode 100755 index 00000000..444bc25d --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsActivity.java @@ -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 initialOtherLikelyURLs; + @Nullable + public Map initialHeaders; + @Nullable + public String initialReferrer; + public List 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) b.getSerializable("headers"); + initialReferrer = b.getString("referrer"); + initialOtherLikelyURLs = b.getStringArrayList("otherLikelyURLs"); + + customSettings = new ChromeCustomTabsSettings(); + customSettings.parse((HashMap) b.getSerializable("settings")); + actionButton = CustomTabsActionButton.fromMap((Map) b.getSerializable("actionButton")); + secondaryToolbar = CustomTabsSecondaryToolbar.fromMap((Map) b.getSerializable("secondaryToolbar")); + List> menuItemList = (List>) b.getSerializable("menuItemList"); + for (Map 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 headers, + @Nullable String referrer, + @Nullable List otherLikelyURLs) { + launchUrlWithSession(customTabsSession, url, headers, referrer, otherLikelyURLs); + } + + public void launchUrlWithSession(@Nullable CustomTabsSession session, + @NonNull String url, + @Nullable Map headers, + @Nullable String referrer, + @Nullable List 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 otherLikelyURLs) { + Uri uri = url != null ? Uri.parse(url) : null; + + List 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(); + } + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsActivitySingleInstance.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsActivitySingleInstance.java new file mode 100755 index 00000000..73d6de79 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsActivitySingleInstance.java @@ -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"; + +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsChannelDelegate.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsChannelDelegate.java new file mode 100644 index 00000000..703a337f --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsChannelDelegate.java @@ -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 headers = (Map) call.argument("headers"); + String referrer = (String) call.argument("referrer"); + List otherLikelyURLs = (List) 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 otherLikelyURLs = (List) 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) 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 obj = new HashMap<>(); + channel.invokeMethod("onServiceConnected", obj); + } + + public void onOpened() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onOpened", obj); + } + + public void onCompletedInitialLoad() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onCompletedInitialLoad", obj); + } + + public void onNavigationEvent(int navigationEvent) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("navigationEvent", navigationEvent); + channel.invokeMethod("onNavigationEvent", obj); + } + + public void onClosed() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onClosed", obj); + } + + public void onItemActionPerform(int id, String url, String title) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map 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 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 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 obj = new HashMap<>(); + channel.invokeMethod("onMessageChannelReady", obj); + } + + public void onPostMessage(@NonNull String message) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("message", message); + channel.invokeMethod("onPostMessage", obj); + } + + public void onVerticalScrollEvent(boolean isDirectionUp) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("isDirectionUp", isDirectionUp); + channel.invokeMethod("onVerticalScrollEvent", obj); + } + + public void onGreatestScrollPercentageIncreased(int scrollPercentage) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("scrollPercentage", scrollPercentage); + channel.invokeMethod("onGreatestScrollPercentageIncreased", obj); + } + + public void onSessionEnded(boolean didUserInteract) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("didUserInteract", didUserInteract); + channel.invokeMethod("onSessionEnded", obj); + } + + @Override + public void dispose() { + super.dispose(); + chromeCustomTabsActivity = null; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsSettings.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsSettings.java new file mode 100755 index 00000000..b512cf48 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeCustomTabsSettings.java @@ -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 { + + 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 additionalTrustedOrigins = new ArrayList<>(); + public TrustedWebActivityDisplayMode displayMode = null; + public Integer screenOrientation = ScreenOrientation.DEFAULT; + public List startAnimations = new ArrayList<>(); + public List exitAnimations = new ArrayList<>(); + public Boolean alwaysUseBrowserUI = false; + + @NonNull + @Override + public ChromeCustomTabsSettings parse(@NonNull Map options) { + for (Map.Entry 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) value; + break; + case "displayMode": + Map displayModeMap = (Map) 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> startAnimationsList = (List>) value; + for (Map startAnimation : startAnimationsList) { + AndroidResource androidResource = AndroidResource.fromMap(startAnimation); + if (androidResource != null) { + startAnimations.add(AndroidResource.fromMap(startAnimation)); + } + } + break; + case "exitAnimations": + List> exitAnimationsList = (List>) value; + for (Map 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 toMap() { + Map 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 getRealSettings(@NonNull ChromeCustomTabsActivity chromeCustomTabsActivity) { + Map 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; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeSafariBrowserManager.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeSafariBrowserManager.java new file mode 100755 index 00000000..1fab2799 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ChromeSafariBrowserManager.java @@ -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 shared = new HashMap<>(); + public final Map 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 headers = (HashMap) call.argument("headers"); + String referrer = (String) call.argument("referrer"); + ArrayList otherLikelyURLs = (ArrayList) call.argument("otherLikelyURLs"); + HashMap settings = (HashMap) call.argument("settings"); + HashMap actionButton = (HashMap) call.argument("actionButton"); + HashMap secondaryToolbar = (HashMap) call.argument("secondaryToolbar"); + List> menuItemList = (List>) 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 packages = (ArrayList) 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 headers, + @Nullable String referrer, @Nullable ArrayList otherLikelyURLs, + HashMap settings, HashMap actionButton, + HashMap secondaryToolbar, + List> 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.getOrDefault(settings, "isSingleInstance", false); + Boolean isTrustedWebActivity = Util.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.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 browserList = browsers.values(); + for (ChromeCustomTabsActivity browser : browserList) { + if (browser != null) { + browser.close(); + browser.dispose(); + } + } + browsers.clear(); + shared.remove(this.id); + plugin = null; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/CustomTabActivityHelper.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/CustomTabActivityHelper.java new file mode 100755 index 00000000..61d759c8 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/CustomTabActivityHelper.java @@ -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 headers, + @Nullable Uri referrer, + int requestCode) { + intent.setData(uri); + if (headers != null) { + Bundle bundleHeaders = new Bundle(); + for (Map.Entry 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 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 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 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(); + } +} \ No newline at end of file diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/CustomTabsHelper.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/CustomTabsHelper.java new file mode 100755 index 00000000..6e3a216b --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/CustomTabsHelper.java @@ -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 not 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 resolvedActivityList = pm.queryIntentActivities(activityIntent, flags); + List 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 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}; + } +} \ No newline at end of file diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/KeepAliveService.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/KeepAliveService.java new file mode 100755 index 00000000..87dc3af8 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/KeepAliveService.java @@ -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; + } +} \ No newline at end of file diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/NoHistoryCustomTabsActivityCallbacks.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/NoHistoryCustomTabsActivityCallbacks.java new file mode 100644 index 00000000..3ada33e7 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/NoHistoryCustomTabsActivityCallbacks.java @@ -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 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 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; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ServiceConnection.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ServiceConnection.java new file mode 100755 index 00000000..de236154 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ServiceConnection.java @@ -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 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(); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ServiceConnectionCallback.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ServiceConnectionCallback.java new file mode 100755 index 00000000..3212b2f6 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/ServiceConnectionCallback.java @@ -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(); +} \ No newline at end of file diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/TrustedWebActivity.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/TrustedWebActivity.java new file mode 100755 index 00000000..d5f70156 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/TrustedWebActivity.java @@ -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 headers, + @Nullable String referrer, + @Nullable List 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); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/TrustedWebActivitySingleInstance.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/TrustedWebActivitySingleInstance.java new file mode 100755 index 00000000..d77c6782 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/chrome_custom_tabs/TrustedWebActivitySingleInstance.java @@ -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"; + +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlocker.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlocker.java new file mode 100755 index 00000000..55421962 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlocker.java @@ -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 + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerAction.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerAction.java new file mode 100755 index 00000000..1c2f38b1 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerAction.java @@ -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 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 + '\'' + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerActionType.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerActionType.java new file mode 100755 index 00000000..89825a21 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerActionType.java @@ -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; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerHandler.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerHandler.java new file mode 100755 index 00000000..8564dbe8 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerHandler.java @@ -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 ruleList = new ArrayList<>(); + + public ContentBlockerHandler() {} + + public ContentBlockerHandler(List ruleList) { + this.ruleList = ruleList; + } + + public List getRuleList() { + return this.ruleList; + } + + public void setRuleList(List 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 ruleListCopy = new CopyOnWriteArrayList(ruleList); + + for (ContentBlocker contentBlocker : ruleListCopy) { + ContentBlockerTrigger trigger = contentBlocker.getTrigger(); + List 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 responseHeaders = new HashMap<>(); + for (Map.Entry> 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; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerTrigger.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerTrigger.java new file mode 100755 index 00000000..8efb3c07 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerTrigger.java @@ -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 resourceType = new ArrayList<>(); + private List ifDomain = new ArrayList<>(); + private List unlessDomain = new ArrayList<>(); + private List loadType = new ArrayList<>(); + private List ifTopUrl = new ArrayList<>(); + private List unlessTopUrl = new ArrayList<>(); + + public ContentBlockerTrigger(@NonNull String urlFilter, @Nullable Boolean urlFilterIsCaseSensitive, @Nullable List resourceType, + @Nullable List ifDomain, @Nullable List unlessDomain, @Nullable List loadType, + @Nullable List ifTopUrl, @Nullable List 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 map) { + String urlFilter = (String) map.get("url-filter"); + Boolean urlFilterIsCaseSensitive = (Boolean) map.get("url-filter-is-case-sensitive"); + List resourceTypeStringList = (List) map.get("resource-type"); + List resourceType = new ArrayList<>(); + if (resourceTypeStringList != null) { + for (String type : resourceTypeStringList) { + resourceType.add(ContentBlockerTriggerResourceType.fromValue(type)); + } + } else { + resourceType.addAll(Arrays.asList(ContentBlockerTriggerResourceType.values())); + } + List ifDomain = (List) map.get("if-domain"); + List unlessDomain = (List) map.get("unless-domain"); + List loadType = (List) map.get("load-type"); + List ifTopUrl = (List) map.get("if-top-url"); + List unlessTopUrl = (List) 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 getResourceType() { + return resourceType; + } + + public void setResourceType(List resourceType) { + this.resourceType = resourceType; + } + + public List getIfDomain() { + return ifDomain; + } + + public void setIfDomain(List ifDomain) { + this.ifDomain = ifDomain; + } + + public List getUnlessDomain() { + return unlessDomain; + } + + public void setUnlessDomain(List unlessDomain) { + this.unlessDomain = unlessDomain; + } + + public List getLoadType() { + return loadType; + } + + public void setLoadType(List loadType) { + this.loadType = loadType; + } + + public List getIfTopUrl() { + return ifTopUrl; + } + + public void setIfTopUrl(List ifTopUrl) { + this.ifTopUrl = ifTopUrl; + } + + public List getUnlessTopUrl() { + return unlessTopUrl; + } + + public void setUnlessTopUrl(List 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 + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerTriggerResourceType.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerTriggerResourceType.java new file mode 100755 index 00000000..25eb60b5 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/content_blocker/ContentBlockerTriggerResourceType.java @@ -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; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/CredentialDatabase.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/CredentialDatabase.java new file mode 100755 index 00000000..5d83b7e6 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/CredentialDatabase.java @@ -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 getHttpAuthCredentials(String host, String protocol, String realm, Integer port) { + List 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)); + } + } +} \ No newline at end of file diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/CredentialDatabaseHandler.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/CredentialDatabaseHandler.java new file mode 100755 index 00000000..23052a18 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/CredentialDatabaseHandler.java @@ -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> allCredentials = new ArrayList<>(); + if (credentialDatabase != null) { + List protectionSpaces = credentialDatabase.protectionSpaceDao.getAll(); + for (URLProtectionSpace protectionSpace : protectionSpaces) { + List> credentials = new ArrayList<>(); + for (URLCredential credential : credentialDatabase.credentialDao.getAllByProtectionSpaceId(protectionSpace.getId())) { + credentials.add(credential.toMap()); + } + Map obj = new HashMap<>(); + obj.put("protectionSpace", protectionSpace.toMap()); + obj.put("credentials", credentials); + allCredentials.add(obj); + } + } + result.success(allCredentials); + } + break; + case "getHttpAuthCredentials": + { + List> 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; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/CredentialDatabaseHelper.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/CredentialDatabaseHelper.java new file mode 100755 index 00000000..ff67e280 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/CredentialDatabaseHelper.java @@ -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); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLCredentialContract.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLCredentialContract.java new file mode 100755 index 00000000..d837b557 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLCredentialContract.java @@ -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"; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLCredentialDao.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLCredentialDao.java new file mode 100755 index 00000000..2c39a41e --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLCredentialDao.java @@ -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 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 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); + } + +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLProtectionSpaceContract.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLProtectionSpaceContract.java new file mode 100755 index 00000000..347ab5fd --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLProtectionSpaceContract.java @@ -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"; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLProtectionSpaceDao.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLProtectionSpaceDao.java new file mode 100755 index 00000000..db85276f --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/credential_database/URLProtectionSpaceDao.java @@ -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 getAll() { + SQLiteDatabase readableDatabase = credentialDatabaseHelper.getReadableDatabase(); + + Cursor cursor = readableDatabase.query( + URLProtectionSpaceContract.FeedEntry.TABLE_NAME, + projection, + null, + null, + null, + null, + null + ); + + List 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); + } +} \ No newline at end of file diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/find_interaction/FindInteractionChannelDelegate.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/find_interaction/FindInteractionChannelDelegate.java new file mode 100644 index 00000000..a0be164c --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/find_interaction/FindInteractionChannelDelegate.java @@ -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 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; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/find_interaction/FindInteractionController.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/find_interaction/FindInteractionController.java new file mode 100644 index 00000000..64f93427 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/find_interaction/FindInteractionController.java @@ -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; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/find_interaction/FindInteractionSettings.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/find_interaction/FindInteractionSettings.java new file mode 100644 index 00000000..1b4d0c6a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/find_interaction/FindInteractionSettings.java @@ -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 { + public static final String LOG_TAG = "FindInteractionSettings"; + + + @NonNull + @Override + public FindInteractionSettings parse(@NonNull Map settings) { +// for (Map.Entry pair : settings.entrySet()) { +// String key = pair.getKey(); +// Object value = pair.getValue(); +// if (value == null) { +// continue; +// } +// +// switch (key) { +// +// } +// } + + return this; + } + + @NonNull + public Map toMap() { + Map settings = new HashMap<>(); + return settings; + } + + @NonNull + @Override + public Map getRealSettings(@NonNull FindInteractionController findInteractionController) { + Map realSettings = toMap(); + return realSettings; + } + +} \ No newline at end of file diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/headless_in_app_webview/HeadlessInAppWebView.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/headless_in_app_webview/HeadlessInAppWebView.java new file mode 100644 index 00000000..129c1925 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/headless_in_app_webview/HeadlessInAppWebView.java @@ -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 params) { + if (flutterWebView != null) { + View view = flutterWebView.getView(); + if (view != null) { + final Map initialSize = (Map) 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; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/headless_in_app_webview/HeadlessInAppWebViewManager.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/headless_in_app_webview/HeadlessInAppWebViewManager.java new file mode 100755 index 00000000..619e174f --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/headless_in_app_webview/HeadlessInAppWebViewManager.java @@ -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 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 params = (HashMap) call.argument("params"); + run(id, params); + } + result.success(true); + break; + default: + result.notImplemented(); + } + } + + public void run(String id, HashMap 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 headlessInAppWebViews = webViews.values(); + for (HeadlessInAppWebView headlessInAppWebView : headlessInAppWebViews) { + if (headlessInAppWebView != null) { + headlessInAppWebView.dispose(); + } + } + webViews.clear(); + plugin = null; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/headless_in_app_webview/HeadlessWebViewChannelDelegate.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/headless_in_app_webview/HeadlessWebViewChannelDelegate.java new file mode 100644 index 00000000..96d922c3 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/headless_in_app_webview/HeadlessWebViewChannelDelegate.java @@ -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 sizeMap = (Map) 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 obj = new HashMap<>(); + channel.invokeMethod("onWebViewCreated", obj); + } + + @Override + public void dispose() { + super.dispose(); + headlessWebView = null; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/ActivityResultListener.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/ActivityResultListener.java new file mode 100644 index 00000000..a50f333d --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/ActivityResultListener.java @@ -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); +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserActivity.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserActivity.java new file mode 100755 index 00000000..6cb763c4 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserActivity.java @@ -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 activityResultListeners = new ArrayList<>(); + @Nullable + public InAppBrowserManager manager; + @Nullable + public InAppBrowserChannelDelegate channelDelegate; + public List 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 settingsMap = (Map) b.getSerializable("settings"); + customSettings.parse(settingsMap); + + windowId = b.getInt("windowId"); + + setContentView(R.layout.activity_web_view); + + Map pullToRefreshInitialSettings = (Map) 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 contextMenu = (Map) b.getSerializable("contextMenu"); + List> initialUserScripts = (List>) b.getSerializable("initialUserScripts"); + List> menuItemList = (List>) b.getSerializable("menuItems"); + for (Map menuItem : menuItemList) { + menuItems.add(InAppBrowserMenuItem.fromMap(menuItem)); + } + + InAppWebViewSettings webViewSettings = new InAppWebViewSettings(); + webViewSettings.parse(settingsMap); + webView.customSettings = webViewSettings; + webView.contextMenu = contextMenu; + + List userScripts = new ArrayList<>(); + if (initialUserScripts != null) { + for (Map 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 initialUrlRequest = (Map) 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 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 getCustomSettings() { + Map webViewSettingsMap = webView != null ? webView.getCustomSettings() : null; + if (customSettings == null || webViewSettingsMap == null) + return null; + + Map 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 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(); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserChannelDelegate.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserChannelDelegate.java new file mode 100644 index 00000000..7f4ec377 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserChannelDelegate.java @@ -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 obj = new HashMap<>(); + channel.invokeMethod("onBrowserCreated", obj); + } + + public void onMenuItemClicked(InAppBrowserMenuItem menuItem) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("id", menuItem.getId()); + channel.invokeMethod("onMenuItemClicked", obj); + } + + public void onExit() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onExit", obj); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserDelegate.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserDelegate.java new file mode 100644 index 00000000..514866ff --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserDelegate.java @@ -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 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); +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserManager.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserManager.java new file mode 100755 index 00000000..b2d648ea --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserManager.java @@ -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 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) 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 activities = pm.queryIntentActivities(intent, 0); + ArrayList targetIntents = new ArrayList(); + 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 arguments) { + String id = (String) arguments.get("id"); + Map urlRequest = (Map) 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 settings = (Map) arguments.get("settings"); + Map contextMenu = (Map) arguments.get("contextMenu"); + Integer windowId = (Integer) arguments.get("windowId"); + List> initialUserScripts = (List>) arguments.get("initialUserScripts"); + Map pullToRefreshInitialSettings = (Map) arguments.get("pullToRefreshSettings"); + List> menuItems = (List>) 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; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserSettings.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserSettings.java new file mode 100755 index 00000000..bdec8217 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/in_app_browser/InAppBrowserSettings.java @@ -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 { + + 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 settings) { + for (Map.Entry 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 toMap() { + Map 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 getRealSettings(@NonNull InAppBrowserActivity inAppBrowserActivity) { + Map 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; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/InterceptAjaxRequestJS.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/InterceptAjaxRequestJS.java new file mode 100644 index 00000000..2ec2e017 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/InterceptAjaxRequestJS.java @@ -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);"; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/InterceptFetchRequestJS.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/InterceptFetchRequestJS.java new file mode 100644 index 00000000..eebff12c --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/InterceptFetchRequestJS.java @@ -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);"; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/JavaScriptBridgeJS.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/JavaScriptBridgeJS.java new file mode 100644 index 00000000..55659d47 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/JavaScriptBridgeJS.java @@ -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;" + + " }" + + "})();"; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/OnLoadResourceJS.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/OnLoadResourceJS.java new file mode 100644 index 00000000..b386bd7d --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/OnLoadResourceJS.java @@ -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']});" + + "})();"; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/OnWindowBlurEventJS.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/OnWindowBlurEventJS.java new file mode 100644 index 00000000..746f8782 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/OnWindowBlurEventJS.java @@ -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');" + + " });" + + "})();"; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/OnWindowFocusEventJS.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/OnWindowFocusEventJS.java new file mode 100644 index 00000000..cfdae9a2 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/OnWindowFocusEventJS.java @@ -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');" + + " });" + + "})();"; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/PluginScriptsUtil.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/PluginScriptsUtil.java new file mode 100644 index 00000000..91af1fdd --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/PluginScriptsUtil.java @@ -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();" + + " });" + + "})();"; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/PrintJS.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/PrintJS.java new file mode 100644 index 00000000..bb108481 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/PrintJS.java @@ -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();" + + " }" + + "};"; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/PromisePolyfillJS.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/PromisePolyfillJS.java new file mode 100644 index 00000000..af10c5d8 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/plugin_scripts_js/PromisePolyfillJS.java @@ -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;i2&&void 0!==arguments[2])||arguments[2],o=arguments[3];return function(t,e){if(!t)throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\");return!e||\"object\"!=typeof e&&\"function\"!=typeof e?t:e}(this,t.call(this,e,r,n,o))}return function(t,e){if(\"function\"!=typeof e&&null!==e)throw new TypeError(\"Super expression must either be null or a function, not \"+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),e.prototype._init=function(t,e){this._result={},this._enumerate(e)},e.prototype._enumerate=function(t){var e=Object.keys(t),r=e.length,n=this.promise;this._remaining=r;for(var o=void 0,i=void 0,s=0;n._state===a&&s jobs = new HashMap<>(); + + public PrintJobManager(@NonNull final InAppWebViewFlutterPlugin plugin) { + super(); + this.plugin = plugin; + } + + public void dispose() { + Collection printJobControllers = jobs.values(); + for (PrintJobController job : printJobControllers) { + if (job != null) { + job.dispose(); + } + } + jobs.clear(); + plugin = null; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/print_job/PrintJobSettings.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/print_job/PrintJobSettings.java new file mode 100755 index 00000000..7df47bbb --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/print_job/PrintJobSettings.java @@ -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 { + + 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 settings) { + for (Map.Entry 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) value); +// break; + case "mediaSize": + mediaSize = MediaSizeExt.fromMap((Map) value); + break; + case "colorMode": + colorMode = (Integer) value; + break; + case "duplexMode": + duplexMode = (Integer) value; + break; + case "resolution": + resolution = ResolutionExt.fromMap((Map) value); + break; + } + } + + return this; + } + + @NonNull + @Override + public Map toMap() { + Map 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 getRealSettings(@NonNull PrintJobController printJobController) { + Map realSettings = toMap(); + return realSettings; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/process_global_config/ProcessGlobalConfigManager.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/process_global_config/ProcessGlobalConfigManager.java new file mode 100755 index 00000000..0b63e3f4 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/process_global_config/ProcessGlobalConfigManager.java @@ -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) 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; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/process_global_config/ProcessGlobalConfigSettings.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/process_global_config/ProcessGlobalConfigSettings.java new file mode 100644 index 00000000..0736be34 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/process_global_config/ProcessGlobalConfigSettings.java @@ -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 { + public static final String LOG_TAG = "ProcessGlobalConfigSettings"; + + @Nullable + public String dataDirectorySuffix; + @Nullable + public DirectoryBasePaths directoryBasePaths; + + @NonNull + @Override + public ProcessGlobalConfigSettings parse(@NonNull Map settings) { + for (Map.Entry 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) 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 toMap() { + Map settings = new HashMap<>(); + settings.put("dataDirectorySuffix", dataDirectorySuffix); + return settings; + } + + @NonNull + @Override + public Map getRealSettings(@NonNull ProcessGlobalConfig processGlobalConfig) { + Map realSettings = toMap(); + return realSettings; + } + + static class DirectoryBasePaths implements ISettings { + public static final String LOG_TAG = "ProcessGlobalConfigSettings"; + + public String cacheDirectoryBasePath; + public String dataDirectoryBasePath; + + @NonNull + @Override + public DirectoryBasePaths parse(@NonNull Map settings) { + for (Map.Entry 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 toMap() { + Map settings = new HashMap<>(); + settings.put("cacheDirectoryBasePath", cacheDirectoryBasePath); + settings.put("dataDirectoryBasePath", dataDirectoryBasePath); + return settings; + } + + @NonNull + @Override + public Map getRealSettings(@NonNull Object obj) { + Map realSettings = toMap(); + return realSettings; + } + } +} \ No newline at end of file diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/proxy/ProxyManager.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/proxy/ProxyManager.java new file mode 100755 index 00000000..a576cfe7 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/proxy/ProxyManager.java @@ -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 settingsMap = (HashMap) 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; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/proxy/ProxySettings.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/proxy/ProxySettings.java new file mode 100644 index 00000000..4834131f --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/proxy/ProxySettings.java @@ -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 { + List bypassRules = new ArrayList<>(); + List directs = new ArrayList<>(); + List proxyRules = new ArrayList<>(); + Boolean bypassSimpleHostnames = null; + Boolean removeImplicitRules = null; + Boolean reverseBypassEnabled = false; + + @NonNull + @Override + public ProxySettings parse(@NonNull Map settings) { + for (Map.Entry pair : settings.entrySet()) { + String key = pair.getKey(); + Object value = pair.getValue(); + if (value == null) { + continue; + } + + switch (key) { + case "bypassRules": + bypassRules = (List) value; + break; + case "directs": + directs = (List) value; + break; + case "proxyRules": + proxyRules = new ArrayList<>(); + List> proxyRuleMapList = (List>) value; + for (Map 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 toMap() { + List> proxyRuleMapList = new ArrayList<>(); + for (ProxyRuleExt proxyRuleExt : proxyRules) { + proxyRuleMapList.add(proxyRuleExt.toMap()); + } + Map 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 getRealSettings(@NonNull ProxyConfig proxyConfig) { + Map realSettings = toMap(); + List> proxyRuleMapList = new ArrayList<>(); + List proxyRules = proxyConfig.getProxyRules(); + for (ProxyConfig.ProxyRule proxyRule : proxyRules) { + Map 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; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/pull_to_refresh/PullToRefreshChannelDelegate.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/pull_to_refresh/PullToRefreshChannelDelegate.java new file mode 100644 index 00000000..8ec3eb57 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/pull_to_refresh/PullToRefreshChannelDelegate.java @@ -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 obj = new HashMap<>(); + channel.invokeMethod("onRefresh", obj); + } + + @Override + public void dispose() { + super.dispose(); + pullToRefreshView = null; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/pull_to_refresh/PullToRefreshLayout.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/pull_to_refresh/PullToRefreshLayout.java new file mode 100644 index 00000000..dfbc0bfd --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/pull_to_refresh/PullToRefreshLayout.java @@ -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(); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/pull_to_refresh/PullToRefreshSettings.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/pull_to_refresh/PullToRefreshSettings.java new file mode 100644 index 00000000..866ed890 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/pull_to_refresh/PullToRefreshSettings.java @@ -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 { + 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 settings) { + for (Map.Entry 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 toMap() { + Map 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 getRealSettings(@NonNull PullToRefreshLayout pullToRefreshLayout) { + Map realSettings = toMap(); + return realSettings; + } + +} \ No newline at end of file diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/service_worker/ServiceWorkerChannelDelegate.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/service_worker/ServiceWorkerChannelDelegate.java new file mode 100755 index 00000000..19a4ea38 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/service_worker/ServiceWorkerChannelDelegate.java @@ -0,0 +1,147 @@ +package com.pichillilorenzo.flutter_inappwebview_android.service_worker; + +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.webkit.ServiceWorkerControllerCompat; +import androidx.webkit.ServiceWorkerWebSettingsCompat; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.Util; +import com.pichillilorenzo.flutter_inappwebview_android.types.BaseCallbackResultImpl; +import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl; +import com.pichillilorenzo.flutter_inappwebview_android.types.SyncBaseCallbackResultImpl; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceRequestExt; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceResponseExt; + +import java.util.Map; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +@RequiresApi(api = Build.VERSION_CODES.N) +public class ServiceWorkerChannelDelegate extends ChannelDelegateImpl { + @Nullable + private ServiceWorkerManager serviceWorkerManager; + + public ServiceWorkerChannelDelegate(@NonNull ServiceWorkerManager serviceWorkerManager, @NonNull MethodChannel channel) { + super(channel); + this.serviceWorkerManager = serviceWorkerManager; + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + ServiceWorkerManager.init(); + ServiceWorkerControllerCompat serviceWorkerController = ServiceWorkerManager.serviceWorkerController; + ServiceWorkerWebSettingsCompat serviceWorkerWebSettings = (serviceWorkerController != null) ? + serviceWorkerController.getServiceWorkerWebSettings() : null; + + switch (call.method) { + case "setServiceWorkerClient": + if (serviceWorkerManager != null) { + Boolean isNull = (Boolean) call.argument("isNull"); + serviceWorkerManager.setServiceWorkerClient(isNull); + result.success(true); + } else { + result.success(false); + } + break; + case "getAllowContentAccess": + if (serviceWorkerWebSettings != null && WebViewFeature.isFeatureSupported(WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS)) { + result.success(serviceWorkerWebSettings.getAllowContentAccess()); + } else { + result.success(false); + } + break; + case "getAllowFileAccess": + if (serviceWorkerWebSettings != null && WebViewFeature.isFeatureSupported(WebViewFeature.SERVICE_WORKER_FILE_ACCESS)) { + result.success(serviceWorkerWebSettings.getAllowFileAccess()); + } else { + result.success(false); + } + break; + case "getBlockNetworkLoads": + if (serviceWorkerWebSettings != null && WebViewFeature.isFeatureSupported(WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS)) { + result.success(serviceWorkerWebSettings.getBlockNetworkLoads()); + } else { + result.success(false); + } + break; + case "getCacheMode": + if (serviceWorkerWebSettings != null && WebViewFeature.isFeatureSupported(WebViewFeature.SERVICE_WORKER_CACHE_MODE)) { + result.success(serviceWorkerWebSettings.getCacheMode()); + } else { + result.success(null); + } + break; + case "setAllowContentAccess": + if (serviceWorkerWebSettings != null && WebViewFeature.isFeatureSupported(WebViewFeature.SERVICE_WORKER_CONTENT_ACCESS)) { + Boolean allow = (Boolean) call.argument("allow"); + serviceWorkerWebSettings.setAllowContentAccess(allow); + } + result.success(true); + break; + case "setAllowFileAccess": + if (serviceWorkerWebSettings != null && WebViewFeature.isFeatureSupported(WebViewFeature.SERVICE_WORKER_FILE_ACCESS)) { + Boolean allow = (Boolean) call.argument("allow"); + serviceWorkerWebSettings.setAllowFileAccess(allow); + } + result.success(true); + break; + case "setBlockNetworkLoads": + if (serviceWorkerWebSettings != null && WebViewFeature.isFeatureSupported(WebViewFeature.SERVICE_WORKER_BLOCK_NETWORK_LOADS)) { + Boolean flag = (Boolean) call.argument("flag"); + serviceWorkerWebSettings.setBlockNetworkLoads(flag); + } + result.success(true); + break; + case "setCacheMode": + if (serviceWorkerWebSettings != null && WebViewFeature.isFeatureSupported(WebViewFeature.SERVICE_WORKER_CACHE_MODE)) { + Integer mode = (Integer) call.argument("mode"); + serviceWorkerWebSettings.setCacheMode(mode); + } + result.success(true); + break; + default: + result.notImplemented(); + } + } + + public static class ShouldInterceptRequestCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public WebResourceResponseExt decodeResult(@Nullable Object obj) { + return WebResourceResponseExt.fromMap((Map) obj); + } + } + + public void shouldInterceptRequest(WebResourceRequestExt request, @NonNull ShouldInterceptRequestCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) return; + channel.invokeMethod("shouldInterceptRequest", request.toMap(), callback); + } + + public static class SyncShouldInterceptRequestCallback extends SyncBaseCallbackResultImpl { + @Nullable + @Override + public WebResourceResponseExt decodeResult(@Nullable Object obj) { + return (new ShouldInterceptRequestCallback()).decodeResult(obj); + } + } + + @Nullable + public WebResourceResponseExt shouldInterceptRequest(WebResourceRequestExt request) throws InterruptedException { + MethodChannel channel = getChannel(); + if (channel == null) return null; + final SyncShouldInterceptRequestCallback callback = new SyncShouldInterceptRequestCallback(); + return Util.invokeMethodAndWaitResult(channel, "shouldInterceptRequest", request.toMap(), callback); + } + + @Override + public void dispose() { + super.dispose(); + serviceWorkerManager = null; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/service_worker/ServiceWorkerManager.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/service_worker/ServiceWorkerManager.java new file mode 100755 index 00000000..4dde63e7 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/service_worker/ServiceWorkerManager.java @@ -0,0 +1,115 @@ +package com.pichillilorenzo.flutter_inappwebview_android.service_worker; + +import android.os.Build; +import android.util.Log; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.webkit.ServiceWorkerClientCompat; +import androidx.webkit.ServiceWorkerControllerCompat; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceRequestExt; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceResponseExt; + +import java.io.ByteArrayInputStream; +import java.util.Map; + +import io.flutter.plugin.common.MethodChannel; + +@RequiresApi(api = Build.VERSION_CODES.N) +public class ServiceWorkerManager implements Disposable { + protected static final String LOG_TAG = "ServiceWorkerManager"; + public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_serviceworkercontroller"; + + @Nullable + public ServiceWorkerChannelDelegate channelDelegate; + @Nullable + public static ServiceWorkerControllerCompat serviceWorkerController; + @Nullable + public InAppWebViewFlutterPlugin plugin; + + public ServiceWorkerManager(@NonNull final InAppWebViewFlutterPlugin plugin) { + this.plugin = plugin; + final MethodChannel channel = new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME); + this.channelDelegate = new ServiceWorkerChannelDelegate(this, channel); + } + + public static void init() { + if (serviceWorkerController == null && + WebViewFeature.isFeatureSupported(WebViewFeature.SERVICE_WORKER_BASIC_USAGE)) { + serviceWorkerController = ServiceWorkerControllerCompat.getInstance(); + } + } + + public void setServiceWorkerClient(Boolean isNull) { + if (serviceWorkerController != null) { + // set ServiceWorkerClient as null makes the app crashes, so just set a dummy ServiceWorkerClientCompat. + // https://github.com/pichillilorenzo/flutter_inappwebview/issues/1151 + serviceWorkerController.setServiceWorkerClient(isNull ? dummyServiceWorkerClientCompat() : new ServiceWorkerClientCompat() { + @Nullable + @Override + public WebResourceResponse shouldInterceptRequest(@NonNull WebResourceRequest request) { + WebResourceRequestExt requestExt = WebResourceRequestExt.fromWebResourceRequest(request); + + WebResourceResponseExt response = null; + if (channelDelegate != null) { + try { + response = channelDelegate.shouldInterceptRequest(requestExt); + } catch (InterruptedException e) { + Log.e(LOG_TAG, "", e); + return null; + } + } + + if (response != null) { + String contentType = response.getContentType(); + String contentEncoding = response.getContentEncoding(); + byte[] data = response.getData(); + Map responseHeaders = response.getHeaders(); + Integer statusCode = response.getStatusCode(); + String reasonPhrase = response.getReasonPhrase(); + + ByteArrayInputStream inputStream = (data != null) ? new ByteArrayInputStream(data) : null; + + if (statusCode != null && reasonPhrase != null) { + return new WebResourceResponse(contentType, contentEncoding, statusCode, reasonPhrase, responseHeaders, inputStream); + } else { + return new WebResourceResponse(contentType, contentEncoding, inputStream); + } + } + + return null; + } + }); + } + } + + private ServiceWorkerClientCompat dummyServiceWorkerClientCompat() { + return DummyServiceWorkerClientCompat.INSTANCE; + } + + @Override + public void dispose() { + if (channelDelegate != null) { + channelDelegate.dispose(); + channelDelegate = null; + } + plugin = null; + } + + private static final class DummyServiceWorkerClientCompat extends ServiceWorkerClientCompat { + static final ServiceWorkerClientCompat INSTANCE = new DummyServiceWorkerClientCompat(); + + @Nullable + @Override + public WebResourceResponse shouldInterceptRequest(@NonNull WebResourceRequest request) { + return null; + } + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/tracing/TracingControllerChannelDelegate.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/tracing/TracingControllerChannelDelegate.java new file mode 100755 index 00000000..e6b957a5 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/tracing/TracingControllerChannelDelegate.java @@ -0,0 +1,78 @@ +package com.pichillilorenzo.flutter_inappwebview_android.tracing; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.TracingConfig; +import androidx.webkit.TracingController; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.util.Map; +import java.util.concurrent.Executors; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class TracingControllerChannelDelegate extends ChannelDelegateImpl { + @Nullable + private TracingControllerManager tracingControllerManager; + + public TracingControllerChannelDelegate(@NonNull TracingControllerManager tracingControllerManager, @NonNull MethodChannel channel) { + super(channel); + this.tracingControllerManager = tracingControllerManager; + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + TracingControllerManager.init(); + TracingController tracingController = TracingControllerManager.tracingController; + + switch (call.method) { + case "isTracing": + if (tracingController != null) { + result.success(tracingController.isTracing()); + } else { + result.success(false); + } + break; + case "start": + if (tracingController != null && WebViewFeature.isFeatureSupported(WebViewFeature.TRACING_CONTROLLER_BASIC_USAGE)) { + Map settingsMap = (Map) call.argument("settings"); + TracingSettings settings = new TracingSettings(); + settings.parse(settingsMap); + TracingConfig config = TracingControllerManager.buildTracingConfig(settings); + tracingController.start(config); + result.success(true); + } else { + result.success(false); + } + break; + case "stop": + if (tracingController != null && WebViewFeature.isFeatureSupported(WebViewFeature.TRACING_CONTROLLER_BASIC_USAGE)) { + String filePath = (String) call.argument("filePath"); + try { + result.success(tracingController.stop( + filePath != null ? new FileOutputStream(filePath) : null, + Executors.newSingleThreadExecutor())); + } catch (FileNotFoundException e) { + e.printStackTrace(); + result.success(false); + } + } else { + result.success(false); + } + break; + default: + result.notImplemented(); + } + } + + @Override + public void dispose() { + super.dispose(); + tracingControllerManager = null; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/tracing/TracingControllerManager.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/tracing/TracingControllerManager.java new file mode 100755 index 00000000..6c48c5bb --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/tracing/TracingControllerManager.java @@ -0,0 +1,61 @@ +package com.pichillilorenzo.flutter_inappwebview_android.tracing; + +import androidx.annotation.Nullable; +import androidx.webkit.TracingConfig; +import androidx.webkit.TracingController; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable; + +import io.flutter.plugin.common.MethodChannel; + +public class TracingControllerManager implements Disposable { + protected static final String LOG_TAG = "TracingControllerMan"; + public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_tracingcontroller"; + + @Nullable + public TracingControllerChannelDelegate channelDelegate; + @Nullable + public static TracingController tracingController; + @Nullable + public InAppWebViewFlutterPlugin plugin; + + public TracingControllerManager(final InAppWebViewFlutterPlugin plugin) { + this.plugin = plugin; + final MethodChannel channel = new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME); + this.channelDelegate = new TracingControllerChannelDelegate(this, channel); + } + + public static void init() { + if (tracingController == null && + WebViewFeature.isFeatureSupported(WebViewFeature.TRACING_CONTROLLER_BASIC_USAGE)) { + tracingController = TracingController.getInstance(); + } + } + + public static TracingConfig buildTracingConfig(TracingSettings settings) { + TracingConfig.Builder builder = new TracingConfig.Builder(); + for (Object category : settings.categories) { + if (category instanceof String) { + builder.addCategories((String) category); + } + if (category instanceof Integer) { + builder.addCategories((Integer) category); + } + } + if (settings.tracingMode != null) { + builder.setTracingMode(settings.tracingMode); + } + return builder.build(); + } + + @Override + public void dispose() { + if (channelDelegate != null) { + channelDelegate.dispose(); + channelDelegate = null; + } + plugin = null; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/tracing/TracingSettings.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/tracing/TracingSettings.java new file mode 100755 index 00000000..bcf0bd55 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/tracing/TracingSettings.java @@ -0,0 +1,61 @@ +package com.pichillilorenzo.flutter_inappwebview_android.tracing; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.TracingController; + +import com.pichillilorenzo.flutter_inappwebview_android.ISettings; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class TracingSettings implements ISettings { + + public static final String LOG_TAG = "TracingSettings"; + + @NonNull + public List categories = new ArrayList<>(); + @Nullable + public Integer tracingMode; + + @NonNull + @Override + public TracingSettings parse(@NonNull Map settings) { + for (Map.Entry pair : settings.entrySet()) { + String key = pair.getKey(); + Object value = pair.getValue(); + if (value == null) { + continue; + } + + switch (key) { + case "categories": + categories = (List) value; + break; + case "tracingMode": + tracingMode = (Integer) value; + break; + } + } + + return this; + } + + @NonNull + @Override + public Map toMap() { + Map settings = new HashMap<>(); + settings.put("categories", categories); + settings.put("tracingMode", tracingMode); + return settings; + } + + @NonNull + @Override + public Map getRealSettings(@NonNull TracingController tracingController) { + Map realSettings = toMap(); + return realSettings; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/AndroidResource.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/AndroidResource.java new file mode 100644 index 00000000..6db0d083 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/AndroidResource.java @@ -0,0 +1,103 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class AndroidResource { + @NonNull + private String name; + @Nullable + private String defType; + @Nullable + private String defPackage; + + public AndroidResource(@NonNull String name, @Nullable String defType, @Nullable String defPackage) { + this.name = name; + this.defType = defType; + this.defPackage = defPackage; + } + + @Nullable + public static AndroidResource fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String name = (String) map.get("name"); + String defType = (String) map.get("defType"); + String defPackage = (String) map.get("defPackage"); + return new AndroidResource(name, defType, defPackage); + } + + public Map toMap() { + Map urlRequestMap = new HashMap<>(); + urlRequestMap.put("name", name); + urlRequestMap.put("defType", defType); + urlRequestMap.put("defPackage", defPackage); + return urlRequestMap; + } + + @NonNull + public String getName() { + return name; + } + + public void setName(@NonNull String name) { + this.name = name; + } + + @Nullable + public String getDefType() { + return defType; + } + + public void setDefType(@Nullable String defType) { + this.defType = defType; + } + + @Nullable + public String getDefPackage() { + return defPackage; + } + + public void setDefPackage(@Nullable String defPackage) { + this.defPackage = defPackage; + } + + public int getIdentifier(@NonNull Context ctx) { + return ctx.getResources().getIdentifier(name, defType, defPackage); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + AndroidResource that = (AndroidResource) o; + + if (!name.equals(that.name)) return false; + if (defType != null ? !defType.equals(that.defType) : that.defType != null) return false; + return defPackage != null ? defPackage.equals(that.defPackage) : that.defPackage == null; + } + + @Override + public int hashCode() { + int result = name.hashCode(); + result = 31 * result + (defType != null ? defType.hashCode() : 0); + result = 31 * result + (defPackage != null ? defPackage.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "AndroidResource{" + + "name='" + name + '\'' + + ", type='" + defType + '\'' + + ", defPackage='" + defPackage + '\'' + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/BaseCallbackResultImpl.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/BaseCallbackResultImpl.java new file mode 100644 index 00000000..bd23e53a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/BaseCallbackResultImpl.java @@ -0,0 +1,47 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class BaseCallbackResultImpl implements ICallbackResult { + @Override + public boolean nonNullSuccess(@NonNull T result) { + return true; + } + + @Override + public boolean nullSuccess() { + return true; + } + + @Override + public void defaultBehaviour(@Nullable T result) {} + + @Override + public void success(@Nullable Object obj) { + T result = decodeResult(obj); + boolean shouldRunDefaultBehaviour; + if (result == null) { + shouldRunDefaultBehaviour = nullSuccess(); + } else { + shouldRunDefaultBehaviour = nonNullSuccess(result); + } + if (shouldRunDefaultBehaviour) { + defaultBehaviour(result); + } + } + + @Nullable + @Override + public T decodeResult(@Nullable Object obj) { + return null; + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) {} + + @Override + public void notImplemented() { + defaultBehaviour(null); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ChannelDelegateImpl.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ChannelDelegateImpl.java new file mode 100644 index 00000000..450a61b0 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ChannelDelegateImpl.java @@ -0,0 +1,37 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.CallSuper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class ChannelDelegateImpl implements IChannelDelegate { + @Nullable + private MethodChannel channel; + + public ChannelDelegateImpl(@NonNull MethodChannel channel) { + this.channel = channel; + this.channel.setMethodCallHandler(this); + } + + @Override + @Nullable + public MethodChannel getChannel() { + return channel; + } + + @CallSuper + public void dispose() { + if (channel != null) { + channel.setMethodCallHandler(null); + channel = null; + } + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ClientCertChallenge.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ClientCertChallenge.java new file mode 100644 index 00000000..1bb5afeb --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ClientCertChallenge.java @@ -0,0 +1,85 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.Nullable; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public class ClientCertChallenge extends URLAuthenticationChallenge { + @Nullable + private Principal[] principals; + @Nullable + private String[] keyTypes; + + public ClientCertChallenge(URLProtectionSpace protectionSpace, @Nullable Principal[] principals, @Nullable String[] keyTypes) { + super(protectionSpace); + this.principals = principals; + this.keyTypes = keyTypes; + } + + public Map toMap() { + List principalList = null; + if (principals != null) { + principalList = new ArrayList<>(); + for (Principal principal : principals) { + principalList.add(principal.getName()); + } + } + + Map challengeMap = super.toMap(); + challengeMap.put("principals", principalList); + challengeMap.put("keyTypes", keyTypes != null ? Arrays.asList(keyTypes) : null); + return challengeMap; + } + + @Nullable + public Principal[] getPrincipals() { + return principals; + } + + public void setPrincipals(@Nullable Principal[] principals) { + this.principals = principals; + } + + @Nullable + public String[] getKeyTypes() { + return keyTypes; + } + + public void setKeyTypes(@Nullable String[] keyTypes) { + this.keyTypes = keyTypes; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + ClientCertChallenge that = (ClientCertChallenge) o; + + // Probably incorrect - comparing Object[] arrays with Arrays.equals + if (!Arrays.equals(principals, that.principals)) return false; + // Probably incorrect - comparing Object[] arrays with Arrays.equals + return Arrays.equals(keyTypes, that.keyTypes); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + Arrays.hashCode(principals); + result = 31 * result + Arrays.hashCode(keyTypes); + return result; + } + + @Override + public String toString() { + return "ClientCertChallenge{" + + "principals=" + Arrays.toString(principals) + + ", keyTypes=" + Arrays.toString(keyTypes) + + "} " + super.toString(); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ClientCertResponse.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ClientCertResponse.java new file mode 100644 index 00000000..cbd4349d --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ClientCertResponse.java @@ -0,0 +1,105 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Map; + +public class ClientCertResponse { + @NonNull + private String certificatePath; + @Nullable + private String certificatePassword; + @NonNull + private String keyStoreType; + @Nullable + private Integer action; + + public ClientCertResponse(@NonNull String certificatePath, @Nullable String certificatePassword, @NonNull String keyStoreType, @Nullable Integer action) { + this.certificatePath = certificatePath; + this.certificatePassword = certificatePassword; + this.keyStoreType = keyStoreType; + this.action = action; + } + + @Nullable + public static ClientCertResponse fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String certificatePath = (String) map.get("certificatePath"); + String certificatePassword = (String) map.get("certificatePassword"); + String keyStoreType = (String) map.get("keyStoreType"); + Integer action = (Integer) map.get("action"); + return new ClientCertResponse(certificatePath, certificatePassword, keyStoreType, action); + } + + @NonNull + public String getCertificatePath() { + return certificatePath; + } + + public void setCertificatePath(@NonNull String certificatePath) { + this.certificatePath = certificatePath; + } + + @Nullable + public String getCertificatePassword() { + return certificatePassword; + } + + public void setCertificatePassword(@Nullable String certificatePassword) { + this.certificatePassword = certificatePassword; + } + + @NonNull + public String getKeyStoreType() { + return keyStoreType; + } + + public void setKeyStoreType(@NonNull String keyStoreType) { + this.keyStoreType = keyStoreType; + } + + @Nullable + public Integer getAction() { + return action; + } + + public void setAction(@Nullable Integer action) { + this.action = action; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ClientCertResponse that = (ClientCertResponse) o; + + if (!certificatePath.equals(that.certificatePath)) return false; + if (certificatePassword != null ? !certificatePassword.equals(that.certificatePassword) : that.certificatePassword != null) + return false; + if (!keyStoreType.equals(that.keyStoreType)) return false; + return action != null ? action.equals(that.action) : that.action == null; + } + + @Override + public int hashCode() { + int result = certificatePath.hashCode(); + result = 31 * result + (certificatePassword != null ? certificatePassword.hashCode() : 0); + result = 31 * result + keyStoreType.hashCode(); + result = 31 * result + (action != null ? action.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "ClientCertResponse{" + + "certificatePath='" + certificatePath + '\'' + + ", certificatePassword='" + certificatePassword + '\'' + + ", keyStoreType='" + keyStoreType + '\'' + + ", action=" + action + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ContentWorld.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ContentWorld.java new file mode 100644 index 00000000..8c1fd25a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ContentWorld.java @@ -0,0 +1,63 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Map; + +public class ContentWorld { + @NonNull + private String name; + + public static final ContentWorld PAGE = new ContentWorld("page"); + public static final ContentWorld DEFAULT_CLIENT = new ContentWorld("defaultClient"); + + private ContentWorld(@NonNull String name) { + this.name = name; + } + + public static ContentWorld world(@NonNull String name) { + return new ContentWorld(name); + } + + @Nullable + public static ContentWorld fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String name = (String) map.get("name"); + assert name != null; + return new ContentWorld(name); + } + + @NonNull + public String getName() { + return name; + } + + public void setName(@NonNull String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ContentWorld that = (ContentWorld) o; + + return name.equals(that.name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public String toString() { + return "ContentWorld{" + + "name='" + name + '\'' + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CreateWindowAction.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CreateWindowAction.java new file mode 100644 index 00000000..d12b225f --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CreateWindowAction.java @@ -0,0 +1,70 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import java.util.Map; + +public class CreateWindowAction extends NavigationAction { + int windowId; + boolean isDialog; + + public CreateWindowAction(URLRequest request, boolean isForMainFrame, boolean hasGesture, boolean isRedirect, int windowId, boolean isDialog) { + super(request, isForMainFrame, hasGesture, isRedirect); + this.windowId = windowId; + this.isDialog = isDialog; + } + + public Map toMap() { + Map createWindowActionMap = super.toMap(); + createWindowActionMap.put("windowId", windowId); + createWindowActionMap.put("isDialog", isDialog); + createWindowActionMap.put("windowFeatures", null); + return createWindowActionMap; + } + + public int getWindowId() { + return windowId; + } + + public void setWindowId(int windowId) { + this.windowId = windowId; + } + + public boolean isDialog() { + return isDialog; + } + + public void setDialog(boolean dialog) { + isDialog = dialog; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + CreateWindowAction that = (CreateWindowAction) o; + + if (windowId != that.windowId) return false; + return isDialog == that.isDialog; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + windowId; + result = 31 * result + (isDialog ? 1 : 0); + return result; + } + + @Override + public String toString() { + return "CreateWindowAction{" + + "windowId=" + windowId + + ", isDialog=" + isDialog + + ", request=" + request + + ", isForMainFrame=" + isForMainFrame + + ", hasGesture=" + hasGesture + + ", isRedirect=" + isRedirect + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomSchemeResponse.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomSchemeResponse.java new file mode 100644 index 00000000..50bac102 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomSchemeResponse.java @@ -0,0 +1,89 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Arrays; +import java.util.Map; + +public class CustomSchemeResponse { + @NonNull + private byte[] data; + @NonNull + private String contentType; + @NonNull + private String contentEncoding; + + public CustomSchemeResponse(@NonNull byte[] data, @NonNull String contentType, @NonNull String contentEncoding) { + this.data = data; + this.contentType = contentType; + this.contentEncoding = contentEncoding; + } + + @Nullable + public static CustomSchemeResponse fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + byte[] data = (byte[]) map.get("data"); + String contentType = (String) map.get("contentType"); + String contentEncoding = (String) map.get("contentEncoding"); + return new CustomSchemeResponse(data, contentType, contentEncoding); + } + + @NonNull + public byte[] getData() { + return data; + } + + public void setData(@NonNull byte[] data) { + this.data = data; + } + + @NonNull + public String getContentType() { + return contentType; + } + + public void setContentType(@NonNull String contentType) { + this.contentType = contentType; + } + + @NonNull + public String getContentEncoding() { + return contentEncoding; + } + + public void setContentEncoding(@NonNull String contentEncoding) { + this.contentEncoding = contentEncoding; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CustomSchemeResponse that = (CustomSchemeResponse) o; + + if (!Arrays.equals(data, that.data)) return false; + if (!contentType.equals(that.contentType)) return false; + return contentEncoding.equals(that.contentEncoding); + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(data); + result = 31 * result + contentType.hashCode(); + result = 31 * result + contentEncoding.hashCode(); + return result; + } + + @Override + public String toString() { + return "CustomSchemeResponse{" + + "data=" + Arrays.toString(data) + + ", contentType='" + contentType + '\'' + + ", contentEncoding='" + contentEncoding + '\'' + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomTabsActionButton.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomTabsActionButton.java new file mode 100644 index 00000000..b408d32f --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomTabsActionButton.java @@ -0,0 +1,104 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Arrays; +import java.util.Map; + +public class CustomTabsActionButton { + private int id; + + @NonNull + private byte[] icon; + + @NonNull + private String description; + + private boolean shouldTint; + + public CustomTabsActionButton(int id, @NonNull byte[] icon, @NonNull String description, boolean shouldTint) { + this.id = id; + this.icon = icon; + this.description = description; + this.shouldTint = shouldTint; + } + + @Nullable + public static CustomTabsActionButton fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + int id = (int) map.get("id"); + byte[] icon = (byte[]) map.get("icon"); + String description = (String) map.get("description"); + boolean shouldTint = (boolean) map.get("shouldTint"); + return new CustomTabsActionButton(id, icon, description, shouldTint); + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @NonNull + public byte[] getIcon() { + return icon; + } + + public void setIcon(@NonNull byte[] icon) { + this.icon = icon; + } + + @NonNull + public String getDescription() { + return description; + } + + public void setDescription(@NonNull String description) { + this.description = description; + } + + public boolean isShouldTint() { + return shouldTint; + } + + public void setShouldTint(boolean shouldTint) { + this.shouldTint = shouldTint; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CustomTabsActionButton that = (CustomTabsActionButton) o; + + if (id != that.id) return false; + if (shouldTint != that.shouldTint) return false; + if (!Arrays.equals(icon, that.icon)) return false; + return description.equals(that.description); + } + + @Override + public int hashCode() { + int result = id; + result = 31 * result + Arrays.hashCode(icon); + result = 31 * result + description.hashCode(); + result = 31 * result + (shouldTint ? 1 : 0); + return result; + } + + @Override + public String toString() { + return "CustomTabsActionButton{" + + "id=" + id + + ", icon=" + Arrays.toString(icon) + + ", description='" + description + '\'' + + ", shouldTint=" + shouldTint + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomTabsMenuItem.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomTabsMenuItem.java new file mode 100644 index 00000000..34cf9970 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomTabsMenuItem.java @@ -0,0 +1,71 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Map; + +public class CustomTabsMenuItem { + private int id; + + @NonNull + private String label; + + public CustomTabsMenuItem(int id, @NonNull String label) { + this.id = id; + this.label = label; + } + + @Nullable + public static CustomTabsMenuItem fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + int id = (int) map.get("id"); + String label = (String) map.get("label"); + return new CustomTabsMenuItem(id, label); + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @NonNull + public String getLabel() { + return label; + } + + public void setLabel(@NonNull String label) { + this.label = label; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CustomTabsMenuItem that = (CustomTabsMenuItem) o; + + if (id != that.id) return false; + return label.equals(that.label); + } + + @Override + public int hashCode() { + int result = id; + result = 31 * result + label.hashCode(); + return result; + } + + @Override + public String toString() { + return "CustomTabsMenuItem{" + + "id=" + id + + ", label='" + label + '\'' + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomTabsSecondaryToolbar.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomTabsSecondaryToolbar.java new file mode 100644 index 00000000..7db298ec --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/CustomTabsSecondaryToolbar.java @@ -0,0 +1,83 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class CustomTabsSecondaryToolbar { + @NonNull + private AndroidResource layout; + @NonNull + private List clickableIDs = new ArrayList<>(); + + public CustomTabsSecondaryToolbar(@NonNull AndroidResource layout, @NonNull List clickableIDs) { + this.layout = layout; + this.clickableIDs = clickableIDs; + } + + @Nullable + public static CustomTabsSecondaryToolbar fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + AndroidResource layout = AndroidResource.fromMap((Map) map.get("layout")); + List clickableIDs = new ArrayList<>(); + List> clickableIDList = (List>) map.get("clickableIDs"); + if (clickableIDList != null) { + for (Map clickableIDMap : clickableIDList) { + AndroidResource clickableID = AndroidResource.fromMap((Map) clickableIDMap.get("id")); + if (clickableID != null) { + clickableIDs.add(clickableID); + } + } + } + return new CustomTabsSecondaryToolbar(layout, clickableIDs); + } + + @NonNull + public AndroidResource getLayout() { + return layout; + } + + public void setLayout(@NonNull AndroidResource layout) { + this.layout = layout; + } + + @NonNull + public List getClickableIDs() { + return clickableIDs; + } + + public void setClickableIDs(@NonNull List clickableIDs) { + this.clickableIDs = clickableIDs; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CustomTabsSecondaryToolbar that = (CustomTabsSecondaryToolbar) o; + + if (!layout.equals(that.layout)) return false; + return clickableIDs.equals(that.clickableIDs); + } + + @Override + public int hashCode() { + int result = layout.hashCode(); + result = 31 * result + clickableIDs.hashCode(); + return result; + } + + @Override + public String toString() { + return "CustomTabsSecondaryToolbar{" + + "layout=" + layout + + ", clickableIDs=" + clickableIDs + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/Disposable.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/Disposable.java new file mode 100644 index 00000000..0cd54c0d --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/Disposable.java @@ -0,0 +1,5 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +public interface Disposable { + void dispose(); +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/DownloadStartRequest.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/DownloadStartRequest.java new file mode 100644 index 00000000..3dd8d3b1 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/DownloadStartRequest.java @@ -0,0 +1,150 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class DownloadStartRequest { + + @NonNull + private String url; + @NonNull + private String userAgent; + @NonNull + private String contentDisposition; + @NonNull + private String mimeType; + private long contentLength; + @Nullable + private String suggestedFilename; + @Nullable + private String textEncodingName; + + public DownloadStartRequest(@NonNull String url, @NonNull String userAgent, @NonNull String contentDisposition, @NonNull String mimeType, long contentLength, @Nullable String suggestedFilename, @Nullable String textEncodingName) { + this.url = url; + this.userAgent = userAgent; + this.contentDisposition = contentDisposition; + this.mimeType = mimeType; + this.contentLength = contentLength; + this.suggestedFilename = suggestedFilename; + this.textEncodingName = textEncodingName; + } + + public Map toMap() { + Map obj = new HashMap<>(); + obj.put("url", url); + obj.put("userAgent", userAgent); + obj.put("contentDisposition", contentDisposition); + obj.put("mimeType", mimeType); + obj.put("contentLength", contentLength); + obj.put("suggestedFilename", suggestedFilename); + obj.put("textEncodingName", textEncodingName); + return obj; + } + + @NonNull + public String getUrl() { + return url; + } + + public void setUrl(@NonNull String url) { + this.url = url; + } + + @NonNull + public String getUserAgent() { + return userAgent; + } + + public void setUserAgent(@NonNull String userAgent) { + this.userAgent = userAgent; + } + + @NonNull + public String getContentDisposition() { + return contentDisposition; + } + + public void setContentDisposition(@NonNull String contentDisposition) { + this.contentDisposition = contentDisposition; + } + + @NonNull + public String getMimeType() { + return mimeType; + } + + public void setMimeType(@NonNull String mimeType) { + this.mimeType = mimeType; + } + + public long getContentLength() { + return contentLength; + } + + public void setContentLength(long contentLength) { + this.contentLength = contentLength; + } + + @Nullable + public String getSuggestedFilename() { + return suggestedFilename; + } + + public void setSuggestedFilename(@Nullable String suggestedFilename) { + this.suggestedFilename = suggestedFilename; + } + + @Nullable + public String getTextEncodingName() { + return textEncodingName; + } + + public void setTextEncodingName(@Nullable String textEncodingName) { + this.textEncodingName = textEncodingName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + DownloadStartRequest that = (DownloadStartRequest) o; + + if (contentLength != that.contentLength) return false; + if (!url.equals(that.url)) return false; + if (!userAgent.equals(that.userAgent)) return false; + if (!contentDisposition.equals(that.contentDisposition)) return false; + if (!mimeType.equals(that.mimeType)) return false; + if (suggestedFilename != null ? !suggestedFilename.equals(that.suggestedFilename) : that.suggestedFilename != null) + return false; + return textEncodingName != null ? textEncodingName.equals(that.textEncodingName) : that.textEncodingName == null; + } + + @Override + public int hashCode() { + int result = url.hashCode(); + result = 31 * result + userAgent.hashCode(); + result = 31 * result + contentDisposition.hashCode(); + result = 31 * result + mimeType.hashCode(); + result = 31 * result + (int) (contentLength ^ (contentLength >>> 32)); + result = 31 * result + (suggestedFilename != null ? suggestedFilename.hashCode() : 0); + result = 31 * result + (textEncodingName != null ? textEncodingName.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "DownloadStartRequest{" + + "url='" + url + '\'' + + ", userAgent='" + userAgent + '\'' + + ", contentDisposition='" + contentDisposition + '\'' + + ", mimeType='" + mimeType + '\'' + + ", contentLength=" + contentLength + + ", suggestedFilename='" + suggestedFilename + '\'' + + ", textEncodingName='" + textEncodingName + '\'' + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/FindSession.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/FindSession.java new file mode 100644 index 00000000..a4df7d73 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/FindSession.java @@ -0,0 +1,76 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import java.util.HashMap; +import java.util.Map; + +public class FindSession { + private int resultCount; + private int highlightedResultIndex; + private int searchResultDisplayStyle = 2; // matches NONE of iOS + + public FindSession(int resultCount, int highlightedResultIndex) { + this.resultCount = resultCount; + this.highlightedResultIndex = highlightedResultIndex; + } + + public Map toMap() { + Map obj = new HashMap<>(); + obj.put("resultCount", resultCount); + obj.put("highlightedResultIndex", highlightedResultIndex); + obj.put("searchResultDisplayStyle", searchResultDisplayStyle); + return obj; + } + + public int getResultCount() { + return resultCount; + } + + public void setResultCount(int resultCount) { + this.resultCount = resultCount; + } + + public int getHighlightedResultIndex() { + return highlightedResultIndex; + } + + public void setHighlightedResultIndex(int highlightedResultIndex) { + this.highlightedResultIndex = highlightedResultIndex; + } + + public int getSearchResultDisplayStyle() { + return searchResultDisplayStyle; + } + + public void setSearchResultDisplayStyle(int searchResultDisplayStyle) { + this.searchResultDisplayStyle = searchResultDisplayStyle; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FindSession that = (FindSession) o; + + if (resultCount != that.resultCount) return false; + if (highlightedResultIndex != that.highlightedResultIndex) return false; + return searchResultDisplayStyle == that.searchResultDisplayStyle; + } + + @Override + public int hashCode() { + int result = resultCount; + result = 31 * result + highlightedResultIndex; + result = 31 * result + searchResultDisplayStyle; + return result; + } + + @Override + public String toString() { + return "FindSession{" + + "resultCount=" + resultCount + + ", highlightedResultIndex=" + highlightedResultIndex + + ", searchResultDisplayStyle=" + searchResultDisplayStyle + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/GeolocationPermissionShowPromptResponse.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/GeolocationPermissionShowPromptResponse.java new file mode 100644 index 00000000..14340e9a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/GeolocationPermissionShowPromptResponse.java @@ -0,0 +1,84 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Map; + +public class GeolocationPermissionShowPromptResponse { + @NonNull + private String origin; + boolean allow; + boolean retain; + + public GeolocationPermissionShowPromptResponse(@NonNull String origin, boolean allow, boolean retain) { + this.origin = origin; + this.allow = allow; + this.retain = retain; + } + + @Nullable + public static GeolocationPermissionShowPromptResponse fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String origin = (String) map.get("origin"); + boolean allow = (boolean) map.get("allow"); + boolean retain = (boolean) map.get("retain"); + return new GeolocationPermissionShowPromptResponse(origin, allow, retain); + } + + @NonNull + public String getOrigin() { + return origin; + } + + public void setOrigin(@NonNull String origin) { + this.origin = origin; + } + + public boolean isAllow() { + return allow; + } + + public void setAllow(boolean allow) { + this.allow = allow; + } + + public boolean isRetain() { + return retain; + } + + public void setRetain(boolean retain) { + this.retain = retain; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + GeolocationPermissionShowPromptResponse that = (GeolocationPermissionShowPromptResponse) o; + + if (allow != that.allow) return false; + if (retain != that.retain) return false; + return origin.equals(that.origin); + } + + @Override + public int hashCode() { + int result = origin.hashCode(); + result = 31 * result + (allow ? 1 : 0); + result = 31 * result + (retain ? 1 : 0); + return result; + } + + @Override + public String toString() { + return "GeolocationPermissionShowPromptResponse{" + + "origin='" + origin + '\'' + + ", allow=" + allow + + ", retain=" + retain + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/HitTestResult.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/HitTestResult.java new file mode 100644 index 00000000..87615fa5 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/HitTestResult.java @@ -0,0 +1,79 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.webkit.WebView; + +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class HitTestResult { + private int type; + @Nullable + private String extra; + + public HitTestResult(int type, @Nullable String extra) { + this.type = type; + this.extra = extra; + } + + @Nullable + static public HitTestResult fromWebViewHitTestResult(@Nullable WebView.HitTestResult hitTestResult) { + if (hitTestResult == null) { + return null; + } + + return new HitTestResult(hitTestResult.getType(), hitTestResult.getExtra()); + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + + @Nullable + public String getExtra() { + return extra; + } + + public void setExtra(@Nullable String extra) { + this.extra = extra; + } + + @Nullable + public Map toMap() { + Map hitTestResultMap = new HashMap<>(); + hitTestResultMap.put("type", type); + hitTestResultMap.put("extra", extra); + return hitTestResultMap; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + HitTestResult that = (HitTestResult) o; + + if (type != that.type) return false; + return extra != null ? extra.equals(that.extra) : that.extra == null; + } + + @Override + public int hashCode() { + int result = type; + result = 31 * result + (extra != null ? extra.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "HitTestResultMap{" + + "type=" + type + + ", extra='" + extra + '\'' + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/HttpAuthResponse.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/HttpAuthResponse.java new file mode 100644 index 00000000..123a1b02 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/HttpAuthResponse.java @@ -0,0 +1,102 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Map; + +public class HttpAuthResponse { + @NonNull + private String username; + @NonNull + private String password; + boolean permanentPersistence; + @Nullable + private Integer action; + + public HttpAuthResponse(@NonNull String username, @NonNull String password, boolean permanentPersistence, @Nullable Integer action) { + this.username = username; + this.password = password; + this.permanentPersistence = permanentPersistence; + this.action = action; + } + + @Nullable + public static HttpAuthResponse fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String username = (String) map.get("username"); + String password = (String) map.get("password"); + boolean permanentPersistence = (boolean) map.get("permanentPersistence"); + Integer action = (Integer) map.get("action"); + return new HttpAuthResponse(username, password, permanentPersistence, action); + } + + @NonNull + public String getUsername() { + return username; + } + + public void setUsername(@NonNull String username) { + this.username = username; + } + + @NonNull + public String getPassword() { + return password; + } + + public void setPassword(@NonNull String password) { + this.password = password; + } + + public boolean isPermanentPersistence() { + return permanentPersistence; + } + + public void setPermanentPersistence(boolean permanentPersistence) { + this.permanentPersistence = permanentPersistence; + } + + @Nullable + public Integer getAction() { + return action; + } + + public void setAction(@Nullable Integer action) { + this.action = action; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + HttpAuthResponse that = (HttpAuthResponse) o; + + if (permanentPersistence != that.permanentPersistence) return false; + if (!username.equals(that.username)) return false; + if (!password.equals(that.password)) return false; + return action != null ? action.equals(that.action) : that.action == null; + } + + @Override + public int hashCode() { + int result = username.hashCode(); + result = 31 * result + password.hashCode(); + result = 31 * result + (permanentPersistence ? 1 : 0); + result = 31 * result + (action != null ? action.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "HttpAuthResponse{" + + "username='" + username + '\'' + + ", password='" + password + '\'' + + ", permanentPersistence=" + permanentPersistence + + ", action=" + action + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/HttpAuthenticationChallenge.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/HttpAuthenticationChallenge.java new file mode 100644 index 00000000..66edfa4a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/HttpAuthenticationChallenge.java @@ -0,0 +1,71 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.Nullable; + +import java.util.Map; + +public class HttpAuthenticationChallenge extends URLAuthenticationChallenge { + private int previousFailureCount; + @Nullable + URLCredential proposedCredential; + + public HttpAuthenticationChallenge(URLProtectionSpace protectionSpace, int previousFailureCount, @Nullable URLCredential proposedCredential) { + super(protectionSpace); + this.previousFailureCount = previousFailureCount; + this.proposedCredential = proposedCredential; + } + + public Map toMap() { + Map challengeMap = super.toMap(); + challengeMap.put("previousFailureCount", previousFailureCount); + challengeMap.put("proposedCredential", (proposedCredential != null) ? proposedCredential.toMap() : null); + challengeMap.put("failureResponse", null); + challengeMap.put("error", null); + return challengeMap; + } + + public int getPreviousFailureCount() { + return previousFailureCount; + } + + public void setPreviousFailureCount(int previousFailureCount) { + this.previousFailureCount = previousFailureCount; + } + + @Nullable + public URLCredential getProposedCredential() { + return proposedCredential; + } + + public void setProposedCredential(@Nullable URLCredential proposedCredential) { + this.proposedCredential = proposedCredential; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + HttpAuthenticationChallenge that = (HttpAuthenticationChallenge) o; + + if (previousFailureCount != that.previousFailureCount) return false; + return proposedCredential != null ? proposedCredential.equals(that.proposedCredential) : that.proposedCredential == null; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + previousFailureCount; + result = 31 * result + (proposedCredential != null ? proposedCredential.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "HttpAuthenticationChallenge{" + + "previousFailureCount=" + previousFailureCount + + ", proposedCredential=" + proposedCredential + + "} " + super.toString(); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ICallbackResult.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ICallbackResult.java new file mode 100644 index 00000000..02515fca --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ICallbackResult.java @@ -0,0 +1,13 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import io.flutter.plugin.common.MethodChannel; + +public interface ICallbackResult extends MethodChannel.Result { + boolean nonNullSuccess(@NonNull T result); + boolean nullSuccess(); + void defaultBehaviour(@Nullable T result); + @Nullable T decodeResult(@Nullable Object obj); +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/IChannelDelegate.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/IChannelDelegate.java new file mode 100644 index 00000000..4d1ca8f8 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/IChannelDelegate.java @@ -0,0 +1,10 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.Nullable; + +import io.flutter.plugin.common.MethodChannel; + +public interface IChannelDelegate extends MethodChannel.MethodCallHandler, Disposable { + @Nullable + MethodChannel getChannel(); +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/InAppBrowserMenuItem.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/InAppBrowserMenuItem.java new file mode 100644 index 00000000..85bba6e3 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/InAppBrowserMenuItem.java @@ -0,0 +1,146 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.Util; + +import java.util.Map; +import java.util.Objects; + +public class InAppBrowserMenuItem { + private int id; + + @NonNull + private String title; + + @Nullable + private Integer order; + + @Nullable + private Object icon; + + @Nullable + private String iconColor; + + private boolean showAsAction; + + public InAppBrowserMenuItem(int id, @NonNull String title, @Nullable Integer order, @Nullable Object icon, + @Nullable String iconColor, boolean showAsAction) { + this.id = id; + this.title = title; + this.order = order; + this.icon = icon; + this.iconColor = iconColor; + this.showAsAction = showAsAction; + } + + @Nullable + public static InAppBrowserMenuItem fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + int id = (int) map.get("id"); + String title = (String) map.get("title"); + Integer order = (Integer) map.get("order"); + Object icon = map.get("icon"); + if (icon instanceof Map) { + icon = AndroidResource.fromMap((Map) map.get("icon")); + } else if (!(icon instanceof byte[])) { + icon = null; + } + String iconColor = (String) map.get("iconColor"); + boolean showAsAction = Util.getOrDefault( map, "showAsAction", false); + return new InAppBrowserMenuItem(id, title, order, icon, iconColor, showAsAction); + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @NonNull + public String getTitle() { + return title; + } + + public void setTitle(@NonNull String title) { + this.title = title; + } + + @Nullable + public Integer getOrder() { + return order; + } + + public void setOrder(@Nullable Integer order) { + this.order = order; + } + + @Nullable + public Object getIcon() { + return icon; + } + + public void setIcon(@Nullable Object icon) { + this.icon = icon; + } + + @Nullable + public String getIconColor() { + return iconColor; + } + + public void setIconColor(@Nullable String iconColor) { + this.iconColor = iconColor; + } + + public boolean isShowAsAction() { + return showAsAction; + } + + public void setShowAsAction(boolean showAsAction) { + this.showAsAction = showAsAction; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + InAppBrowserMenuItem that = (InAppBrowserMenuItem) o; + + if (id != that.id) return false; + if (showAsAction != that.showAsAction) return false; + if (!title.equals(that.title)) return false; + if (!Objects.equals(order, that.order)) return false; + if (!Objects.equals(icon, that.icon)) return false; + return Objects.equals(iconColor, that.iconColor); + } + + @Override + public int hashCode() { + int result = id; + result = 31 * result + title.hashCode(); + result = 31 * result + (order != null ? order.hashCode() : 0); + result = 31 * result + (icon != null ? icon.hashCode() : 0); + result = 31 * result + (iconColor != null ? iconColor.hashCode() : 0); + result = 31 * result + (showAsAction ? 1 : 0); + return result; + } + + @Override + public String toString() { + return "InAppBrowserMenuItem{" + + "id=" + id + + ", title='" + title + '\'' + + ", order=" + order + + ", icon=" + icon + + ", iconColor='" + iconColor + '\'' + + ", showAsAction=" + showAsAction + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsAlertResponse.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsAlertResponse.java new file mode 100644 index 00000000..6f3f0925 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsAlertResponse.java @@ -0,0 +1,98 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.Nullable; + +import java.util.Map; + +public class JsAlertResponse { + private String message; + private String confirmButtonTitle; + private boolean handledByClient; + @Nullable + private Integer action; + + public JsAlertResponse(String message, String confirmButtonTitle, boolean handledByClient, @Nullable Integer action) { + this.message = message; + this.confirmButtonTitle = confirmButtonTitle; + this.handledByClient = handledByClient; + this.action = action; + } + + @Nullable + public static JsAlertResponse fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String message = (String) map.get("message"); + String confirmButtonTitle = (String) map.get("confirmButtonTitle"); + boolean handledByClient = (boolean) map.get("handledByClient"); + Integer action = (Integer) map.get("action"); + return new JsAlertResponse(message, confirmButtonTitle, handledByClient, action); + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getConfirmButtonTitle() { + return confirmButtonTitle; + } + + public void setConfirmButtonTitle(String confirmButtonTitle) { + this.confirmButtonTitle = confirmButtonTitle; + } + + public boolean isHandledByClient() { + return handledByClient; + } + + public void setHandledByClient(boolean handledByClient) { + this.handledByClient = handledByClient; + } + + @Nullable + public Integer getAction() { + return action; + } + + public void setAction(@Nullable Integer action) { + this.action = action; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + JsAlertResponse that = (JsAlertResponse) o; + + if (handledByClient != that.handledByClient) return false; + if (message != null ? !message.equals(that.message) : that.message != null) return false; + if (confirmButtonTitle != null ? !confirmButtonTitle.equals(that.confirmButtonTitle) : that.confirmButtonTitle != null) + return false; + return action != null ? action.equals(that.action) : that.action == null; + } + + @Override + public int hashCode() { + int result = message != null ? message.hashCode() : 0; + result = 31 * result + (confirmButtonTitle != null ? confirmButtonTitle.hashCode() : 0); + result = 31 * result + (handledByClient ? 1 : 0); + result = 31 * result + (action != null ? action.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "JsAlertResponse{" + + "message='" + message + '\'' + + ", confirmButtonTitle='" + confirmButtonTitle + '\'' + + ", handledByClient=" + handledByClient + + ", action=" + action + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsBeforeUnloadResponse.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsBeforeUnloadResponse.java new file mode 100644 index 00000000..ead00e00 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsBeforeUnloadResponse.java @@ -0,0 +1,113 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.Nullable; + +import java.util.Map; + +public class JsBeforeUnloadResponse { + private String message; + private String confirmButtonTitle; + private String cancelButtonTitle; + private boolean handledByClient; + @Nullable + private Integer action; + + public JsBeforeUnloadResponse(String message, String confirmButtonTitle, String cancelButtonTitle, boolean handledByClient, @Nullable Integer action) { + this.message = message; + this.confirmButtonTitle = confirmButtonTitle; + this.cancelButtonTitle = cancelButtonTitle; + this.handledByClient = handledByClient; + this.action = action; + } + + @Nullable + public static JsBeforeUnloadResponse fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String message = (String) map.get("message"); + String confirmButtonTitle = (String) map.get("confirmButtonTitle"); + String cancelButtonTitle = (String) map.get("cancelButtonTitle"); + boolean handledByClient = (boolean) map.get("handledByClient"); + Integer action = (Integer) map.get("action"); + return new JsBeforeUnloadResponse(message, confirmButtonTitle, cancelButtonTitle, handledByClient, action); + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getConfirmButtonTitle() { + return confirmButtonTitle; + } + + public void setConfirmButtonTitle(String confirmButtonTitle) { + this.confirmButtonTitle = confirmButtonTitle; + } + + public String getCancelButtonTitle() { + return cancelButtonTitle; + } + + public void setCancelButtonTitle(String cancelButtonTitle) { + this.cancelButtonTitle = cancelButtonTitle; + } + + public boolean isHandledByClient() { + return handledByClient; + } + + public void setHandledByClient(boolean handledByClient) { + this.handledByClient = handledByClient; + } + + @Nullable + public Integer getAction() { + return action; + } + + public void setAction(@Nullable Integer action) { + this.action = action; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + JsBeforeUnloadResponse that = (JsBeforeUnloadResponse) o; + + if (handledByClient != that.handledByClient) return false; + if (message != null ? !message.equals(that.message) : that.message != null) return false; + if (confirmButtonTitle != null ? !confirmButtonTitle.equals(that.confirmButtonTitle) : that.confirmButtonTitle != null) + return false; + if (cancelButtonTitle != null ? !cancelButtonTitle.equals(that.cancelButtonTitle) : that.cancelButtonTitle != null) + return false; + return action != null ? action.equals(that.action) : that.action == null; + } + + @Override + public int hashCode() { + int result = message != null ? message.hashCode() : 0; + result = 31 * result + (confirmButtonTitle != null ? confirmButtonTitle.hashCode() : 0); + result = 31 * result + (cancelButtonTitle != null ? cancelButtonTitle.hashCode() : 0); + result = 31 * result + (handledByClient ? 1 : 0); + result = 31 * result + (action != null ? action.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "JsConfirmResponse{" + + "message='" + message + '\'' + + ", confirmButtonTitle='" + confirmButtonTitle + '\'' + + ", cancelButtonTitle='" + cancelButtonTitle + '\'' + + ", handledByClient=" + handledByClient + + ", action=" + action + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsConfirmResponse.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsConfirmResponse.java new file mode 100644 index 00000000..af65e41a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsConfirmResponse.java @@ -0,0 +1,113 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.Nullable; + +import java.util.Map; + +public class JsConfirmResponse { + private String message; + private String confirmButtonTitle; + private String cancelButtonTitle; + private boolean handledByClient; + @Nullable + private Integer action; + + public JsConfirmResponse(String message, String confirmButtonTitle, String cancelButtonTitle, boolean handledByClient, @Nullable Integer action) { + this.message = message; + this.confirmButtonTitle = confirmButtonTitle; + this.cancelButtonTitle = cancelButtonTitle; + this.handledByClient = handledByClient; + this.action = action; + } + + @Nullable + public static JsConfirmResponse fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String message = (String) map.get("message"); + String confirmButtonTitle = (String) map.get("confirmButtonTitle"); + String cancelButtonTitle = (String) map.get("cancelButtonTitle"); + boolean handledByClient = (boolean) map.get("handledByClient"); + Integer action = (Integer) map.get("action"); + return new JsConfirmResponse(message, confirmButtonTitle, cancelButtonTitle, handledByClient, action); + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getConfirmButtonTitle() { + return confirmButtonTitle; + } + + public void setConfirmButtonTitle(String confirmButtonTitle) { + this.confirmButtonTitle = confirmButtonTitle; + } + + public String getCancelButtonTitle() { + return cancelButtonTitle; + } + + public void setCancelButtonTitle(String cancelButtonTitle) { + this.cancelButtonTitle = cancelButtonTitle; + } + + public boolean isHandledByClient() { + return handledByClient; + } + + public void setHandledByClient(boolean handledByClient) { + this.handledByClient = handledByClient; + } + + @Nullable + public Integer getAction() { + return action; + } + + public void setAction(@Nullable Integer action) { + this.action = action; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + JsConfirmResponse that = (JsConfirmResponse) o; + + if (handledByClient != that.handledByClient) return false; + if (message != null ? !message.equals(that.message) : that.message != null) return false; + if (confirmButtonTitle != null ? !confirmButtonTitle.equals(that.confirmButtonTitle) : that.confirmButtonTitle != null) + return false; + if (cancelButtonTitle != null ? !cancelButtonTitle.equals(that.cancelButtonTitle) : that.cancelButtonTitle != null) + return false; + return action != null ? action.equals(that.action) : that.action == null; + } + + @Override + public int hashCode() { + int result = message != null ? message.hashCode() : 0; + result = 31 * result + (confirmButtonTitle != null ? confirmButtonTitle.hashCode() : 0); + result = 31 * result + (cancelButtonTitle != null ? cancelButtonTitle.hashCode() : 0); + result = 31 * result + (handledByClient ? 1 : 0); + result = 31 * result + (action != null ? action.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "JsConfirmResponse{" + + "message='" + message + '\'' + + ", confirmButtonTitle='" + confirmButtonTitle + '\'' + + ", cancelButtonTitle='" + cancelButtonTitle + '\'' + + ", handledByClient=" + handledByClient + + ", action=" + action + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsPromptResponse.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsPromptResponse.java new file mode 100644 index 00000000..fb63aef7 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/JsPromptResponse.java @@ -0,0 +1,145 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.Nullable; + +import java.util.Map; + +public class JsPromptResponse { + private String message; + private String defaultValue; + private String confirmButtonTitle; + private String cancelButtonTitle; + private boolean handledByClient; + @Nullable + private String value; + @Nullable + private Integer action; + + public JsPromptResponse(String message, String defaultValue, String confirmButtonTitle, + String cancelButtonTitle, boolean handledByClient, @Nullable String value, @Nullable Integer action) { + this.message = message; + this.defaultValue = defaultValue; + this.confirmButtonTitle = confirmButtonTitle; + this.cancelButtonTitle = cancelButtonTitle; + this.handledByClient = handledByClient; + this.value = value; + this.action = action; + } + + @Nullable + public static JsPromptResponse fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String message = (String) map.get("message"); + String defaultValue = (String) map.get("defaultValue"); + String confirmButtonTitle = (String) map.get("confirmButtonTitle"); + String cancelButtonTitle = (String) map.get("cancelButtonTitle"); + boolean handledByClient = (boolean) map.get("handledByClient"); + String value = (String) map.get("value"); + Integer action = (Integer) map.get("action"); + return new JsPromptResponse(message, defaultValue, confirmButtonTitle, cancelButtonTitle, handledByClient, value, action); + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getDefaultValue() { + return defaultValue; + } + + public void setDefaultValue(String defaultValue) { + this.defaultValue = defaultValue; + } + + public String getConfirmButtonTitle() { + return confirmButtonTitle; + } + + public void setConfirmButtonTitle(String confirmButtonTitle) { + this.confirmButtonTitle = confirmButtonTitle; + } + + public String getCancelButtonTitle() { + return cancelButtonTitle; + } + + public void setCancelButtonTitle(String cancelButtonTitle) { + this.cancelButtonTitle = cancelButtonTitle; + } + + public boolean isHandledByClient() { + return handledByClient; + } + + public void setHandledByClient(boolean handledByClient) { + this.handledByClient = handledByClient; + } + + @Nullable + public String getValue() { + return value; + } + + public void setValue(@Nullable String value) { + this.value = value; + } + + @Nullable + public Integer getAction() { + return action; + } + + public void setAction(@Nullable Integer action) { + this.action = action; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + JsPromptResponse that = (JsPromptResponse) o; + + if (handledByClient != that.handledByClient) return false; + if (message != null ? !message.equals(that.message) : that.message != null) return false; + if (defaultValue != null ? !defaultValue.equals(that.defaultValue) : that.defaultValue != null) + return false; + if (confirmButtonTitle != null ? !confirmButtonTitle.equals(that.confirmButtonTitle) : that.confirmButtonTitle != null) + return false; + if (cancelButtonTitle != null ? !cancelButtonTitle.equals(that.cancelButtonTitle) : that.cancelButtonTitle != null) + return false; + if (value != null ? !value.equals(that.value) : that.value != null) return false; + return action != null ? action.equals(that.action) : that.action == null; + } + + @Override + public int hashCode() { + int result = message != null ? message.hashCode() : 0; + result = 31 * result + (defaultValue != null ? defaultValue.hashCode() : 0); + result = 31 * result + (confirmButtonTitle != null ? confirmButtonTitle.hashCode() : 0); + result = 31 * result + (cancelButtonTitle != null ? cancelButtonTitle.hashCode() : 0); + result = 31 * result + (handledByClient ? 1 : 0); + result = 31 * result + (value != null ? value.hashCode() : 0); + result = 31 * result + (action != null ? action.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "JsPromptResponse{" + + "message='" + message + '\'' + + ", defaultValue='" + defaultValue + '\'' + + ", confirmButtonTitle='" + confirmButtonTitle + '\'' + + ", cancelButtonTitle='" + cancelButtonTitle + '\'' + + ", handledByClient=" + handledByClient + + ", value='" + value + '\'' + + ", action=" + action + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/MarginsExt.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/MarginsExt.java new file mode 100644 index 00000000..161254d5 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/MarginsExt.java @@ -0,0 +1,150 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.os.Build; +import android.print.PrintAttributes; + +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import java.util.HashMap; +import java.util.Map; + +@RequiresApi(api = Build.VERSION_CODES.KITKAT) +public class MarginsExt { + private double top; + private double right; + private double bottom; + private double left; + + public MarginsExt() {} + + public MarginsExt(double top, double right, double bottom, double left) { + this.top = top; + this.right = right; + this.bottom = bottom; + this.left = left; + } + + @Nullable + public static MarginsExt fromMargins(@Nullable PrintAttributes.Margins margins) { + if (margins == null) { + return null; + } + MarginsExt marginsExt = new MarginsExt(); + marginsExt.top = milsToPixels(margins.getTopMils()); + marginsExt.right = milsToPixels(margins.getRightMils()); + marginsExt.bottom = milsToPixels(margins.getBottomMils()); + marginsExt.left = milsToPixels(margins.getLeftMils()); + return marginsExt; + } + + @Nullable + public static MarginsExt fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + return new MarginsExt( + (double) map.get("top"), + (double) map.get("right"), + (double) map.get("bottom"), + (double) map.get("left")); + } + + public PrintAttributes.Margins toMargins() { + return new PrintAttributes.Margins( + pixelsToMils(left), + pixelsToMils(top), + pixelsToMils(right), + pixelsToMils(bottom) + ); + } + + // from mils to pixels + private static double milsToPixels(int mils) { + return mils * 0.09600001209449; + } + + // from pixels to mils + private static int pixelsToMils(double pixels) { + return (int) Math.round(pixels * 10.416665354331); + } + + public Map toMap() { + Map obj = new HashMap<>(); + obj.put("top", top); + obj.put("right", right); + obj.put("bottom", bottom); + obj.put("left", left); + return obj; + } + + public double getTop() { + return top; + } + + public void setTop(double top) { + this.top = top; + } + + public double getRight() { + return right; + } + + public void setRight(double right) { + this.right = right; + } + + public double getBottom() { + return bottom; + } + + public void setBottom(double bottom) { + this.bottom = bottom; + } + + public double getLeft() { + return left; + } + + public void setLeft(double left) { + this.left = left; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MarginsExt that = (MarginsExt) o; + + if (Double.compare(that.top, top) != 0) return false; + if (Double.compare(that.right, right) != 0) return false; + if (Double.compare(that.bottom, bottom) != 0) return false; + return Double.compare(that.left, left) == 0; + } + + @Override + public int hashCode() { + int result; + long temp; + temp = Double.doubleToLongBits(top); + result = (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(right); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(bottom); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(left); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public String toString() { + return "MarginsExt{" + + "top=" + top + + ", right=" + right + + ", bottom=" + bottom + + ", left=" + left + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/MediaSizeExt.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/MediaSizeExt.java new file mode 100644 index 00000000..cd2618f2 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/MediaSizeExt.java @@ -0,0 +1,134 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.os.Build; +import android.print.PrintAttributes; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import java.util.HashMap; +import java.util.Map; + +@RequiresApi(api = Build.VERSION_CODES.KITKAT) +public class MediaSizeExt { + @NonNull + private String id; + @Nullable + private String label; + private int widthMils; + private int heightMils; + + public MediaSizeExt(@NonNull String id, @Nullable String label, int widthMils, int heightMils) { + this.id = id; + this.label = label; + this.widthMils = widthMils; + this.heightMils = heightMils; + } + + @Nullable + public static MediaSizeExt fromMediaSize(@Nullable PrintAttributes.MediaSize mediaSize) { + if (mediaSize == null) { + return null; + } + return new MediaSizeExt( + mediaSize.getId(), + null, + mediaSize.getHeightMils(), + mediaSize.getWidthMils() + ); + } + + @Nullable + public static MediaSizeExt fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String id = (String) map.get("id"); + String label = (String) map.get("label"); + int widthMils = (int) map.get("widthMils"); + int heightMils = (int) map.get("heightMils"); + return new MediaSizeExt(id, label, widthMils, heightMils); + } + + public PrintAttributes.MediaSize toMediaSize() { + return new PrintAttributes.MediaSize( + id, "Custom", widthMils, heightMils + ); + } + + public Map toMap() { + Map obj = new HashMap<>(); + obj.put("id", id); + obj.put("label", label); + obj.put("heightMils", heightMils); + obj.put("widthMils", widthMils); + return obj; + } + + @NonNull + public String getId() { + return id; + } + + public void setId(@NonNull String id) { + this.id = id; + } + + @Nullable + public String getLabel() { + return label; + } + + public void setLabel(@Nullable String label) { + this.label = label; + } + + public int getWidthMils() { + return widthMils; + } + + public void setWidthMils(int widthMils) { + this.widthMils = widthMils; + } + + public int getHeightMils() { + return heightMils; + } + + public void setHeightMils(int heightMils) { + this.heightMils = heightMils; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MediaSizeExt that = (MediaSizeExt) o; + + if (widthMils != that.widthMils) return false; + if (heightMils != that.heightMils) return false; + if (!id.equals(that.id)) return false; + return label != null ? label.equals(that.label) : that.label == null; + } + + @Override + public int hashCode() { + int result = id.hashCode(); + result = 31 * result + (label != null ? label.hashCode() : 0); + result = 31 * result + widthMils; + result = 31 * result + heightMils; + return result; + } + + @Override + public String toString() { + return "MediaSizeExt{" + + "id='" + id + '\'' + + ", label='" + label + '\'' + + ", widthMils=" + widthMils + + ", heightMils=" + heightMils + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/NavigationAction.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/NavigationAction.java new file mode 100644 index 00000000..faf5e596 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/NavigationAction.java @@ -0,0 +1,95 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import java.util.HashMap; +import java.util.Map; + +public class NavigationAction { + URLRequest request; + boolean isForMainFrame; + boolean hasGesture; + boolean isRedirect; + + public NavigationAction(URLRequest request, boolean isForMainFrame, boolean hasGesture, boolean isRedirect) { + this.request = request; + this.isForMainFrame = isForMainFrame; + this.hasGesture = hasGesture; + this.isRedirect = isRedirect; + } + + public Map toMap() { + Map navigationActionMap = new HashMap<>(); + navigationActionMap.put("request", request.toMap()); + navigationActionMap.put("isForMainFrame", isForMainFrame); + navigationActionMap.put("hasGesture", hasGesture); + navigationActionMap.put("isRedirect", isRedirect); + navigationActionMap.put("navigationType", null); + navigationActionMap.put("sourceFrame", null); + navigationActionMap.put("targetFrame", null); + navigationActionMap.put("shouldPerformDownload", null); + return navigationActionMap; + } + + public URLRequest getRequest() { + return request; + } + + public void setRequest(URLRequest request) { + this.request = request; + } + + public boolean isForMainFrame() { + return isForMainFrame; + } + + public void setForMainFrame(boolean forMainFrame) { + isForMainFrame = forMainFrame; + } + + public boolean isHasGesture() { + return hasGesture; + } + + public void setHasGesture(boolean hasGesture) { + this.hasGesture = hasGesture; + } + + public boolean isRedirect() { + return isRedirect; + } + + public void setRedirect(boolean redirect) { + isRedirect = redirect; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + NavigationAction that = (NavigationAction) o; + + if (isForMainFrame != that.isForMainFrame) return false; + if (hasGesture != that.hasGesture) return false; + if (isRedirect != that.isRedirect) return false; + return request.equals(that.request); + } + + @Override + public int hashCode() { + int result = request.hashCode(); + result = 31 * result + (isForMainFrame ? 1 : 0); + result = 31 * result + (hasGesture ? 1 : 0); + result = 31 * result + (isRedirect ? 1 : 0); + return result; + } + + @Override + public String toString() { + return "NavigationAction{" + + "request=" + request + + ", isForMainFrame=" + isForMainFrame + + ", hasGesture=" + hasGesture + + ", isRedirect=" + isRedirect + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/NavigationActionPolicy.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/NavigationActionPolicy.java new file mode 100644 index 00000000..c6e0fbba --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/NavigationActionPolicy.java @@ -0,0 +1,33 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +public enum NavigationActionPolicy { + CANCEL(0), + ALLOW(1); + + private final int value; + + private NavigationActionPolicy(int value) { + this.value = value; + } + + public boolean equalsValue(int otherValue) { + return value == otherValue; + } + + public static NavigationActionPolicy fromValue(int value) { + for(NavigationActionPolicy type : NavigationActionPolicy.values()) { + if(value == type.value) + return type; + } + throw new IllegalArgumentException("No enum constant: " + value); + } + + public int rawValue() { + return this.value; + } + + @Override + public String toString() { + return String.valueOf(this.value); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PermissionResponse.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PermissionResponse.java new file mode 100644 index 00000000..f3848dd7 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PermissionResponse.java @@ -0,0 +1,73 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.List; +import java.util.Map; + +public class PermissionResponse { + @NonNull + private List resources; + @Nullable + private Integer action; + + public PermissionResponse(@NonNull List resources, @Nullable Integer action) { + this.resources = resources; + this.action = action; + } + + @Nullable + public static PermissionResponse fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + List resources = (List) map.get("resources"); + Integer action = (Integer) map.get("action"); + return new PermissionResponse(resources, action); + } + + @NonNull + public List getResources() { + return resources; + } + + public void setResources(@NonNull List resources) { + this.resources = resources; + } + + @Nullable + public Integer getAction() { + return action; + } + + public void setAction(@Nullable Integer action) { + this.action = action; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PermissionResponse that = (PermissionResponse) o; + + if (!resources.equals(that.resources)) return false; + return action != null ? action.equals(that.action) : that.action == null; + } + + @Override + public int hashCode() { + int result = resources.hashCode(); + result = 31 * result + (action != null ? action.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "PermissionResponse{" + + "resources=" + resources + + ", action=" + action + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PluginScript.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PluginScript.java new file mode 100644 index 00000000..b16f2280 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PluginScript.java @@ -0,0 +1,48 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Set; + +public class PluginScript extends UserScript { + private boolean requiredInAllContentWorlds; + + public PluginScript(@Nullable String groupName, @NonNull String source, @NonNull UserScriptInjectionTime injectionTime, @Nullable ContentWorld contentWorld, boolean requiredInAllContentWorlds, @Nullable Set allowedOriginRules) { + super(groupName, source, injectionTime, contentWorld, allowedOriginRules); + this.requiredInAllContentWorlds = requiredInAllContentWorlds; + } + + public boolean isRequiredInAllContentWorlds() { + return requiredInAllContentWorlds; + } + + public void setRequiredInAllContentWorlds(boolean requiredInAllContentWorlds) { + this.requiredInAllContentWorlds = requiredInAllContentWorlds; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + + PluginScript that = (PluginScript) o; + + return requiredInAllContentWorlds == that.requiredInAllContentWorlds; + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + (requiredInAllContentWorlds ? 1 : 0); + return result; + } + + @Override + public String toString() { + return "PluginScript{" + + "requiredInContentWorld=" + requiredInAllContentWorlds + + "} " + super.toString(); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PreferredContentModeOptionType.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PreferredContentModeOptionType.java new file mode 100755 index 00000000..bb5bfa0a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PreferredContentModeOptionType.java @@ -0,0 +1,29 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +public enum PreferredContentModeOptionType { + RECOMMENDED (0), + MOBILE (1), + DESKTOP (2); + + private final int value; + + private PreferredContentModeOptionType(int value) { + this.value = value; + } + + public boolean equalsValue(int otherValue) { + return value == otherValue; + } + + public static PreferredContentModeOptionType fromValue(int value) { + for( PreferredContentModeOptionType type : PreferredContentModeOptionType.values()) { + if(value == type.toValue()) + return type; + } + throw new IllegalArgumentException("No enum constant: " + value); + } + + public int toValue() { + return this.value; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PrintAttributesExt.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PrintAttributesExt.java new file mode 100644 index 00000000..77b65528 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PrintAttributesExt.java @@ -0,0 +1,56 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.os.Build; +import android.print.PrintAttributes; + +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import java.util.HashMap; +import java.util.Map; + +@RequiresApi(api = Build.VERSION_CODES.KITKAT) +public class PrintAttributesExt { + private int colorMode; + @Nullable + private Integer duplex; + @Nullable + private Integer orientation; + @Nullable + private MediaSizeExt mediaSize; + @Nullable + private ResolutionExt resolution; + @Nullable + private MarginsExt margins; + + @Nullable + public static PrintAttributesExt fromPrintAttributes(@Nullable PrintAttributes attributes) { + if (attributes == null) { + return null; + } + PrintAttributesExt attributesExt = new PrintAttributesExt(); + attributesExt.colorMode = attributes.getColorMode(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + attributesExt.duplex = attributes.getDuplexMode(); + } + PrintAttributes.MediaSize mediaSize = attributes.getMediaSize(); + if (mediaSize != null) { + attributesExt.mediaSize = MediaSizeExt.fromMediaSize(mediaSize); + attributesExt.orientation = mediaSize.isPortrait() ? 0 : 1; + } + attributesExt.resolution = ResolutionExt.fromResolution(attributes.getResolution()); + attributesExt.margins = MarginsExt.fromMargins(attributes.getMinMargins()); + return attributesExt; + } + + public Map toMap() { + Map obj = new HashMap<>(); + obj.put("colorMode", colorMode); + obj.put("duplex", duplex); + obj.put("orientation", orientation); + obj.put("mediaSize", mediaSize != null ? mediaSize.toMap() : null); + obj.put("resolution", resolution != null ? resolution.toMap() : null); + obj.put("margins", margins != null ? margins.toMap() : null); + return obj; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PrintJobInfoExt.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PrintJobInfoExt.java new file mode 100644 index 00000000..e58d851a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/PrintJobInfoExt.java @@ -0,0 +1,56 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.os.Build; +import android.print.PrintJobInfo; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import java.util.HashMap; +import java.util.Map; + +@RequiresApi(api = Build.VERSION_CODES.KITKAT) +public class PrintJobInfoExt { + private int state; + private int copies; + @Nullable + private Integer numberOfPages; + private long creationTime; + @NonNull + private String label; + @Nullable + private String printerId; + @Nullable + private PrintAttributesExt attributes; + + @Nullable + public static PrintJobInfoExt fromPrintJobInfo(@Nullable PrintJobInfo info) { + if (info == null) { + return null; + } + PrintJobInfoExt printJobInfoExt = new PrintJobInfoExt(); + printJobInfoExt.state = info.getState(); + printJobInfoExt.copies = info.getCopies(); + printJobInfoExt.numberOfPages = info.getPages() != null ? info.getPages().length : null; + printJobInfoExt.creationTime = info.getCreationTime(); + printJobInfoExt.label = info.getLabel(); + printJobInfoExt.printerId = info.getPrinterId() != null ? info.getPrinterId().getLocalId() : null; + printJobInfoExt.attributes = PrintAttributesExt.fromPrintAttributes(info.getAttributes()); + return printJobInfoExt; + } + + public Map toMap() { + Map obj = new HashMap<>(); + obj.put("state", state); + obj.put("copies", copies); + obj.put("numberOfPages", numberOfPages); + obj.put("creationTime", creationTime); + obj.put("label", label); + Map printer = new HashMap<>(); + printer.put("id", printerId); + obj.put("printer", printer); + obj.put("attributes", attributes != null ? attributes.toMap() : null); + return obj; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ProxyRuleExt.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ProxyRuleExt.java new file mode 100644 index 00000000..52081a38 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ProxyRuleExt.java @@ -0,0 +1,83 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.ProxyConfig; + +import java.util.HashMap; +import java.util.Map; + +public class ProxyRuleExt { + @Nullable + @ProxyConfig.ProxyScheme + private String schemeFilter; + @NonNull + private String url; + + public ProxyRuleExt(@Nullable @ProxyConfig.ProxyScheme String schemeFilter, @NonNull String url) { + this.schemeFilter = schemeFilter; + this.url = url; + } + + @Nullable + public static ProxyRuleExt fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String url = (String) map.get("url"); + String schemeFilter = (String) map.get("schemeFilter"); + return new ProxyRuleExt(schemeFilter, url); + } + + public Map toMap() { + Map proxyRuleMap = new HashMap<>(); + proxyRuleMap.put("url", url); + proxyRuleMap.put("schemeFilter", schemeFilter); + return proxyRuleMap; + } + + @Nullable + public String getSchemeFilter() { + return schemeFilter; + } + + public void setSchemeFilter(@Nullable String schemeFilter) { + this.schemeFilter = schemeFilter; + } + + @NonNull + public String getUrl() { + return url; + } + + public void setUrl(@NonNull String url) { + this.url = url; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ProxyRuleExt that = (ProxyRuleExt) o; + + if (schemeFilter != null ? !schemeFilter.equals(that.schemeFilter) : that.schemeFilter != null) + return false; + return url.equals(that.url); + } + + @Override + public int hashCode() { + int result = schemeFilter != null ? schemeFilter.hashCode() : 0; + result = 31 * result + url.hashCode(); + return result; + } + + @Override + public String toString() { + return "ProxyRuleExt{" + + "schemeFilter='" + schemeFilter + '\'' + + ", url='" + url + '\'' + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ResolutionExt.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ResolutionExt.java new file mode 100644 index 00000000..daca1117 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ResolutionExt.java @@ -0,0 +1,134 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.os.Build; +import android.print.PrintAttributes; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import java.util.HashMap; +import java.util.Map; + +@RequiresApi(api = Build.VERSION_CODES.KITKAT) +public class ResolutionExt { + @NonNull + private String id; + @NonNull + private String label; + private int verticalDpi; + private int horizontalDpi; + + public ResolutionExt(@NonNull String id, @NonNull String label, int verticalDpi, int horizontalDpi) { + this.id = id; + this.label = label; + this.verticalDpi = verticalDpi; + this.horizontalDpi = horizontalDpi; + } + + @Nullable + public static ResolutionExt fromResolution(@Nullable PrintAttributes.Resolution resolution) { + if (resolution == null) { + return null; + } + return new ResolutionExt( + resolution.getId(), + resolution.getLabel(), + resolution.getVerticalDpi(), + resolution.getHorizontalDpi() + ); + } + + @Nullable + public static ResolutionExt fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String id = (String) map.get("id"); + String label = (String) map.get("label"); + int verticalDpi = (int) map.get("verticalDpi"); + int horizontalDpi = (int) map.get("horizontalDpi"); + return new ResolutionExt(id, label, verticalDpi, horizontalDpi); + } + + public PrintAttributes.Resolution toResolution() { + return new PrintAttributes.Resolution( + id, label, horizontalDpi, verticalDpi + ); + } + + public Map toMap() { + Map obj = new HashMap<>(); + obj.put("id", id); + obj.put("label", label); + obj.put("verticalDpi", verticalDpi); + obj.put("horizontalDpi", horizontalDpi); + return obj; + } + + @NonNull + public String getId() { + return id; + } + + public void setId(@NonNull String id) { + this.id = id; + } + + @NonNull + public String getLabel() { + return label; + } + + public void setLabel(@NonNull String label) { + this.label = label; + } + + public int getVerticalDpi() { + return verticalDpi; + } + + public void setVerticalDpi(int verticalDpi) { + this.verticalDpi = verticalDpi; + } + + public int getHorizontalDpi() { + return horizontalDpi; + } + + public void setHorizontalDpi(int horizontalDpi) { + this.horizontalDpi = horizontalDpi; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ResolutionExt that = (ResolutionExt) o; + + if (verticalDpi != that.verticalDpi) return false; + if (horizontalDpi != that.horizontalDpi) return false; + if (!id.equals(that.id)) return false; + return label.equals(that.label); + } + + @Override + public int hashCode() { + int result = id.hashCode(); + result = 31 * result + label.hashCode(); + result = 31 * result + verticalDpi; + result = 31 * result + horizontalDpi; + return result; + } + + @Override + public String toString() { + return "ResolutionExt{" + + "id='" + id + '\'' + + ", label='" + label + '\'' + + ", verticalDpi=" + verticalDpi + + ", horizontalDpi=" + horizontalDpi + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SafeBrowsingResponse.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SafeBrowsingResponse.java new file mode 100644 index 00000000..765fe258 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SafeBrowsingResponse.java @@ -0,0 +1,69 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.Nullable; + +import java.util.Map; + +public class SafeBrowsingResponse { + private boolean report; + @Nullable + private Integer action; + + public SafeBrowsingResponse(boolean report, @Nullable Integer action) { + this.report = report; + this.action = action; + } + + @Nullable + public static SafeBrowsingResponse fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + boolean report = (boolean) map.get("report"); + Integer action = (Integer) map.get("action"); + return new SafeBrowsingResponse(report, action); + } + + public boolean isReport() { + return report; + } + + public void setReport(boolean report) { + this.report = report; + } + + @Nullable + public Integer getAction() { + return action; + } + + public void setAction(@Nullable Integer action) { + this.action = action; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SafeBrowsingResponse that = (SafeBrowsingResponse) o; + + if (report != that.report) return false; + return action != null ? action.equals(that.action) : that.action == null; + } + + @Override + public int hashCode() { + int result = (report ? 1 : 0); + result = 31 * result + (action != null ? action.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "SafeBrowsingResponse{" + + "report=" + report + + ", action=" + action + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ServerTrustAuthResponse.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ServerTrustAuthResponse.java new file mode 100644 index 00000000..eebf7bd9 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ServerTrustAuthResponse.java @@ -0,0 +1,54 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.Nullable; + +import java.util.Map; + +public class ServerTrustAuthResponse { + @Nullable + private Integer action; + + public ServerTrustAuthResponse(@Nullable Integer action) { + this.action = action; + } + + @Nullable + public static ServerTrustAuthResponse fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + Integer action = (Integer) map.get("action"); + return new ServerTrustAuthResponse(action); + } + + @Nullable + public Integer getAction() { + return action; + } + + public void setAction(@Nullable Integer action) { + this.action = action; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ServerTrustAuthResponse that = (ServerTrustAuthResponse) o; + + return action != null ? action.equals(that.action) : that.action == null; + } + + @Override + public int hashCode() { + return action != null ? action.hashCode() : 0; + } + + @Override + public String toString() { + return "ServerTrustAuthResponse{" + + "action=" + action + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ServerTrustChallenge.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ServerTrustChallenge.java new file mode 100644 index 00000000..17616453 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/ServerTrustChallenge.java @@ -0,0 +1,12 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +public class ServerTrustChallenge extends URLAuthenticationChallenge { + public ServerTrustChallenge(URLProtectionSpace protectionSpace) { + super(protectionSpace); + } + + @Override + public String toString() { + return "ServerTrustChallenge{} " + super.toString(); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/Size2D.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/Size2D.java new file mode 100644 index 00000000..da512690 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/Size2D.java @@ -0,0 +1,81 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class Size2D { + private double width; + private double height; + + public Size2D(double width, double height) { + this.width = width; + this.height = height; + } + + @Nullable + public static Size2D fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + Double width = (Double) map.get("width"); + Double height = (Double) map.get("height"); + assert width != null; + assert height != null; + return new Size2D(width, height); + } + + public Map toMap() { + Map sizeMap = new HashMap<>(); + sizeMap.put("width", width); + sizeMap.put("height", height); + return sizeMap; + } + + public double getWidth() { + return width; + } + + public void setWidth(double width) { + this.width = width; + } + + public double getHeight() { + return height; + } + + public void setHeight(double height) { + this.height = height; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Size2D size = (Size2D) o; + + if (Double.compare(size.width, width) != 0) return false; + return Double.compare(size.height, height) == 0; + } + + @Override + public int hashCode() { + int result; + long temp; + temp = Double.doubleToLongBits(width); + result = (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(height); + result = 31 * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public String toString() { + return "Size{" + + "width=" + width + + ", height=" + height + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SslCertificateExt.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SslCertificateExt.java new file mode 100644 index 00000000..32087459 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SslCertificateExt.java @@ -0,0 +1,77 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.net.http.SslCertificate; +import android.os.Build; + +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.Util; + +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Map; + +public class SslCertificateExt extends SslCertificate { + + private SslCertificateExt(X509Certificate certificate) { + super(certificate); + } + + @Nullable + static public Map toMap(@Nullable SslCertificate sslCertificate) { + if (sslCertificate == null) { + return null; + } + + DName issuedByName = sslCertificate.getIssuedBy(); + Map issuedBy = null; + if (issuedByName != null) { + issuedBy = new HashMap<>(); + issuedBy.put("CName", issuedByName.getCName()); + issuedBy.put("DName", issuedByName.getDName()); + issuedBy.put("OName", issuedByName.getOName()); + issuedBy.put("UName", issuedByName.getUName()); + } + + DName issuedToName = sslCertificate.getIssuedTo(); + Map issuedTo = null; + if (issuedToName != null) { + issuedTo = new HashMap<>(); + issuedTo.put("CName", issuedToName.getCName()); + issuedTo.put("DName", issuedToName.getDName()); + issuedTo.put("OName", issuedToName.getOName()); + issuedTo.put("UName", issuedToName.getUName()); + } + + byte[] x509CertificateData = null; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + try { + X509Certificate certificate = sslCertificate.getX509Certificate(); + if (certificate != null) { + x509CertificateData = certificate.getEncoded(); + } + } catch (CertificateEncodingException e) { + e.printStackTrace(); + } + } else { + try { + x509CertificateData = Util.getX509CertFromSslCertHack(sslCertificate).getEncoded(); + } catch (CertificateEncodingException e) { + e.printStackTrace(); + } + } + + long validNotAfterDate = sslCertificate.getValidNotAfterDate().getTime(); + long validNotBeforeDate = sslCertificate.getValidNotBeforeDate().getTime(); + + Map sslCertificateMap = new HashMap<>(); + sslCertificateMap.put("issuedBy", issuedBy); + sslCertificateMap.put("issuedTo", issuedTo); + sslCertificateMap.put("validNotAfterDate", validNotAfterDate); + sslCertificateMap.put("validNotBeforeDate", validNotBeforeDate); + sslCertificateMap.put("x509Certificate", x509CertificateData); + return sslCertificateMap; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SslErrorExt.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SslErrorExt.java new file mode 100644 index 00000000..f8831320 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SslErrorExt.java @@ -0,0 +1,56 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.net.http.SslCertificate; +import android.net.http.SslError; + +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class SslErrorExt extends SslError { + + private SslErrorExt(int error, SslCertificate certificate, String url) { + super(error, certificate, url); + } + + @Nullable + static public Map toMap(SslError sslError) { + if (sslError == null) { + return null; + } + + int primaryError = sslError.getPrimaryError(); + + String message; + switch (primaryError) { + case SslError.SSL_DATE_INVALID: + message = "The date of the certificate is invalid"; + break; + case SslError.SSL_EXPIRED: + message = "The certificate has expired"; + break; + case SslError.SSL_IDMISMATCH: + message = "Hostname mismatch"; + break; + case SslError.SSL_INVALID: + message = "A generic error occurred"; + break; + case SslError.SSL_NOTYETVALID: + message = "The certificate is not yet valid"; + break; + case SslError.SSL_UNTRUSTED: + message = "The certificate authority is not trusted"; + break; + default: + message = null; + break; + } + + Map urlProtectionSpaceMap = new HashMap<>(); + urlProtectionSpaceMap.put("code", primaryError >= 0 ? primaryError : null); + urlProtectionSpaceMap.put("message", message); + return urlProtectionSpaceMap; + } + +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SyncBaseCallbackResultImpl.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SyncBaseCallbackResultImpl.java new file mode 100644 index 00000000..1fd2ee7e --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/SyncBaseCallbackResultImpl.java @@ -0,0 +1,47 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.CallSuper; +import androidx.annotation.Nullable; + +import java.util.concurrent.CountDownLatch; + +public class SyncBaseCallbackResultImpl extends BaseCallbackResultImpl { + final public CountDownLatch latch = new CountDownLatch(1); + @Nullable + public T result = null; + + @CallSuper + @Override + public void defaultBehaviour(@Nullable T result) { + latch.countDown(); + } + + @Override + public void success(@Nullable Object obj) { + T result = decodeResult(obj); + this.result = result; + boolean shouldRunDefaultBehaviour; + if (result == null) { + shouldRunDefaultBehaviour = nullSuccess(); + } else { + shouldRunDefaultBehaviour = nonNullSuccess(result); + } + if (shouldRunDefaultBehaviour) { + defaultBehaviour(result); + } else { + latch.countDown(); + } + } + + @CallSuper + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + latch.countDown(); + } + + @CallSuper + @Override + public void notImplemented() { + defaultBehaviour(null); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLAuthenticationChallenge.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLAuthenticationChallenge.java new file mode 100644 index 00000000..7d015071 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLAuthenticationChallenge.java @@ -0,0 +1,48 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import java.util.HashMap; +import java.util.Map; + +public class URLAuthenticationChallenge { + private URLProtectionSpace protectionSpace; + + public URLAuthenticationChallenge(URLProtectionSpace protectionSpace) { + this.protectionSpace = protectionSpace; + } + + public Map toMap() { + Map challengeMap = new HashMap<>(); + challengeMap.put("protectionSpace", protectionSpace.toMap()); + return challengeMap; + } + + public URLProtectionSpace getProtectionSpace() { + return protectionSpace; + } + + public void setProtectionSpace(URLProtectionSpace protectionSpace) { + this.protectionSpace = protectionSpace; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + URLAuthenticationChallenge challenge = (URLAuthenticationChallenge) o; + + return protectionSpace.equals(challenge.protectionSpace); + } + + @Override + public int hashCode() { + return protectionSpace.hashCode(); + } + + @Override + public String toString() { + return "URLAuthenticationChallenge{" + + "protectionSpace=" + protectionSpace + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLCredential.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLCredential.java new file mode 100644 index 00000000..0bf72519 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLCredential.java @@ -0,0 +1,101 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class URLCredential { + @Nullable + private Long id; + @Nullable + private String username; + @Nullable + private String password; + @Nullable + private Long protectionSpaceId; + + public URLCredential(@Nullable String username, @Nullable String password) { + this.username = username; + this.password = password; + } + + public URLCredential (@Nullable Long id, @NonNull String username, @NonNull String password, @Nullable Long protectionSpaceId) { + this.id = id; + this.username = username; + this.password = password; + this.protectionSpaceId = protectionSpaceId; + } + + public Map toMap() { + Map urlCredentialMap = new HashMap<>(); + urlCredentialMap.put("username", username); + urlCredentialMap.put("password", password); + urlCredentialMap.put("certificates", null); + urlCredentialMap.put("persistence", null); + return urlCredentialMap; + } + + @Nullable + public Long getId() { + return id; + } + + public void setId(@Nullable Long id) { + this.id = id; + } + + @Nullable + public String getUsername() { + return username; + } + + public void setUsername(@Nullable String username) { + this.username = username; + } + + @Nullable + public String getPassword() { + return password; + } + + public void setPassword(@Nullable String password) { + this.password = password; + } + + @Nullable + public Long getProtectionSpaceId() { + return protectionSpaceId; + } + + public void setProtectionSpaceId(@Nullable Long protectionSpaceId) { + this.protectionSpaceId = protectionSpaceId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + URLCredential that = (URLCredential) o; + + if (username != null ? !username.equals(that.username) : that.username != null) return false; + return password != null ? password.equals(that.password) : that.password == null; + } + + @Override + public int hashCode() { + int result = username != null ? username.hashCode() : 0; + result = 31 * result + (password != null ? password.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "URLCredential{" + + "username='" + username + '\'' + + ", password='" + password + '\'' + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLProtectionSpace.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLProtectionSpace.java new file mode 100644 index 00000000..a27e97fe --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLProtectionSpace.java @@ -0,0 +1,155 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.net.http.SslCertificate; +import android.net.http.SslError; + +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class URLProtectionSpace { + @Nullable + private Long id; + private String host; + private String protocol; + @Nullable + private String realm; + private int port; + @Nullable + private SslCertificate sslCertificate; + @Nullable + private SslError sslError; + + public URLProtectionSpace(String host, String protocol, @Nullable String realm, int port, @Nullable SslCertificate sslCertificate, @Nullable SslError sslError) { + this.host = host; + this.protocol = protocol; + this.realm = realm; + this.port = port; + this.sslCertificate = sslCertificate; + this.sslError = sslError; + } + + public URLProtectionSpace(@Nullable Long id, String host, String protocol, @Nullable String realm, int port) { + this.id = id; + this.host = host; + this.protocol = protocol; + this.realm = realm; + this.port = port; + } + + public Map toMap() { + Map urlProtectionSpaceMap = new HashMap<>(); + urlProtectionSpaceMap.put("host", host); + urlProtectionSpaceMap.put("protocol", protocol); + urlProtectionSpaceMap.put("realm", realm); + urlProtectionSpaceMap.put("port", port); + urlProtectionSpaceMap.put("sslCertificate", SslCertificateExt.toMap(sslCertificate)); + urlProtectionSpaceMap.put("sslError", SslErrorExt.toMap(sslError)); + urlProtectionSpaceMap.put("authenticationMethod", null); + urlProtectionSpaceMap.put("distinguishedNames", null); + urlProtectionSpaceMap.put("receivesCredentialSecurely", null); + urlProtectionSpaceMap.put("isProxy", null); + urlProtectionSpaceMap.put("proxyType", null); + return urlProtectionSpaceMap; + } + + @Nullable + public Long getId() { + return id; + } + + public void setId(@Nullable Long id) { + this.id = id; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public String getProtocol() { + return protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + @Nullable + public String getRealm() { + return realm; + } + + public void setRealm(@Nullable String realm) { + this.realm = realm; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + @Nullable + public SslCertificate getSslCertificate() { + return sslCertificate; + } + + public void setSslCertificate(@Nullable SslCertificate sslCertificateExt) { + this.sslCertificate = sslCertificateExt; + } + + @Nullable + public SslError getSslError() { + return sslError; + } + + public void setSslError(@Nullable SslError sslError) { + this.sslError = sslError; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + URLProtectionSpace that = (URLProtectionSpace) o; + + if (port != that.port) return false; + if (!host.equals(that.host)) return false; + if (!protocol.equals(that.protocol)) return false; + if (realm != null ? !realm.equals(that.realm) : that.realm != null) return false; + if (sslCertificate != null ? !sslCertificate.equals(that.sslCertificate) : that.sslCertificate != null) + return false; + return sslError != null ? sslError.equals(that.sslError) : that.sslError == null; + } + + @Override + public int hashCode() { + int result = host.hashCode(); + result = 31 * result + protocol.hashCode(); + result = 31 * result + (realm != null ? realm.hashCode() : 0); + result = 31 * result + port; + result = 31 * result + (sslCertificate != null ? sslCertificate.hashCode() : 0); + result = 31 * result + (sslError != null ? sslError.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "URLProtectionSpace{" + + "host='" + host + '\'' + + ", protocol='" + protocol + '\'' + + ", realm='" + realm + '\'' + + ", port=" + port + + ", sslCertificate=" + sslCertificate + + ", sslError=" + sslError + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLRequest.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLRequest.java new file mode 100644 index 00000000..d9d598d2 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/URLRequest.java @@ -0,0 +1,128 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.Nullable; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class URLRequest { + @Nullable + private String url; + @Nullable + private String method; + @Nullable + private byte[] body; + @Nullable + private Map headers; + + public URLRequest(@Nullable String url, @Nullable String method, @Nullable byte[] body, @Nullable Map headers) { + this.url = url; + this.method = method; + this.body = body; + this.headers = headers; + } + + @Nullable + public static URLRequest fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String url = (String) map.get("url"); + if (url == null) { + url = "about:blank"; + } + String method = (String) map.get("method"); + byte[] body = (byte[]) map.get("body"); + Map headers = (Map) map.get("headers"); + return new URLRequest(url, method, body, headers); + } + + public Map toMap() { + Map urlRequestMap = new HashMap<>(); + urlRequestMap.put("url", url); + urlRequestMap.put("method", method); + urlRequestMap.put("headers", headers); + urlRequestMap.put("body", body); + urlRequestMap.put("allowsCellularAccess", null); + urlRequestMap.put("allowsConstrainedNetworkAccess", null); + urlRequestMap.put("allowsExpensiveNetworkAccess", null); + urlRequestMap.put("cachePolicy", null); + urlRequestMap.put("httpShouldHandleCookies", null); + urlRequestMap.put("httpShouldUsePipelining", null); + urlRequestMap.put("networkServiceType", null); + urlRequestMap.put("timeoutInterval", null); + urlRequestMap.put("mainDocumentURL", null); + urlRequestMap.put("assumesHTTP3Capable", null); + urlRequestMap.put("attribution", null); + return urlRequestMap; + } + + @Nullable + public String getUrl() { + return url; + } + + public void setUrl(@Nullable String url) { + this.url = url; + } + + @Nullable + public String getMethod() { + return method; + } + + public void setMethod(@Nullable String method) { + this.method = method; + } + + @Nullable + public byte[] getBody() { + return body; + } + + public void setBody(@Nullable byte[] body) { + this.body = body; + } + + @Nullable + public Map getHeaders() { + return headers; + } + + public void setHeaders(@Nullable Map headers) { + this.headers = headers; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + URLRequest that = (URLRequest) o; + + if (url != null ? !url.equals(that.url) : that.url != null) return false; + if (method != null ? !method.equals(that.method) : that.method != null) return false; + if (!Arrays.equals(body, that.body)) return false; + return headers != null ? headers.equals(that.headers) : that.headers == null; + } + + @Override + public int hashCode() { + int result = url != null ? url.hashCode() : 0; + result = 31 * result + (method != null ? method.hashCode() : 0); + result = 31 * result + Arrays.hashCode(body); + result = 31 * result + (headers != null ? headers.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "URLRequest{" + + "url='" + url + '\'' + + ", method='" + method + '\'' + + ", body=" + Arrays.toString(body) + + ", headers=" + headers + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/UserContentController.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/UserContentController.java new file mode 100644 index 00000000..f51dd1b9 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/UserContentController.java @@ -0,0 +1,473 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.annotation.SuppressLint; +import android.text.TextUtils; +import android.webkit.WebView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.ScriptHandler; +import androidx.webkit.WebViewCompat; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.Util; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.JavaScriptBridgeJS; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.PluginScriptsUtil; + +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@SuppressLint("RestrictedApi") +public class UserContentController implements Disposable { + protected static final String LOG_TAG = "UserContentController"; + + @NonNull + private final Set contentWorlds = new HashSet() {{ + add(ContentWorld.PAGE); + }}; + + private final Map scriptHandlerMap = new HashMap<>(); + + @Nullable + private ScriptHandler contentWorldsCreatorScript; + + @NonNull + private final Map> userOnlyScripts = new HashMap>() {{ + put(UserScriptInjectionTime.AT_DOCUMENT_START, new LinkedHashSet()); + put(UserScriptInjectionTime.AT_DOCUMENT_END, new LinkedHashSet()); + }}; + @NonNull + private final Map> pluginScripts = new HashMap>() {{ + put(UserScriptInjectionTime.AT_DOCUMENT_START, new LinkedHashSet()); + put(UserScriptInjectionTime.AT_DOCUMENT_END, new LinkedHashSet()); + }}; + + @Nullable + public WebView webView; + + public UserContentController(WebView webView) { + this.webView = webView; + } + + public String generateWrappedCodeForDocumentStart() { + return Util.replaceAll( + DOCUMENT_READY_WRAPPER_JS_SOURCE, + PluginScriptsUtil.VAR_PLACEHOLDER_VALUE, + generateCodeForDocumentStart()); + } + + public String generateWrappedCodeForDocumentEnd() { + UserScriptInjectionTime injectionTime = UserScriptInjectionTime.AT_DOCUMENT_END; + String js = ""; + if (!WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + // try to reload scripts if they were not loaded during the AT_DOCUMENT_START event + js += generateCodeForDocumentStart(); + } + js += generatePluginScriptsCodeAt(injectionTime); + js += generateUserOnlyScriptsCodeAt(injectionTime); + js = USER_SCRIPTS_AT_DOCUMENT_END_WRAPPER_JS_SOURCE.replace(PluginScriptsUtil.VAR_PLACEHOLDER_VALUE, js); + return js; + } + + public String generateCodeForDocumentStart() { + UserScriptInjectionTime injectionTime = UserScriptInjectionTime.AT_DOCUMENT_START; + String js = ""; + js += generatePluginScriptsCodeAt(injectionTime); + js += generateContentWorldsCreatorCode(); + js += generateUserOnlyScriptsCodeAt(injectionTime); + js = USER_SCRIPTS_AT_DOCUMENT_START_WRAPPER_JS_SOURCE.replace(PluginScriptsUtil.VAR_PLACEHOLDER_VALUE, js); + return js; + } + + public String generateContentWorldsCreatorCode() { + if (this.contentWorlds.size() == 1) { + return ""; + } + + StringBuilder source = new StringBuilder(); + LinkedHashSet pluginScriptsRequired = this.getPluginScriptsRequiredInAllContentWorlds(); + for (PluginScript script : pluginScriptsRequired) { + source.append(script.getSource()); + } + List contentWorldsNames = new ArrayList<>(); + for (ContentWorld contentWorld : this.contentWorlds) { + if (contentWorld.equals(ContentWorld.PAGE)) { + continue; + } + contentWorldsNames.add("'" + escapeContentWorldName(contentWorld.getName()) + "'"); + } + + return CONTENT_WORLDS_GENERATOR_JS_SOURCE + .replace(PluginScriptsUtil.VAR_CONTENT_WORLD_NAME_ARRAY, TextUtils.join(", ", contentWorldsNames)) + .replace(PluginScriptsUtil.VAR_JSON_SOURCE_ENCODED, escapeCode(source.toString())); + + } + + public String generatePluginScriptsCodeAt(UserScriptInjectionTime injectionTime) { + StringBuilder js = new StringBuilder(); + LinkedHashSet scripts = this.getPluginScriptsAt(injectionTime); + for (PluginScript script : scripts) { + String source = ";" + script.getSource(); + source = wrapSourceCodeInContentWorld(script.getContentWorld(), source); + js.append(source); + } + return js.toString(); + } + + public String generateUserOnlyScriptsCodeAt(UserScriptInjectionTime injectionTime) { + StringBuilder js = new StringBuilder(); + LinkedHashSet scripts = this.getUserOnlyScriptsAt(injectionTime); + for (UserScript script : scripts) { + String source = ";" + script.getSource(); + source = wrapSourceCodeInContentWorld(script.getContentWorld(), source); + js.append(source); + } + return js.toString(); + } + + public String generateCodeForScriptEvaluation(String source, @Nullable ContentWorld contentWorld) { + if (contentWorld != null && !contentWorld.equals(ContentWorld.PAGE)) { + StringBuilder sourceWrapped = new StringBuilder(); + if (!contentWorlds.contains(contentWorld)) { + contentWorlds.add(contentWorld); + + StringBuilder pluginScriptsSource = new StringBuilder(); + LinkedHashSet pluginScriptsRequired = this.getPluginScriptsRequiredInAllContentWorlds(); + for (PluginScript script : pluginScriptsRequired) { + pluginScriptsSource.append(script.getSource()); + } + String contentWorldCreatorCode = CONTENT_WORLDS_GENERATOR_JS_SOURCE + .replace(PluginScriptsUtil.VAR_CONTENT_WORLD_NAME_ARRAY, "'" + escapeContentWorldName(contentWorld.getName()) + "'") + .replace(PluginScriptsUtil.VAR_JSON_SOURCE_ENCODED, escapeCode(pluginScriptsSource.toString())); + sourceWrapped.append(contentWorldCreatorCode).append(";"); + } + return sourceWrapped.append(wrapSourceCodeInContentWorld(contentWorld, source)).toString(); + } + return source; + } + + public String wrapSourceCodeInContentWorld(@Nullable ContentWorld contentWorld, String source) { + String sourceWrapped = contentWorld == null || contentWorld.equals(ContentWorld.PAGE) ? source : + CONTENT_WORLD_WRAPPER_JS_SOURCE + .replace(PluginScriptsUtil.VAR_CONTENT_WORLD_NAME, escapeContentWorldName(contentWorld.getName())) + .replace(PluginScriptsUtil.VAR_JSON_SOURCE_ENCODED, escapeCode(source)); + + return sourceWrapped; + } + + public static String escapeCode(String code) { + String escapedCode = JSONObject.quote(code); + // escapedCode = escapedCode.substring(1, escapedCode.length() - 1); + return escapedCode; + } + + public static String escapeContentWorldName(String name) { + return name.replaceAll("'", "\\\\'"); + } + + public LinkedHashSet getUserOnlyScriptsAt(UserScriptInjectionTime injectionTime) { + return new LinkedHashSet<>(this.userOnlyScripts.get(injectionTime)); + } + + private void updateContentWorldsCreatorScript() { + String source = generateContentWorldsCreatorCode(); + if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + if (contentWorldsCreatorScript != null) { + contentWorldsCreatorScript.remove(); + } + if (!source.isEmpty() && webView != null) { + contentWorldsCreatorScript = WebViewCompat.addDocumentStartJavaScript( + webView, + source, + new HashSet() {{ + add("*"); + }} + ); + } + } + } + + public boolean addUserOnlyScript(UserScript userOnlyScript) { + ContentWorld contentWorld = userOnlyScript.getContentWorld(); + if (contentWorld != null) { + contentWorlds.add(contentWorld); + } + this.updateContentWorldsCreatorScript(); + if (webView != null && userOnlyScript.getInjectionTime() == UserScriptInjectionTime.AT_DOCUMENT_START + && WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + ScriptHandler scriptHandler = WebViewCompat.addDocumentStartJavaScript( + webView, + wrapSourceCodeInContentWorld(userOnlyScript.getContentWorld(), userOnlyScript.getSource()), + userOnlyScript.getAllowedOriginRules() + ); + this.scriptHandlerMap.put(userOnlyScript, scriptHandler); + } + return this.userOnlyScripts.get(userOnlyScript.getInjectionTime()).add(userOnlyScript); + } + + public void addUserOnlyScripts(List userOnlyScripts) { + for (UserScript userOnlyScript : userOnlyScripts) { + this.addUserOnlyScript(userOnlyScript); + } + } + + public boolean removeUserOnlyScript(UserScript userOnlyScript) { + if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + ScriptHandler scriptHandler = this.scriptHandlerMap.get(userOnlyScript); + if (scriptHandler != null) { + scriptHandler.remove(); + this.scriptHandlerMap.remove(userOnlyScript); + } + this.updateContentWorldsCreatorScript(); + } + return this.userOnlyScripts.get(userOnlyScript.getInjectionTime()).remove(userOnlyScript); + } + + public boolean removeUserOnlyScriptAt(int index, UserScriptInjectionTime injectionTime) { + UserScript userOnlyScript = new ArrayList<>(this.userOnlyScripts.get(injectionTime)).get(index); + return this.removeUserOnlyScript(userOnlyScript); + } + + public void removeAllUserOnlyScripts() { + if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + for (UserScript userOnlyScript : this.userOnlyScripts.get(UserScriptInjectionTime.AT_DOCUMENT_START)) { + ScriptHandler scriptHandler = this.scriptHandlerMap.get(userOnlyScript); + if (scriptHandler != null) { + scriptHandler.remove(); + this.scriptHandlerMap.remove(userOnlyScript); + } + } + } + this.userOnlyScripts.get(UserScriptInjectionTime.AT_DOCUMENT_START).clear(); + this.userOnlyScripts.get(UserScriptInjectionTime.AT_DOCUMENT_END).clear(); + } + + public LinkedHashSet getPluginScriptsAt(UserScriptInjectionTime injectionTime) { + return new LinkedHashSet<>(this.pluginScripts.get(injectionTime)); + } + + public LinkedHashSet getPluginScriptsRequiredInAllContentWorlds() { + LinkedHashSet pluginScriptsRequired = new LinkedHashSet<>(); + LinkedHashSet scripts = this.getPluginScriptsAt(UserScriptInjectionTime.AT_DOCUMENT_START); + for (PluginScript script : scripts) { + if (script.isRequiredInAllContentWorlds()) { + pluginScriptsRequired.add(script); + } + } + return pluginScriptsRequired; + } + + public boolean addPluginScript(PluginScript pluginScript) { + ContentWorld contentWorld = pluginScript.getContentWorld(); + if (contentWorld != null) { + contentWorlds.add(contentWorld); + } + this.updateContentWorldsCreatorScript(); + if (webView != null && pluginScript.getInjectionTime() == UserScriptInjectionTime.AT_DOCUMENT_START + && WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + ScriptHandler scriptHandler = WebViewCompat.addDocumentStartJavaScript( + webView, + wrapSourceCodeInContentWorld(pluginScript.getContentWorld(), pluginScript.getSource()), + pluginScript.getAllowedOriginRules() + ); + this.scriptHandlerMap.put(pluginScript, scriptHandler); + } + return this.pluginScripts.get(pluginScript.getInjectionTime()).add(pluginScript); + } + + public void addPluginScripts(List pluginScripts) { + for (PluginScript pluginScript : pluginScripts) { + this.addPluginScript(pluginScript); + } + } + + public boolean removePluginScript(PluginScript pluginScript) { + if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + ScriptHandler scriptHandler = this.scriptHandlerMap.get(pluginScript); + if (scriptHandler != null) { + scriptHandler.remove(); + this.scriptHandlerMap.remove(pluginScript); + } + this.updateContentWorldsCreatorScript(); + } + return this.pluginScripts.get(pluginScript.getInjectionTime()).remove(pluginScript); + } + + public void removeAllPluginScripts() { + if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + for (PluginScript pluginScript : this.pluginScripts.get(UserScriptInjectionTime.AT_DOCUMENT_START)) { + ScriptHandler scriptHandler = this.scriptHandlerMap.get(pluginScript); + if (scriptHandler != null) { + scriptHandler.remove(); + this.scriptHandlerMap.remove(pluginScript); + } + } + } + this.pluginScripts.get(UserScriptInjectionTime.AT_DOCUMENT_START).clear(); + this.pluginScripts.get(UserScriptInjectionTime.AT_DOCUMENT_END).clear(); + } + + public LinkedHashSet getUserOnlyScriptAsList() { + LinkedHashSet userOnlyScripts = new LinkedHashSet<>(); + Collection> collection = this.userOnlyScripts.values(); + for (LinkedHashSet list : collection) { + userOnlyScripts.addAll(list); + } + return userOnlyScripts; + } + + public LinkedHashSet getPluginScriptAsList() { + LinkedHashSet pluginScripts = new LinkedHashSet<>(); + Collection> collection = this.pluginScripts.values(); + for (LinkedHashSet list : collection) { + pluginScripts.addAll(list); + } + return pluginScripts; + } + + public void resetContentWorlds() { + this.contentWorlds.clear(); + this.contentWorlds.add(ContentWorld.PAGE); + + LinkedHashSet pluginScripts = this.getPluginScriptAsList(); + for (PluginScript pluginScript : pluginScripts) { + ContentWorld contentWorld = pluginScript.getContentWorld(); + this.contentWorlds.add(contentWorld); + } + + LinkedHashSet userOnlyScripts = this.getUserOnlyScriptAsList(); + for (UserScript userOnlyScript : userOnlyScripts) { + ContentWorld contentWorld = userOnlyScript.getContentWorld(); + this.contentWorlds.add(contentWorld); + } + } + + public boolean containsPluginScript(PluginScript pluginScript) { + return this.getPluginScriptAsList().contains(pluginScript); + } + + public boolean containsPluginScriptByGroupName(String groupName) { + LinkedHashSet pluginScripts = this.getPluginScriptAsList(); + for (PluginScript pluginScript : pluginScripts) { + if (Util.objEquals(groupName, pluginScript.getGroupName())) { + return true; + } + } + return false; + } + + public boolean containsUserOnlyScript(UserScript userOnlyScript) { + return this.getUserOnlyScriptAsList().contains(userOnlyScript); + } + + public boolean containsUserOnlyScriptByGroupName(String groupName) { + LinkedHashSet userOnlyScripts = this.getUserOnlyScriptAsList(); + for (UserScript userOnlyScript : userOnlyScripts) { + if (Util.objEquals(groupName, userOnlyScript.getGroupName())) { + return true; + } + } + + return false; + } + + public void removePluginScriptsByGroupName(String groupName) { + LinkedHashSet pluginScripts = this.getPluginScriptAsList(); + for (PluginScript pluginScript : pluginScripts) { + if (Util.objEquals(groupName, pluginScript.getGroupName())) { + this.removePluginScript(pluginScript); + } + } + } + + public void removeUserOnlyScriptsByGroupName(String groupName) { + LinkedHashSet userOnlyScripts = this.getUserOnlyScriptAsList(); + for (UserScript userOnlyScript : userOnlyScripts) { + if (Util.objEquals(groupName, userOnlyScript.getGroupName())) { + this.removeUserOnlyScript(userOnlyScript); + } + } + } + + @NonNull + public LinkedHashSet getContentWorlds() { + return new LinkedHashSet<>(this.contentWorlds); + } + + private static final String USER_SCRIPTS_AT_DOCUMENT_START_WRAPPER_JS_SOURCE = "if (window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + " != null && (window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._userScriptsAtDocumentStartLoaded == null || !window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._userScriptsAtDocumentStartLoaded)) {" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._userScriptsAtDocumentStartLoaded = true;" + + " " + PluginScriptsUtil.VAR_PLACEHOLDER_VALUE + + "}"; + + private static final String USER_SCRIPTS_AT_DOCUMENT_END_WRAPPER_JS_SOURCE = "if (window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + " != null && (window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._userScriptsAtDocumentEndLoaded == null || !window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._userScriptsAtDocumentEndLoaded)) {" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._userScriptsAtDocumentEndLoaded = true;" + + " " + PluginScriptsUtil.VAR_PLACEHOLDER_VALUE + + "}"; + + private static final String CONTENT_WORLDS_GENERATOR_JS_SOURCE = "(function() {" + + " var interval = setInterval(function() {" + + " if (document.body == null) {return;}" + + " var contentWorldNames = [" + PluginScriptsUtil.VAR_CONTENT_WORLD_NAME_ARRAY + "];" + + " for (var contentWorldName of contentWorldNames) {" + + " var iframeId = '" + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "_' + contentWorldName;" + + " var iframe = document.getElementById(iframeId);" + + " if (iframe == null) {" + + " iframe = document.createElement('iframe');" + + " iframe.id = iframeId;" + + " iframe.style = 'display: none; z-index: 0; position: absolute; width: 0px; height: 0px';" + + " document.body.append(iframe);" + + " }" + + " if (iframe.contentWindow.document.getElementById('" + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "_plugin_scripts') == null) {" + + " var script = iframe.contentWindow.document.createElement('script');" + + " script.id = '" + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "_plugin_scripts';" + + " script.innerHTML = " + PluginScriptsUtil.VAR_JSON_SOURCE_ENCODED + ";" + + " iframe.contentWindow.document.body.append(script);" + + " }" + + " }" + + " clearInterval(interval);" + + " });" + + "})();"; + + private static final String CONTENT_WORLD_WRAPPER_JS_SOURCE = "(function() {" + + " var interval = setInterval(function() {" + + " if (document.body == null) {return;}" + + " var iframeId = '" + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "_" + PluginScriptsUtil.VAR_CONTENT_WORLD_NAME + "';" + + " var iframe = document.getElementById(iframeId);" + + " if (iframe == null) {" + + " iframe = document.createElement('iframe');" + + " iframe.id = iframeId;" + + " iframe.style = 'display: none; z-index: 0; position: absolute; width: 0px; height: 0px';" + + " document.body.append(iframe);" + + " }" + + " if (iframe.contentWindow.document.querySelector('#" + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "_plugin_scripts') == null) {" + + " return;" + + " }" + + " var script = iframe.contentWindow.document.createElement('script');" + + " script.innerHTML = " + PluginScriptsUtil.VAR_JSON_SOURCE_ENCODED + ";" + + " iframe.contentWindow.document.body.append(script);" + + " clearInterval(interval);" + + " });" + + "})();"; + + private static final String DOCUMENT_READY_WRAPPER_JS_SOURCE = "if (document.readyState === 'interactive' || document.readyState === 'complete') { " + + " " + PluginScriptsUtil.VAR_PLACEHOLDER_VALUE + + "}"; + + @Override + public void dispose() { + if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT) && contentWorldsCreatorScript != null) { + contentWorldsCreatorScript.remove(); + } + removeAllUserOnlyScripts(); + removeAllPluginScripts(); + webView = null; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/UserScript.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/UserScript.java new file mode 100644 index 00000000..1f8e5604 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/UserScript.java @@ -0,0 +1,129 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class UserScript { + @Nullable + private String groupName; + @NonNull + private String source; + @NonNull + private UserScriptInjectionTime injectionTime; + @NonNull + private ContentWorld contentWorld; + @NonNull + private Set allowedOriginRules = new HashSet<>(); + + public UserScript(@Nullable String groupName, @NonNull String source, + @NonNull UserScriptInjectionTime injectionTime, @Nullable ContentWorld contentWorld, + @Nullable Set allowedOriginRules) { + this.groupName = groupName; + this.source = source; + this.injectionTime = injectionTime; + this.contentWorld = contentWorld == null ? ContentWorld.PAGE : contentWorld; + this.allowedOriginRules = allowedOriginRules == null ? new HashSet() {{ + add("*"); + }} : allowedOriginRules; + } + + @Nullable + public static UserScript fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String groupName = (String) map.get("groupName"); + String source = (String) map.get("source"); + UserScriptInjectionTime injectionTime = UserScriptInjectionTime.fromValue((int) map.get("injectionTime")); + ContentWorld contentWorld = ContentWorld.fromMap((Map) map.get("contentWorld")); + Set allowedOriginRules = new HashSet<>((List) map.get("allowedOriginRules")); + assert source != null; + return new UserScript(groupName, source, injectionTime, contentWorld, allowedOriginRules); + } + + @Nullable + public String getGroupName() { + return groupName; + } + + public void setGroupName(@Nullable String groupName) { + this.groupName = groupName; + } + + @NonNull + public String getSource() { + return source; + } + + public void setSource(@NonNull String source) { + this.source = source; + } + + @NonNull + public UserScriptInjectionTime getInjectionTime() { + return injectionTime; + } + + public void setInjectionTime(@NonNull UserScriptInjectionTime injectionTime) { + this.injectionTime = injectionTime; + } + + @NonNull + public ContentWorld getContentWorld() { + return contentWorld; + } + + public void setContentWorld(@Nullable ContentWorld contentWorld) { + this.contentWorld = contentWorld == null ? ContentWorld.PAGE : contentWorld; + } + + @NonNull + public Set getAllowedOriginRules() { + return allowedOriginRules; + } + + public void setAllowedOriginRules(@NonNull Set allowedOriginRules) { + this.allowedOriginRules = allowedOriginRules; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + UserScript that = (UserScript) o; + + if (groupName != null ? !groupName.equals(that.groupName) : that.groupName != null) + return false; + if (!source.equals(that.source)) return false; + if (injectionTime != that.injectionTime) return false; + if (!contentWorld.equals(that.contentWorld)) return false; + return allowedOriginRules.equals(that.allowedOriginRules); + } + + @Override + public int hashCode() { + int result = groupName != null ? groupName.hashCode() : 0; + result = 31 * result + source.hashCode(); + result = 31 * result + injectionTime.hashCode(); + result = 31 * result + contentWorld.hashCode(); + result = 31 * result + allowedOriginRules.hashCode(); + return result; + } + + @Override + public String toString() { + return "UserScript{" + + "groupName='" + groupName + '\'' + + ", source='" + source + '\'' + + ", injectionTime=" + injectionTime + + ", contentWorld=" + contentWorld + + ", allowedOriginRules=" + allowedOriginRules + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/UserScriptInjectionTime.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/UserScriptInjectionTime.java new file mode 100644 index 00000000..bfceeb85 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/UserScriptInjectionTime.java @@ -0,0 +1,28 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +public enum UserScriptInjectionTime { + AT_DOCUMENT_START (0), + AT_DOCUMENT_END (1); + + private final int value; + + private UserScriptInjectionTime(int value) { + this.value = value; + } + + public boolean equalsValue(int otherValue) { + return value == otherValue; + } + + public static UserScriptInjectionTime fromValue(int value) { + for( UserScriptInjectionTime type : UserScriptInjectionTime.values()) { + if(value == type.toValue()) + return type; + } + throw new IllegalArgumentException("No enum constant: " + value); + } + + public int toValue() { + return this.value; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessage.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessage.java new file mode 100644 index 00000000..21ed0f03 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessage.java @@ -0,0 +1,23 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.Nullable; + +import java.util.List; + +public class WebMessage { + @Nullable + public String data; + @Nullable + public List ports; + + public WebMessage(@Nullable String data, @Nullable List ports) { + this.data = data; + this.ports = ports; + } + + public void dispose() { + if (ports != null) { + ports.clear(); + } + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessageCompatExt.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessageCompatExt.java new file mode 100644 index 00000000..b635b3c4 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessageCompatExt.java @@ -0,0 +1,117 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.WebMessageCompat; +import androidx.webkit.WebViewFeature; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class WebMessageCompatExt { + @Nullable + private Object data; + private @WebMessageCompat.Type int type; + + @Nullable + private List ports; + + public WebMessageCompatExt(@Nullable Object data, int type, @Nullable List ports) { + this.data = data; + this.type = type; + this.ports = ports; + } + + public static WebMessageCompatExt fromMapWebMessageCompat(@NonNull WebMessageCompat message) { + Object data; + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER) && message.getType() == WebMessageCompat.TYPE_ARRAY_BUFFER) { + data = message.getArrayBuffer(); + } else { + data = message.getData(); + } + return new WebMessageCompatExt(data, message.getType(), null); + } + + @Nullable + public static WebMessageCompatExt fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + Object data = map.get("data"); + Integer type = (Integer) map.get("type"); + List> portMapList = (List>) map.get("ports"); + List ports = null; + if (portMapList != null && !portMapList.isEmpty()) { + ports = new ArrayList<>(); + for (Map portMap : portMapList) { + ports.add(WebMessagePortCompatExt.fromMap(portMap)); + } + } + return new WebMessageCompatExt(data, type, ports); + } + + public Map toMap() { + Map proxyRuleMap = new HashMap<>(); + proxyRuleMap.put("data", data); + proxyRuleMap.put("type", type); + return proxyRuleMap; + } + + @Nullable + public Object getData() { + return data; + } + + public void setData(@Nullable Object data) { + this.data = data; + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + + @Nullable + public List getPorts() { + return ports; + } + + public void setPorts(@Nullable List ports) { + this.ports = ports; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + WebMessageCompatExt that = (WebMessageCompatExt) o; + + if (type != that.type) return false; + if (!Objects.equals(data, that.data)) return false; + return Objects.equals(ports, that.ports); + } + + @Override + public int hashCode() { + int result = data != null ? data.hashCode() : 0; + result = 31 * result + type; + result = 31 * result + (ports != null ? ports.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "WebMessageCompatExt{" + + "data=" + data + + ", type=" + type + + ", ports=" + ports + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessagePort.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessagePort.java new file mode 100644 index 00000000..a9b6cdf2 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessagePort.java @@ -0,0 +1,136 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.text.TextUtils; +import android.webkit.ValueCallback; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.Util; +import com.pichillilorenzo.flutter_inappwebview_android.webview.InAppWebViewInterface; +import com.pichillilorenzo.flutter_inappwebview_android.webview.web_message.WebMessageChannel; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.JavaScriptBridgeJS; + +import java.util.ArrayList; +import java.util.List; + +public class WebMessagePort { + public String name; + @Nullable + public WebMessageChannel webMessageChannel; + public boolean isClosed = false; + public boolean isTransferred = false; + public boolean isStarted = false; + + public WebMessagePort(String name, @NonNull WebMessageChannel webMessageChannel) { + this.name = name; + this.webMessageChannel = webMessageChannel; + } + + public void setWebMessageCallback(final ValueCallback callback) throws Exception { + if (isClosed || isTransferred) { + throw new Exception("Port is already closed or transferred"); + } + this.isStarted = true; + InAppWebViewInterface webView = webMessageChannel != null && webMessageChannel.webView != null ? webMessageChannel.webView : null; + if (webView != null) { + int index = name.equals("port1") ? 0 : 1; + webView.evaluateJavascript("(function() {" + + " var webMessageChannel = " + JavaScriptBridgeJS.WEB_MESSAGE_CHANNELS_VARIABLE_NAME + "['" + webMessageChannel.id + "'];" + + " if (webMessageChannel != null) {" + + " webMessageChannel." + this.name + ".onmessage = function (event) {" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('onWebMessagePortMessageReceived', {" + + " 'webMessageChannelId': '" + webMessageChannel.id + "'," + + " 'index': " + index + "," + + " 'message': event.data" + + " });" + + " }" + + " }" + + "})();", null, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + if (callback != null) { + callback.onReceiveValue(null); + } + } + }); + } else { + if (callback != null) { + callback.onReceiveValue(null); + } + } + } + + public void postMessage(WebMessage message, final ValueCallback callback) throws Exception { + if (isClosed || isTransferred) { + throw new Exception("Port is already closed or transferred"); + } + InAppWebViewInterface webView = webMessageChannel != null && webMessageChannel.webView != null ? webMessageChannel.webView : null; + if (webView != null) { + String portsString = "null"; + List ports = message.ports; + if (ports != null) { + List portArrayString = new ArrayList<>(); + for (WebMessagePort port : ports) { + if (port == this) { + throw new Exception("Source port cannot be transferred"); + } + if (port.isStarted) { + throw new Exception("Port is already started"); + } + if (port.isClosed || port.isTransferred) { + throw new Exception("Port is already closed or transferred"); + } + port.isTransferred = true; + portArrayString.add(JavaScriptBridgeJS.WEB_MESSAGE_CHANNELS_VARIABLE_NAME + "['" + webMessageChannel.id + "']." + port.name); + } + portsString = "[" + TextUtils.join(", ", portArrayString) + "]"; + } + String data = message.data != null ? Util.replaceAll(message.data, "\'", "\\'") : "null"; + String source = "(function() {" + + " var webMessageChannel = " + JavaScriptBridgeJS.WEB_MESSAGE_CHANNELS_VARIABLE_NAME + "['" + webMessageChannel.id + "'];" + + " if (webMessageChannel != null) {" + + " webMessageChannel." + this.name + ".postMessage('" + data + "', " + portsString + ");" + + " }" + + "})();"; + webView.evaluateJavascript(source, null, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + callback.onReceiveValue(null); + } + }); + } else { + callback.onReceiveValue(null); + } + message.dispose(); + } + + public void close(final ValueCallback callback) throws Exception { + if (isTransferred) { + throw new Exception("Port is already transferred"); + } + isClosed = true; + InAppWebViewInterface webView = webMessageChannel != null && webMessageChannel.webView != null ? webMessageChannel.webView : null; + if (webView != null) { + String source = "(function() {" + + " var webMessageChannel = " + JavaScriptBridgeJS.WEB_MESSAGE_CHANNELS_VARIABLE_NAME + "['" + webMessageChannel.id + "'];" + + " if (webMessageChannel != null) {" + + " webMessageChannel." + this.name + ".close();" + + " }" + + "})();"; + webView.evaluateJavascript(source, null, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + callback.onReceiveValue(null); + } + }); + } else { + callback.onReceiveValue(null); + } + } + + public void dispose() { + isClosed = true; + webMessageChannel = null; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessagePortCompatExt.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessagePortCompatExt.java new file mode 100644 index 00000000..a03e4d65 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebMessagePortCompatExt.java @@ -0,0 +1,78 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class WebMessagePortCompatExt { + private int index; + @NonNull + private String webMessageChannelId; + + public WebMessagePortCompatExt(int index, @NonNull String webMessageChannelId) { + this.index = index; + this.webMessageChannelId = webMessageChannelId; + } + + @Nullable + public static WebMessagePortCompatExt fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + Integer index = (Integer) map.get("index"); + String webMessageChannelId = (String) map.get("webMessageChannelId"); + return new WebMessagePortCompatExt(index, webMessageChannelId); + } + + public Map toMap() { + Map proxyRuleMap = new HashMap<>(); + proxyRuleMap.put("index", index); + proxyRuleMap.put("webMessageChannelId", webMessageChannelId); + return proxyRuleMap; + } + + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + + @NonNull + public String getWebMessageChannelId() { + return webMessageChannelId; + } + + public void setWebMessageChannelId(@NonNull String webMessageChannelId) { + this.webMessageChannelId = webMessageChannelId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + WebMessagePortCompatExt that = (WebMessagePortCompatExt) o; + + if (index != that.index) return false; + return webMessageChannelId.equals(that.webMessageChannelId); + } + + @Override + public int hashCode() { + int result = index; + result = 31 * result + webMessageChannelId.hashCode(); + return result; + } + + @Override + public String toString() { + return "WebMessagePortCompatExt{" + + "index=" + index + + ", webMessageChannelId='" + webMessageChannelId + '\'' + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebResourceErrorExt.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebResourceErrorExt.java new file mode 100644 index 00000000..bf23cc73 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebResourceErrorExt.java @@ -0,0 +1,91 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.os.Build; +import android.webkit.WebResourceError; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.webkit.WebResourceErrorCompat; +import androidx.webkit.WebViewFeature; + +import java.util.HashMap; +import java.util.Map; + +public class WebResourceErrorExt { + private int type; + @NonNull + private String description; + + public WebResourceErrorExt(int type, @NonNull String description) { + this.type = type; + this.description = description; + } + + @RequiresApi(Build.VERSION_CODES.M) + static public WebResourceErrorExt fromWebResourceError(@NonNull WebResourceError error) { + return new WebResourceErrorExt(error.getErrorCode(), error.getDescription().toString()); + } + + @RequiresApi(Build.VERSION_CODES.M) + static public WebResourceErrorExt fromWebResourceError(@NonNull WebResourceErrorCompat error) { + int type = -1; + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE)) { + type = error.getErrorCode(); + } + String description = ""; + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_DESCRIPTION)) { + description = error.getDescription().toString(); + } + return new WebResourceErrorExt(type, description); + } + + public Map toMap() { + Map webResourceErrorMap = new HashMap<>(); + webResourceErrorMap.put("type", getType()); + webResourceErrorMap.put("description", getDescription()); + return webResourceErrorMap; + } + + public int getType() { + return type; + } + + public void setType(int type) { + this.type = type; + } + + @NonNull + public String getDescription() { + return description; + } + + public void setDescription(@NonNull String description) { + this.description = description; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + WebResourceErrorExt that = (WebResourceErrorExt) o; + + if (type != that.type) return false; + return description.equals(that.description); + } + + @Override + public int hashCode() { + int result = type; + result = 31 * result + description.hashCode(); + return result; + } + + @Override + public String toString() { + return "WebResourceErrorExt{" + + "type=" + type + + ", description='" + description + '\'' + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebResourceRequestExt.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebResourceRequestExt.java new file mode 100644 index 00000000..b33d8072 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebResourceRequestExt.java @@ -0,0 +1,146 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.os.Build; +import android.webkit.WebResourceRequest; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.webkit.WebResourceRequestCompat; +import androidx.webkit.WebViewFeature; + +import java.util.HashMap; +import java.util.Map; + +public class WebResourceRequestExt { + @NonNull + private String url; + private Map headers; + private boolean isRedirect; + private boolean hasGesture; + private boolean isForMainFrame; + private String method; + + public WebResourceRequestExt(@NonNull String url, Map headers, boolean isRedirect, boolean hasGesture, boolean isForMainFrame, String method) { + this.url = url; + this.headers = headers; + this.isRedirect = isRedirect; + this.hasGesture = hasGesture; + this.isForMainFrame = isForMainFrame; + this.method = method; + } + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + static public WebResourceRequestExt fromWebResourceRequest(@NonNull WebResourceRequest request) { + boolean isRedirect = false; + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_REQUEST_IS_REDIRECT)) { + isRedirect = WebResourceRequestCompat.isRedirect(request); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + isRedirect = request.isRedirect(); + } + return new WebResourceRequestExt(request.getUrl().toString(), + request.getRequestHeaders(), + isRedirect, + request.hasGesture(), + request.isForMainFrame(), + request.getMethod() + ); + } + + public Map toMap() { + Map webResourceRequestMap = new HashMap<>(); + webResourceRequestMap.put("url", url); + webResourceRequestMap.put("headers", headers); + webResourceRequestMap.put("isRedirect", isRedirect); + webResourceRequestMap.put("hasGesture", hasGesture); + webResourceRequestMap.put("isForMainFrame", isForMainFrame); + webResourceRequestMap.put("method", method); + return webResourceRequestMap; + } + + @NonNull + public String getUrl() { + return url; + } + + public void setUrl(@NonNull String url) { + this.url = url; + } + + public Map getHeaders() { + return headers; + } + + public void setHeaders(Map headers) { + this.headers = headers; + } + + public boolean isRedirect() { + return isRedirect; + } + + public void setRedirect(boolean redirect) { + isRedirect = redirect; + } + + public boolean isHasGesture() { + return hasGesture; + } + + public void setHasGesture(boolean hasGesture) { + this.hasGesture = hasGesture; + } + + public boolean isForMainFrame() { + return isForMainFrame; + } + + public void setForMainFrame(boolean forMainFrame) { + isForMainFrame = forMainFrame; + } + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + WebResourceRequestExt that = (WebResourceRequestExt) o; + + if (isRedirect != that.isRedirect) return false; + if (hasGesture != that.hasGesture) return false; + if (isForMainFrame != that.isForMainFrame) return false; + if (!url.equals(that.url)) return false; + if (headers != null ? !headers.equals(that.headers) : that.headers != null) return false; + return method != null ? method.equals(that.method) : that.method == null; + } + + @Override + public int hashCode() { + int result = url.hashCode(); + result = 31 * result + (headers != null ? headers.hashCode() : 0); + result = 31 * result + (isRedirect ? 1 : 0); + result = 31 * result + (hasGesture ? 1 : 0); + result = 31 * result + (isForMainFrame ? 1 : 0); + result = 31 * result + (method != null ? method.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "WebResourceRequestExt{" + + "url=" + url + + ", headers=" + headers + + ", isRedirect=" + isRedirect + + ", hasGesture=" + hasGesture + + ", isForMainFrame=" + isForMainFrame + + ", method='" + method + '\'' + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebResourceResponseExt.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebResourceResponseExt.java new file mode 100644 index 00000000..824ccc0f --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebResourceResponseExt.java @@ -0,0 +1,177 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.os.Build; +import android.webkit.WebResourceResponse; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.Util; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class WebResourceResponseExt { + @Nullable + private String contentType; + @Nullable + private String contentEncoding; + @Nullable + private Integer statusCode; + @Nullable + private String reasonPhrase; + @Nullable + private Map headers; + @Nullable + private byte[] data; + + public WebResourceResponseExt(@Nullable String contentType, @Nullable String contentEncoding, @Nullable Integer statusCode, + @Nullable String reasonPhrase, @Nullable Map headers, @Nullable byte[] data) { + this.contentType = contentType; + this.contentEncoding = contentEncoding; + this.statusCode = statusCode; + this.reasonPhrase = reasonPhrase; + this.headers = headers; + this.data = data; + } + + static public WebResourceResponseExt fromWebResourceResponse(@NonNull WebResourceResponse response) { + Integer statusCode = null; + String reasonPhrase = null; + Map headers = null; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + statusCode = response.getStatusCode(); + reasonPhrase = response.getReasonPhrase(); + headers = response.getResponseHeaders(); + } + return new WebResourceResponseExt(response.getMimeType(), + response.getEncoding(), + statusCode, + reasonPhrase, + headers, + Util.readAllBytes(response.getData()) + ); + } + + @Nullable + public static WebResourceResponseExt fromMap(@Nullable Map map) { + if (map == null) { + return null; + } + String contentType = (String) map.get("contentType"); + String contentEncoding = (String) map.get("contentEncoding"); + Integer statusCode = (Integer) map.get("statusCode"); + String reasonPhrase = (String) map.get("reasonPhrase"); + Map headers = (Map) map.get("headers"); + byte[] data = (byte[]) map.get("data"); + return new WebResourceResponseExt(contentType, contentEncoding, statusCode, reasonPhrase, headers, data); + } + + public Map toMap() { + Map webResourceResponseMap = new HashMap<>(); + webResourceResponseMap.put("contentType", contentType); + webResourceResponseMap.put("contentEncoding", contentEncoding); + webResourceResponseMap.put("statusCode", statusCode); + webResourceResponseMap.put("reasonPhrase", reasonPhrase); + webResourceResponseMap.put("headers", headers); + webResourceResponseMap.put("data", data); + return webResourceResponseMap; + } + + @Nullable + public String getContentType() { + return contentType; + } + + public void setContentType(@Nullable String contentType) { + this.contentType = contentType; + } + + @Nullable + public String getContentEncoding() { + return contentEncoding; + } + + public void setContentEncoding(@Nullable String contentEncoding) { + this.contentEncoding = contentEncoding; + } + + @Nullable + public Integer getStatusCode() { + return statusCode; + } + + public void setStatusCode(@Nullable Integer statusCode) { + this.statusCode = statusCode; + } + + @Nullable + public String getReasonPhrase() { + return reasonPhrase; + } + + public void setReasonPhrase(@Nullable String reasonPhrase) { + this.reasonPhrase = reasonPhrase; + } + + @Nullable + public Map getHeaders() { + return headers; + } + + public void setHeaders(@Nullable Map headers) { + this.headers = headers; + } + + @Nullable + public byte[] getData() { + return data; + } + + public void setData(@Nullable byte[] data) { + this.data = data; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + WebResourceResponseExt that = (WebResourceResponseExt) o; + + if (contentType != null ? !contentType.equals(that.contentType) : that.contentType != null) + return false; + if (contentEncoding != null ? !contentEncoding.equals(that.contentEncoding) : that.contentEncoding != null) + return false; + if (statusCode != null ? !statusCode.equals(that.statusCode) : that.statusCode != null) + return false; + if (reasonPhrase != null ? !reasonPhrase.equals(that.reasonPhrase) : that.reasonPhrase != null) + return false; + if (headers != null ? !headers.equals(that.headers) : that.headers != null) return false; + return Arrays.equals(data, that.data); + } + + @Override + public int hashCode() { + int result = contentType != null ? contentType.hashCode() : 0; + result = 31 * result + (contentEncoding != null ? contentEncoding.hashCode() : 0); + result = 31 * result + (statusCode != null ? statusCode.hashCode() : 0); + result = 31 * result + (reasonPhrase != null ? reasonPhrase.hashCode() : 0); + result = 31 * result + (headers != null ? headers.hashCode() : 0); + result = 31 * result + Arrays.hashCode(data); + return result; + } + + @Override + public String toString() { + return "WebResourceResponseExt{" + + "contentType='" + contentType + '\'' + + ", contentEncoding='" + contentEncoding + '\'' + + ", statusCode=" + statusCode + + ", reasonPhrase='" + reasonPhrase + '\'' + + ", headers=" + headers + + ", data=" + Arrays.toString(data) + + '}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebViewAssetLoaderExt.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebViewAssetLoaderExt.java new file mode 100644 index 00000000..0a0c747e --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/types/WebViewAssetLoaderExt.java @@ -0,0 +1,209 @@ +package com.pichillilorenzo.flutter_inappwebview_android.types; + +import android.content.Context; +import android.os.Build; +import android.util.Log; +import android.webkit.WebResourceResponse; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.WebViewAssetLoader; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.Util; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.flutter.plugin.common.MethodChannel; + +public class WebViewAssetLoaderExt implements Disposable { + @Nullable + public WebViewAssetLoader loader; + @NonNull + public List customPathHandlers; + + public WebViewAssetLoaderExt(@Nullable WebViewAssetLoader loader, @NonNull List customPathHandlers) { + this.loader = loader; + this.customPathHandlers = customPathHandlers; + } + + @Nullable + public static WebViewAssetLoaderExt fromMap(@Nullable Map map, @NonNull InAppWebViewFlutterPlugin plugin, @NonNull Context context) { + if (map == null) { + return null; + } + WebViewAssetLoader.Builder builder = new WebViewAssetLoader.Builder(); + String domain = (String) map.get("domain"); + Boolean httpAllowed = (Boolean) map.get("httpAllowed"); + List> pathHandlers = (List>) map.get("pathHandlers"); + List customPathHandlers = new ArrayList<>(); + if (domain != null && !domain.isEmpty()) { + builder.setDomain(domain); + } + if (httpAllowed != null) { + builder.setHttpAllowed(httpAllowed); + } + if (pathHandlers != null) { + for (Map pathHandler : pathHandlers) { + String type = (String) pathHandler.get("type"); + String path = (String) pathHandler.get("path"); + if (type == null || path == null) { + continue; + } + switch (type) { + case "AssetsPathHandler": + WebViewAssetLoader.AssetsPathHandler assetsPathHandler = + new WebViewAssetLoader.AssetsPathHandler(context); + builder.addPathHandler(path, assetsPathHandler); + break; + case "InternalStoragePathHandler": + String directory = (String) pathHandler.get("directory"); + if (directory == null) { + continue; + } + File dir = new File(directory); + WebViewAssetLoader.InternalStoragePathHandler internalStoragePathHandler = + new WebViewAssetLoader.InternalStoragePathHandler(context, dir); + builder.addPathHandler(path, internalStoragePathHandler); + break; + case "ResourcesPathHandler": + WebViewAssetLoader.ResourcesPathHandler resourcesPathHandler = new WebViewAssetLoader.ResourcesPathHandler(context); + builder.addPathHandler(path, resourcesPathHandler); + break; + default: + String id = (String) pathHandler.get("id"); + if (id == null) { + continue; + } + PathHandlerExt customPathHandler = new PathHandlerExt(id, plugin); + builder.addPathHandler(path, customPathHandler); + customPathHandlers.add(customPathHandler); + break; + } + } + } + return new WebViewAssetLoaderExt(builder.build(), customPathHandlers); + } + + @Override + public void dispose() { + for (PathHandlerExt pathHandler : customPathHandlers) { + pathHandler.dispose(); + } + customPathHandlers.clear(); + } + + public static class PathHandlerExt implements WebViewAssetLoader.PathHandler, Disposable { + + protected static final String LOG_TAG = "PathHandlerExt"; + public static final String METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_custompathhandler_"; + + @NonNull + public String id; + @Nullable + public PathHandlerExtChannelDelegate channelDelegate; + + public PathHandlerExt(@NonNull String id, @NonNull InAppWebViewFlutterPlugin plugin) { + this.id = id; + final MethodChannel channel = new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME_PREFIX + id); + this.channelDelegate = new PathHandlerExtChannelDelegate(this, channel); + } + + @Nullable + @Override + public WebResourceResponse handle(@NonNull String path) { + if (channelDelegate != null) { + WebResourceResponseExt response = null; + + try { + response = channelDelegate.handle(path); + } catch (InterruptedException e) { + Log.e(LOG_TAG, "", e); + return null; + } + + if (response != null) { + String contentType = response.getContentType(); + String contentEncoding = response.getContentEncoding(); + byte[] data = response.getData(); + Map responseHeaders = response.getHeaders(); + Integer statusCode = response.getStatusCode(); + String reasonPhrase = response.getReasonPhrase(); + + ByteArrayInputStream inputStream = (data != null) ? new ByteArrayInputStream(data) : null; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && statusCode != null && reasonPhrase != null) { + return new WebResourceResponse(contentType, contentEncoding, statusCode, reasonPhrase, responseHeaders, inputStream); + } else { + return new WebResourceResponse(contentType, contentEncoding, inputStream); + } + } + } + return null; + } + + @Override + public void dispose() { + if (channelDelegate != null) { + channelDelegate.dispose(); + channelDelegate = null; + } + } + } + + public static class PathHandlerExtChannelDelegate extends ChannelDelegateImpl { + + @Nullable + private PathHandlerExt pathHandler; + + public PathHandlerExtChannelDelegate(@NonNull PathHandlerExt pathHandler, @NonNull MethodChannel channel) { + super(channel); + this.pathHandler = pathHandler; + } + + public static class HandleCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public WebResourceResponseExt decodeResult(@Nullable Object obj) { + return WebResourceResponseExt.fromMap((Map) obj); + } + } + + public void handle(String path, @NonNull HandleCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("path", path); + channel.invokeMethod("handle", obj, callback); + } + + public static class SyncHandleCallback extends SyncBaseCallbackResultImpl { + @Nullable + @Override + public WebResourceResponseExt decodeResult(@Nullable Object obj) { + return (new HandleCallback()).decodeResult(obj); + } + } + + @Nullable + public WebResourceResponseExt handle(String path) throws InterruptedException { + MethodChannel channel = getChannel(); + if (channel == null) return null; + final SyncHandleCallback callback = new SyncHandleCallback(); + Map obj = new HashMap<>(); + obj.put("path", path); + return Util.invokeMethodAndWaitResult(channel, "handle", obj, callback); + } + + @Override + public void dispose() { + super.dispose(); + pathHandler = null; + } + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/ContextMenuSettings.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/ContextMenuSettings.java new file mode 100644 index 00000000..fb0f1946 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/ContextMenuSettings.java @@ -0,0 +1,49 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview; + +import androidx.annotation.NonNull; + +import com.pichillilorenzo.flutter_inappwebview_android.ISettings; + +import java.util.HashMap; +import java.util.Map; + +public class ContextMenuSettings implements ISettings { + public static final String LOG_TAG = "ContextMenuOptions"; + + public Boolean hideDefaultSystemContextMenuItems = false; + + @NonNull + @Override + public ContextMenuSettings parse(@NonNull Map options) { + for (Map.Entry pair : options.entrySet()) { + String key = pair.getKey(); + Object value = pair.getValue(); + if (value == null) { + continue; + } + + switch (key) { + case "hideDefaultSystemContextMenuItems": + hideDefaultSystemContextMenuItems = (Boolean) value; + break; + } + } + + return this; + } + + @NonNull + public Map toMap() { + Map options = new HashMap<>(); + options.put("hideDefaultSystemContextMenuItems", hideDefaultSystemContextMenuItems); + return options; + } + + @NonNull + @Override + public Map getRealSettings(@NonNull Object obj) { + Map realOptions = toMap(); + return realOptions; + } + +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/FlutterWebViewFactory.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/FlutterWebViewFactory.java new file mode 100755 index 00000000..6acd91e4 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/FlutterWebViewFactory.java @@ -0,0 +1,81 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.headless_in_app_webview.HeadlessInAppWebView; +import com.pichillilorenzo.flutter_inappwebview_android.headless_in_app_webview.HeadlessInAppWebViewManager; +import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.FlutterWebView; + +import java.util.HashMap; + +import io.flutter.plugin.common.StandardMessageCodec; +import io.flutter.plugin.platform.PlatformView; +import io.flutter.plugin.platform.PlatformViewFactory; + +public class FlutterWebViewFactory extends PlatformViewFactory { + public static final String VIEW_TYPE_ID = "com.pichillilorenzo/flutter_inappwebview"; + private final InAppWebViewFlutterPlugin plugin; + + public FlutterWebViewFactory(final InAppWebViewFlutterPlugin plugin) { + super(StandardMessageCodec.INSTANCE); + this.plugin = plugin; + } + + @Override + public PlatformView create(Context context, int id, Object args) { + HashMap params = (HashMap) args; + FlutterWebView flutterWebView = null; + Object viewId = id; + + String keepAliveId = (String) params.get("keepAliveId"); + String headlessWebViewId = (String) params.get("headlessWebViewId"); + + HeadlessInAppWebViewManager headlessInAppWebViewManager = plugin.headlessInAppWebViewManager; + if (headlessWebViewId != null && headlessInAppWebViewManager != null) { + HeadlessInAppWebView headlessInAppWebView = headlessInAppWebViewManager.webViews.get(headlessWebViewId); + if (headlessInAppWebView != null) { + flutterWebView = headlessInAppWebView.disposeAndGetFlutterWebView(); + if (flutterWebView != null) { + flutterWebView.keepAliveId = keepAliveId; + } + } + } + + InAppWebViewManager inAppWebViewManager = plugin.inAppWebViewManager; + if (keepAliveId != null && flutterWebView == null && inAppWebViewManager != null) { + flutterWebView = inAppWebViewManager.keepAliveWebViews.get(keepAliveId); + if (flutterWebView != null) { + // be sure to remove the view from the previous parent. + View view = flutterWebView.getView(); + if (view != null) { + ViewGroup parent = (ViewGroup) view.getParent(); + if (parent != null) { + parent.removeView(view); + } + } + } + } + + boolean shouldMakeInitialLoad = flutterWebView == null; + if (flutterWebView == null) { + if (keepAliveId != null) { + viewId = keepAliveId; + } + flutterWebView = new FlutterWebView(plugin, context, viewId, params); + } + + if (keepAliveId != null && inAppWebViewManager != null) { + inAppWebViewManager.keepAliveWebViews.put(keepAliveId, flutterWebView); + } + + if (shouldMakeInitialLoad) { + flutterWebView.makeInitialLoad(params); + } + + return flutterWebView; + } +} + diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/InAppWebViewInterface.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/InAppWebViewInterface.java new file mode 100644 index 00000000..ece31292 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/InAppWebViewInterface.java @@ -0,0 +1,119 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview; + +import android.content.Context; +import android.net.Uri; +import android.net.http.SslCertificate; +import android.os.Looper; +import android.webkit.ValueCallback; +import android.webkit.WebMessage; +import android.webkit.WebView; + +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.in_app_browser.InAppBrowserDelegate; +import com.pichillilorenzo.flutter_inappwebview_android.print_job.PrintJobSettings; +import com.pichillilorenzo.flutter_inappwebview_android.types.ContentWorld; +import com.pichillilorenzo.flutter_inappwebview_android.types.HitTestResult; +import com.pichillilorenzo.flutter_inappwebview_android.types.URLRequest; +import com.pichillilorenzo.flutter_inappwebview_android.types.UserContentController; +import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebViewSettings; +import com.pichillilorenzo.flutter_inappwebview_android.webview.web_message.WebMessageChannel; +import com.pichillilorenzo.flutter_inappwebview_android.webview.web_message.WebMessageListener; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import io.flutter.plugin.common.MethodChannel; + +public interface InAppWebViewInterface { + Context getContext(); + String getUrl(); + String getTitle(); + int getProgress(); + void loadUrl(URLRequest urlRequest); + void postUrl(String url, byte[] postData); + void loadDataWithBaseURL(String baseUrl, String data, + String mimeType, String encoding, String historyUrl); + void loadFile(String assetFilePath) throws IOException; + void evaluateJavascript(String source, ContentWorld contentWorld, ValueCallback resultCallback); + void injectJavascriptFileFromUrl(String urlFile, Map scriptHtmlTagAttributes); + void injectCSSCode(String source); + void injectCSSFileFromUrl(String urlFile, Map cssLinkHtmlTagAttributes); + void reload(); + void goBack(); + boolean canGoBack(); + void goForward(); + boolean canGoForward(); + void goBackOrForward(int steps); + boolean canGoBackOrForward(int steps); + void stopLoading(); + boolean isLoading(); + void takeScreenshot(Map screenshotConfiguration, MethodChannel.Result result); + void setSettings(InAppWebViewSettings newSettings, HashMap newSettingsMap); + Map getCustomSettings(); + HashMap getCopyBackForwardList(); + void clearAllCache(); + void clearSslPreferences(); + void findAllAsync(String find); + void findNext(boolean forward); + void clearMatches(); + void scrollTo(Integer x, Integer y, Boolean animated); + void scrollBy(Integer x, Integer y, Boolean animated); + void onPause(); + void onResume(); + void pauseTimers(); + void resumeTimers(); + @Nullable + String printCurrentPage(@Nullable PrintJobSettings settings); + int getContentHeight(); + void getContentHeight(ValueCallback callback); + void getContentWidth(ValueCallback callback); + void zoomBy(float zoomFactor); + String getOriginalUrl(); + void getSelectedText(ValueCallback callback); + WebView.HitTestResult getHitTestResult(); + void getHitTestResult(ValueCallback callback); + boolean pageDown(boolean bottom); + boolean pageUp(boolean top); + void saveWebArchive(String basename, boolean autoname, ValueCallback callback); + boolean zoomIn(); + boolean zoomOut(); + void clearFocus(); + Map requestFocusNodeHref(); + Map requestImageRef(); + int getScrollX(); + int getScrollY(); + SslCertificate getCertificate(); + void clearHistory(); + void callAsyncJavaScript(String functionBody, Map arguments, ContentWorld contentWorld, ValueCallback resultCallback); + void isSecureContext(final ValueCallback resultCallback); + WebMessageChannel createCompatWebMessageChannel(); + WebMessageChannel createWebMessageChannel(ValueCallback callback); + void postWebMessage(WebMessage message, Uri targetOrigin); + void postWebMessage(com.pichillilorenzo.flutter_inappwebview_android.types.WebMessage message, Uri targetOrigin, ValueCallback callback) throws Exception; + void addWebMessageListener(WebMessageListener webMessageListener) throws Exception; + boolean canScrollVertically(); + boolean canScrollHorizontally(); + float getZoomScale(); + void getZoomScale(ValueCallback callback); + Map getContextMenu(); + void setContextMenu(Map contextMenu); + InAppWebViewFlutterPlugin getPlugin(); + void setPlugin(InAppWebViewFlutterPlugin plugin); + InAppBrowserDelegate getInAppBrowserDelegate(); + void setInAppBrowserDelegate(InAppBrowserDelegate inAppBrowserDelegate); + UserContentController getUserContentController(); + void setUserContentController(UserContentController userContentController); + Map getWebMessageChannels(); + void setWebMessageChannels(Map webMessageChannels); + void disposeWebMessageChannels(); + void disposeWebMessageListeners(); + Looper getWebViewLooper(); + boolean isInFullscreen(); + void setInFullscreen(boolean inFullscreen); + @Nullable + WebViewChannelDelegate getChannelDelegate(); + void setChannelDelegate(@Nullable WebViewChannelDelegate eventWebViewChannelDelegate); +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/InAppWebViewManager.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/InAppWebViewManager.java new file mode 100755 index 00000000..623354ae --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/InAppWebViewManager.java @@ -0,0 +1,219 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.os.Build; +import android.os.Message; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.ValueCallback; +import android.webkit.WebSettings; +import android.webkit.WebView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.WebViewCompat; +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.webview.in_app_webview.FlutterWebView; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class InAppWebViewManager extends ChannelDelegateImpl { + protected static final String LOG_TAG = "InAppWebViewManager"; + public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_manager"; + + @Nullable + public InAppWebViewFlutterPlugin plugin; + + public final Map keepAliveWebViews = new HashMap<>(); + + public final Map windowWebViewMessages = new HashMap<>(); + public int windowAutoincrementId = 0; + + public InAppWebViewManager(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 "getDefaultUserAgent": + if (plugin != null) { + result.success(WebSettings.getDefaultUserAgent(plugin.applicationContext)); + } else { + result.success(null); + } + break; + case "clearClientCertPreferences": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + WebView.clearClientCertPreferences(new Runnable() { + @Override + public void run() { + result.success(true); + } + }); + } else { + result.success(false); + } + break; + case "getSafeBrowsingPrivacyPolicyUrl": + if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_PRIVACY_POLICY_URL)) { + result.success(WebViewCompat.getSafeBrowsingPrivacyPolicyUrl().toString()); + } else + result.success(null); + break; + case "setSafeBrowsingAllowlist": + if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ALLOWLIST)) { + Set hosts = new HashSet<>((List) call.argument("hosts")); + WebViewCompat.setSafeBrowsingAllowlist(hosts, new ValueCallback() { + @Override + public void onReceiveValue(Boolean value) { + result.success(value); + } + }); + } + else if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_WHITELIST)) { + List hosts = (List) call.argument("hosts"); + WebViewCompat.setSafeBrowsingWhitelist(hosts, new ValueCallback() { + @Override + public void onReceiveValue(Boolean value) { + result.success(value); + } + }); + } else + result.success(false); + break; + case "getCurrentWebViewPackage": + { + Context context = null; + if (plugin != null) { + context = plugin.activity; + if (context == null) { + context = plugin.applicationContext; + } + } + PackageInfo packageInfo = context != null ? WebViewCompat.getCurrentWebViewPackage(context) : null; + result.success(packageInfo != null ? convertWebViewPackageToMap(packageInfo) : null); + } + break; + case "setWebContentsDebuggingEnabled": + { + boolean debuggingEnabled = (boolean) call.argument("debuggingEnabled"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + WebView.setWebContentsDebuggingEnabled(debuggingEnabled); + } + } + result.success(true); + break; + case "getVariationsHeader": + if (WebViewFeature.isFeatureSupported(WebViewFeature.GET_VARIATIONS_HEADER)) { + result.success(WebViewCompat.getVariationsHeader()); + } + else { + result.success(null); + } + break; + case "isMultiProcessEnabled": + if (WebViewFeature.isFeatureSupported(WebViewFeature.MULTI_PROCESS)) { + result.success(WebViewCompat.isMultiProcessEnabled()); + } + else { + result.success(false); + } + break; + case "disableWebView": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + WebView.disableWebView(); + } + result.success(true); + break; + case "disposeKeepAlive": + final String keepAliveId = (String) call.argument("keepAliveId"); + if (keepAliveId != null) { + disposeKeepAlive(keepAliveId); + } + result.success(true); + break; + case "clearAllCache": + { + Context context = null; + if (plugin != null) { + context = plugin.activity; + if (context == null) { + context = plugin.applicationContext; + } + if (context != null) { + boolean includeDiskFiles = (boolean) call.argument("includeDiskFiles"); + clearAllCache(context, includeDiskFiles); + } + } + } + result.success(true); + break; + default: + result.notImplemented(); + } + } + + @NonNull + public Map convertWebViewPackageToMap(@NonNull PackageInfo webViewPackageInfo) { + HashMap webViewPackageInfoMap = new HashMap<>(); + + webViewPackageInfoMap.put("versionName", webViewPackageInfo.versionName); + webViewPackageInfoMap.put("packageName", webViewPackageInfo.packageName); + + return webViewPackageInfoMap; + } + + public void disposeKeepAlive(@NonNull String keepAliveId) { + FlutterWebView flutterWebView = keepAliveWebViews.get(keepAliveId); + if (flutterWebView != null) { + flutterWebView.keepAliveId = null; + // be sure to remove the view from the previous parent. + View view = flutterWebView.getView(); + if (view != null) { + ViewGroup parent = (ViewGroup) view.getParent(); + if (parent != null) { + parent.removeView(view); + } + } + flutterWebView.dispose(); + } + if (keepAliveWebViews.containsKey(keepAliveId)) { + keepAliveWebViews.put(keepAliveId, null); + } + } + + public void clearAllCache(@NonNull Context context, boolean includeDiskFiles) { + WebView tempWebView = new WebView(context); + tempWebView.clearCache(includeDiskFiles); + tempWebView.destroy(); + } + + @Override + public void dispose() { + super.dispose(); + Collection flutterWebViews = keepAliveWebViews.values(); + for (FlutterWebView flutterWebView : flutterWebViews) { + String keepAliveId = flutterWebView.keepAliveId; + if (keepAliveId != null) { + disposeKeepAlive(flutterWebView.keepAliveId); + } + } + keepAliveWebViews.clear(); + windowWebViewMessages.clear(); + plugin = null; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/JavaScriptBridgeInterface.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/JavaScriptBridgeInterface.java new file mode 100755 index 00000000..615a1f86 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/JavaScriptBridgeInterface.java @@ -0,0 +1,173 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview; + +import android.os.Build; +import android.os.Handler; +import android.util.Log; +import android.webkit.JavascriptInterface; +import android.webkit.ValueCallback; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.print_job.PrintJobController; +import com.pichillilorenzo.flutter_inappwebview_android.print_job.PrintJobSettings; +import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebView; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.JavaScriptBridgeJS; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +public class JavaScriptBridgeInterface { + private static final String LOG_TAG = "JSBridgeInterface"; + private InAppWebView inAppWebView; + + public JavaScriptBridgeInterface(InAppWebView inAppWebView) { + this.inAppWebView = inAppWebView; + } + + @JavascriptInterface + public void _hideContextMenu() { + if (inAppWebView == null) { + return; + } + + final Handler handler = new Handler(inAppWebView.getWebViewLooper()); + handler.post(new Runnable() { + @Override + public void run() { + if (inAppWebView != null && inAppWebView.floatingContextMenu != null) { + inAppWebView.hideContextMenu(); + } + } + }); + } + + @JavascriptInterface + public void _callHandler(final String handlerName, final String _callHandlerID, final String args) { + if (inAppWebView == null) { + return; + } + + // java.lang.RuntimeException: Methods marked with @UiThread must be executed on the main thread. + // https://github.com/pichillilorenzo/flutter_inappwebview/issues/98 + final Handler handler = new Handler(inAppWebView.getWebViewLooper()); + handler.post(new Runnable() { + @Override + public void run() { + if (inAppWebView == null) { + // The webview has already been disposed, ignore. + return; + } + + if (handlerName.equals("onPrintRequest") && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + PrintJobSettings settings = new PrintJobSettings(); + settings.handledByClient = true; + final String printJobId = inAppWebView.printCurrentPage(settings); + if (inAppWebView != null && inAppWebView.channelDelegate != null) { + inAppWebView.channelDelegate.onPrintRequest(inAppWebView.getUrl(), printJobId, new WebViewChannelDelegate.PrintRequestCallback() { + @Override + public boolean nonNullSuccess(@NonNull Boolean handledByClient) { + return !handledByClient; + } + + @Override + public void defaultBehaviour(@Nullable Boolean handledByClient) { + if (inAppWebView != null && inAppWebView.plugin != null && inAppWebView.plugin.printJobManager != null) { + PrintJobController printJobController = inAppWebView.plugin.printJobManager.jobs.get(printJobId); + if (printJobController != null) { + printJobController.disposeNoCancel(); + } + } + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }); + } + return; + } else if (handlerName.equals("callAsyncJavaScript")) { + try { + JSONArray arguments = new JSONArray(args); + JSONObject jsonObject = arguments.getJSONObject(0); + String resultUuid = jsonObject.getString("resultUuid"); + ValueCallback callAsyncJavaScriptCallback = inAppWebView.callAsyncJavaScriptCallbacks.get(resultUuid); + if (callAsyncJavaScriptCallback != null) { + callAsyncJavaScriptCallback.onReceiveValue(jsonObject.toString()); + inAppWebView.callAsyncJavaScriptCallbacks.remove(resultUuid); + } + } catch (JSONException e) { + Log.e(LOG_TAG, "", e); + } + return; + } else if (handlerName.equals("evaluateJavaScriptWithContentWorld")) { + try { + JSONArray arguments = new JSONArray(args); + JSONObject jsonObject = arguments.getJSONObject(0); + String resultUuid = jsonObject.getString("resultUuid"); + ValueCallback evaluateJavaScriptCallback = inAppWebView.evaluateJavaScriptContentWorldCallbacks.get(resultUuid); + if (evaluateJavaScriptCallback != null) { + evaluateJavaScriptCallback.onReceiveValue(jsonObject.has("value") ? jsonObject.get("value").toString() : "null"); + inAppWebView.evaluateJavaScriptContentWorldCallbacks.remove(resultUuid); + } + } catch (JSONException e) { + Log.e(LOG_TAG, "", e); + } + return; + } + + if (inAppWebView.channelDelegate != null) { + // invoke flutter javascript handler and send back flutter data as a JSON Object to javascript + inAppWebView.channelDelegate.onCallJsHandler(handlerName, args, new WebViewChannelDelegate.CallJsHandlerCallback() { + @Override + public void defaultBehaviour(@Nullable Object json) { + if (inAppWebView == null) { + // The webview has already been disposed, ignore. + return; + } + String sourceCode = "if (window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "[" + _callHandlerID + "] != null) { " + + "window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "[" + _callHandlerID + "].resolve(" + json + "); " + + "delete window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "[" + _callHandlerID + "]; " + + "}"; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + inAppWebView.evaluateJavascript(sourceCode, (ValueCallback) null); + } + else { + inAppWebView.loadUrl("javascript:" + sourceCode); + } + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + String message = errorCode + ((errorMessage != null) ? ", " + errorMessage : ""); + Log.e(LOG_TAG, message); + + if (inAppWebView == null) { + // The webview has already been disposed, ignore. + return; + } + + String sourceCode = "if (window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "[" + _callHandlerID + "] != null) { " + + "window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "[" + _callHandlerID + "].reject(new Error(" + JSONObject.quote(message) + ")); " + + "delete window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "[" + _callHandlerID + "]; " + + "}"; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + inAppWebView.evaluateJavascript(sourceCode, (ValueCallback) null); + } + else { + inAppWebView.loadUrl("javascript:" + sourceCode); + } + } + }); + } + } + }); + } + + public void dispose() { + inAppWebView = null; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/PlatformWebView.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/PlatformWebView.java new file mode 100644 index 00000000..93ae83fa --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/PlatformWebView.java @@ -0,0 +1,9 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview; + +import java.util.HashMap; + +import io.flutter.plugin.platform.PlatformView; + +public interface PlatformWebView extends PlatformView { + void makeInitialLoad(HashMap params); +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/WebViewChannelDelegate.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/WebViewChannelDelegate.java new file mode 100644 index 00000000..5f87b93c --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/WebViewChannelDelegate.java @@ -0,0 +1,1318 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview; + +import android.net.Uri; +import android.os.Build; +import android.webkit.ValueCallback; +import android.webkit.WebView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.WebMessageCompat; +import androidx.webkit.WebMessagePortCompat; +import androidx.webkit.WebViewCompat; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.Util; +import com.pichillilorenzo.flutter_inappwebview_android.find_interaction.FindInteractionChannelDelegate; +import com.pichillilorenzo.flutter_inappwebview_android.in_app_browser.InAppBrowserActivity; +import com.pichillilorenzo.flutter_inappwebview_android.in_app_browser.InAppBrowserSettings; +import com.pichillilorenzo.flutter_inappwebview_android.print_job.PrintJobSettings; +import com.pichillilorenzo.flutter_inappwebview_android.types.BaseCallbackResultImpl; +import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl; +import com.pichillilorenzo.flutter_inappwebview_android.types.ClientCertChallenge; +import com.pichillilorenzo.flutter_inappwebview_android.types.ClientCertResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.ContentWorld; +import com.pichillilorenzo.flutter_inappwebview_android.types.CreateWindowAction; +import com.pichillilorenzo.flutter_inappwebview_android.types.CustomSchemeResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.DownloadStartRequest; +import com.pichillilorenzo.flutter_inappwebview_android.types.GeolocationPermissionShowPromptResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.HitTestResult; +import com.pichillilorenzo.flutter_inappwebview_android.types.HttpAuthResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.HttpAuthenticationChallenge; +import com.pichillilorenzo.flutter_inappwebview_android.types.JsAlertResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.JsBeforeUnloadResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.JsConfirmResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.JsPromptResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.NavigationAction; +import com.pichillilorenzo.flutter_inappwebview_android.types.NavigationActionPolicy; +import com.pichillilorenzo.flutter_inappwebview_android.types.PermissionResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.SafeBrowsingResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.ServerTrustAuthResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.ServerTrustChallenge; +import com.pichillilorenzo.flutter_inappwebview_android.types.SslCertificateExt; +import com.pichillilorenzo.flutter_inappwebview_android.types.SyncBaseCallbackResultImpl; +import com.pichillilorenzo.flutter_inappwebview_android.types.URLRequest; +import com.pichillilorenzo.flutter_inappwebview_android.types.UserScript; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebMessageCompatExt; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebMessagePortCompatExt; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceErrorExt; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceRequestExt; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceResponseExt; +import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebView; +import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebViewSettings; +import com.pichillilorenzo.flutter_inappwebview_android.webview.web_message.WebMessageChannel; +import com.pichillilorenzo.flutter_inappwebview_android.webview.web_message.WebMessageListener; + +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.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class WebViewChannelDelegate extends ChannelDelegateImpl { + static final String LOG_TAG = "WebViewChannelDelegate"; + + @Nullable + private InAppWebView webView; + + public WebViewChannelDelegate(@NonNull InAppWebView webView, @NonNull MethodChannel channel) { + super(channel); + this.webView = webView; + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull final MethodChannel.Result result) { + WebViewChannelDelegateMethods method = null; + try { + method = WebViewChannelDelegateMethods.valueOf(call.method); + } catch (IllegalArgumentException e) { + result.notImplemented(); + return; + } + switch (method) { + case getUrl: + result.success((webView != null) ? webView.getUrl() : null); + break; + case getTitle: + result.success((webView != null) ? webView.getTitle() : null); + break; + case getProgress: + result.success((webView != null) ? webView.getProgress() : null); + break; + case loadUrl: + if (webView != null) { + Map urlRequest = (Map) call.argument("urlRequest"); + webView.loadUrl(URLRequest.fromMap(urlRequest)); + } + result.success(true); + break; + case postUrl: + if (webView != null) { + String url = (String) call.argument("url"); + byte[] postData = (byte[]) call.argument("postData"); + webView.postUrl(url, postData); + } + result.success(true); + break; + case loadData: + if (webView != null) { + String data = (String) call.argument("data"); + String mimeType = (String) call.argument("mimeType"); + String encoding = (String) call.argument("encoding"); + String baseUrl = (String) call.argument("baseUrl"); + String historyUrl = (String) call.argument("historyUrl"); + webView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl); + } + result.success(true); + break; + case loadFile: + if (webView != null) { + String assetFilePath = (String) call.argument("assetFilePath"); + try { + webView.loadFile(assetFilePath); + } catch (IOException e) { + e.printStackTrace(); + result.error(LOG_TAG, e.getMessage(), null); + return; + } + } + result.success(true); + break; + case evaluateJavascript: + if (webView != null) { + String source = (String) call.argument("source"); + Map contentWorldMap = (Map) call.argument("contentWorld"); + ContentWorld contentWorld = ContentWorld.fromMap(contentWorldMap); + webView.evaluateJavascript(source, contentWorld, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + result.success(value); + } + }); + } else { + result.success(null); + } + break; + case injectJavascriptFileFromUrl: + if (webView != null) { + String urlFile = (String) call.argument("urlFile"); + Map scriptHtmlTagAttributes = (Map) call.argument("scriptHtmlTagAttributes"); + webView.injectJavascriptFileFromUrl(urlFile, scriptHtmlTagAttributes); + } + result.success(true); + break; + case injectCSSCode: + if (webView != null) { + String source = (String) call.argument("source"); + webView.injectCSSCode(source); + } + result.success(true); + break; + case injectCSSFileFromUrl: + if (webView != null) { + String urlFile = (String) call.argument("urlFile"); + Map cssLinkHtmlTagAttributes = (Map) call.argument("cssLinkHtmlTagAttributes"); + webView.injectCSSFileFromUrl(urlFile, cssLinkHtmlTagAttributes); + } + result.success(true); + break; + case reload: + if (webView != null) + webView.reload(); + result.success(true); + break; + case goBack: + if (webView != null) + webView.goBack(); + result.success(true); + break; + case canGoBack: + result.success((webView != null) && webView.canGoBack()); + break; + case goForward: + if (webView != null) + webView.goForward(); + result.success(true); + break; + case canGoForward: + result.success((webView != null) && webView.canGoForward()); + break; + case goBackOrForward: + if (webView != null) + webView.goBackOrForward((Integer) call.argument("steps")); + result.success(true); + break; + case canGoBackOrForward: + result.success((webView != null) && webView.canGoBackOrForward((Integer) call.argument("steps"))); + break; + case stopLoading: + if (webView != null) + webView.stopLoading(); + result.success(true); + break; + case isLoading: + result.success((webView != null) && webView.isLoading()); + break; + case takeScreenshot: + if (webView != null) { + Map screenshotConfiguration = (Map) call.argument("screenshotConfiguration"); + webView.takeScreenshot(screenshotConfiguration, result); + } else + result.success(null); + break; + case setSettings: + if (webView != null && webView.getInAppBrowserDelegate() instanceof InAppBrowserActivity) { + InAppBrowserActivity inAppBrowserActivity = (InAppBrowserActivity) webView.getInAppBrowserDelegate(); + InAppBrowserSettings inAppBrowserSettings = new InAppBrowserSettings(); + HashMap inAppBrowserSettingsMap = (HashMap) call.argument("settings"); + inAppBrowserSettings.parse(inAppBrowserSettingsMap); + inAppBrowserActivity.setSettings(inAppBrowserSettings, inAppBrowserSettingsMap); + } else if (webView != null) { + InAppWebViewSettings inAppWebViewSettings = new InAppWebViewSettings(); + HashMap inAppWebViewSettingsMap = (HashMap) call.argument("settings"); + inAppWebViewSettings.parse(inAppWebViewSettingsMap); + webView.setSettings(inAppWebViewSettings, inAppWebViewSettingsMap); + } + result.success(true); + break; + case getSettings: + if (webView != null && webView.getInAppBrowserDelegate() instanceof InAppBrowserActivity) { + InAppBrowserActivity inAppBrowserActivity = (InAppBrowserActivity) webView.getInAppBrowserDelegate(); + result.success(inAppBrowserActivity.getCustomSettings()); + } else { + result.success((webView != null) ? webView.getCustomSettings() : null); + } + break; + case close: + if (webView != null && webView.getInAppBrowserDelegate() instanceof InAppBrowserActivity) { + InAppBrowserActivity inAppBrowserActivity = (InAppBrowserActivity) webView.getInAppBrowserDelegate(); + inAppBrowserActivity.close(result); + } else { + result.notImplemented(); + } + break; + case show: + if (webView != null && webView.getInAppBrowserDelegate() instanceof InAppBrowserActivity) { + InAppBrowserActivity inAppBrowserActivity = (InAppBrowserActivity) webView.getInAppBrowserDelegate(); + inAppBrowserActivity.show(); + result.success(true); + } else { + result.notImplemented(); + } + break; + case hide: + if (webView != null && webView.getInAppBrowserDelegate() instanceof InAppBrowserActivity) { + InAppBrowserActivity inAppBrowserActivity = (InAppBrowserActivity) webView.getInAppBrowserDelegate(); + inAppBrowserActivity.hide(); + result.success(true); + } else { + result.notImplemented(); + } + break; + case isHidden: + if (webView != null && webView.getInAppBrowserDelegate() instanceof InAppBrowserActivity) { + InAppBrowserActivity inAppBrowserActivity = (InAppBrowserActivity) webView.getInAppBrowserDelegate(); + result.success(inAppBrowserActivity.isHidden); + } else { + result.notImplemented(); + } + break; + case getCopyBackForwardList: + result.success((webView != null) ? webView.getCopyBackForwardList() : null); + break; + case startSafeBrowsing: + if (webView != null && WebViewFeature.isFeatureSupported(WebViewFeature.START_SAFE_BROWSING)) { + WebViewCompat.startSafeBrowsing(webView.getContext(), new ValueCallback() { + @Override + public void onReceiveValue(Boolean success) { + result.success(success); + } + }); + } else { + result.success(false); + } + break; + case clearCache: + if (webView != null) + webView.clearAllCache(); + result.success(true); + break; + case clearSslPreferences: + if (webView != null) + webView.clearSslPreferences(); + result.success(true); + break; + case findAll: + if (webView != null) { + String find = (String) call.argument("find"); + webView.findAllAsync(find); + } + result.success(true); + break; + case findNext: + if (webView != null) { + Boolean forward = (Boolean) call.argument("forward"); + webView.findNext(forward); + } + result.success(true); + break; + case clearMatches: + if (webView != null) { + webView.clearMatches(); + } + result.success(true); + break; + case scrollTo: + if (webView != null) { + Integer x = (Integer) call.argument("x"); + Integer y = (Integer) call.argument("y"); + Boolean animated = (Boolean) call.argument("animated"); + webView.scrollTo(x, y, animated); + } + result.success(true); + break; + case scrollBy: + if (webView != null) { + Integer x = (Integer) call.argument("x"); + Integer y = (Integer) call.argument("y"); + Boolean animated = (Boolean) call.argument("animated"); + webView.scrollBy(x, y, animated); + } + result.success(true); + break; + case pause: + if (webView != null) { + webView.onPause(); + } + result.success(true); + break; + case resume: + if (webView != null) { + webView.onResume(); + } + result.success(true); + break; + case pauseTimers: + if (webView != null) { + webView.pauseTimers(); + } + result.success(true); + break; + case resumeTimers: + if (webView != null) { + webView.resumeTimers(); + } + result.success(true); + break; + case printCurrentPage: + if (webView != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + PrintJobSettings settings = new PrintJobSettings(); + Map settingsMap = (Map) call.argument("settings"); + if (settingsMap != null) { + settings.parse(settingsMap); + } + result.success(webView.printCurrentPage(settings)); + } else { + result.success(null); + } + break; + case getContentHeight: + if (webView instanceof InAppWebView) { + result.success(webView.getContentHeight()); + } else { + result.success(null); + } + break; + case getContentWidth: + if (webView instanceof InAppWebView) { + webView.getContentWidth(new ValueCallback() { + @Override + public void onReceiveValue(@Nullable Integer contentWidth) { + result.success(contentWidth); + } + }); + } else { + result.success(null); + } + break; + case zoomBy: + if (webView != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + double zoomFactor = (double) call.argument("zoomFactor"); + webView.zoomBy((float) zoomFactor); + } + result.success(true); + break; + case getOriginalUrl: + result.success((webView != null) ? webView.getOriginalUrl() : null); + break; + case getZoomScale: + if (webView instanceof InAppWebView) { + result.success(webView.getZoomScale()); + } else { + result.success(null); + } + break; + case getSelectedText: + if ((webView instanceof InAppWebView && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)) { + webView.getSelectedText(new ValueCallback() { + @Override + public void onReceiveValue(String value) { + result.success(value); + } + }); + } else { + result.success(null); + } + break; + case getHitTestResult: + if (webView instanceof InAppWebView) { + result.success(HitTestResult.fromWebViewHitTestResult(webView.getHitTestResult()).toMap()); + } else { + result.success(null); + } + break; + case pageDown: + if (webView != null) { + boolean bottom = (boolean) call.argument("bottom"); + result.success(webView.pageDown(bottom)); + } else { + result.success(false); + } + break; + case pageUp: + if (webView != null) { + boolean top = (boolean) call.argument("top"); + result.success(webView.pageUp(top)); + } else { + result.success(false); + } + break; + case saveWebArchive: + if (webView != null) { + String filePath = (String) call.argument("filePath"); + boolean autoname = (boolean) call.argument("autoname"); + webView.saveWebArchive(filePath, autoname, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + result.success(value); + } + }); + } else { + result.success(null); + } + break; + case zoomIn: + if (webView != null) { + result.success(webView.zoomIn()); + } else { + result.success(false); + } + break; + case zoomOut: + if (webView != null) { + result.success(webView.zoomOut()); + } else { + result.success(false); + } + break; + case clearFocus: + if (webView != null) { + webView.clearFocus(); + } + result.success(true); + break; + case setContextMenu: + if (webView != null) { + Map contextMenu = (Map) call.argument("contextMenu"); + webView.setContextMenu(contextMenu); + } + result.success(true); + break; + case requestFocusNodeHref: + if (webView != null) { + result.success(webView.requestFocusNodeHref()); + } else { + result.success(null); + } + break; + case requestImageRef: + if (webView != null) { + result.success(webView.requestImageRef()); + } else { + result.success(null); + } + break; + case getScrollX: + if (webView != null) { + result.success(webView.getScrollX()); + } else { + result.success(null); + } + break; + case getScrollY: + if (webView != null) { + result.success(webView.getScrollY()); + } else { + result.success(null); + } + break; + case getCertificate: + if (webView != null) { + result.success(SslCertificateExt.toMap(webView.getCertificate())); + } else { + result.success(null); + } + break; + case clearHistory: + if (webView != null) { + webView.clearHistory(); + } + result.success(true); + break; + case addUserScript: + if (webView != null && webView.getUserContentController() != null) { + Map userScriptMap = (Map) call.argument("userScript"); + UserScript userScript = UserScript.fromMap(userScriptMap); + result.success(webView.getUserContentController().addUserOnlyScript(userScript)); + } else { + result.success(false); + } + break; + case removeUserScript: + if (webView != null && webView.getUserContentController() != null) { + Integer index = (Integer) call.argument("index"); + Map userScriptMap = (Map) call.argument("userScript"); + UserScript userScript = UserScript.fromMap(userScriptMap); + result.success(webView.getUserContentController().removeUserOnlyScriptAt(index, userScript.getInjectionTime())); + } else { + result.success(false); + } + break; + case removeUserScriptsByGroupName: + if (webView != null && webView.getUserContentController() != null) { + String groupName = (String) call.argument("groupName"); + webView.getUserContentController().removeUserOnlyScriptsByGroupName(groupName); + } + result.success(true); + break; + case removeAllUserScripts: + if (webView != null && webView.getUserContentController() != null) { + webView.getUserContentController().removeAllUserOnlyScripts(); + } + result.success(true); + break; + case callAsyncJavaScript: + if (webView != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + String functionBody = (String) call.argument("functionBody"); + Map functionArguments = (Map) call.argument("arguments"); + Map contentWorldMap = (Map) call.argument("contentWorld"); + ContentWorld contentWorld = ContentWorld.fromMap(contentWorldMap); + webView.callAsyncJavaScript(functionBody, functionArguments, contentWorld, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + result.success(value); + } + }); + } else { + result.success(null); + } + break; + case isSecureContext: + if (webView != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + webView.isSecureContext(new ValueCallback() { + @Override + public void onReceiveValue(Boolean value) { + result.success(value); + } + }); + } else { + result.success(false); + } + break; + case createWebMessageChannel: + if (webView != null) { + if (webView instanceof InAppWebView && WebViewFeature.isFeatureSupported(WebViewFeature.CREATE_WEB_MESSAGE_CHANNEL)) { + result.success(webView.createCompatWebMessageChannel().toMap()); + } else { + result.success(null); + } + } else { + result.success(null); + } + break; + case postWebMessage: + if (webView != null && WebViewFeature.isFeatureSupported(WebViewFeature.POST_WEB_MESSAGE)) { + WebMessageCompatExt message = WebMessageCompatExt.fromMap((Map) call.argument("message")); + String targetOrigin = (String) call.argument("targetOrigin"); + List compatPorts = new ArrayList<>(); + List portsExt = message.getPorts(); + if (portsExt != null) { + for (WebMessagePortCompatExt portExt : portsExt) { + WebMessageChannel webMessageChannel = webView.getWebMessageChannels().get(portExt.getWebMessageChannelId()); + if (webMessageChannel != null) { + if (webView instanceof InAppWebView) { + compatPorts.add(webMessageChannel.compatPorts.get(portExt.getIndex())); + } + } + } + } + Object data = message.getData(); + if (webView instanceof InAppWebView) { + try { + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER) && data != null && + message.getType() == WebMessageCompat.TYPE_ARRAY_BUFFER) { + WebViewCompat.postWebMessage((WebView) webView, + new WebMessageCompat((byte[]) data, compatPorts.toArray(new WebMessagePortCompat[0])), + Uri.parse(targetOrigin)); + } else { + WebViewCompat.postWebMessage((WebView) webView, + new WebMessageCompat(data != null ? data.toString() : null, compatPorts.toArray(new WebMessagePortCompat[0])), + Uri.parse(targetOrigin)); + } + result.success(true); + } catch (Exception e) { + result.error(LOG_TAG, e.getMessage(), null); + } + } + } else { + result.success(true); + } + break; + case addWebMessageListener: + if (webView != null) { + Map webMessageListenerMap = (Map) call.argument("webMessageListener"); + WebMessageListener webMessageListener = WebMessageListener.fromMap(webView, webView.getPlugin().messenger, webMessageListenerMap); + if (webView instanceof InAppWebView && WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { + try { + webView.addWebMessageListener(webMessageListener); + result.success(true); + } catch (Exception e) { + result.error(LOG_TAG, e.getMessage(), null); + } + } else { + result.success(true); + } + } else { + result.success(true); + } + break; + case canScrollVertically: + if (webView != null) { + result.success(webView.canScrollVertically()); + } else { + result.success(false); + } + break; + case canScrollHorizontally: + if (webView != null) { + result.success(webView.canScrollHorizontally()); + } else { + result.success(false); + } + break; + case isInFullscreen: + if (webView != null) { + result.success(webView.isInFullscreen()); + } else { + result.success(false); + } + break; + case clearFormData: + if (webView != null) { + webView.clearFormData(); + } + result.success(true); + } + } + + /** + * @deprecated Use {@link FindInteractionChannelDelegate#onFindResultReceived} instead. + */ + @Deprecated + public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("activeMatchOrdinal", activeMatchOrdinal); + obj.put("numberOfMatches", numberOfMatches); + obj.put("isDoneCounting", isDoneCounting); + channel.invokeMethod("onFindResultReceived", obj); + } + + public void onLongPressHitTestResult(HitTestResult hitTestResult) { + MethodChannel channel = getChannel(); + if (channel == null) return; + channel.invokeMethod("onLongPressHitTestResult", hitTestResult.toMap()); + } + + public void onScrollChanged(int x, int y) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("x", x); + obj.put("y", y); + channel.invokeMethod("onScrollChanged", obj); + } + + public void onDownloadStartRequest(DownloadStartRequest downloadStartRequest) { + MethodChannel channel = getChannel(); + if (channel == null) return; + channel.invokeMethod("onDownloadStartRequest", downloadStartRequest.toMap()); + } + + public void onCreateContextMenu(HitTestResult hitTestResult) { + MethodChannel channel = getChannel(); + if (channel == null) return; + channel.invokeMethod("onCreateContextMenu", hitTestResult.toMap()); + } + + public void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("x", scrollX); + obj.put("y", scrollY); + obj.put("clampedX", clampedX); + obj.put("clampedY", clampedY); + channel.invokeMethod("onOverScrolled", obj); + } + + public void onContextMenuActionItemClicked(int itemId, String itemTitle) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("id", itemId); + obj.put("androidId", itemId); + obj.put("iosId", null); + obj.put("title", itemTitle); + channel.invokeMethod("onContextMenuActionItemClicked", obj); + } + + public void onHideContextMenu() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onHideContextMenu", obj); + } + + public void onEnterFullscreen() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onEnterFullscreen", obj); + } + + public void onExitFullscreen() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onExitFullscreen", obj); + } + + public static class JsAlertCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public JsAlertResponse decodeResult(@Nullable Object obj) { + return JsAlertResponse.fromMap((Map) obj); + } + } + + public void onJsAlert(String url, String message, Boolean isMainFrame, @NonNull JsAlertCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + Map obj = new HashMap<>(); + obj.put("url", url); + obj.put("message", message); + obj.put("isMainFrame", isMainFrame); + channel.invokeMethod("onJsAlert", obj, callback); + } + + public static class JsConfirmCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public JsConfirmResponse decodeResult(@Nullable Object obj) { + return JsConfirmResponse.fromMap((Map) obj); + } + } + + public void onJsConfirm(String url, String message, Boolean isMainFrame, @NonNull JsConfirmCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + Map obj = new HashMap<>(); + obj.put("url", url); + obj.put("message", message); + obj.put("isMainFrame", isMainFrame); + channel.invokeMethod("onJsConfirm", obj, callback); + } + + public static class JsPromptCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public JsPromptResponse decodeResult(@Nullable Object obj) { + return JsPromptResponse.fromMap((Map) obj); + } + } + + public void onJsPrompt(String url, String message, String defaultValue, Boolean isMainFrame, @NonNull JsPromptCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + Map obj = new HashMap<>(); + obj.put("url", url); + obj.put("message", message); + obj.put("defaultValue", defaultValue); + obj.put("isMainFrame", isMainFrame); + channel.invokeMethod("onJsPrompt", obj, callback); + } + + public static class JsBeforeUnloadCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public JsBeforeUnloadResponse decodeResult(@Nullable Object obj) { + return JsBeforeUnloadResponse.fromMap((Map) obj); + } + } + + public void onJsBeforeUnload(String url, String message, @NonNull JsBeforeUnloadCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + Map obj = new HashMap<>(); + obj.put("url", url); + obj.put("message", message); + channel.invokeMethod("onJsBeforeUnload", obj, callback); + } + + public static class CreateWindowCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public Boolean decodeResult(@Nullable Object obj) { + return (obj instanceof Boolean) && (boolean) obj; + } + } + + public void onCreateWindow(CreateWindowAction createWindowAction, @NonNull CreateWindowCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + channel.invokeMethod("onCreateWindow", createWindowAction.toMap(), callback); + } + + public void onCloseWindow() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onCloseWindow", obj); + } + + public static class GeolocationPermissionsShowPromptCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public GeolocationPermissionShowPromptResponse decodeResult(@Nullable Object obj) { + return GeolocationPermissionShowPromptResponse.fromMap((Map) obj); + } + } + + public void onGeolocationPermissionsShowPrompt(String origin, @NonNull GeolocationPermissionsShowPromptCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + Map obj = new HashMap<>(); + obj.put("origin", origin); + channel.invokeMethod("onGeolocationPermissionsShowPrompt", obj, callback); + } + + public void onGeolocationPermissionsHidePrompt() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onGeolocationPermissionsHidePrompt", obj); + } + + public void onConsoleMessage(String message, int messageLevel) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("message", message); + obj.put("messageLevel", messageLevel); + channel.invokeMethod("onConsoleMessage", obj); + } + + public void onProgressChanged(int progress) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("progress", progress); + channel.invokeMethod("onProgressChanged", obj); + } + + public void onTitleChanged(String title) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("title", title); + channel.invokeMethod("onTitleChanged", obj); + } + + public void onReceivedIcon(byte[] icon) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("icon", icon); + channel.invokeMethod("onReceivedIcon", obj); + } + + public void onReceivedTouchIconUrl(String url, boolean precomposed) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("url", url); + obj.put("precomposed", precomposed); + channel.invokeMethod("onReceivedTouchIconUrl", obj); + } + + public static class PermissionRequestCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public PermissionResponse decodeResult(@Nullable Object obj) { + return PermissionResponse.fromMap((Map) obj); + } + } + + public void onPermissionRequest(String origin, List resources, Object frame, @NonNull PermissionRequestCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + Map obj = new HashMap<>(); + obj.put("origin", origin); + obj.put("resources", resources); + obj.put("frame", frame); + channel.invokeMethod("onPermissionRequest", obj, callback); + } + + public void onPermissionRequestCanceled(String origin, List resources) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("origin", origin); + obj.put("resources", resources); + channel.invokeMethod("onPermissionRequestCanceled", obj); + } + + public static class ShouldOverrideUrlLoadingCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public NavigationActionPolicy decodeResult(@Nullable Object obj) { + Integer action = obj instanceof Integer ? (Integer) obj : NavigationActionPolicy.CANCEL.rawValue(); + return NavigationActionPolicy.fromValue(action); + } + } + + public void shouldOverrideUrlLoading(NavigationAction navigationAction, @NonNull ShouldOverrideUrlLoadingCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + channel.invokeMethod("shouldOverrideUrlLoading", navigationAction.toMap(), callback); + } + + public void onLoadStart(String url) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("url", url); + channel.invokeMethod("onLoadStart", obj); + } + + public void onLoadStop(String url) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("url", url); + channel.invokeMethod("onLoadStop", obj); + } + + public void onUpdateVisitedHistory(String url, boolean isReload) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("url", url); + obj.put("isReload", isReload); + channel.invokeMethod("onUpdateVisitedHistory", obj); + } + + public void onReceivedError(WebResourceRequestExt request, WebResourceErrorExt error) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("request", request.toMap()); + obj.put("error", error.toMap()); + channel.invokeMethod("onReceivedError", obj); + } + + public void onReceivedHttpError(WebResourceRequestExt request, WebResourceResponseExt errorResponse) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("request", request.toMap()); + obj.put("errorResponse", errorResponse.toMap()); + channel.invokeMethod("onReceivedHttpError", obj); + } + + public static class ReceivedHttpAuthRequestCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public HttpAuthResponse decodeResult(@Nullable Object obj) { + return HttpAuthResponse.fromMap((Map) obj); + } + } + + public void onReceivedHttpAuthRequest(HttpAuthenticationChallenge challenge, @NonNull ReceivedHttpAuthRequestCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + channel.invokeMethod("onReceivedHttpAuthRequest", challenge.toMap(), callback); + } + + public static class ReceivedServerTrustAuthRequestCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public ServerTrustAuthResponse decodeResult(@Nullable Object obj) { + return ServerTrustAuthResponse.fromMap((Map) obj); + } + } + + public void onReceivedServerTrustAuthRequest(ServerTrustChallenge challenge, @NonNull ReceivedServerTrustAuthRequestCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + channel.invokeMethod("onReceivedServerTrustAuthRequest", challenge.toMap(), callback); + } + + public static class ReceivedClientCertRequestCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public ClientCertResponse decodeResult(@Nullable Object obj) { + return ClientCertResponse.fromMap((Map) obj); + } + } + + public void onReceivedClientCertRequest(ClientCertChallenge challenge, @NonNull ReceivedClientCertRequestCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + channel.invokeMethod("onReceivedClientCertRequest", challenge.toMap(), callback); + } + + public void onZoomScaleChanged(float oldScale, float newScale) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("oldScale", oldScale); + obj.put("newScale", newScale); + channel.invokeMethod("onZoomScaleChanged", obj); + } + + public static class SafeBrowsingHitCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public SafeBrowsingResponse decodeResult(@Nullable Object obj) { + return SafeBrowsingResponse.fromMap((Map) obj); + } + } + + public void onSafeBrowsingHit(String url, int threatType, @NonNull SafeBrowsingHitCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + Map obj = new HashMap<>(); + obj.put("url", url); + obj.put("threatType", threatType); + channel.invokeMethod("onSafeBrowsingHit", obj, callback); + } + + public static class FormResubmissionCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public Integer decodeResult(@Nullable Object obj) { + return obj instanceof Integer ? (Integer) obj : null; + } + } + + public void onFormResubmission(String url, @NonNull FormResubmissionCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + Map obj = new HashMap<>(); + obj.put("url", url); + channel.invokeMethod("onFormResubmission", obj, callback); + } + + public void onPageCommitVisible(String url) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("url", url); + channel.invokeMethod("onPageCommitVisible", obj); + } + + public void onRenderProcessGone(boolean didCrash, int rendererPriorityAtExit) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("didCrash", didCrash); + obj.put("rendererPriorityAtExit", rendererPriorityAtExit); + channel.invokeMethod("onRenderProcessGone", obj); + } + + public void onReceivedLoginRequest(String realm, String account, String args) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("realm", realm); + obj.put("account", account); + obj.put("args", args); + channel.invokeMethod("onReceivedLoginRequest", obj); + } + + public static class LoadResourceWithCustomSchemeCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public CustomSchemeResponse decodeResult(@Nullable Object obj) { + return CustomSchemeResponse.fromMap((Map) obj); + } + } + + public void onLoadResourceWithCustomScheme(WebResourceRequestExt request, @NonNull LoadResourceWithCustomSchemeCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + Map obj = new HashMap<>(); + obj.put("request", request.toMap()); + channel.invokeMethod("onLoadResourceWithCustomScheme", obj, callback); + } + + public static class SyncLoadResourceWithCustomSchemeCallback extends SyncBaseCallbackResultImpl { + @Nullable + @Override + public CustomSchemeResponse decodeResult(@Nullable Object obj) { + return (new LoadResourceWithCustomSchemeCallback()).decodeResult(obj); + } + } + + @Nullable + public CustomSchemeResponse onLoadResourceWithCustomScheme(WebResourceRequestExt request) throws InterruptedException { + MethodChannel channel = getChannel(); + if (channel == null) return null; + final Map obj = new HashMap<>(); + obj.put("request", request.toMap()); + final SyncLoadResourceWithCustomSchemeCallback callback = new SyncLoadResourceWithCustomSchemeCallback(); + return Util.invokeMethodAndWaitResult(channel, "onLoadResourceWithCustomScheme", obj, callback); + } + + public static class ShouldInterceptRequestCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public WebResourceResponseExt decodeResult(@Nullable Object obj) { + return WebResourceResponseExt.fromMap((Map) obj); + } + } + + public void shouldInterceptRequest(WebResourceRequestExt request, @NonNull ShouldInterceptRequestCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + channel.invokeMethod("shouldInterceptRequest", request.toMap(), callback); + } + + public static class SyncShouldInterceptRequestCallback extends SyncBaseCallbackResultImpl { + @Nullable + @Override + public WebResourceResponseExt decodeResult(@Nullable Object obj) { + return (new ShouldInterceptRequestCallback()).decodeResult(obj); + } + } + + @Nullable + public WebResourceResponseExt shouldInterceptRequest(WebResourceRequestExt request) throws InterruptedException { + MethodChannel channel = getChannel(); + if (channel == null) return null; + final SyncShouldInterceptRequestCallback callback = new SyncShouldInterceptRequestCallback(); + return Util.invokeMethodAndWaitResult(channel, "shouldInterceptRequest", request.toMap(), callback); + } + + public static class RenderProcessUnresponsiveCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public Integer decodeResult(@Nullable Object obj) { + return obj instanceof Integer ? (Integer) obj : null; + } + } + + public void onRenderProcessUnresponsive(String url, @NonNull RenderProcessUnresponsiveCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + Map obj = new HashMap<>(); + obj.put("url", url); + channel.invokeMethod("onRenderProcessUnresponsive", obj, callback); + } + + public static class RenderProcessResponsiveCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public Integer decodeResult(@Nullable Object obj) { + return obj instanceof Integer ? (Integer) obj : null; + } + } + + public void onRenderProcessResponsive(String url, @NonNull RenderProcessResponsiveCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + Map obj = new HashMap<>(); + obj.put("url", url); + channel.invokeMethod("onRenderProcessResponsive", obj, callback); + } + + public static class CallJsHandlerCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public Object decodeResult(@Nullable Object obj) { + return obj; + } + } + + public void onCallJsHandler(String handlerName, String args, @NonNull CallJsHandlerCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + Map obj = new HashMap<>(); + obj.put("handlerName", handlerName); + obj.put("args", args); + channel.invokeMethod("onCallJsHandler", obj, callback); + } + + public static class PrintRequestCallback extends BaseCallbackResultImpl { + @Nullable + @Override + public Boolean decodeResult(@Nullable Object obj) { + return (obj instanceof Boolean) && (boolean) obj; + } + } + + public void onPrintRequest(String url, String printJobId, @NonNull PrintRequestCallback callback) { + MethodChannel channel = getChannel(); + if (channel == null) { + callback.defaultBehaviour(null); + return; + } + Map obj = new HashMap<>(); + obj.put("url", url); + obj.put("printJobId", printJobId); + channel.invokeMethod("onPrintRequest", obj, callback); + } + + public void onRequestFocus() { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + channel.invokeMethod("onRequestFocus", obj); + } + + @Override + public void dispose() { + super.dispose(); + webView = null; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/WebViewChannelDelegateMethods.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/WebViewChannelDelegateMethods.java new file mode 100644 index 00000000..3c6d6ae1 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/WebViewChannelDelegateMethods.java @@ -0,0 +1,86 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview; + +public enum WebViewChannelDelegateMethods { + getUrl, + getTitle, + getProgress, + loadUrl, + postUrl, + loadData, + loadFile, + evaluateJavascript, + injectJavascriptFileFromUrl, + injectCSSCode, + injectCSSFileFromUrl, + reload, + goBack, + canGoBack, + goForward, + canGoForward, + goBackOrForward, + canGoBackOrForward, + stopLoading, + isLoading, + takeScreenshot, + setSettings, + getSettings, + close, + show, + hide, + isHidden, + getCopyBackForwardList, + startSafeBrowsing, + /** + * @deprecated + */ + @Deprecated + clearCache, + clearSslPreferences, + /** + * @deprecated + */ + @Deprecated + findAll, + findNext, + clearMatches, + scrollTo, + scrollBy, + pause, + resume, + pauseTimers, + resumeTimers, + printCurrentPage, + getContentHeight, + getContentWidth, + zoomBy, + getOriginalUrl, + getZoomScale, + getSelectedText, + getHitTestResult, + pageDown, + pageUp, + saveWebArchive, + zoomIn, + zoomOut, + clearFocus, + setContextMenu, + requestFocusNodeHref, + requestImageRef, + getScrollX, + getScrollY, + getCertificate, + clearHistory, + addUserScript, + removeUserScript, + removeUserScriptsByGroupName, + removeAllUserScripts, + callAsyncJavaScript, + isSecureContext, + createWebMessageChannel, + postWebMessage, + addWebMessageListener, + canScrollVertically, + canScrollHorizontally, + isInFullscreen, + clearFormData +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/DisplayListenerProxy.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/DisplayListenerProxy.java new file mode 100755 index 00000000..ebe487ef --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/DisplayListenerProxy.java @@ -0,0 +1,122 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview; + +import static android.hardware.display.DisplayManager.DisplayListener; + +import android.annotation.TargetApi; +import android.hardware.display.DisplayManager; +import android.os.Build; +import android.util.Log; +import java.lang.reflect.Field; +import java.util.ArrayList; + +/** + * Works around an Android WebView bug by filtering some DisplayListener invocations. + * https://github.com/flutter/plugins/blob/master/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java + */ +@TargetApi(Build.VERSION_CODES.KITKAT) +public +class DisplayListenerProxy { + private static final String TAG = "DisplayListenerProxy"; + + private ArrayList listenersBeforeWebView; + + /** Should be called prior to the webview's initialization. */ + public void onPreWebViewInitialization(DisplayManager displayManager) { + listenersBeforeWebView = yoinkDisplayListeners(displayManager); + } + + /** Should be called after the webview's initialization. */ + public void onPostWebViewInitialization(final DisplayManager displayManager) { + final ArrayList webViewListeners = yoinkDisplayListeners(displayManager); + // We recorded the list of listeners prior to initializing webview, any new listeners we see + // after initializing the webview are listeners added by the webview. + webViewListeners.removeAll(listenersBeforeWebView); + + if (webViewListeners.isEmpty()) { + // The Android WebView registers a single display listener per process (even if there + // are multiple WebView instances) so this list is expected to be non-empty only the + // first time a webview is initialized. + // Note that in an add2app scenario if the application had instantiated a non Flutter + // WebView prior to instantiating the Flutter WebView we are not able to get a reference + // to the WebView's display listener and can't work around the bug. + // + // This means that webview resizes in add2app Flutter apps with a non Flutter WebView + // running on a system with a webview prior to 58.0.3029.125 may crash (the Android's + // behavior seems to be racy so it doesn't always happen). + return; + } + + for (DisplayListener webViewListener : webViewListeners) { + // Note that while DisplayManager.unregisterDisplayListener throws when given an + // unregistered listener, this isn't an issue as the WebView code never calls + // unregisterDisplayListener. + displayManager.unregisterDisplayListener(webViewListener); + + // We never explicitly unregister this listener as the webview's listener is never + // unregistered (it's released when the process is terminated). + displayManager.registerDisplayListener( + new DisplayListener() { + @Override + public void onDisplayAdded(int displayId) { + for (DisplayListener webViewListener : webViewListeners) { + webViewListener.onDisplayAdded(displayId); + } + } + + @Override + public void onDisplayRemoved(int displayId) { + for (DisplayListener webViewListener : webViewListeners) { + webViewListener.onDisplayRemoved(displayId); + } + } + + @Override + public void onDisplayChanged(int displayId) { + if (displayManager.getDisplay(displayId) == null) { + return; + } + for (DisplayListener webViewListener : webViewListeners) { + webViewListener.onDisplayChanged(displayId); + } + } + }, + null); + } + } + + @SuppressWarnings({"unchecked", "PrivateApi"}) + private static ArrayList yoinkDisplayListeners(DisplayManager displayManager) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + // We cannot use reflection on Android P, but it shouldn't matter as it shipped + // with WebView 66.0.3359.158 and the WebView version the bug this code is working around was + // fixed in 61.0.3116.0. + return new ArrayList<>(); + } + try { + Field displayManagerGlobalField = DisplayManager.class.getDeclaredField("mGlobal"); + displayManagerGlobalField.setAccessible(true); + Object displayManagerGlobal = displayManagerGlobalField.get(displayManager); + Field displayListenersField = + displayManagerGlobal.getClass().getDeclaredField("mDisplayListeners"); + displayListenersField.setAccessible(true); + ArrayList delegates = + (ArrayList) displayListenersField.get(displayManagerGlobal); + + Field listenerField = null; + ArrayList listeners = new ArrayList<>(); + for (Object delegate : delegates) { + if (listenerField == null) { + listenerField = delegate.getClass().getField("mListener"); + listenerField.setAccessible(true); + } + DisplayManager.DisplayListener listener = + (DisplayManager.DisplayListener) listenerField.get(delegate); + listeners.add(listener); + } + return listeners; + } catch (NoSuchFieldException | IllegalAccessException e) { + Log.w(TAG, "Could not extract WebView's display listeners. " + e); + return new ArrayList<>(); + } + } +} \ No newline at end of file diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/FlutterWebView.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/FlutterWebView.java new file mode 100755 index 00000000..5605b68e --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/FlutterWebView.java @@ -0,0 +1,189 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.os.Message; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.WebView; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.find_interaction.FindInteractionController; +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.webview.PlatformWebView; +import com.pichillilorenzo.flutter_inappwebview_android.types.URLRequest; +import com.pichillilorenzo.flutter_inappwebview_android.types.UserScript; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class FlutterWebView implements PlatformWebView { + + static final String LOG_TAG = "IAWFlutterWebView"; + + @Nullable + public InAppWebView webView; + @Nullable + public PullToRefreshLayout pullToRefreshLayout; + @Nullable + public String keepAliveId; + + public FlutterWebView(final InAppWebViewFlutterPlugin plugin, final Context context, Object id, + HashMap params) { + DisplayListenerProxy displayListenerProxy = new DisplayListenerProxy(); + DisplayManager displayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); + displayListenerProxy.onPreWebViewInitialization(displayManager); + + keepAliveId = (String) params.get("keepAliveId"); + + Map initialSettings = (Map) params.get("initialSettings"); + Map contextMenu = (Map) params.get("contextMenu"); + Integer windowId = (Integer) params.get("windowId"); + List> initialUserScripts = (List>) params.get("initialUserScripts"); + Map pullToRefreshInitialSettings = (Map) params.get("pullToRefreshSettings"); + + InAppWebViewSettings customSettings = new InAppWebViewSettings(); + customSettings.parse(initialSettings); + + List userScripts = new ArrayList<>(); + if (initialUserScripts != null) { + for (Map initialUserScript : initialUserScripts) { + userScripts.add(UserScript.fromMap(initialUserScript)); + } + } + + webView = new InAppWebView(context, plugin, id, windowId, customSettings, contextMenu, + customSettings.useHybridComposition ? null : plugin.flutterView, userScripts); + displayListenerProxy.onPostWebViewInitialization(displayManager); + + // set MATCH_PARENT layout params to the WebView, otherwise it won't take all the available space! + webView.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + PullToRefreshSettings pullToRefreshSettings = new PullToRefreshSettings(); + pullToRefreshSettings.parse(pullToRefreshInitialSettings); + pullToRefreshLayout = new PullToRefreshLayout(context, plugin, id, pullToRefreshSettings); + pullToRefreshLayout.addView(webView); + pullToRefreshLayout.prepare(); + + FindInteractionController findInteractionController = new FindInteractionController(webView, plugin, id, null); + webView.findInteractionController = findInteractionController; + findInteractionController.prepare(); + + webView.prepare(); + } + + @Override + public View getView() { + return pullToRefreshLayout != null ? pullToRefreshLayout : webView; + } + + @SuppressLint("RestrictedApi") + public void makeInitialLoad(HashMap params) { + if (webView == null) { + return; + } + + Integer windowId = (Integer) params.get("windowId"); + Map initialUrlRequest = (Map) params.get("initialUrlRequest"); + final String initialFile = (String) params.get("initialFile"); + final Map initialData = (Map) params.get("initialData"); + + if (windowId != null) { + 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(); + if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + // for some reason, if a WebView is created using a window id, + // the initial plugin and user scripts injected + // with WebViewCompat.addDocumentStartJavaScript will not be added! + // https://github.com/pichillilorenzo/flutter_inappwebview/issues/1455 + // + // Also, calling the prepareAndAddUserScripts method right after won't work, + // so use the View.post method here. + webView.post(new Runnable() { + @Override + public void run() { + if (webView != null) { + webView.prepareAndAddUserScripts(); + } + } + }); + } + } + } + } else { + if (initialFile != null) { + try { + webView.loadFile(initialFile); + } catch (IOException e) { + Log.e(LOG_TAG, initialFile + " asset file cannot be found!", e); + } + } + else if (initialData != null) { + String data = initialData.get("data"); + String mimeType = initialData.get("mimeType"); + String encoding = initialData.get("encoding"); + String baseUrl = initialData.get("baseUrl"); + String historyUrl = initialData.get("historyUrl"); + webView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl); + } + else if (initialUrlRequest != null) { + URLRequest urlRequest = URLRequest.fromMap(initialUrlRequest); + if (urlRequest != null) { + webView.loadUrl(urlRequest); + } + } + } + } + + @Override + public void dispose() { + if (keepAliveId == null && webView != null) { + webView.dispose(); + webView = null; + + if (pullToRefreshLayout != null) { + pullToRefreshLayout.dispose(); + pullToRefreshLayout = null; + } + } + } + + @Override + public void onInputConnectionLocked() { + if (webView != null && webView.inAppBrowserDelegate == null && !webView.customSettings.useHybridComposition) + webView.lockInputConnection(); + } + + @Override + public void onInputConnectionUnlocked() { + if (webView != null && webView.inAppBrowserDelegate == null && !webView.customSettings.useHybridComposition) + webView.unlockInputConnection(); + } + + @Override + public void onFlutterViewAttached(@NonNull View flutterView) { + if (webView != null && !webView.customSettings.useHybridComposition) { + webView.setContainerView(flutterView); + } + } + + @Override + public void onFlutterViewDetached() { + if (webView != null && !webView.customSettings.useHybridComposition) { + webView.setContainerView(null); + } + } +} \ No newline at end of file diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebView.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebView.java new file mode 100755 index 00000000..727d8d4e --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebView.java @@ -0,0 +1,2111 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview; + +import static android.content.Context.INPUT_METHOD_SERVICE; +import static com.pichillilorenzo.flutter_inappwebview_android.types.PreferredContentModeOptionType.fromValue; + +import android.animation.ObjectAnimator; +import android.animation.PropertyValuesHolder; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Point; +import android.graphics.drawable.ColorDrawable; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.print.PrintAttributes; +import android.print.PrintDocumentAdapter; +import android.print.PrintManager; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.view.ActionMode; +import android.view.ContextMenu; +import android.view.GestureDetector; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.ViewTreeObserver; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; +import android.webkit.CookieManager; +import android.webkit.DownloadListener; +import android.webkit.URLUtil; +import android.webkit.ValueCallback; +import android.webkit.WebBackForwardList; +import android.webkit.WebChromeClient; +import android.webkit.WebHistoryItem; +import android.webkit.WebSettings; +import android.webkit.WebStorage; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.HorizontalScrollView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.webkit.WebSettingsCompat; +import androidx.webkit.WebViewCompat; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.R; +import com.pichillilorenzo.flutter_inappwebview_android.Util; +import com.pichillilorenzo.flutter_inappwebview_android.content_blocker.ContentBlocker; +import com.pichillilorenzo.flutter_inappwebview_android.content_blocker.ContentBlockerAction; +import com.pichillilorenzo.flutter_inappwebview_android.content_blocker.ContentBlockerHandler; +import com.pichillilorenzo.flutter_inappwebview_android.content_blocker.ContentBlockerTrigger; +import com.pichillilorenzo.flutter_inappwebview_android.find_interaction.FindInteractionController; +import com.pichillilorenzo.flutter_inappwebview_android.in_app_browser.InAppBrowserDelegate; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.InterceptAjaxRequestJS; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.InterceptFetchRequestJS; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.JavaScriptBridgeJS; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.OnLoadResourceJS; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.OnWindowBlurEventJS; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.OnWindowFocusEventJS; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.PluginScriptsUtil; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.PrintJS; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.PromisePolyfillJS; +import com.pichillilorenzo.flutter_inappwebview_android.print_job.PrintJobController; +import com.pichillilorenzo.flutter_inappwebview_android.print_job.PrintJobSettings; +import com.pichillilorenzo.flutter_inappwebview_android.pull_to_refresh.PullToRefreshLayout; +import com.pichillilorenzo.flutter_inappwebview_android.types.ContentWorld; +import com.pichillilorenzo.flutter_inappwebview_android.types.DownloadStartRequest; +import com.pichillilorenzo.flutter_inappwebview_android.types.PluginScript; +import com.pichillilorenzo.flutter_inappwebview_android.types.PreferredContentModeOptionType; +import com.pichillilorenzo.flutter_inappwebview_android.types.URLRequest; +import com.pichillilorenzo.flutter_inappwebview_android.types.UserContentController; +import com.pichillilorenzo.flutter_inappwebview_android.types.UserScript; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebViewAssetLoaderExt; +import com.pichillilorenzo.flutter_inappwebview_android.webview.ContextMenuSettings; +import com.pichillilorenzo.flutter_inappwebview_android.webview.InAppWebViewInterface; +import com.pichillilorenzo.flutter_inappwebview_android.webview.JavaScriptBridgeInterface; +import com.pichillilorenzo.flutter_inappwebview_android.webview.WebViewChannelDelegate; +import com.pichillilorenzo.flutter_inappwebview_android.webview.web_message.WebMessageChannel; +import com.pichillilorenzo.flutter_inappwebview_android.webview.web_message.WebMessageListener; + +import org.json.JSONObject; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.regex.Pattern; + +import io.flutter.plugin.common.MethodChannel; + +final public class InAppWebView extends InputAwareWebView implements InAppWebViewInterface { + protected static final String LOG_TAG = "InAppWebView"; + public static final String METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_"; + + @Nullable + public InAppWebViewFlutterPlugin plugin; + @Nullable + public InAppBrowserDelegate inAppBrowserDelegate; + public Object id; + @Nullable + public Integer windowId; + @Nullable + public InAppWebViewClient inAppWebViewClient; + @Nullable + public InAppWebViewClientCompat inAppWebViewClientCompat; + @Nullable + public InAppWebViewChromeClient inAppWebViewChromeClient; + @Nullable + public InAppWebViewRenderProcessClient inAppWebViewRenderProcessClient; + @Nullable + public WebViewChannelDelegate channelDelegate; + @Nullable + public JavaScriptBridgeInterface javaScriptBridgeInterface; + public InAppWebViewSettings customSettings = new InAppWebViewSettings(); + public boolean isLoading = false; + private boolean inFullscreen = false; + public float zoomScale = 1.0f; + public ContentBlockerHandler contentBlockerHandler = new ContentBlockerHandler(); + public Pattern regexToCancelSubFramesLoadingCompiled; + @Nullable + public GestureDetector gestureDetector = null; + @Nullable + public LinearLayout floatingContextMenu = null; + @Nullable + public Map contextMenu = null; + public Handler mainLooperHandler = new Handler(getWebViewLooper()); + static Handler mHandler = new Handler(); + + public Runnable checkScrollStoppedTask; + public int initialPositionScrollStoppedTask; + public int newCheckScrollStoppedTask = 100; // ms + + public Runnable checkContextMenuShouldBeClosedTask; + public int newCheckContextMenuShouldBeClosedTaskTask = 100; // ms + + public UserContentController userContentController = new UserContentController(this); + + public Map> callAsyncJavaScriptCallbacks = new HashMap<>(); + public Map> evaluateJavaScriptContentWorldCallbacks = new HashMap<>(); + + public Map webMessageChannels = new HashMap<>(); + public List webMessageListeners = new ArrayList<>(); + + private List initialUserOnlyScripts = new ArrayList<>(); + + @Nullable + public FindInteractionController findInteractionController; + + @Nullable + public WebViewAssetLoaderExt webViewAssetLoaderExt; + + @Nullable + private PluginScript interceptOnlyAsyncAjaxRequestsPluginScript; + + public InAppWebView(Context context) { + super(context); + } + + public InAppWebView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public InAppWebView(Context context, AttributeSet attrs, int defaultStyle) { + super(context, attrs, defaultStyle); + } + + public InAppWebView(Context context, @NonNull InAppWebViewFlutterPlugin plugin, + @NonNull Object id, @Nullable Integer windowId, InAppWebViewSettings customSettings, + @Nullable Map contextMenu, View containerView, + List userScripts) { + super(context, containerView, customSettings.useHybridComposition); + if (!customSettings.gopeedProfileId.isEmpty()) { + WebViewCompat.setProfile(this, customSettings.gopeedProfileId); + } + this.plugin = plugin; + this.id = id; + final MethodChannel channel = new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME_PREFIX + id); + this.channelDelegate = new WebViewChannelDelegate(this, channel); + this.windowId = windowId; + this.customSettings = customSettings; + this.contextMenu = contextMenu; + this.initialUserOnlyScripts = userScripts; + if (plugin != null && plugin.activity != null) { + plugin.activity.registerForContextMenu(this); + } + } + + public WebViewClient createWebViewClient(InAppBrowserDelegate inAppBrowserDelegate) { + // bug https://bugs.chromium.org/p/chromium/issues/detail?id=925887 + PackageInfo packageInfo = WebViewCompat.getCurrentWebViewPackage(getContext()); + if (packageInfo == null) { + Log.d(LOG_TAG, "Using InAppWebViewClient implementation"); + return new InAppWebViewClient(inAppBrowserDelegate); + } + + boolean isChromiumWebView = "com.android.webview".equals(packageInfo.packageName) || + "com.google.android.webview".equals(packageInfo.packageName) || + "com.android.chrome".equals(packageInfo.packageName); + boolean isChromiumWebViewBugFixed = false; + if (isChromiumWebView) { + String versionName = packageInfo.versionName != null ? packageInfo.versionName : ""; + try { + int majorVersion = versionName.contains(".") ? + Integer.parseInt(versionName.split("\\.")[0]) : 0; + isChromiumWebViewBugFixed = majorVersion >= 73; + } catch (NumberFormatException ignored) {} + } + + if (isChromiumWebViewBugFixed || !isChromiumWebView) { + Log.d(LOG_TAG, "Using InAppWebViewClientCompat implementation"); + return new InAppWebViewClientCompat(inAppBrowserDelegate); + } else { + Log.d(LOG_TAG, "Using InAppWebViewClient implementation"); + return new InAppWebViewClient(inAppBrowserDelegate); + } + } + + @SuppressLint("RestrictedApi") + private CookieManager gopeedCookieManager() { + if (!customSettings.gopeedProfileId.isEmpty()) { + return WebViewCompat.getProfile(this).getCookieManager(); + } + return CookieManager.getInstance(); + } + + public void prepare() { + if (plugin != null) { + webViewAssetLoaderExt = WebViewAssetLoaderExt.fromMap(customSettings.webViewAssetLoader, plugin, getContext()); + } + + javaScriptBridgeInterface = new JavaScriptBridgeInterface(this); + addJavascriptInterface(javaScriptBridgeInterface, JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME); + + inAppWebViewChromeClient = new InAppWebViewChromeClient(plugin, this, inAppBrowserDelegate); + setWebChromeClient(inAppWebViewChromeClient); + + WebViewClient webViewClient = createWebViewClient(inAppBrowserDelegate); + if (webViewClient instanceof InAppWebViewClientCompat) { + inAppWebViewClientCompat = (InAppWebViewClientCompat) webViewClient; + setWebViewClient(inAppWebViewClientCompat); + } else if (webViewClient instanceof InAppWebViewClient) { + inAppWebViewClient = (InAppWebViewClient) webViewClient; + setWebViewClient(inAppWebViewClient); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && WebViewFeature.isFeatureSupported(WebViewFeature.WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE)) { + inAppWebViewRenderProcessClient = new InAppWebViewRenderProcessClient(); + WebViewCompat.setWebViewRenderProcessClient(this, inAppWebViewRenderProcessClient); + } + + if (windowId == null || !WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + // for some reason, if a WebView is created using a window id, + // the initial plugin and user scripts injected + // with WebViewCompat.addDocumentStartJavaScript will not be added! + // https://github.com/pichillilorenzo/flutter_inappwebview/issues/1455 + prepareAndAddUserScripts(); + } + + if (customSettings.useOnDownloadStart) + setDownloadListener(new DownloadStartListener()); + + WebSettings settings = getSettings(); + + settings.setJavaScriptEnabled(customSettings.javaScriptEnabled); + settings.setJavaScriptCanOpenWindowsAutomatically(customSettings.javaScriptCanOpenWindowsAutomatically); + settings.setBuiltInZoomControls(customSettings.builtInZoomControls); + settings.setDisplayZoomControls(customSettings.displayZoomControls); + settings.setSupportMultipleWindows(customSettings.supportMultipleWindows); + + if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ENABLE)) + WebSettingsCompat.setSafeBrowsingEnabled(settings, customSettings.safeBrowsingEnabled); + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + settings.setSafeBrowsingEnabled(customSettings.safeBrowsingEnabled); + + settings.setMediaPlaybackRequiresUserGesture(customSettings.mediaPlaybackRequiresUserGesture); + + settings.setDatabaseEnabled(customSettings.databaseEnabled); + settings.setDomStorageEnabled(customSettings.domStorageEnabled); + + if (customSettings.userAgent != null && !customSettings.userAgent.isEmpty()) + settings.setUserAgentString(customSettings.userAgent); + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) + settings.setUserAgentString(WebSettings.getDefaultUserAgent(getContext())); + + if (customSettings.applicationNameForUserAgent != null && !customSettings.applicationNameForUserAgent.isEmpty()) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + String userAgent = (customSettings.userAgent != null && !customSettings.userAgent.isEmpty()) ? customSettings.userAgent : WebSettings.getDefaultUserAgent(getContext()); + String userAgentWithApplicationName = userAgent + " " + customSettings.applicationNameForUserAgent; + settings.setUserAgentString(userAgentWithApplicationName); + } + } + + if (customSettings.clearCache) + clearAllCache(); + else if (customSettings.clearSessionCache) + gopeedCookieManager().removeSessionCookie(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + gopeedCookieManager().setAcceptThirdPartyCookies(this, customSettings.thirdPartyCookiesEnabled); + + settings.setLoadWithOverviewMode(customSettings.loadWithOverviewMode); + settings.setUseWideViewPort(customSettings.useWideViewPort); + settings.setSupportZoom(customSettings.supportZoom); + settings.setTextZoom(customSettings.textZoom); + + setVerticalScrollBarEnabled(!customSettings.disableVerticalScroll && customSettings.verticalScrollBarEnabled); + setHorizontalScrollBarEnabled(!customSettings.disableHorizontalScroll && customSettings.horizontalScrollBarEnabled); + + if (customSettings.transparentBackground) + setBackgroundColor(Color.TRANSPARENT); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && customSettings.mixedContentMode != null) + settings.setMixedContentMode(customSettings.mixedContentMode); + + settings.setAllowContentAccess(customSettings.allowContentAccess); + settings.setAllowFileAccess(customSettings.allowFileAccess); + settings.setAllowFileAccessFromFileURLs(customSettings.allowFileAccessFromFileURLs); + settings.setAllowUniversalAccessFromFileURLs(customSettings.allowUniversalAccessFromFileURLs); + setCacheEnabled(customSettings.cacheEnabled); + if (customSettings.appCachePath != null && !customSettings.appCachePath.isEmpty() && customSettings.cacheEnabled) { + // removed from Android API 33+ (https://developer.android.com/sdk/api_diff/33/changes) + // settings.setAppCachePath(customSettings.appCachePath); + Util.invokeMethodIfExists(settings, "setAppCachePath", customSettings.appCachePath); + } + settings.setBlockNetworkImage(customSettings.blockNetworkImage); + settings.setBlockNetworkLoads(customSettings.blockNetworkLoads); + if (customSettings.cacheMode != null) + settings.setCacheMode(customSettings.cacheMode); + settings.setCursiveFontFamily(customSettings.cursiveFontFamily); + settings.setDefaultFixedFontSize(customSettings.defaultFixedFontSize); + settings.setDefaultFontSize(customSettings.defaultFontSize); + settings.setDefaultTextEncodingName(customSettings.defaultTextEncodingName); + if (customSettings.disabledActionModeMenuItems != null) { + if (WebViewFeature.isFeatureSupported(WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS)) + WebSettingsCompat.setDisabledActionModeMenuItems(settings, customSettings.disabledActionModeMenuItems); + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + settings.setDisabledActionModeMenuItems(customSettings.disabledActionModeMenuItems); + } + settings.setFantasyFontFamily(customSettings.fantasyFontFamily); + settings.setFixedFontFamily(customSettings.fixedFontFamily); + if (customSettings.forceDark != null) { + if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) + WebSettingsCompat.setForceDark(settings, customSettings.forceDark); + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + settings.setForceDark(customSettings.forceDark); + } + if (customSettings.forceDarkStrategy != null && WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) { + WebSettingsCompat.setForceDarkStrategy(settings, customSettings.forceDarkStrategy); + } + settings.setGeolocationEnabled(customSettings.geolocationEnabled); + if (customSettings.layoutAlgorithm != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && customSettings.layoutAlgorithm.equals(WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING)) { + settings.setLayoutAlgorithm(customSettings.layoutAlgorithm); + } else { + settings.setLayoutAlgorithm(customSettings.layoutAlgorithm); + } + } + settings.setLoadsImagesAutomatically(customSettings.loadsImagesAutomatically); + settings.setMinimumFontSize(customSettings.minimumFontSize); + settings.setMinimumLogicalFontSize(customSettings.minimumLogicalFontSize); + setInitialScale(customSettings.initialScale); + settings.setNeedInitialFocus(customSettings.needInitialFocus); + if (WebViewFeature.isFeatureSupported(WebViewFeature.OFF_SCREEN_PRERASTER)) + WebSettingsCompat.setOffscreenPreRaster(settings, customSettings.offscreenPreRaster); + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + settings.setOffscreenPreRaster(customSettings.offscreenPreRaster); + settings.setSansSerifFontFamily(customSettings.sansSerifFontFamily); + settings.setSerifFontFamily(customSettings.serifFontFamily); + settings.setStandardFontFamily(customSettings.standardFontFamily); + if (customSettings.preferredContentMode != null && + customSettings.preferredContentMode == PreferredContentModeOptionType.DESKTOP.toValue()) { + setDesktopMode(true); + } + settings.setSaveFormData(customSettings.saveFormData); + if (customSettings.incognito) + setIncognito(true); + if (customSettings.useHybridComposition) { + if (customSettings.hardwareAcceleration) + setLayerType(View.LAYER_TYPE_HARDWARE, null); + else + setLayerType(View.LAYER_TYPE_NONE, null); + } + if (customSettings.regexToCancelSubFramesLoading != null) { + regexToCancelSubFramesLoadingCompiled = Pattern.compile(customSettings.regexToCancelSubFramesLoading); + } + setScrollBarStyle(customSettings.scrollBarStyle); + if (customSettings.scrollBarDefaultDelayBeforeFade != null) { + setScrollBarDefaultDelayBeforeFade(customSettings.scrollBarDefaultDelayBeforeFade); + } else { + customSettings.scrollBarDefaultDelayBeforeFade = getScrollBarDefaultDelayBeforeFade(); + } + setScrollbarFadingEnabled(customSettings.scrollbarFadingEnabled); + if (customSettings.scrollBarFadeDuration != null) { + setScrollBarFadeDuration(customSettings.scrollBarFadeDuration); + } else { + customSettings.scrollBarFadeDuration = getScrollBarFadeDuration(); + } + setVerticalScrollbarPosition(customSettings.verticalScrollbarPosition); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (customSettings.verticalScrollbarThumbColor != null) + setVerticalScrollbarThumbDrawable(new ColorDrawable(Color.parseColor(customSettings.verticalScrollbarThumbColor))); + if (customSettings.verticalScrollbarTrackColor != null) + setVerticalScrollbarTrackDrawable(new ColorDrawable(Color.parseColor(customSettings.verticalScrollbarTrackColor))); + if (customSettings.horizontalScrollbarThumbColor != null) + setHorizontalScrollbarThumbDrawable(new ColorDrawable(Color.parseColor(customSettings.horizontalScrollbarThumbColor))); + if (customSettings.horizontalScrollbarTrackColor != null) + setHorizontalScrollbarTrackDrawable(new ColorDrawable(Color.parseColor(customSettings.horizontalScrollbarTrackColor))); + } + + setOverScrollMode(customSettings.overScrollMode); + if (customSettings.networkAvailable != null) { + setNetworkAvailable(customSettings.networkAvailable); + } + if (customSettings.rendererPriorityPolicy != null && !customSettings.rendererPriorityPolicy.isEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + setRendererPriorityPolicy( + (int) customSettings.rendererPriorityPolicy.get("rendererRequestedPriority"), + (boolean) customSettings.rendererPriorityPolicy.get("waivedWhenNotVisible")); + } + + if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + WebSettingsCompat.setAlgorithmicDarkeningAllowed(settings, customSettings.algorithmicDarkeningAllowed); + } + if (WebViewFeature.isFeatureSupported(WebViewFeature.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY)) { + WebSettingsCompat.setEnterpriseAuthenticationAppLinkPolicyEnabled(settings, customSettings.enterpriseAuthenticationAppLinkPolicyEnabled); + } + if (customSettings.requestedWithHeaderOriginAllowList != null && + WebViewFeature.isFeatureSupported(WebViewFeature.REQUESTED_WITH_HEADER_ALLOW_LIST)) { + WebSettingsCompat.setRequestedWithHeaderOriginAllowList(settings, customSettings.requestedWithHeaderOriginAllowList); + } + + contentBlockerHandler.getRuleList().clear(); + for (Map> contentBlocker : customSettings.contentBlockers) { + // compile ContentBlockerTrigger urlFilter + ContentBlockerTrigger trigger = ContentBlockerTrigger.fromMap(contentBlocker.get("trigger")); + ContentBlockerAction action = ContentBlockerAction.fromMap(contentBlocker.get("action")); + contentBlockerHandler.getRuleList().add(new ContentBlocker(trigger, action)); + } + + setFindListener(new FindListener() { + @Override + public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) { + if (findInteractionController != null && findInteractionController.channelDelegate != null) + findInteractionController.channelDelegate.onFindResultReceived(activeMatchOrdinal, numberOfMatches, isDoneCounting); + if (channelDelegate != null) + channelDelegate.onFindResultReceived(activeMatchOrdinal, numberOfMatches, isDoneCounting); + } + }); + + gestureDetector = new GestureDetector(this.getContext(), new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onSingleTapUp(MotionEvent ev) { + if (floatingContextMenu != null) { + hideContextMenu(); + } + return super.onSingleTapUp(ev); + } + }); + + checkScrollStoppedTask = new Runnable() { + @Override + public void run() { + int newPosition = getScrollY(); + if (initialPositionScrollStoppedTask - newPosition == 0) { + // has stopped + onScrollStopped(); + } else { + initialPositionScrollStoppedTask = getScrollY(); + mainLooperHandler.postDelayed(checkScrollStoppedTask, newCheckScrollStoppedTask); + } + } + }; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && !customSettings.useHybridComposition) { + checkContextMenuShouldBeClosedTask = new Runnable() { + @Override + public void run() { + if (floatingContextMenu != null) { + evaluateJavascript(PluginScriptsUtil.CHECK_CONTEXT_MENU_SHOULD_BE_HIDDEN_JS_SOURCE, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + if (value == null || value.equals("true")) { + if (floatingContextMenu != null) { + hideContextMenu(); + } + } else { + mainLooperHandler.postDelayed(checkContextMenuShouldBeClosedTask, newCheckContextMenuShouldBeClosedTaskTask); + } + } + }); + } + } + }; + } + + setOnTouchListener(new OnTouchListener() { + float m_downX; + float m_downY; + + @Override + public boolean onTouch(View v, MotionEvent event) { + gestureDetector.onTouchEvent(event); + + if (event.getAction() == MotionEvent.ACTION_UP) { + checkScrollStoppedTask.run(); + } + + if (customSettings.disableHorizontalScroll && customSettings.disableVerticalScroll) { + return (event.getAction() == MotionEvent.ACTION_MOVE); + } else if (customSettings.disableHorizontalScroll || customSettings.disableVerticalScroll) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: { + // save the x + m_downX = event.getX(); + // save the y + m_downY = event.getY(); + break; + } + case MotionEvent.ACTION_MOVE: + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: { + if (customSettings.disableHorizontalScroll) { + // set x so that it doesn't move + event.setLocation(m_downX, event.getY()); + } else { + // set y so that it doesn't move + event.setLocation(event.getX(), m_downY); + } + break; + } + } + } + return false; + } + }); + + setOnLongClickListener(new OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + com.pichillilorenzo.flutter_inappwebview_android.types.HitTestResult hitTestResult = + com.pichillilorenzo.flutter_inappwebview_android.types.HitTestResult.fromWebViewHitTestResult(getHitTestResult()); + if (channelDelegate != null) channelDelegate.onLongPressHitTestResult(hitTestResult); + return false; + } + }); + } + + public void prepareAndAddUserScripts() { + userContentController.addPluginScript(PromisePolyfillJS.PROMISE_POLYFILL_JS_PLUGIN_SCRIPT); + userContentController.addPluginScript(JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT); + userContentController.addPluginScript(PrintJS.PRINT_JS_PLUGIN_SCRIPT); + userContentController.addPluginScript(OnWindowBlurEventJS.ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT); + userContentController.addPluginScript(OnWindowFocusEventJS.ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT); + interceptOnlyAsyncAjaxRequestsPluginScript = InterceptAjaxRequestJS.createInterceptOnlyAsyncAjaxRequestsPluginScript(customSettings.interceptOnlyAsyncAjaxRequests); + if (customSettings.useShouldInterceptAjaxRequest) { + userContentController.addPluginScript(interceptOnlyAsyncAjaxRequestsPluginScript); + userContentController.addPluginScript(InterceptAjaxRequestJS.INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT); + } + if (customSettings.useShouldInterceptFetchRequest) { + userContentController.addPluginScript(InterceptFetchRequestJS.INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT); + } + if (customSettings.useOnLoadResource) { + userContentController.addPluginScript(OnLoadResourceJS.ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT); + } + if (!customSettings.useHybridComposition) { + userContentController.addPluginScript(PluginScriptsUtil.CHECK_GLOBAL_KEY_DOWN_EVENT_TO_HIDE_CONTEXT_MENU_JS_PLUGIN_SCRIPT); + } + this.userContentController.addUserOnlyScripts(this.initialUserOnlyScripts); + } + + public void setIncognito(boolean enabled) { + WebSettings settings = getSettings(); + if (enabled) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + gopeedCookieManager().removeAllCookies(null); + } else { + gopeedCookieManager().removeAllCookie(); + } + + // Disable caching + settings.setCacheMode(WebSettings.LOAD_NO_CACHE); + + // removed from Android API 33+ (https://developer.android.com/sdk/api_diff/33/changes) + // settings.setAppCacheEnabled(false); + Util.invokeMethodIfExists(settings, "setAppCacheEnabled", false); + + clearHistory(); + clearCache(true); + + // No form data or autofill enabled + clearFormData(); + settings.setSavePassword(false); + settings.setSaveFormData(false); + } else { + settings.setCacheMode(WebSettings.LOAD_DEFAULT); + + // removed from Android API 33+ (https://developer.android.com/sdk/api_diff/33/changes) + // settings.setAppCacheEnabled(true); + Util.invokeMethodIfExists(settings, "setAppCacheEnabled", true); + + settings.setSavePassword(true); + settings.setSaveFormData(true); + } + } + + public void setCacheEnabled(boolean enabled) { + WebSettings settings = getSettings(); + if (enabled) { + Context ctx = getContext(); + if (ctx != null) { + // removed from Android API 33+ (https://developer.android.com/sdk/api_diff/33/changes) + // settings.setAppCachePath(ctx.getCacheDir().getAbsolutePath()); + Util.invokeMethodIfExists(settings, "setAppCachePath", ctx.getCacheDir().getAbsolutePath()); + + settings.setCacheMode(WebSettings.LOAD_DEFAULT); + + // removed from Android API 33+ (https://developer.android.com/sdk/api_diff/33/changes) + // settings.setAppCacheEnabled(true); + Util.invokeMethodIfExists(settings, "setAppCacheEnabled", true); + } + } else { + settings.setCacheMode(WebSettings.LOAD_NO_CACHE); + + // removed from Android API 33+ (https://developer.android.com/sdk/api_diff/33/changes) + // settings.setAppCacheEnabled(false); + Util.invokeMethodIfExists(settings, "setAppCacheEnabled", false); + } + } + + public void loadUrl(URLRequest urlRequest) { + String url = urlRequest.getUrl(); + String method = urlRequest.getMethod(); + if (method != null && method.equals("POST")) { + byte[] postData = urlRequest.getBody(); + postUrl(url, postData); + return; + } + Map headers = urlRequest.getHeaders(); + if (headers != null) { + loadUrl(url, headers); + return; + } + loadUrl(url); + } + + public void loadFile(String assetFilePath) throws IOException { + if (plugin == null) { + return; + } + + loadUrl(Util.getUrlAsset(plugin, assetFilePath)); + } + + public boolean isLoading() { + return isLoading; + } + + /** + * @deprecated + */ + @Deprecated + private void clearCookies() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + gopeedCookieManager().removeAllCookies(new ValueCallback() { + @Override + public void onReceiveValue(Boolean aBoolean) { + + } + }); + } else { + gopeedCookieManager().removeAllCookie(); + } + } + + /** + * @deprecated + */ + @Deprecated + public void clearAllCache() { + clearCache(true); + clearCookies(); + clearFormData(); + WebStorage.getInstance().deleteAllData(); + } + + public void takeScreenshot(final @Nullable Map screenshotConfiguration, final MethodChannel.Result result) { + final float pixelDensity = Util.getPixelDensity(getContext()); + + mainLooperHandler.post(new Runnable() { + @Override + public void run() { + try { + Bitmap screenshotBitmap = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(screenshotBitmap); + c.translate(-getScrollX(), -getScrollY()); + draw(c); + + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + Bitmap.CompressFormat compressFormat = Bitmap.CompressFormat.PNG; + int quality = 100; + + if (screenshotConfiguration != null) { + Map rect = (Map) screenshotConfiguration.get("rect"); + if (rect != null) { + int rectX = (int) Math.floor(rect.get("x") * pixelDensity + 0.5); + int rectY = (int) Math.floor(rect.get("y") * pixelDensity + 0.5); + int rectWidth = Math.min(screenshotBitmap.getWidth(), (int) Math.floor(rect.get("width") * pixelDensity + 0.5)); + int rectHeight = Math.min(screenshotBitmap.getHeight(), (int) Math.floor(rect.get("height") * pixelDensity + 0.5)); + screenshotBitmap = Bitmap.createBitmap( + screenshotBitmap, + rectX, + rectY, + rectWidth, + rectHeight); + } + + Double snapshotWidth = (Double) screenshotConfiguration.get("snapshotWidth"); + if (snapshotWidth != null) { + int dstWidth = (int) Math.floor(snapshotWidth * pixelDensity + 0.5); + float ratioBitmap = (float) screenshotBitmap.getWidth() / (float) screenshotBitmap.getHeight(); + int dstHeight = (int) ((float) dstWidth / ratioBitmap); + screenshotBitmap = Bitmap.createScaledBitmap(screenshotBitmap, dstWidth, dstHeight, true); + } + + try { + compressFormat = Bitmap.CompressFormat.valueOf((String) screenshotConfiguration.get("compressFormat")); + } catch (IllegalArgumentException e) { + Log.e(LOG_TAG, "", e); + } + + quality = (Integer) screenshotConfiguration.get("quality"); + } + + screenshotBitmap.compress( + compressFormat, + quality, + byteArrayOutputStream); + + try { + byteArrayOutputStream.close(); + } catch (IOException e) { + Log.e(LOG_TAG, "", e); + } + screenshotBitmap.recycle(); + result.success(byteArrayOutputStream.toByteArray()); + + } catch (IllegalArgumentException e) { + Log.e(LOG_TAG, "", e); + result.success(null); + } + } + }); + } + + @SuppressLint("RestrictedApi") + public void setSettings(InAppWebViewSettings newCustomSettings, HashMap newSettingsMap) { + + WebSettings settings = getSettings(); + + if (newSettingsMap.get("javaScriptEnabled") != null && customSettings.javaScriptEnabled != newCustomSettings.javaScriptEnabled) + settings.setJavaScriptEnabled(newCustomSettings.javaScriptEnabled); + + if (newSettingsMap.get("useShouldInterceptAjaxRequest") != null && customSettings.useShouldInterceptAjaxRequest != newCustomSettings.useShouldInterceptAjaxRequest) { + enablePluginScriptAtRuntime( + InterceptAjaxRequestJS.FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE, + newCustomSettings.useShouldInterceptAjaxRequest, + InterceptAjaxRequestJS.INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT + ); + } + + if (newSettingsMap.get("interceptOnlyAsyncAjaxRequests") != null && customSettings.interceptOnlyAsyncAjaxRequests != newCustomSettings.interceptOnlyAsyncAjaxRequests) { + enablePluginScriptAtRuntime( + InterceptAjaxRequestJS.FLAG_VARIABLE_FOR_INTERCEPT_ONLY_ASYNC_AJAX_REQUESTS_JS_SOURCE, + newCustomSettings.interceptOnlyAsyncAjaxRequests, + interceptOnlyAsyncAjaxRequestsPluginScript + ); + } + + if (newSettingsMap.get("useShouldInterceptFetchRequest") != null && customSettings.useShouldInterceptFetchRequest != newCustomSettings.useShouldInterceptFetchRequest) { + enablePluginScriptAtRuntime( + InterceptFetchRequestJS.FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE, + newCustomSettings.useShouldInterceptFetchRequest, + InterceptFetchRequestJS.INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT + ); + } + + if (newSettingsMap.get("useOnLoadResource") != null && customSettings.useOnLoadResource != newCustomSettings.useOnLoadResource) { + enablePluginScriptAtRuntime( + OnLoadResourceJS.FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE, + newCustomSettings.useOnLoadResource, + OnLoadResourceJS.ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT + ); + } + + if (newSettingsMap.get("javaScriptCanOpenWindowsAutomatically") != null && customSettings.javaScriptCanOpenWindowsAutomatically != newCustomSettings.javaScriptCanOpenWindowsAutomatically) + settings.setJavaScriptCanOpenWindowsAutomatically(newCustomSettings.javaScriptCanOpenWindowsAutomatically); + + if (newSettingsMap.get("builtInZoomControls") != null && customSettings.builtInZoomControls != newCustomSettings.builtInZoomControls) + settings.setBuiltInZoomControls(newCustomSettings.builtInZoomControls); + + if (newSettingsMap.get("displayZoomControls") != null && customSettings.displayZoomControls != newCustomSettings.displayZoomControls) + settings.setDisplayZoomControls(newCustomSettings.displayZoomControls); + + if (newSettingsMap.get("safeBrowsingEnabled") != null && customSettings.safeBrowsingEnabled != newCustomSettings.safeBrowsingEnabled) { + if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ENABLE)) + WebSettingsCompat.setSafeBrowsingEnabled(settings, newCustomSettings.safeBrowsingEnabled); + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + settings.setSafeBrowsingEnabled(newCustomSettings.safeBrowsingEnabled); + } + + if (newSettingsMap.get("mediaPlaybackRequiresUserGesture") != null && customSettings.mediaPlaybackRequiresUserGesture != newCustomSettings.mediaPlaybackRequiresUserGesture) + settings.setMediaPlaybackRequiresUserGesture(newCustomSettings.mediaPlaybackRequiresUserGesture); + + if (newSettingsMap.get("databaseEnabled") != null && customSettings.databaseEnabled != newCustomSettings.databaseEnabled) + settings.setDatabaseEnabled(newCustomSettings.databaseEnabled); + + if (newSettingsMap.get("domStorageEnabled") != null && customSettings.domStorageEnabled != newCustomSettings.domStorageEnabled) + settings.setDomStorageEnabled(newCustomSettings.domStorageEnabled); + + if (newSettingsMap.get("userAgent") != null && !customSettings.userAgent.equals(newCustomSettings.userAgent) && !newCustomSettings.userAgent.isEmpty()) + settings.setUserAgentString(newCustomSettings.userAgent); + + if (newSettingsMap.get("applicationNameForUserAgent") != null && !customSettings.applicationNameForUserAgent.equals(newCustomSettings.applicationNameForUserAgent) && !newCustomSettings.applicationNameForUserAgent.isEmpty()) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + String userAgent = (newCustomSettings.userAgent != null && !newCustomSettings.userAgent.isEmpty()) ? newCustomSettings.userAgent : WebSettings.getDefaultUserAgent(getContext()); + String userAgentWithApplicationName = userAgent + " " + customSettings.applicationNameForUserAgent; + settings.setUserAgentString(userAgentWithApplicationName); + } + } + + if (newSettingsMap.get("clearCache") != null && newCustomSettings.clearCache) + clearAllCache(); + else if (newSettingsMap.get("clearSessionCache") != null && newCustomSettings.clearSessionCache) + gopeedCookieManager().removeSessionCookie(); + + if (newSettingsMap.get("thirdPartyCookiesEnabled") != null && customSettings.thirdPartyCookiesEnabled != newCustomSettings.thirdPartyCookiesEnabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + gopeedCookieManager().setAcceptThirdPartyCookies(this, newCustomSettings.thirdPartyCookiesEnabled); + + if (newSettingsMap.get("useWideViewPort") != null && customSettings.useWideViewPort != newCustomSettings.useWideViewPort) + settings.setUseWideViewPort(newCustomSettings.useWideViewPort); + + if (newSettingsMap.get("supportZoom") != null && customSettings.supportZoom != newCustomSettings.supportZoom) + settings.setSupportZoom(newCustomSettings.supportZoom); + + if (newSettingsMap.get("textZoom") != null && !customSettings.textZoom.equals(newCustomSettings.textZoom)) + settings.setTextZoom(newCustomSettings.textZoom); + + if (newSettingsMap.get("verticalScrollBarEnabled") != null && customSettings.verticalScrollBarEnabled != newCustomSettings.verticalScrollBarEnabled) + setVerticalScrollBarEnabled(newCustomSettings.verticalScrollBarEnabled); + + if (newSettingsMap.get("horizontalScrollBarEnabled") != null && customSettings.horizontalScrollBarEnabled != newCustomSettings.horizontalScrollBarEnabled) + setHorizontalScrollBarEnabled(newCustomSettings.horizontalScrollBarEnabled); + + if (newSettingsMap.get("transparentBackground") != null && customSettings.transparentBackground != newCustomSettings.transparentBackground) { + if (newCustomSettings.transparentBackground) { + setBackgroundColor(Color.TRANSPARENT); + } else { + setBackgroundColor(Color.parseColor("#FFFFFF")); + } + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + if (newSettingsMap.get("mixedContentMode") != null && (customSettings.mixedContentMode == null || !customSettings.mixedContentMode.equals(newCustomSettings.mixedContentMode))) + settings.setMixedContentMode(newCustomSettings.mixedContentMode); + + if (newSettingsMap.get("supportMultipleWindows") != null && customSettings.supportMultipleWindows != newCustomSettings.supportMultipleWindows) + settings.setSupportMultipleWindows(newCustomSettings.supportMultipleWindows); + + if (newSettingsMap.get("useOnDownloadStart") != null && customSettings.useOnDownloadStart != newCustomSettings.useOnDownloadStart) { + if (newCustomSettings.useOnDownloadStart) { + setDownloadListener(new DownloadStartListener()); + } else { + setDownloadListener(null); + } + } + + if (newSettingsMap.get("allowContentAccess") != null && customSettings.allowContentAccess != newCustomSettings.allowContentAccess) + settings.setAllowContentAccess(newCustomSettings.allowContentAccess); + + if (newSettingsMap.get("allowFileAccess") != null && customSettings.allowFileAccess != newCustomSettings.allowFileAccess) + settings.setAllowFileAccess(newCustomSettings.allowFileAccess); + + if (newSettingsMap.get("allowFileAccessFromFileURLs") != null && customSettings.allowFileAccessFromFileURLs != newCustomSettings.allowFileAccessFromFileURLs) + settings.setAllowFileAccessFromFileURLs(newCustomSettings.allowFileAccessFromFileURLs); + + if (newSettingsMap.get("allowUniversalAccessFromFileURLs") != null && customSettings.allowUniversalAccessFromFileURLs != newCustomSettings.allowUniversalAccessFromFileURLs) + settings.setAllowUniversalAccessFromFileURLs(newCustomSettings.allowUniversalAccessFromFileURLs); + + if (newSettingsMap.get("cacheEnabled") != null && customSettings.cacheEnabled != newCustomSettings.cacheEnabled) + setCacheEnabled(newCustomSettings.cacheEnabled); + + if (newSettingsMap.get("appCachePath") != null && (customSettings.appCachePath == null || !customSettings.appCachePath.equals(newCustomSettings.appCachePath))) { + // removed from Android API 33+ (https://developer.android.com/sdk/api_diff/33/changes) + // settings.setAppCachePath(newCustomSettings.appCachePath); + Util.invokeMethodIfExists(settings, "setAppCachePath", newCustomSettings.appCachePath); + } + + if (newSettingsMap.get("blockNetworkImage") != null && customSettings.blockNetworkImage != newCustomSettings.blockNetworkImage) + settings.setBlockNetworkImage(newCustomSettings.blockNetworkImage); + + if (newSettingsMap.get("blockNetworkLoads") != null && customSettings.blockNetworkLoads != newCustomSettings.blockNetworkLoads) + settings.setBlockNetworkLoads(newCustomSettings.blockNetworkLoads); + + if (newSettingsMap.get("cacheMode") != null && !customSettings.cacheMode.equals(newCustomSettings.cacheMode)) + settings.setCacheMode(newCustomSettings.cacheMode); + + if (newSettingsMap.get("cursiveFontFamily") != null && !customSettings.cursiveFontFamily.equals(newCustomSettings.cursiveFontFamily)) + settings.setCursiveFontFamily(newCustomSettings.cursiveFontFamily); + + if (newSettingsMap.get("defaultFixedFontSize") != null && !customSettings.defaultFixedFontSize.equals(newCustomSettings.defaultFixedFontSize)) + settings.setDefaultFixedFontSize(newCustomSettings.defaultFixedFontSize); + + if (newSettingsMap.get("defaultFontSize") != null && !customSettings.defaultFontSize.equals(newCustomSettings.defaultFontSize)) + settings.setDefaultFontSize(newCustomSettings.defaultFontSize); + + if (newSettingsMap.get("defaultTextEncodingName") != null && !customSettings.defaultTextEncodingName.equals(newCustomSettings.defaultTextEncodingName)) + settings.setDefaultTextEncodingName(newCustomSettings.defaultTextEncodingName); + + if (newSettingsMap.get("disabledActionModeMenuItems") != null && + (customSettings.disabledActionModeMenuItems == null || + !customSettings.disabledActionModeMenuItems.equals(newCustomSettings.disabledActionModeMenuItems))) { + if (WebViewFeature.isFeatureSupported(WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS)) + WebSettingsCompat.setDisabledActionModeMenuItems(settings, newCustomSettings.disabledActionModeMenuItems); + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + settings.setDisabledActionModeMenuItems(newCustomSettings.disabledActionModeMenuItems); + } + + if (newSettingsMap.get("fantasyFontFamily") != null && !customSettings.fantasyFontFamily.equals(newCustomSettings.fantasyFontFamily)) + settings.setFantasyFontFamily(newCustomSettings.fantasyFontFamily); + + if (newSettingsMap.get("fixedFontFamily") != null && !customSettings.fixedFontFamily.equals(newCustomSettings.fixedFontFamily)) + settings.setFixedFontFamily(newCustomSettings.fixedFontFamily); + + if (newSettingsMap.get("forceDark") != null && !customSettings.forceDark.equals(newCustomSettings.forceDark)) { + if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) + WebSettingsCompat.setForceDark(settings, newCustomSettings.forceDark); + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + settings.setForceDark(newCustomSettings.forceDark); + } + + if (newSettingsMap.get("forceDarkStrategy") != null && + !customSettings.forceDarkStrategy.equals(newCustomSettings.forceDarkStrategy) && + WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) { + WebSettingsCompat.setForceDarkStrategy(settings, newCustomSettings.forceDarkStrategy); + } + + if (newSettingsMap.get("geolocationEnabled") != null && customSettings.geolocationEnabled != newCustomSettings.geolocationEnabled) + settings.setGeolocationEnabled(newCustomSettings.geolocationEnabled); + + if (newSettingsMap.get("layoutAlgorithm") != null && customSettings.layoutAlgorithm != newCustomSettings.layoutAlgorithm) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && newCustomSettings.layoutAlgorithm.equals(WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING)) { + settings.setLayoutAlgorithm(newCustomSettings.layoutAlgorithm); + } else { + settings.setLayoutAlgorithm(newCustomSettings.layoutAlgorithm); + } + } + + if (newSettingsMap.get("loadWithOverviewMode") != null && customSettings.loadWithOverviewMode != newCustomSettings.loadWithOverviewMode) + settings.setLoadWithOverviewMode(newCustomSettings.loadWithOverviewMode); + + if (newSettingsMap.get("loadsImagesAutomatically") != null && customSettings.loadsImagesAutomatically != newCustomSettings.loadsImagesAutomatically) + settings.setLoadsImagesAutomatically(newCustomSettings.loadsImagesAutomatically); + + if (newSettingsMap.get("minimumFontSize") != null && !customSettings.minimumFontSize.equals(newCustomSettings.minimumFontSize)) + settings.setMinimumFontSize(newCustomSettings.minimumFontSize); + + if (newSettingsMap.get("minimumLogicalFontSize") != null && !customSettings.minimumLogicalFontSize.equals(newCustomSettings.minimumLogicalFontSize)) + settings.setMinimumLogicalFontSize(newCustomSettings.minimumLogicalFontSize); + + if (newSettingsMap.get("initialScale") != null && !customSettings.initialScale.equals(newCustomSettings.initialScale)) + setInitialScale(newCustomSettings.initialScale); + + if (newSettingsMap.get("needInitialFocus") != null && customSettings.needInitialFocus != newCustomSettings.needInitialFocus) + settings.setNeedInitialFocus(newCustomSettings.needInitialFocus); + + if (newSettingsMap.get("offscreenPreRaster") != null && customSettings.offscreenPreRaster != newCustomSettings.offscreenPreRaster) { + if (WebViewFeature.isFeatureSupported(WebViewFeature.OFF_SCREEN_PRERASTER)) + WebSettingsCompat.setOffscreenPreRaster(settings, newCustomSettings.offscreenPreRaster); + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + settings.setOffscreenPreRaster(newCustomSettings.offscreenPreRaster); + } + + if (newSettingsMap.get("sansSerifFontFamily") != null && !customSettings.sansSerifFontFamily.equals(newCustomSettings.sansSerifFontFamily)) + settings.setSansSerifFontFamily(newCustomSettings.sansSerifFontFamily); + + if (newSettingsMap.get("serifFontFamily") != null && !customSettings.serifFontFamily.equals(newCustomSettings.serifFontFamily)) + settings.setSerifFontFamily(newCustomSettings.serifFontFamily); + + if (newSettingsMap.get("standardFontFamily") != null && !customSettings.standardFontFamily.equals(newCustomSettings.standardFontFamily)) + settings.setStandardFontFamily(newCustomSettings.standardFontFamily); + + if (newSettingsMap.get("preferredContentMode") != null && !customSettings.preferredContentMode.equals(newCustomSettings.preferredContentMode)) { + switch (fromValue(newCustomSettings.preferredContentMode)) { + case DESKTOP: + setDesktopMode(true); + break; + case MOBILE: + case RECOMMENDED: + setDesktopMode(false); + break; + } + } + + if (newSettingsMap.get("saveFormData") != null && customSettings.saveFormData != newCustomSettings.saveFormData) + settings.setSaveFormData(newCustomSettings.saveFormData); + + if (newSettingsMap.get("incognito") != null && customSettings.incognito != newCustomSettings.incognito) + setIncognito(newCustomSettings.incognito); + + if (customSettings.useHybridComposition) { + if (newSettingsMap.get("hardwareAcceleration") != null && customSettings.hardwareAcceleration != newCustomSettings.hardwareAcceleration) { + if (newCustomSettings.hardwareAcceleration) + setLayerType(View.LAYER_TYPE_HARDWARE, null); + else + setLayerType(View.LAYER_TYPE_NONE, null); + } + } + + if (newSettingsMap.get("regexToCancelSubFramesLoading") != null && (customSettings.regexToCancelSubFramesLoading == null || + !customSettings.regexToCancelSubFramesLoading.equals(newCustomSettings.regexToCancelSubFramesLoading))) { + if (newCustomSettings.regexToCancelSubFramesLoading == null) + regexToCancelSubFramesLoadingCompiled = null; + else + regexToCancelSubFramesLoadingCompiled = Pattern.compile(customSettings.regexToCancelSubFramesLoading); + } + + if (newCustomSettings.contentBlockers != null) { + contentBlockerHandler.getRuleList().clear(); + for (Map> contentBlocker : newCustomSettings.contentBlockers) { + // compile ContentBlockerTrigger urlFilter + ContentBlockerTrigger trigger = ContentBlockerTrigger.fromMap(contentBlocker.get("trigger")); + ContentBlockerAction action = ContentBlockerAction.fromMap(contentBlocker.get("action")); + contentBlockerHandler.getRuleList().add(new ContentBlocker(trigger, action)); + } + } + + if (newSettingsMap.get("scrollBarStyle") != null && !customSettings.scrollBarStyle.equals(newCustomSettings.scrollBarStyle)) + setScrollBarStyle(newCustomSettings.scrollBarStyle); + + if (newSettingsMap.get("scrollBarDefaultDelayBeforeFade") != null && (customSettings.scrollBarDefaultDelayBeforeFade == null || + !customSettings.scrollBarDefaultDelayBeforeFade.equals(newCustomSettings.scrollBarDefaultDelayBeforeFade))) + setScrollBarDefaultDelayBeforeFade(newCustomSettings.scrollBarDefaultDelayBeforeFade); + + if (newSettingsMap.get("scrollbarFadingEnabled") != null && !customSettings.scrollbarFadingEnabled.equals(newCustomSettings.scrollbarFadingEnabled)) + setScrollbarFadingEnabled(newCustomSettings.scrollbarFadingEnabled); + + if (newSettingsMap.get("scrollBarFadeDuration") != null && (customSettings.scrollBarFadeDuration == null || + !customSettings.scrollBarFadeDuration.equals(newCustomSettings.scrollBarFadeDuration))) + setScrollBarFadeDuration(newCustomSettings.scrollBarFadeDuration); + + if (newSettingsMap.get("verticalScrollbarPosition") != null && !customSettings.verticalScrollbarPosition.equals(newCustomSettings.verticalScrollbarPosition)) + setVerticalScrollbarPosition(newCustomSettings.verticalScrollbarPosition); + + if (newSettingsMap.get("disableVerticalScroll") != null && customSettings.disableVerticalScroll != newCustomSettings.disableVerticalScroll) + setVerticalScrollBarEnabled(!newCustomSettings.disableVerticalScroll && newCustomSettings.verticalScrollBarEnabled); + + if (newSettingsMap.get("disableHorizontalScroll") != null && customSettings.disableHorizontalScroll != newCustomSettings.disableHorizontalScroll) + setHorizontalScrollBarEnabled(!newCustomSettings.disableHorizontalScroll && newCustomSettings.horizontalScrollBarEnabled); + + if (newSettingsMap.get("overScrollMode") != null && !customSettings.overScrollMode.equals(newCustomSettings.overScrollMode)) + setOverScrollMode(newCustomSettings.overScrollMode); + + if (newSettingsMap.get("networkAvailable") != null && customSettings.networkAvailable != newCustomSettings.networkAvailable) + setNetworkAvailable(newCustomSettings.networkAvailable); + + if (newSettingsMap.get("rendererPriorityPolicy") != null && (customSettings.rendererPriorityPolicy == null || + (customSettings.rendererPriorityPolicy.get("rendererRequestedPriority") != newCustomSettings.rendererPriorityPolicy.get("rendererRequestedPriority") || + customSettings.rendererPriorityPolicy.get("waivedWhenNotVisible") != newCustomSettings.rendererPriorityPolicy.get("waivedWhenNotVisible"))) && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + setRendererPriorityPolicy( + (int) newCustomSettings.rendererPriorityPolicy.get("rendererRequestedPriority"), + (boolean) newCustomSettings.rendererPriorityPolicy.get("waivedWhenNotVisible")); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (newSettingsMap.get("verticalScrollbarThumbColor") != null && !Util.objEquals(customSettings.verticalScrollbarThumbColor, newCustomSettings.verticalScrollbarThumbColor)) + setVerticalScrollbarThumbDrawable(new ColorDrawable(Color.parseColor(newCustomSettings.verticalScrollbarThumbColor))); + + if (newSettingsMap.get("verticalScrollbarTrackColor") != null && !Util.objEquals(customSettings.verticalScrollbarTrackColor, newCustomSettings.verticalScrollbarTrackColor)) + setVerticalScrollbarTrackDrawable(new ColorDrawable(Color.parseColor(newCustomSettings.verticalScrollbarTrackColor))); + + if (newSettingsMap.get("horizontalScrollbarThumbColor") != null && !Util.objEquals(customSettings.horizontalScrollbarThumbColor, newCustomSettings.horizontalScrollbarThumbColor)) + setHorizontalScrollbarThumbDrawable(new ColorDrawable(Color.parseColor(newCustomSettings.horizontalScrollbarThumbColor))); + + if (newSettingsMap.get("horizontalScrollbarTrackColor") != null && !Util.objEquals(customSettings.horizontalScrollbarTrackColor, newCustomSettings.horizontalScrollbarTrackColor)) + setHorizontalScrollbarTrackDrawable(new ColorDrawable(Color.parseColor(newCustomSettings.horizontalScrollbarTrackColor))); + } + + if (newSettingsMap.get("algorithmicDarkeningAllowed") != null && + !Util.objEquals(customSettings.algorithmicDarkeningAllowed, newCustomSettings.algorithmicDarkeningAllowed) && + WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + WebSettingsCompat.setAlgorithmicDarkeningAllowed(settings, newCustomSettings.algorithmicDarkeningAllowed); + } + if (newSettingsMap.get("enterpriseAuthenticationAppLinkPolicyEnabled") != null && + !Util.objEquals(customSettings.enterpriseAuthenticationAppLinkPolicyEnabled, newCustomSettings.enterpriseAuthenticationAppLinkPolicyEnabled) && + WebViewFeature.isFeatureSupported(WebViewFeature.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY)) { + WebSettingsCompat.setEnterpriseAuthenticationAppLinkPolicyEnabled(settings, newCustomSettings.enterpriseAuthenticationAppLinkPolicyEnabled); + } + if (newSettingsMap.get("requestedWithHeaderOriginAllowList") != null && + !Util.objEquals(customSettings.requestedWithHeaderOriginAllowList, newCustomSettings.requestedWithHeaderOriginAllowList) && + WebViewFeature.isFeatureSupported(WebViewFeature.REQUESTED_WITH_HEADER_ALLOW_LIST)) { + WebSettingsCompat.setRequestedWithHeaderOriginAllowList(settings, newCustomSettings.requestedWithHeaderOriginAllowList); + } + + if (plugin != null) { + if (webViewAssetLoaderExt != null) { + webViewAssetLoaderExt.dispose(); + } + webViewAssetLoaderExt = WebViewAssetLoaderExt.fromMap(customSettings.webViewAssetLoader, plugin, getContext()); + } + + customSettings = newCustomSettings; + } + + public Map getCustomSettings() { + return (customSettings != null) ? customSettings.getRealSettings(this) : null; + } + + public void enablePluginScriptAtRuntime(final String flagVariable, + final boolean enable, + final PluginScript pluginScript) { + evaluateJavascript("window." + flagVariable, null, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + boolean alreadyLoaded = value != null && !value.equalsIgnoreCase("null"); + if (alreadyLoaded) { + String enableSource = "window." + flagVariable + " = " + enable + ";"; + evaluateJavascript(enableSource, null, null); + if (!enable) { + userContentController.removePluginScript(pluginScript); + } + } else if (enable) { + evaluateJavascript(pluginScript.getSource(), null, null); + userContentController.addPluginScript(pluginScript); + } + } + }); + } + + public void injectDeferredObject(String source, @Nullable final ContentWorld contentWorld, String jsWrapper, @Nullable final ValueCallback resultCallback) { + final String resultUuid = contentWorld != null && !contentWorld.equals(ContentWorld.PAGE) ? UUID.randomUUID().toString() : null; + String scriptToInject = source; + if (jsWrapper != null) { + org.json.JSONArray jsonEsc = new org.json.JSONArray(); + jsonEsc.put(source); + String jsonRepr = jsonEsc.toString(); + String jsonSourceString = jsonRepr.substring(1, jsonRepr.length() - 1); + scriptToInject = String.format(jsWrapper, jsonSourceString); + } + if (resultUuid != null && resultCallback != null) { + evaluateJavaScriptContentWorldCallbacks.put(resultUuid, resultCallback); + scriptToInject = Util.replaceAll(PluginScriptsUtil.EVALUATE_JAVASCRIPT_WITH_CONTENT_WORLD_WRAPPER_JS_SOURCE, + PluginScriptsUtil.VAR_RANDOM_NAME, "_" + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "_" + Math.round(Math.random() * 1000000)) + .replace(PluginScriptsUtil.VAR_PLACEHOLDER_VALUE, UserContentController.escapeCode(source)) + .replace(PluginScriptsUtil.VAR_RESULT_UUID, resultUuid); + } + final String finalScriptToInject = scriptToInject; + mainLooperHandler.post(new Runnable() { + @Override + public void run() { + String scriptToInject = userContentController.generateCodeForScriptEvaluation(finalScriptToInject, contentWorld); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + // This action will have the side-effect of blurring the currently focused element + loadUrl("javascript:" + scriptToInject.replaceAll("[\r\n]+", "")); + if (contentWorld != null && resultCallback != null) { + resultCallback.onReceiveValue(""); + } + } else { + evaluateJavascript(scriptToInject, new ValueCallback() { + @Override + public void onReceiveValue(String s) { + if (resultUuid != null || resultCallback == null) + return; + resultCallback.onReceiveValue(s); + } + }); + } + } + }); + } + + public void evaluateJavascript(String source, @Nullable ContentWorld contentWorld, @Nullable ValueCallback resultCallback) { + injectDeferredObject(source, contentWorld, null, resultCallback); + } + + public void injectJavascriptFileFromUrl(String urlFile, @Nullable Map scriptHtmlTagAttributes) { + String scriptAttributes = ""; + if (scriptHtmlTagAttributes != null) { + String typeAttr = (String) scriptHtmlTagAttributes.get("type"); + if (typeAttr != null) { + scriptAttributes += " script.type = '" + typeAttr.replaceAll("'", "\\\\'") + "'; "; + } + String idAttr = (String) scriptHtmlTagAttributes.get("id"); + if (idAttr != null) { + String scriptIdEscaped = idAttr.replaceAll("'", "\\\\'"); + scriptAttributes += " script.id = '" + scriptIdEscaped + "'; "; + scriptAttributes += " script.onload = function() {" + + " if (window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + " != null) {" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('onInjectedScriptLoaded', '" + scriptIdEscaped + "');" + + " }" + + "};"; + scriptAttributes += " script.onerror = function() {" + + " if (window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + " != null) {" + + " window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + ".callHandler('onInjectedScriptError', '" + scriptIdEscaped + "');" + + " }" + + "};"; + } + Boolean asyncAttr = (Boolean) scriptHtmlTagAttributes.get("async"); + if (asyncAttr != null && asyncAttr) { + scriptAttributes += " script.async = true; "; + } + Boolean deferAttr = (Boolean) scriptHtmlTagAttributes.get("defer"); + if (deferAttr != null && deferAttr) { + scriptAttributes += " script.defer = true; "; + } + String crossOriginAttr = (String) scriptHtmlTagAttributes.get("crossOrigin"); + if (crossOriginAttr != null) { + scriptAttributes += " script.crossOrigin = '" + crossOriginAttr.replaceAll("'", "\\\\'") + "'; "; + } + String integrityAttr = (String) scriptHtmlTagAttributes.get("integrity"); + if (integrityAttr != null) { + scriptAttributes += " script.integrity = '" + integrityAttr.replaceAll("'", "\\\\'") + "'; "; + } + Boolean noModuleAttr = (Boolean) scriptHtmlTagAttributes.get("noModule"); + if (noModuleAttr != null && noModuleAttr) { + scriptAttributes += " script.noModule = true; "; + } + String nonceAttr = (String) scriptHtmlTagAttributes.get("nonce"); + if (nonceAttr != null) { + scriptAttributes += " script.nonce = '" + nonceAttr.replaceAll("'", "\\\\'") + "'; "; + } + String referrerPolicyAttr = (String) scriptHtmlTagAttributes.get("referrerPolicy"); + if (referrerPolicyAttr != null) { + scriptAttributes += " script.referrerPolicy = '" + referrerPolicyAttr.replaceAll("'", "\\\\'") + "'; "; + } + } + String jsWrapper = "(function(d) { var script = d.createElement('script'); " + scriptAttributes + + " script.src = %s; if (d.body != null) { d.body.appendChild(script); } })(document);"; + injectDeferredObject(urlFile, null, jsWrapper, null); + } + + public void injectCSSCode(String source) { + String jsWrapper = "(function(d) { var style = d.createElement('style'); style.innerHTML = %s;" + + " if (d.head != null) { d.head.appendChild(style); } })(document);"; + injectDeferredObject(source, null, jsWrapper, null); + } + + public void injectCSSFileFromUrl(String urlFile, @Nullable Map cssLinkHtmlTagAttributes) { + String cssLinkAttributes = ""; + String alternateStylesheet = ""; + if (cssLinkHtmlTagAttributes != null) { + String idAttr = (String) cssLinkHtmlTagAttributes.get("id"); + if (idAttr != null) { + cssLinkAttributes += " link.id = '" + idAttr.replaceAll("'", "\\\\'") + "'; "; + } + String mediaAttr = (String) cssLinkHtmlTagAttributes.get("media"); + if (mediaAttr != null) { + cssLinkAttributes += " link.media = '" + mediaAttr.replaceAll("'", "\\\\'") + "'; "; + } + String crossOriginAttr = (String) cssLinkHtmlTagAttributes.get("crossOrigin"); + if (crossOriginAttr != null) { + cssLinkAttributes += " link.crossOrigin = '" + crossOriginAttr.replaceAll("'", "\\\\'") + "'; "; + } + String integrityAttr = (String) cssLinkHtmlTagAttributes.get("integrity"); + if (integrityAttr != null) { + cssLinkAttributes += " link.integrity = '" + integrityAttr.replaceAll("'", "\\\\'") + "'; "; + } + String referrerPolicyAttr = (String) cssLinkHtmlTagAttributes.get("referrerPolicy"); + if (referrerPolicyAttr != null) { + cssLinkAttributes += " link.referrerPolicy = '" + referrerPolicyAttr.replaceAll("'", "\\\\'") + "'; "; + } + Boolean disabledAttr = (Boolean) cssLinkHtmlTagAttributes.get("disabled"); + if (disabledAttr != null && disabledAttr) { + cssLinkAttributes += " link.disabled = true; "; + } + Boolean alternateAttr = (Boolean) cssLinkHtmlTagAttributes.get("alternate"); + if (alternateAttr != null && alternateAttr) { + alternateStylesheet = "alternate "; + } + String titleAttr = (String) cssLinkHtmlTagAttributes.get("title"); + if (titleAttr != null) { + cssLinkAttributes += " link.title = '" + titleAttr.replaceAll("'", "\\\\'") + "'; "; + } + } + String jsWrapper = "(function(d) { var link = d.createElement('link'); link.rel='" + alternateStylesheet + "stylesheet'; link.type='text/css'; " + + cssLinkAttributes + " link.href = %s; if (d.head != null) { d.head.appendChild(link); } })(document);"; + injectDeferredObject(urlFile, null, jsWrapper, null); + } + + public HashMap getCopyBackForwardList() { + WebBackForwardList currentList = copyBackForwardList(); + int currentSize = currentList.getSize(); + int currentIndex = currentList.getCurrentIndex(); + + List> history = new ArrayList>(); + + for (int i = 0; i < currentSize; i++) { + WebHistoryItem historyItem = currentList.getItemAtIndex(i); + HashMap historyItemMap = new HashMap<>(); + + historyItemMap.put("originalUrl", historyItem.getOriginalUrl()); + historyItemMap.put("title", historyItem.getTitle()); + historyItemMap.put("url", historyItem.getUrl()); + + history.add(historyItemMap); + } + + HashMap result = new HashMap<>(); + + result.put("list", history); + result.put("currentIndex", currentIndex); + + return result; + } + + @Override + protected void onScrollChanged(int x, + int y, + int oldX, + int oldY) { + super.onScrollChanged(x, y, oldX, oldY); + + if (floatingContextMenu != null) { + floatingContextMenu.setAlpha(0f); + floatingContextMenu.setVisibility(View.GONE); + } + + if (channelDelegate != null) channelDelegate.onScrollChanged(x, y); + } + + public void scrollTo(Integer x, Integer y, Boolean animated) { + if (animated) { + PropertyValuesHolder pvhX = PropertyValuesHolder.ofInt("scrollX", x); + PropertyValuesHolder pvhY = PropertyValuesHolder.ofInt("scrollY", y); + ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(this, pvhX, pvhY); + anim.setDuration(300).start(); + } else { + scrollTo(x, y); + } + } + + public void scrollBy(Integer x, Integer y, Boolean animated) { + if (animated) { + PropertyValuesHolder pvhX = PropertyValuesHolder.ofInt("scrollX", getScrollX() + x); + PropertyValuesHolder pvhY = PropertyValuesHolder.ofInt("scrollY", getScrollY() + y); + ObjectAnimator anim = ObjectAnimator.ofPropertyValuesHolder(this, pvhX, pvhY); + anim.setDuration(300).start(); + } else { + scrollBy(x, y); + } + } + + class DownloadStartListener implements DownloadListener { + @Override + public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimeType, long contentLength) { + DownloadStartRequest downloadStartRequest = new DownloadStartRequest( + url, + userAgent, + contentDisposition, + mimeType, + contentLength, + URLUtil.guessFileName(url, contentDisposition, mimeType), + null + ); + if (channelDelegate != null) channelDelegate.onDownloadStartRequest(downloadStartRequest); + } + } + + public void setDesktopMode(final boolean enabled) { + final WebSettings webSettings = getSettings(); + + final String newUserAgent; + if (enabled) { + newUserAgent = webSettings.getUserAgentString().replace("Mobile", "eliboM").replace("Android", "diordnA"); + } else { + newUserAgent = webSettings.getUserAgentString().replace("eliboM", "Mobile").replace("diordnA", "Android"); + } + + webSettings.setUserAgentString(newUserAgent); + webSettings.setUseWideViewPort(enabled); + webSettings.setLoadWithOverviewMode(enabled); + webSettings.setSupportZoom(enabled); + webSettings.setBuiltInZoomControls(enabled); + } + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + @Nullable + public String printCurrentPage(@Nullable PrintJobSettings settings) { + if (plugin != null && plugin.activity != null) { + // Get a PrintManager instance + PrintManager printManager = (PrintManager) plugin.activity.getSystemService(Context.PRINT_SERVICE); + + if (printManager != null) { + PrintAttributes.Builder builder = new PrintAttributes.Builder(); + + String jobName = (getTitle() != null ? getTitle() : getUrl()) + " Document"; + + if (settings != null) { + if (settings.jobName != null && !settings.jobName.isEmpty()) { + jobName = settings.jobName; + } + if (settings.orientation != null) { + int orientation = settings.orientation; + switch (orientation) { + case 0: + // PORTRAIT + builder.setMediaSize(PrintAttributes.MediaSize.UNKNOWN_PORTRAIT); + break; + case 1: + // LANDSCAPE + builder.setMediaSize(PrintAttributes.MediaSize.UNKNOWN_LANDSCAPE); + break; + } + } +// if (settings.margins != null) { +// // for some reason, Android doesn't set the margins +// builder.setMinMargins(settings.margins.toMargins()); +// } + if (settings.mediaSize != null) { + builder.setMediaSize(settings.mediaSize.toMediaSize()); + } + if (settings.colorMode != null) { + builder.setColorMode(settings.colorMode); + } + if (settings.duplexMode != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + builder.setDuplexMode(settings.duplexMode); + } + if (settings.resolution != null) { + builder.setResolution(settings.resolution.toResolution()); + } + } + + // Get a printCurrentPage adapter instance + PrintDocumentAdapter printAdapter; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + printAdapter = createPrintDocumentAdapter(jobName); + } else { + printAdapter = createPrintDocumentAdapter(); + } + + // Create a printCurrentPage job with name and adapter instance + android.print.PrintJob job = printManager.print(jobName, printAdapter, builder.build()); + + if (settings != null && settings.handledByClient && plugin.printJobManager != null) { + String id = UUID.randomUUID().toString(); + PrintJobController printJobController = new PrintJobController(id, job, settings, plugin); + plugin.printJobManager.jobs.put(printJobController.id, printJobController); + return id; + } + } else { + Log.e(LOG_TAG, "No PrintManager available"); + } + } + return null; + } + + @Override + public void onCreateContextMenu(ContextMenu menu) { + super.onCreateContextMenu(menu); + sendOnCreateContextMenuEvent(); + } + + private void sendOnCreateContextMenuEvent() { + com.pichillilorenzo.flutter_inappwebview_android.types.HitTestResult hitTestResult = + com.pichillilorenzo.flutter_inappwebview_android.types.HitTestResult.fromWebViewHitTestResult(getHitTestResult()); + if (channelDelegate != null) channelDelegate.onCreateContextMenu(hitTestResult); + } + + private Point contextMenuPoint = new Point(0, 0); + private Point lastTouch = new Point(0, 0); + + @Override + public boolean onTouchEvent(MotionEvent ev) { + lastTouch = new Point((int) ev.getX(), (int) ev.getY()); + + ViewParent parent = getParent(); + if (parent instanceof PullToRefreshLayout) { + PullToRefreshLayout pullToRefreshLayout = (PullToRefreshLayout) parent; + if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { + pullToRefreshLayout.setEnabled(false); + } + } + + return super.onTouchEvent(ev); + } + + @Override + protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { + super.onOverScrolled(scrollX, scrollY, clampedX, clampedY); + + boolean overScrolledHorizontally = canScrollHorizontally() && clampedX; + boolean overScrolledVertically = canScrollVertically() && clampedY; + + ViewParent parent = getParent(); + if (parent instanceof PullToRefreshLayout && overScrolledVertically && scrollY <= 10) { + PullToRefreshLayout pullToRefreshLayout = (PullToRefreshLayout) parent; + // change over scroll mode to OVER_SCROLL_NEVER in order to disable temporarily the glow effect + setOverScrollMode(OVER_SCROLL_NEVER); + pullToRefreshLayout.setEnabled(pullToRefreshLayout.settings.enabled); + // reset over scroll mode + setOverScrollMode(customSettings.overScrollMode); + } + + if (overScrolledHorizontally || overScrolledVertically) { + if (channelDelegate != null) channelDelegate.onOverScrolled(scrollX, scrollY, overScrolledHorizontally, overScrolledVertically); + } + } + + @Override + public boolean dispatchTouchEvent(MotionEvent event) { + return super.dispatchTouchEvent(event); + } + + @Override + public InputConnection onCreateInputConnection(EditorInfo outAttrs) { + InputConnection connection = super.onCreateInputConnection(outAttrs); + if (connection == null && !customSettings.useHybridComposition && containerView != null) { + // workaround to hide the Keyboard when the user click outside + // on something not focusable such as input or a textarea. + containerView + .getHandler() + .postDelayed( + new Runnable() { + @Override + public void run() { + InputMethodManager imm = + (InputMethodManager) getContext().getSystemService(INPUT_METHOD_SERVICE); + + boolean isAcceptingText = false; + if (imm != null) { + try { + // imm.isAcceptingText() seems to sometimes crash on some devices + isAcceptingText = imm.isAcceptingText(); + } catch (Exception ignored) { + } + } + + if (containerView != null && imm != null && !isAcceptingText) { + imm.hideSoftInputFromWindow( + containerView.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); + } + } + }, + 128); + } + return connection; + } + + @Override + public ActionMode startActionMode(ActionMode.Callback callback) { + if (customSettings.useHybridComposition && !customSettings.disableContextMenu && (contextMenu == null || contextMenu.keySet().size() == 0)) { + return super.startActionMode(callback); + } + return rebuildActionMode(super.startActionMode(callback), callback); + } + + @RequiresApi(api = Build.VERSION_CODES.M) + @Override + public ActionMode startActionMode(ActionMode.Callback callback, int type) { + if (customSettings.useHybridComposition && !customSettings.disableContextMenu && (contextMenu == null || contextMenu.keySet().size() == 0)) { + return super.startActionMode(callback, type); + } + return rebuildActionMode(super.startActionMode(callback, type), callback); + } + + public ActionMode rebuildActionMode( + final ActionMode actionMode, + final ActionMode.Callback callback + ) { + // fix Android 10 clipboard not working properly https://github.com/pichillilorenzo/flutter_inappwebview/issues/678 + if (!customSettings.useHybridComposition && containerView != null) { + onWindowFocusChanged(containerView.isFocused()); + } + + boolean hasBeenRemovedAndRebuilt = false; + if (floatingContextMenu != null) { + hideContextMenu(); + hasBeenRemovedAndRebuilt = true; + } + if (actionMode == null) { + return null; + } + + Menu actionMenu = actionMode.getMenu(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + actionMode.hide(3000); + } + List defaultMenuItems = new ArrayList<>(); + for (int i = 0; i < actionMenu.size(); i++) { + defaultMenuItems.add(actionMenu.getItem(i)); + } + actionMenu.clear(); + actionMode.finish(); + if (customSettings.disableContextMenu) { + return actionMode; + } + + floatingContextMenu = (LinearLayout) LayoutInflater.from(this.getContext()) + .inflate(R.layout.floating_action_mode, this, false); + HorizontalScrollView horizontalScrollView = (HorizontalScrollView) floatingContextMenu.getChildAt(0); + LinearLayout menuItemListLayout = (LinearLayout) horizontalScrollView.getChildAt(0); + + List> customMenuItems = new ArrayList<>(); + ContextMenuSettings contextMenuSettings = new ContextMenuSettings(); + if (contextMenu != null) { + customMenuItems = (List>) contextMenu.get("menuItems"); + Map contextMenuSettingsMap = (Map) contextMenu.get("settings"); + if (contextMenuSettingsMap != null) { + contextMenuSettings.parse(contextMenuSettingsMap); + } + } + customMenuItems = customMenuItems == null ? new ArrayList>() : customMenuItems; + + if (contextMenuSettings.hideDefaultSystemContextMenuItems == null || !contextMenuSettings.hideDefaultSystemContextMenuItems) { + for (final MenuItem menuItem : defaultMenuItems) { + final int itemId = menuItem.getItemId(); + final String itemTitle = menuItem.getTitle().toString(); + + TextView text = (TextView) LayoutInflater.from(this.getContext()) + .inflate(R.layout.floating_action_mode_item, this, false); + text.setText(itemTitle); + text.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + hideContextMenu(); + callback.onActionItemClicked(actionMode, menuItem); + + if (channelDelegate != null) channelDelegate.onContextMenuActionItemClicked(itemId, itemTitle); + } + }); + if (floatingContextMenu != null) { + menuItemListLayout.addView(text); + } + } + } + + for (final Map menuItem : customMenuItems) { + final int itemId = (int) menuItem.get("id"); + final String itemTitle = (String) menuItem.get("title"); + TextView text = (TextView) LayoutInflater.from(this.getContext()) + .inflate(R.layout.floating_action_mode_item, this, false); + text.setText(itemTitle); + text.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + hideContextMenu(); + + if (channelDelegate != null) channelDelegate.onContextMenuActionItemClicked(itemId, itemTitle); + } + }); + if (floatingContextMenu != null) { + menuItemListLayout.addView(text); + } + } + + final int x = (lastTouch != null) ? lastTouch.x : 0; + final int y = (lastTouch != null) ? lastTouch.y : 0; + contextMenuPoint = new Point(x, y); + + if (floatingContextMenu != null) { + floatingContextMenu.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + + @Override + public void onGlobalLayout() { + if (floatingContextMenu != null) { + floatingContextMenu.getViewTreeObserver().removeOnGlobalLayoutListener(this); + if (getSettings().getJavaScriptEnabled()) { + onScrollStopped(); + } else { + onFloatingActionGlobalLayout(x, y); + } + } + } + }); + addView(floatingContextMenu, new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, x, y)); + if (hasBeenRemovedAndRebuilt) { + sendOnCreateContextMenuEvent(); + } + if (checkContextMenuShouldBeClosedTask != null) { + checkContextMenuShouldBeClosedTask.run(); + } + } + + return actionMode; + } + + public void onFloatingActionGlobalLayout(int x, int y) { + int maxWidth = getWidth(); + int maxHeight = getHeight(); + int width = floatingContextMenu.getWidth(); + int height = floatingContextMenu.getHeight(); + int curx = x - (width / 2); + if (curx < 0) { + curx = 0; + } else if (curx + width > maxWidth) { + curx = maxWidth - width; + } + // float size = 12 * scale; + float cury = y - (height * 1.5f); + if (cury < 0) { + cury = y + height; + } + + updateViewLayout( + floatingContextMenu, + new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, curx + getScrollX(), ((int) cury) + getScrollY()) + ); + + mainLooperHandler.post(new Runnable() { + @Override + public void run() { + if (floatingContextMenu != null) { + floatingContextMenu.setVisibility(View.VISIBLE); + floatingContextMenu.animate().alpha(1f).setDuration(100).setListener(null); + } + } + }); + } + + public void hideContextMenu() { + removeView(floatingContextMenu); + floatingContextMenu = null; + + if (channelDelegate != null) channelDelegate.onHideContextMenu(); + } + + public void onScrollStopped() { + if (floatingContextMenu != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + adjustFloatingContextMenuPosition(); + } + } + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + public void adjustFloatingContextMenuPosition() { + evaluateJavascript("(function(){" + + " var selection = window.getSelection();" + + " var rangeY = null;" + + " if (selection != null && selection.rangeCount > 0) {" + + " var range = selection.getRangeAt(0);" + + " var clientRect = range.getClientRects();" + + " if (clientRect.length > 0) {" + + " rangeY = clientRect[0].y;" + + " } else if (document.activeElement != null && document.activeElement.tagName.toLowerCase() !== 'iframe') {" + + " var boundingClientRect = document.activeElement.getBoundingClientRect();" + + " rangeY = boundingClientRect.y;" + + " }" + + " }" + + " return rangeY;" + + "})();", new ValueCallback() { + @Override + public void onReceiveValue(String value) { + if (floatingContextMenu != null) { + if (value != null && !value.equalsIgnoreCase("null")) { + int x = contextMenuPoint.x; + int y = (int) ((Float.parseFloat(value) * Util.getPixelDensity(getContext())) + (floatingContextMenu.getHeight() / 3.5)); + contextMenuPoint.y = y; + onFloatingActionGlobalLayout(x, y); + } else { + floatingContextMenu.setVisibility(View.VISIBLE); + floatingContextMenu.animate().alpha(1f).setDuration(100).setListener(null); + onFloatingActionGlobalLayout(contextMenuPoint.x, contextMenuPoint.y); + } + } + } + }); + } + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + public void getSelectedText(final ValueCallback resultCallback) { + evaluateJavascript(PluginScriptsUtil.GET_SELECTED_TEXT_JS_SOURCE, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + value = (value != null && !value.equalsIgnoreCase("null")) ? value.substring(1, value.length() - 1) : null; + resultCallback.onReceiveValue(value); + } + }); + } + + public Map requestFocusNodeHref() { + Message msg = InAppWebView.mHandler.obtainMessage(); + requestFocusNodeHref(msg); + Bundle bundle = msg.peekData(); + + Map obj = new HashMap<>(); + obj.put("src", bundle.getString("src")); + obj.put("url", bundle.getString("url")); + obj.put("title", bundle.getString("title")); + + return obj; + } + + public Map requestImageRef() { + Message msg = InAppWebView.mHandler.obtainMessage(); + requestImageRef(msg); + Bundle bundle = msg.peekData(); + + Map obj = new HashMap<>(); + obj.put("url", bundle.getString("url")); + + return obj; + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public void callAsyncJavaScript(String functionBody, Map arguments, @Nullable ContentWorld contentWorld, @Nullable ValueCallback resultCallback) { + String resultUuid = UUID.randomUUID().toString(); + if (resultCallback != null) { + callAsyncJavaScriptCallbacks.put(resultUuid, resultCallback); + } + + JSONObject functionArguments = new JSONObject(arguments); + Iterator keys = functionArguments.keys(); + + List functionArgumentNamesList = new ArrayList<>(); + List functionArgumentValuesList = new ArrayList<>(); + while (keys.hasNext()) { + String key = keys.next(); + functionArgumentNamesList.add(key); + functionArgumentValuesList.add("obj." + key); + } + + String functionArgumentNames = TextUtils.join(", ", functionArgumentNamesList); + String functionArgumentValues = TextUtils.join(", ", functionArgumentValuesList); + String functionArgumentsObj = Util.JSONStringify(arguments); + + String sourceToInject = PluginScriptsUtil.CALL_ASYNC_JAVA_SCRIPT_WRAPPER_JS_SOURCE + .replace(PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_NAMES, functionArgumentNames) + .replace(PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_VALUES, functionArgumentValues) + .replace(PluginScriptsUtil.VAR_FUNCTION_ARGUMENTS_OBJ, functionArgumentsObj) + .replace(PluginScriptsUtil.VAR_FUNCTION_BODY, functionBody) + .replace(PluginScriptsUtil.VAR_RESULT_UUID, resultUuid) + .replace(PluginScriptsUtil.VAR_RESULT_UUID, resultUuid); + + sourceToInject = userContentController.generateCodeForScriptEvaluation(sourceToInject, contentWorld); + evaluateJavascript(sourceToInject, null); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public void isSecureContext(final ValueCallback resultCallback) { + evaluateJavascript("window.isSecureContext", new ValueCallback() { + @Override + public void onReceiveValue(String value) { + if (value == null || value.isEmpty() || value.equalsIgnoreCase("null") + || value.equalsIgnoreCase("false")) { + resultCallback.onReceiveValue(false); + return; + } + resultCallback.onReceiveValue(true); + } + }); + } + + public boolean canScrollVertically() { + return computeVerticalScrollRange() > computeVerticalScrollExtent(); + } + + public boolean canScrollHorizontally() { + return computeHorizontalScrollRange() > computeHorizontalScrollExtent(); + } + + public WebMessageChannel createCompatWebMessageChannel() { + String id = UUID.randomUUID().toString(); + WebMessageChannel webMessageChannel = new WebMessageChannel(id, this); + webMessageChannels.put(id, webMessageChannel); + return webMessageChannel; + } + + @Override + public WebMessageChannel createWebMessageChannel(ValueCallback callback) { + WebMessageChannel webMessageChannel = createCompatWebMessageChannel(); + callback.onReceiveValue(webMessageChannel); + return webMessageChannel; + } + + public void addWebMessageListener(@NonNull WebMessageListener webMessageListener) throws Exception { + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { + WebViewCompat.addWebMessageListener(this, webMessageListener.jsObjectName, webMessageListener.allowedOriginRules, webMessageListener.listener); + webMessageListeners.add(webMessageListener); + } + } + + public void disposeWebMessageChannels() { + for (WebMessageChannel webMessageChannel : webMessageChannels.values()) { + webMessageChannel.dispose(); + } + webMessageChannels.clear(); + } + + public void disposeWebMessageListeners() { + for (WebMessageListener webMessageListener : webMessageListeners) { + webMessageListener.dispose(); + } + webMessageListeners.clear(); + } + + @Override + public Looper getWebViewLooper() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + return super.getWebViewLooper(); + } + return Looper.getMainLooper(); + } + + @Override + public boolean isInFullscreen() { + return inFullscreen; + } + + @Override + public void setInFullscreen(boolean inFullscreen) { + this.inFullscreen = inFullscreen; + } + + @Override + public void postWebMessage(com.pichillilorenzo.flutter_inappwebview_android.types.WebMessage message, Uri targetOrigin, ValueCallback callback) throws Exception { + throw new UnsupportedOperationException(); + } + + @Override + protected void onWindowVisibilityChanged(int visibility) { + if (customSettings.allowBackgroundAudioPlaying) { + if (visibility != View.GONE) { + super.onWindowVisibilityChanged(View.VISIBLE); + } + return; + } + super.onWindowVisibilityChanged(visibility); + } + + public float getZoomScale() { + return zoomScale; + } + + @Override + public void getZoomScale(final ValueCallback callback) { + callback.onReceiveValue(zoomScale); + } + + @Nullable + public Map getContextMenu() { + return contextMenu; + } + + public void setContextMenu(@Nullable Map contextMenu) { + this.contextMenu = contextMenu; + } + + @Nullable + public InAppWebViewFlutterPlugin getPlugin() { + return plugin; + } + + public void setPlugin(@Nullable InAppWebViewFlutterPlugin plugin) { + this.plugin = plugin; + } + + @Nullable + public InAppBrowserDelegate getInAppBrowserDelegate() { + return inAppBrowserDelegate; + } + + public void setInAppBrowserDelegate(@Nullable InAppBrowserDelegate inAppBrowserDelegate) { + this.inAppBrowserDelegate = inAppBrowserDelegate; + } + + public UserContentController getUserContentController() { + return userContentController; + } + + public void setUserContentController(UserContentController userContentController) { + this.userContentController = userContentController; + } + + public Map getWebMessageChannels() { + return webMessageChannels; + } + + public void setWebMessageChannels(Map webMessageChannels) { + this.webMessageChannels = webMessageChannels; + } + + @Override + public void getContentHeight(ValueCallback callback) { + callback.onReceiveValue(getContentHeight()); + } + + public void getContentWidth(final ValueCallback callback) { + evaluateJavascript("document.documentElement.scrollWidth;", new ValueCallback() { + @Override + public void onReceiveValue(@Nullable String value) { + Integer contentWidth = null; + if (value != null && !value.equalsIgnoreCase("null")) { + contentWidth = Integer.parseInt(value); + } + callback.onReceiveValue(contentWidth); + } + }); + } + + @Override + public void getHitTestResult(ValueCallback callback) { + callback.onReceiveValue(com.pichillilorenzo.flutter_inappwebview_android.types.HitTestResult.fromWebViewHitTestResult(getHitTestResult())); + } + + @Nullable + @Override + public WebViewChannelDelegate getChannelDelegate() { + return channelDelegate; + } + + @Override + public void setChannelDelegate(@Nullable WebViewChannelDelegate channelDelegate) { + this.channelDelegate = channelDelegate; + } + + @Override + public void dispose() { + if (channelDelegate != null) { + channelDelegate.dispose(); + channelDelegate = null; + } + super.dispose(); + WebSettings settings = getSettings(); + settings.setJavaScriptEnabled(false); + removeJavascriptInterface(JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && WebViewFeature.isFeatureSupported(WebViewFeature.WEB_VIEW_RENDERER_CLIENT_BASIC_USAGE)) { + WebViewCompat.setWebViewRenderProcessClient(this, null); + } + setWebChromeClient(new WebChromeClient()); + setWebViewClient(new WebViewClient() { + public void onPageFinished(WebView view, String url) { + destroy(); + } + }); + interceptOnlyAsyncAjaxRequestsPluginScript = null; + userContentController.dispose(); + if (findInteractionController != null) { + findInteractionController.dispose(); + findInteractionController = null; + } + if (webViewAssetLoaderExt != null) { + webViewAssetLoaderExt.dispose(); + webViewAssetLoaderExt = null; + } + if (windowId != null && plugin != null && plugin.inAppWebViewManager != null) { + plugin.inAppWebViewManager.windowWebViewMessages.remove(windowId); + } + mainLooperHandler.removeCallbacksAndMessages(null); + mHandler.removeCallbacksAndMessages(null); + disposeWebMessageChannels(); + disposeWebMessageListeners(); + removeAllViews(); + if (checkContextMenuShouldBeClosedTask != null) + removeCallbacks(checkContextMenuShouldBeClosedTask); + if (checkScrollStoppedTask != null) + removeCallbacks(checkScrollStoppedTask); + callAsyncJavaScriptCallbacks.clear(); + evaluateJavaScriptContentWorldCallbacks.clear(); + inAppBrowserDelegate = null; + if (inAppWebViewRenderProcessClient != null) { + inAppWebViewRenderProcessClient.dispose(); + inAppWebViewRenderProcessClient = null; + } + if (inAppWebViewChromeClient != null) { + inAppWebViewChromeClient.dispose(); + inAppWebViewChromeClient = null; + } + if (inAppWebViewClientCompat != null) { + inAppWebViewClientCompat.dispose(); + inAppWebViewClientCompat = null; + } + if (inAppWebViewClient != null) { + inAppWebViewClient.dispose(); + inAppWebViewClient = null; + } + if (javaScriptBridgeInterface != null) { + javaScriptBridgeInterface.dispose(); + javaScriptBridgeInterface = null; + } + plugin = null; + loadUrl("about:blank"); + } + + @Override + public void destroy() { + super.destroy(); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewChromeClient.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewChromeClient.java new file mode 100755 index 00000000..2f328f34 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewChromeClient.java @@ -0,0 +1,1330 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview; + +import android.Manifest; +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.res.AssetFileDescriptor; +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.os.Environment; +import android.os.Message; +import android.os.Parcelable; +import android.provider.MediaStore; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.webkit.ConsoleMessage; +import android.webkit.GeolocationPermissions; +import android.webkit.JsPromptResult; +import android.webkit.JsResult; +import android.webkit.MimeTypeMap; +import android.webkit.PermissionRequest; +import android.webkit.ValueCallback; +import android.webkit.WebChromeClient; +import android.webkit.WebView; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; + +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFileProvider; +import com.pichillilorenzo.flutter_inappwebview_android.types.CreateWindowAction; +import com.pichillilorenzo.flutter_inappwebview_android.in_app_browser.ActivityResultListener; +import com.pichillilorenzo.flutter_inappwebview_android.in_app_browser.InAppBrowserDelegate; +import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin; +import com.pichillilorenzo.flutter_inappwebview_android.types.GeolocationPermissionShowPromptResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.JsAlertResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.JsBeforeUnloadResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.JsConfirmResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.JsPromptResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.PermissionResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.URLRequest; +import com.pichillilorenzo.flutter_inappwebview_android.webview.WebViewChannelDelegate; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import io.flutter.plugin.common.PluginRegistry; + +import static android.app.Activity.RESULT_OK; + +public class InAppWebViewChromeClient extends WebChromeClient implements PluginRegistry.ActivityResultListener, ActivityResultListener { + + protected static final String LOG_TAG = "IABWebChromeClient"; + private InAppBrowserDelegate inAppBrowserDelegate; + + private static final int PICKER = 1; + private static final int PICKER_LEGACY = 3; + final String DEFAULT_MIME_TYPES = "*/*"; + final Map dialogs = new HashMap(); + + protected static final FrameLayout.LayoutParams FULLSCREEN_LAYOUT_PARAMS = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER); + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + protected static final int FULLSCREEN_SYSTEM_UI_VISIBILITY_KITKAT = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_FULLSCREEN | + View.SYSTEM_UI_FLAG_IMMERSIVE | + View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + + protected static final int FULLSCREEN_SYSTEM_UI_VISIBILITY = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_LAYOUT_STABLE | + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | + View.SYSTEM_UI_FLAG_FULLSCREEN; + + @Nullable + private View mCustomView; + @Nullable + private WebChromeClient.CustomViewCallback mCustomViewCallback; + private int mOriginalOrientation; + private int mOriginalSystemUiVisibility; + @Nullable + public InAppWebViewFlutterPlugin plugin; + @Nullable + public InAppWebView inAppWebView; + + @Nullable + private ValueCallback filePathCallbackLegacy; + @Nullable + private ValueCallback filePathCallback; + @Nullable + private Uri videoOutputFileUri; + @Nullable + private Uri imageOutputFileUri; + + public InAppWebViewChromeClient(@NonNull final InAppWebViewFlutterPlugin plugin, + @NonNull InAppWebView inAppWebView, InAppBrowserDelegate inAppBrowserDelegate) { + super(); + this.plugin = plugin; + this.inAppWebView = inAppWebView; + this.inAppBrowserDelegate = inAppBrowserDelegate; + if (this.inAppBrowserDelegate != null) { + this.inAppBrowserDelegate.getActivityResultListeners().add(this); + } + + if (plugin.activityPluginBinding != null) + plugin.activityPluginBinding.addActivityResultListener(this); + } + + @Nullable + @Override + public Bitmap getDefaultVideoPoster() { + if (inAppWebView != null && inAppWebView.customSettings.defaultVideoPoster != null) { + final byte[] data = inAppWebView.customSettings.defaultVideoPoster; + BitmapFactory.Options bitmapOptions = new BitmapFactory.Options(); + bitmapOptions.inMutable = true; + return BitmapFactory.decodeByteArray( + data, 0, data.length, bitmapOptions + ); + } + return Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888); + } + + @Override + public void onHideCustomView() { + Activity activity = getActivity(); + if (activity == null) { + return; + } + + View decorView = getRootView(); + if (decorView == null) { + return; + } + if (this.mCustomView != null) { + ((FrameLayout) decorView).removeView(this.mCustomView); + } + this.mCustomView = null; + decorView.setSystemUiVisibility(this.mOriginalSystemUiVisibility); + activity.setRequestedOrientation(this.mOriginalOrientation); + if (this.mCustomViewCallback != null) { + this.mCustomViewCallback.onCustomViewHidden(); + } + this.mCustomViewCallback = null; + activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + + if (inAppWebView != null) { + WebViewChannelDelegate eventWebViewChannelDelegate = inAppWebView.channelDelegate; + if (eventWebViewChannelDelegate != null) + eventWebViewChannelDelegate.onExitFullscreen(); + inAppWebView.setInFullscreen(false); + } + } + + @Override + public void onShowCustomView(final View paramView, final CustomViewCallback paramCustomViewCallback) { + if (this.mCustomView != null) { + onHideCustomView(); + return; + } + + Activity activity = getActivity(); + if (activity == null) { + return; + } + + View decorView = getRootView(); + if (decorView == null) { + return; + } + this.mCustomView = paramView; + this.mOriginalSystemUiVisibility = decorView.getSystemUiVisibility(); + this.mOriginalOrientation = activity.getRequestedOrientation(); + this.mCustomViewCallback = paramCustomViewCallback; + if (this.mCustomView != null) { + this.mCustomView.setBackgroundColor(Color.BLACK); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + decorView.setSystemUiVisibility(FULLSCREEN_SYSTEM_UI_VISIBILITY_KITKAT); + } else { + decorView.setSystemUiVisibility(FULLSCREEN_SYSTEM_UI_VISIBILITY); + } + activity.getWindow().setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS); + ((FrameLayout) decorView).addView(this.mCustomView, FULLSCREEN_LAYOUT_PARAMS); + + if (inAppWebView != null) { + WebViewChannelDelegate eventWebViewChannelDelegate = inAppWebView.channelDelegate; + if (eventWebViewChannelDelegate != null) + eventWebViewChannelDelegate.onEnterFullscreen(); + inAppWebView.setInFullscreen(true); + } + } + + @Override + public boolean onJsAlert(final WebView view, String url, final String message, + final JsResult result) { + if (inAppWebView != null && inAppWebView.channelDelegate != null) { + inAppWebView.channelDelegate.onJsAlert(url, message, null, new WebViewChannelDelegate.JsAlertCallback() { + @Override + public boolean nonNullSuccess(@NonNull JsAlertResponse response) { + if (response.isHandledByClient()) { + Integer action = response.getAction(); + action = action != null ? action : 1; + switch (action) { + case 0: + result.confirm(); + break; + case 1: + default: + result.cancel(); + } + return false; + } + return true; + } + + @Override + public void defaultBehaviour(@Nullable JsAlertResponse response) { + String responseMessage = null; + String confirmButtonTitle = null; + if (response != null) { + responseMessage = response.getMessage(); + confirmButtonTitle = response.getConfirmButtonTitle(); + } + createAlertDialog(message, result, responseMessage, confirmButtonTitle); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + result.cancel(); + } + }); + + return true; + } + + return false; + } + + public void createAlertDialog(String message, final JsResult result, String responseMessage, String confirmButtonTitle) { + String alertMessage = (responseMessage != null && !responseMessage.isEmpty()) ? responseMessage : message; + + DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + result.confirm(); + dialog.dismiss(); + dialogs.remove(dialog); + } + }; + + Activity activity = getActivity(); + if (activity == null) { + return; + } + + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(activity, androidx.appcompat.R.style.Theme_AppCompat_Dialog_Alert); + alertDialogBuilder.setMessage(alertMessage); + if (confirmButtonTitle != null && !confirmButtonTitle.isEmpty()) { + alertDialogBuilder.setPositiveButton(confirmButtonTitle, clickListener); + } else { + alertDialogBuilder.setPositiveButton(android.R.string.ok, clickListener); + } + + alertDialogBuilder.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + result.cancel(); + dialog.dismiss(); + dialogs.remove(dialog); + } + }); + + AlertDialog alertDialog = alertDialogBuilder.create(); + dialogs.put(alertDialog, result); + alertDialog.show(); + } + + @Override + public boolean onJsConfirm(final WebView view, String url, final String message, + final JsResult result) { + if (inAppWebView != null && inAppWebView.channelDelegate != null) { + inAppWebView.channelDelegate.onJsConfirm(url, message, null, new WebViewChannelDelegate.JsConfirmCallback() { + @Override + public boolean nonNullSuccess(@NonNull JsConfirmResponse response) { + if (response.isHandledByClient()) { + Integer action = response.getAction(); + action = action != null ? action : 1; + switch (action) { + case 0: + result.confirm(); + break; + case 1: + default: + result.cancel(); + } + return false; + } + return true; + } + + @Override + public void defaultBehaviour(@Nullable JsConfirmResponse response) { + String responseMessage = null; + String confirmButtonTitle = null; + String cancelButtonTitle = null; + if (response != null) { + responseMessage = response.getMessage(); + confirmButtonTitle = response.getConfirmButtonTitle(); + cancelButtonTitle = response.getCancelButtonTitle(); + } + createConfirmDialog(message, result, responseMessage, confirmButtonTitle, cancelButtonTitle); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + result.cancel(); + } + }); + + return true; + } + + return false; + } + + public void createConfirmDialog(String message, final JsResult result, String responseMessage, String confirmButtonTitle, String cancelButtonTitle) { + String alertMessage = (responseMessage != null && !responseMessage.isEmpty()) ? responseMessage : message; + DialogInterface.OnClickListener confirmClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + result.confirm(); + dialog.dismiss(); + dialogs.remove(dialog); + } + }; + DialogInterface.OnClickListener cancelClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + result.cancel(); + dialog.dismiss(); + dialogs.remove(dialog); + } + }; + + Activity activity = getActivity(); + if (activity == null) { + return; + } + + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(activity, androidx.appcompat.R.style.Theme_AppCompat_Dialog_Alert); + alertDialogBuilder.setMessage(alertMessage); + if (confirmButtonTitle != null && !confirmButtonTitle.isEmpty()) { + alertDialogBuilder.setPositiveButton(confirmButtonTitle, confirmClickListener); + } else { + alertDialogBuilder.setPositiveButton(android.R.string.ok, confirmClickListener); + } + if (cancelButtonTitle != null && !cancelButtonTitle.isEmpty()) { + alertDialogBuilder.setNegativeButton(cancelButtonTitle, cancelClickListener); + } else { + alertDialogBuilder.setNegativeButton(android.R.string.cancel, cancelClickListener); + } + + alertDialogBuilder.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + result.cancel(); + dialog.dismiss(); + dialogs.remove(dialog); + } + }); + + AlertDialog alertDialog = alertDialogBuilder.create(); + dialogs.put(alertDialog, result); + alertDialog.show(); + } + + @Override + public boolean onJsPrompt(final WebView view, String url, final String message, + final String defaultValue, final JsPromptResult result) { + if (inAppWebView != null && inAppWebView.channelDelegate != null) { + inAppWebView.channelDelegate.onJsPrompt(url, message, defaultValue, null, new WebViewChannelDelegate.JsPromptCallback() { + @Override + public boolean nonNullSuccess(@NonNull JsPromptResponse response) { + if (response.isHandledByClient()) { + Integer action = response.getAction(); + action = action != null ? action : 1; + switch (action) { + case 0: + result.confirm(response.getValue()); + break; + case 1: + default: + result.cancel(); + } + return false; + } + return true; + } + + @Override + public void defaultBehaviour(@Nullable JsPromptResponse response) { + String responseMessage = null; + String responseDefaultValue = null; + String value = null; + String confirmButtonTitle = null; + String cancelButtonTitle = null; + if (response != null) { + responseMessage = response.getMessage(); + responseDefaultValue = response.getDefaultValue(); + value = response.getValue(); + confirmButtonTitle = response.getConfirmButtonTitle(); + cancelButtonTitle = response.getCancelButtonTitle(); + } + createPromptDialog(view, message, defaultValue, result, responseMessage, responseDefaultValue, value, cancelButtonTitle, confirmButtonTitle); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + result.cancel(); + } + }); + + return true; + } + + return false; + } + + public void createPromptDialog(WebView view, String message, String defaultValue, final JsPromptResult result, String responseMessage, String responseDefaultValue, String value, String cancelButtonTitle, String confirmButtonTitle) { + FrameLayout layout = new FrameLayout(view.getContext()); + + final EditText input = new EditText(view.getContext()); + input.setMaxLines(1); + input.setText((responseDefaultValue != null && !responseDefaultValue.isEmpty()) ? responseDefaultValue : defaultValue); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT); + input.setLayoutParams(lp); + + layout.setPaddingRelative(45, 15, 45, 0); + layout.addView(input); + + String alertMessage = (responseMessage != null && !responseMessage.isEmpty()) ? responseMessage : message; + + final String finalValue = value; + DialogInterface.OnClickListener confirmClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String text = input.getText().toString(); + result.confirm(finalValue != null ? finalValue : text); + dialog.dismiss(); + dialogs.remove(dialog); + } + }; + DialogInterface.OnClickListener cancelClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + result.cancel(); + dialog.dismiss(); + dialogs.remove(dialog); + } + }; + + Activity activity = getActivity(); + if (activity == null) { + return; + } + + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(activity, androidx.appcompat.R.style.Theme_AppCompat_Dialog_Alert); + alertDialogBuilder.setMessage(alertMessage); + if (confirmButtonTitle != null && !confirmButtonTitle.isEmpty()) { + alertDialogBuilder.setPositiveButton(confirmButtonTitle, confirmClickListener); + } else { + alertDialogBuilder.setPositiveButton(android.R.string.ok, confirmClickListener); + } + if (cancelButtonTitle != null && !cancelButtonTitle.isEmpty()) { + alertDialogBuilder.setNegativeButton(cancelButtonTitle, cancelClickListener); + } else { + alertDialogBuilder.setNegativeButton(android.R.string.cancel, cancelClickListener); + } + + alertDialogBuilder.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + result.cancel(); + dialog.dismiss(); + dialogs.remove(dialog); + } + }); + + AlertDialog alertDialog = alertDialogBuilder.create(); + alertDialog.setView(layout); + dialogs.put(alertDialog, result); + alertDialog.show(); + } + + @Override + public boolean onJsBeforeUnload(final WebView view, String url, final String message, + final JsResult result) { + if (inAppWebView != null && inAppWebView.channelDelegate != null) { + inAppWebView.channelDelegate.onJsBeforeUnload(url, message, new WebViewChannelDelegate.JsBeforeUnloadCallback() { + @Override + public boolean nonNullSuccess(@NonNull JsBeforeUnloadResponse response) { + if (response.isHandledByClient()) { + Integer action = response.getAction(); + action = action != null ? action : 1; + switch (action) { + case 0: + result.confirm(); + break; + case 1: + default: + result.cancel(); + } + return false; + } + return true; + } + + @Override + public void defaultBehaviour(@Nullable JsBeforeUnloadResponse response) { + String responseMessage = null; + String confirmButtonTitle = null; + String cancelButtonTitle = null; + if (response != null) { + responseMessage = response.getMessage(); + confirmButtonTitle = response.getConfirmButtonTitle(); + cancelButtonTitle = response.getCancelButtonTitle(); + } + createBeforeUnloadDialog(message, result, responseMessage, confirmButtonTitle, cancelButtonTitle); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + result.cancel(); + } + }); + + return true; + } + + return false; + } + + public void createBeforeUnloadDialog(String message, final JsResult result, String responseMessage, String confirmButtonTitle, String cancelButtonTitle) { + String alertMessage = (responseMessage != null && !responseMessage.isEmpty()) ? responseMessage : message; + DialogInterface.OnClickListener confirmClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + result.confirm(); + dialog.dismiss(); + dialogs.remove(dialog); + } + }; + DialogInterface.OnClickListener cancelClickListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + result.cancel(); + dialog.dismiss(); + dialogs.remove(dialog); + } + }; + + Activity activity = getActivity(); + if (activity == null) { + return; + } + + AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(activity, androidx.appcompat.R.style.Theme_AppCompat_Dialog_Alert); + alertDialogBuilder.setMessage(alertMessage); + if (confirmButtonTitle != null && !confirmButtonTitle.isEmpty()) { + alertDialogBuilder.setPositiveButton(confirmButtonTitle, confirmClickListener); + } else { + alertDialogBuilder.setPositiveButton(android.R.string.ok, confirmClickListener); + } + if (cancelButtonTitle != null && !cancelButtonTitle.isEmpty()) { + alertDialogBuilder.setNegativeButton(cancelButtonTitle, cancelClickListener); + } else { + alertDialogBuilder.setNegativeButton(android.R.string.cancel, cancelClickListener); + } + + alertDialogBuilder.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + result.cancel(); + dialog.dismiss(); + dialogs.remove(dialog); + } + }); + + AlertDialog alertDialog = alertDialogBuilder.create(); + dialogs.put(alertDialog, result); + alertDialog.show(); + } + + @Override + public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, final Message resultMsg) { + int windowId = 0; + if (plugin != null && plugin.inAppWebViewManager != null) { + plugin.inAppWebViewManager.windowAutoincrementId++; + windowId = plugin.inAppWebViewManager.windowAutoincrementId; + } + + WebView.HitTestResult result = view.getHitTestResult(); + String url = result.getExtra(); + + // Ensure that images with hyperlink return the correct URL, not the image source + if(result.getType() == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE) { + Message href = view.getHandler().obtainMessage(); + view.requestFocusNodeHref(href); + Bundle data = href.getData(); + if (data != null) { + String imageUrl = data.getString("url"); + if(imageUrl != null && !imageUrl.isEmpty()) { + url = imageUrl; + } + } + } + + URLRequest request = new URLRequest(url, "GET", null, null); + CreateWindowAction createWindowAction = new CreateWindowAction( + request, + true, + isUserGesture, + false, + windowId, + isDialog + ); + + if (plugin != null && plugin.inAppWebViewManager != null) { + plugin.inAppWebViewManager.windowWebViewMessages.put(windowId, resultMsg); + } + + if (inAppWebView != null && inAppWebView.channelDelegate != null) { + final int finalWindowId = windowId; + inAppWebView.channelDelegate.onCreateWindow(createWindowAction, new WebViewChannelDelegate.CreateWindowCallback() { + @Override + public boolean nonNullSuccess(@NonNull Boolean handledByClient) { + return !handledByClient; + } + + @Override + public void defaultBehaviour(@Nullable Boolean handledByClient) { + if (plugin != null && plugin.inAppWebViewManager != null) { + plugin.inAppWebViewManager.windowWebViewMessages.remove(finalWindowId); + } + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }); + + return true; + } + + return false; + } + + @Override + public void onCloseWindow(WebView window) { + if (inAppWebView != null && inAppWebView.channelDelegate != null) { + inAppWebView.channelDelegate.onCloseWindow(); + } + + super.onCloseWindow(window); + } + + @Override + public void onGeolocationPermissionsShowPrompt(final String origin, final GeolocationPermissions.Callback callback) { + final WebViewChannelDelegate.GeolocationPermissionsShowPromptCallback resultCallback = new WebViewChannelDelegate.GeolocationPermissionsShowPromptCallback() { + @Override + public boolean nonNullSuccess(@NonNull GeolocationPermissionShowPromptResponse response) { + callback.invoke(response.getOrigin(), response.isAllow(), response.isRetain()); + return false; + } + + @Override + public void defaultBehaviour(@Nullable GeolocationPermissionShowPromptResponse response) { + callback.invoke(origin, false, false); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (inAppWebView != null && inAppWebView.channelDelegate != null) { + inAppWebView.channelDelegate.onGeolocationPermissionsShowPrompt(origin, resultCallback); + } else { + resultCallback.defaultBehaviour(null); + } + } + + @Override + public void onGeolocationPermissionsHidePrompt() { + if (inAppWebView != null && inAppWebView.channelDelegate != null) { + inAppWebView.channelDelegate.onGeolocationPermissionsHidePrompt(); + } + } + + @Override + public boolean onConsoleMessage(ConsoleMessage consoleMessage) { + if (inAppWebView != null && inAppWebView.channelDelegate != null) { + inAppWebView.channelDelegate.onConsoleMessage( + consoleMessage.message(), + consoleMessage.messageLevel().ordinal()); + } + return super.onConsoleMessage(consoleMessage); + } + + @Override + public void onProgressChanged(WebView view, int progress) { + super.onProgressChanged(view, progress); + + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didChangeProgress(progress); + } + + + InAppWebView webView = (InAppWebView) view; + + if (webView.inAppWebViewClientCompat != null) { + webView.inAppWebViewClientCompat.loadCustomJavaScriptOnPageStarted(view); + } else if (webView.inAppWebViewClient != null) { + webView.inAppWebViewClient.loadCustomJavaScriptOnPageStarted(view); + } + + if (webView.channelDelegate != null) { + webView.channelDelegate.onProgressChanged(progress); + } + } + + @Override + public void onReceivedTitle(WebView view, String title) { + super.onReceivedTitle(view, title); + + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didChangeTitle(title); + } + + InAppWebView webView = (InAppWebView) view; + + if (webView.channelDelegate != null) { + webView.channelDelegate.onTitleChanged(title); + } + } + + @Override + public void onReceivedIcon(WebView view, Bitmap icon) { + super.onReceivedIcon(view, icon); + + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + icon.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream); + try { + byteArrayOutputStream.close(); + } catch (IOException e) { + Log.e(LOG_TAG, "", e); + } + icon.recycle(); + + InAppWebView webView = (InAppWebView) view; + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedIcon(byteArrayOutputStream.toByteArray()); + } + } + + @Override + public void onReceivedTouchIconUrl(WebView view, + String url, + boolean precomposed) { + super.onReceivedTouchIconUrl(view, url, precomposed); + + InAppWebView webView = (InAppWebView) view; + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedTouchIconUrl(url, precomposed); + } + } + + @Nullable + protected ViewGroup getRootView() { + Activity activity = getActivity(); + if (activity == null) { + return null; + } + return (ViewGroup) activity.findViewById(android.R.id.content); + } + + protected void openFileChooser(ValueCallback filePathCallback, String acceptType) { + startPickerIntent(filePathCallback, acceptType, null); + } + + protected void openFileChooser(ValueCallback filePathCallback) { + startPickerIntent(filePathCallback, "", null); + } + + protected void openFileChooser(ValueCallback filePathCallback, String acceptType, String capture) { + startPickerIntent(filePathCallback, acceptType, capture); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback, FileChooserParams fileChooserParams) { + String[] acceptTypes = fileChooserParams.getAcceptTypes(); + boolean allowMultiple = fileChooserParams.getMode() == WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE; + boolean captureEnabled = fileChooserParams.isCaptureEnabled(); + return startPickerIntent(filePathCallback, acceptTypes, allowMultiple, captureEnabled); + } + + @Override + public boolean onActivityResult(int requestCode, int resultCode, Intent data) { + if (filePathCallback == null && filePathCallbackLegacy == null) { + return true; + } + + // based off of which button was pressed, we get an activity result and a file + // the camera activity doesn't properly return the filename* (I think?) so we use + // this filename instead + switch (requestCode) { + case PICKER: + Uri[] results = null; + if (resultCode == RESULT_OK) { + results = getSelectedFiles(data, resultCode); + } + + if (filePathCallback != null) { + filePathCallback.onReceiveValue(results); + } + break; + + case PICKER_LEGACY: + Uri result = null; + if (resultCode == RESULT_OK) { + result = data != null ? data.getData() : getCapturedMediaFile(); + } + if (filePathCallbackLegacy != null) { + filePathCallbackLegacy.onReceiveValue(result); + } + break; + } + + filePathCallback = null; + filePathCallbackLegacy = null; + imageOutputFileUri = null; + videoOutputFileUri = null; + + return true; + } + + private Uri[] getSelectedFiles(Intent data, int resultCode) { + // we have one file selected + if (data != null && data.getData() != null) { + if (resultCode == RESULT_OK && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return WebChromeClient.FileChooserParams.parseResult(resultCode, data); + } else { + return null; + } + } + + // we have multiple files selected + if (data != null && data.getClipData() != null) { + final int numSelectedFiles = data.getClipData().getItemCount(); + Uri[] result = new Uri[numSelectedFiles]; + for (int i = 0; i < numSelectedFiles; i++) { + result[i] = data.getClipData().getItemAt(i).getUri(); + } + return result; + } + + // we have a captured image or video file + Uri mediaUri = getCapturedMediaFile(); + if (mediaUri != null) { + return new Uri[]{mediaUri}; + } + + return null; + } + + private boolean isFileNotEmpty(Uri uri) { + Activity activity = getActivity(); + if (activity == null) { + return false; + } + + long length; + try { + AssetFileDescriptor descriptor = activity.getContentResolver().openAssetFileDescriptor(uri, "r"); + length = descriptor.getLength(); + descriptor.close(); + } catch (IOException e) { + return false; + } + + return length > 0; + } + + private Uri getCapturedMediaFile() { + if (imageOutputFileUri != null && isFileNotEmpty(imageOutputFileUri)) { + return imageOutputFileUri; + } + + if (videoOutputFileUri != null && isFileNotEmpty(videoOutputFileUri)) { + return videoOutputFileUri; + } + + return null; + } + + public void startPickerIntent(ValueCallback filePathCallback, String acceptType, @Nullable String capture) { + filePathCallbackLegacy = filePathCallback; + + boolean images = acceptsImages(acceptType); + boolean video = acceptsVideo(acceptType); + + Intent pickerIntent = null; + + if (capture != null) { + if (!needsCameraPermission()) { + if (images) { + pickerIntent = getPhotoIntent(); + } + else if (video) { + pickerIntent = getVideoIntent(); + } + } + } + if (pickerIntent == null) { + Intent fileChooserIntent = getFileChooserIntent(acceptType); + pickerIntent = Intent.createChooser(fileChooserIntent, ""); + + ArrayList extraIntents = new ArrayList<>(); + if (!needsCameraPermission()) { + if (images) { + extraIntents.add(getPhotoIntent()); + } + if (video) { + extraIntents.add(getVideoIntent()); + } + } + pickerIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{})); + } + + Activity activity = getActivity(); + if (activity != null && pickerIntent.resolveActivity(activity.getPackageManager()) != null) { + activity.startActivityForResult(pickerIntent, PICKER_LEGACY); + } else { + Log.d(LOG_TAG, "there is no Activity to handle this Intent"); + } + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public boolean startPickerIntent(final ValueCallback callback, final String[] acceptTypes, + final boolean allowMultiple, final boolean captureEnabled) { + filePathCallback = callback; + + boolean images = acceptsImages(acceptTypes); + boolean video = acceptsVideo(acceptTypes); + + Intent pickerIntent = null; + + if (captureEnabled) { + if (!needsCameraPermission()) { + if (images) { + pickerIntent = getPhotoIntent(); + } + else if (video) { + pickerIntent = getVideoIntent(); + } + } + } + if (pickerIntent == null) { + ArrayList extraIntents = new ArrayList<>(); + if (!needsCameraPermission()) { + if (images) { + extraIntents.add(getPhotoIntent()); + } + if (video) { + extraIntents.add(getVideoIntent()); + } + } + + Intent fileSelectionIntent = getFileChooserIntent(acceptTypes, allowMultiple); + + pickerIntent = new Intent(Intent.ACTION_CHOOSER); + pickerIntent.putExtra(Intent.EXTRA_INTENT, fileSelectionIntent); + pickerIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Parcelable[]{})); + } + + Activity activity = getActivity(); + if (activity != null && pickerIntent.resolveActivity(activity.getPackageManager()) != null) { + activity.startActivityForResult(pickerIntent, PICKER); + } else { + Log.d(LOG_TAG, "there is no Activity to handle this Intent"); + } + + return true; + } + + protected boolean needsCameraPermission() { + boolean needed = false; + + Activity activity = getActivity(); + if (activity == null) { + return true; + } + PackageManager packageManager = activity.getPackageManager(); + try { + String[] requestedPermissions = packageManager.getPackageInfo(activity.getApplicationContext().getPackageName(), PackageManager.GET_PERMISSIONS).requestedPermissions; + if (Arrays.asList(requestedPermissions).contains(Manifest.permission.CAMERA) + && ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + needed = true; + } + } catch (PackageManager.NameNotFoundException e) { + needed = true; + } + + return needed; + } + + private Intent getPhotoIntent() { + Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + imageOutputFileUri = getOutputUri(MediaStore.ACTION_IMAGE_CAPTURE); + intent.putExtra(MediaStore.EXTRA_OUTPUT, imageOutputFileUri); + return intent; + } + + private Intent getVideoIntent() { + Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); + videoOutputFileUri = getOutputUri(MediaStore.ACTION_VIDEO_CAPTURE); + intent.putExtra(MediaStore.EXTRA_OUTPUT, videoOutputFileUri); + return intent; + } + + private Intent getFileChooserIntent(String acceptTypes) { + String _acceptTypes = acceptTypes; + if (acceptTypes.isEmpty()) { + _acceptTypes = DEFAULT_MIME_TYPES; + } + if (acceptTypes.matches("\\.\\w+")) { + _acceptTypes = getMimeTypeFromExtension(acceptTypes.replace(".", "")); + } + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType(_acceptTypes); + return intent; + } + + @RequiresApi(api = Build.VERSION_CODES.KITKAT) + private Intent getFileChooserIntent(String[] acceptTypes, boolean allowMultiple) { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + intent.putExtra(Intent.EXTRA_MIME_TYPES, getAcceptedMimeType(acceptTypes)); + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple); + return intent; + } + + private Boolean acceptsAny(String[] types) { + if (isArrayEmpty(types)) { + return true; + } + + for (String type : types) { + if (type.equals("*/*")) { + return true; + } + } + + return false; + } + + private Boolean acceptsImages(String types) { + String mimeType = types; + if (types.matches("\\.\\w+")) { + mimeType = getMimeTypeFromExtension(types.replace(".", "")); + } + return mimeType.isEmpty() || mimeType.toLowerCase().contains("image"); + } + + private Boolean acceptsImages(String[] types) { + String[] mimeTypes = getAcceptedMimeType(types); + return acceptsAny(types) || arrayContainsString(mimeTypes, "image"); + } + + private Boolean acceptsVideo(String types) { + String mimeType = types; + if (types.matches("\\.\\w+")) { + mimeType = getMimeTypeFromExtension(types.replace(".", "")); + } + return mimeType.isEmpty() || mimeType.toLowerCase().contains("video"); + } + + private Boolean acceptsVideo(String[] types) { + String[] mimeTypes = getAcceptedMimeType(types); + return acceptsAny(types) || arrayContainsString(mimeTypes, "video"); + } + + private Boolean arrayContainsString(String[] array, String pattern) { + for (String content : array) { + if (content != null && content.contains(pattern)) { + return true; + } + } + return false; + } + + private String[] getAcceptedMimeType(String[] types) { + if (isArrayEmpty(types)) { + return new String[]{DEFAULT_MIME_TYPES}; + } + String[] mimeTypes = new String[types.length]; + for (int i = 0; i < types.length; i++) { + String t = types[i]; + // convert file extensions to mime types + if (t.matches("\\.\\w+")) { + String mimeType = getMimeTypeFromExtension(t.replace(".", "")); + mimeTypes[i] = mimeType; + } else { + mimeTypes[i] = t; + } + } + return mimeTypes; + } + + private String getMimeTypeFromExtension(String extension) { + String type = null; + if (extension != null) { + type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + } + return type; + } + + @Nullable + private Uri getOutputUri(String intentType) { + File capturedFile = null; + try { + capturedFile = getCapturedFile(intentType); + } catch (IOException e) { + Log.e(LOG_TAG, "Error occurred while creating the File", e); + } + if (capturedFile == null) { + return null; + } + + // for versions below 6.0 (23) we use the old File creation & permissions model + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return Uri.fromFile(capturedFile); + } + + Activity activity = getActivity(); + if (activity == null) { + return null; + } + // for versions 6.0+ (23) we use the FileProvider to avoid runtime permissions + String fileProviderAuthority = activity.getApplicationContext().getPackageName() + "." + + InAppWebViewFileProvider.fileProviderAuthorityExtension; + try { + return FileProvider.getUriForFile(activity.getApplicationContext(), + fileProviderAuthority, + capturedFile); + } catch (Exception e) { + Log.e(LOG_TAG, "", e); + } + return null; + } + + @Nullable + private File getCapturedFile(String intentType) throws IOException { + String prefix = ""; + String suffix = ""; + String dir = ""; + + if (intentType.equals(MediaStore.ACTION_IMAGE_CAPTURE)) { + prefix = "image"; + suffix = ".jpg"; + dir = Environment.DIRECTORY_PICTURES; + } else if (intentType.equals(MediaStore.ACTION_VIDEO_CAPTURE)) { + prefix = "video"; + suffix = ".mp4"; + dir = Environment.DIRECTORY_MOVIES; + } + + // for versions below 6.0 (23) we use the old File creation & permissions model + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + // only this Directory works on all tested Android versions + // ctx.getExternalFilesDir(dir) was failing on Android 5.0 (sdk 21) + File storageDir = Environment.getExternalStoragePublicDirectory(dir); + String filename = String.format("%s-%d%s", prefix, System.currentTimeMillis(), suffix); + return new File(storageDir, filename); + } + + Activity activity = getActivity(); + if (activity == null) { + return null; + } + File storageDir = activity.getApplicationContext().getExternalFilesDir(null); + return File.createTempFile(prefix, suffix, storageDir); + } + + private Boolean isArrayEmpty(String[] arr) { + // when our array returned from getAcceptTypes() has no values set from the webview + // i.e. , without any "accept" attr + // will be an array with one empty string element, afaik + return arr.length == 0 || (arr.length == 1 && arr[0].length() == 0); + } + + @Override + public void onPermissionRequest(final PermissionRequest request) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + final WebViewChannelDelegate.PermissionRequestCallback callback = new WebViewChannelDelegate.PermissionRequestCallback() { + @Override + public boolean nonNullSuccess(@NonNull PermissionResponse response) { + Integer action = response.getAction(); + if (action != null) { + switch (action) { + case 1: + String[] resources = new String[response.getResources().size()]; + resources = response.getResources().toArray(resources); + request.grant(resources); + break; + case 0: + default: + request.deny(); + } + return false; + } + return true; + } + + @Override + public void defaultBehaviour(@Nullable PermissionResponse response) { + request.deny(); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if(inAppWebView != null && inAppWebView.channelDelegate != null) { + inAppWebView.channelDelegate.onPermissionRequest(request.getOrigin().toString(), + Arrays.asList(request.getResources()), null, callback); + } else { + callback.defaultBehaviour(null); + } + } + } + + @Override + public void onRequestFocus(WebView view) { + if(inAppWebView != null && inAppWebView.channelDelegate != null) { + inAppWebView.channelDelegate.onRequestFocus(); + } + } + + @Override + public void onPermissionRequestCanceled(PermissionRequest request) { + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && + inAppWebView != null && inAppWebView.channelDelegate != null) { + inAppWebView.channelDelegate.onPermissionRequestCanceled(request.getOrigin().toString(), + Arrays.asList(request.getResources())); + } + } + + @Nullable + private Activity getActivity() { + if (inAppBrowserDelegate != null) { + return inAppBrowserDelegate.getActivity(); + } else if (plugin != null) { + return plugin.activity; + } + return null; + } + + public void dispose() { + for (Map.Entry dialog : dialogs.entrySet()) { + dialog.getValue().cancel(); + dialog.getKey().dismiss(); + } + dialogs.clear(); + if (plugin != null && plugin.activityPluginBinding != null) { + plugin.activityPluginBinding.removeActivityResultListener(this); + } + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.getActivityResultListeners().clear(); + inAppBrowserDelegate = null; + } + filePathCallbackLegacy = null; + filePathCallback = null; + videoOutputFileUri = null; + imageOutputFileUri = null; + inAppWebView = null; + plugin = null; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewClient.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewClient.java new file mode 100755 index 00000000..c964c032 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewClient.java @@ -0,0 +1,813 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.net.Uri; +import android.net.http.SslError; +import android.os.Build; +import android.os.Message; +import android.util.Log; +import android.view.KeyEvent; +import android.webkit.ClientCertRequest; +import android.webkit.CookieManager; +import android.webkit.CookieSyncManager; +import android.webkit.HttpAuthHandler; +import android.webkit.RenderProcessGoneDetail; +import android.webkit.SafeBrowsingResponse; +import android.webkit.SslErrorHandler; +import android.webkit.ValueCallback; +import android.webkit.WebResourceError; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.webkit.WebResourceRequestCompat; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.Util; +import com.pichillilorenzo.flutter_inappwebview_android.credential_database.CredentialDatabase; +import com.pichillilorenzo.flutter_inappwebview_android.in_app_browser.InAppBrowserDelegate; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.JavaScriptBridgeJS; +import com.pichillilorenzo.flutter_inappwebview_android.types.ClientCertChallenge; +import com.pichillilorenzo.flutter_inappwebview_android.types.ClientCertResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.CustomSchemeResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.HttpAuthResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.HttpAuthenticationChallenge; +import com.pichillilorenzo.flutter_inappwebview_android.types.NavigationAction; +import com.pichillilorenzo.flutter_inappwebview_android.types.NavigationActionPolicy; +import com.pichillilorenzo.flutter_inappwebview_android.types.ServerTrustAuthResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.ServerTrustChallenge; +import com.pichillilorenzo.flutter_inappwebview_android.types.URLCredential; +import com.pichillilorenzo.flutter_inappwebview_android.types.URLProtectionSpace; +import com.pichillilorenzo.flutter_inappwebview_android.types.URLRequest; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceErrorExt; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceRequestExt; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceResponseExt; +import com.pichillilorenzo.flutter_inappwebview_android.webview.WebViewChannelDelegate; + +import java.io.ByteArrayInputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; + +public class InAppWebViewClient extends WebViewClient { + + protected static final String LOG_TAG = "IAWebViewClient"; + private InAppBrowserDelegate inAppBrowserDelegate; + private static int previousAuthRequestFailureCount = 0; + private static List credentialsProposed = null; + + public InAppWebViewClient(InAppBrowserDelegate inAppBrowserDelegate) { + super(); + this.inAppBrowserDelegate = inAppBrowserDelegate; + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + InAppWebView webView = (InAppWebView) view; + if (webView.customSettings.useShouldOverrideUrlLoading) { + boolean isRedirect = false; + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_REQUEST_IS_REDIRECT)) { + isRedirect = WebResourceRequestCompat.isRedirect(request); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + isRedirect = request.isRedirect(); + } + onShouldOverrideUrlLoading( + webView, + request.getUrl().toString(), + request.getMethod(), + request.getRequestHeaders(), + request.isForMainFrame(), + request.hasGesture(), + isRedirect); + if (webView.regexToCancelSubFramesLoadingCompiled != null) { + if (request.isForMainFrame()) + return true; + else { + Matcher m = webView.regexToCancelSubFramesLoadingCompiled.matcher(request.getUrl().toString()); + return m.matches(); + } + } else { + // There isn't any way to load an URL for a frame that is not the main frame, + // so if the request is not for the main frame, the navigation is allowed. + return request.isForMainFrame(); + } + } + return false; + } + + @Override + public boolean shouldOverrideUrlLoading(WebView webView, String url) { + InAppWebView inAppWebView = (InAppWebView) webView; + if (inAppWebView.customSettings.useShouldOverrideUrlLoading) { + onShouldOverrideUrlLoading(inAppWebView, url, "GET", null,true, false, false); + return true; + } + return false; + } + + private void allowShouldOverrideUrlLoading(WebView webView, String url, + @Nullable Map headers, + boolean isForMainFrame) { + if (isForMainFrame) { + // There isn't any way to load an URL for a frame that is not the main frame, + // so call this only on main frame. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + webView.loadUrl(url, headers); + else + webView.loadUrl(url); + } + } + public void onShouldOverrideUrlLoading(final InAppWebView webView, final String url, + final String method, + @Nullable final Map headers, + final boolean isForMainFrame, boolean hasGesture, + boolean isRedirect) { + URLRequest request = new URLRequest(url, method, null, headers); + NavigationAction navigationAction = new NavigationAction( + request, + isForMainFrame, + hasGesture, + isRedirect + ); + + final WebViewChannelDelegate.ShouldOverrideUrlLoadingCallback callback = new WebViewChannelDelegate.ShouldOverrideUrlLoadingCallback() { + @Override + public boolean nonNullSuccess(@NonNull NavigationActionPolicy result) { + switch (result) { + case ALLOW: + allowShouldOverrideUrlLoading(webView, url, headers, isForMainFrame); + break; + case CANCEL: + default: + break; + } + return false; + } + + @Override + public void defaultBehaviour(@Nullable NavigationActionPolicy result) { + allowShouldOverrideUrlLoading(webView, url, headers, isForMainFrame); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.shouldOverrideUrlLoading(navigationAction, callback); + } else { + callback.defaultBehaviour(null); + } + } + + @SuppressLint("RestrictedApi") + public void loadCustomJavaScriptOnPageStarted(WebView view) { + InAppWebView webView = (InAppWebView) view; + + if (!WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + String source = webView.userContentController.generateWrappedCodeForDocumentStart(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + webView.evaluateJavascript(source, (ValueCallback) null); + } else { + webView.loadUrl("javascript:" + source.replaceAll("[\r\n]+", "")); + } + } + } + + public void loadCustomJavaScriptOnPageFinished(WebView view) { + InAppWebView webView = (InAppWebView) view; + + String source = webView.userContentController.generateWrappedCodeForDocumentEnd(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + webView.evaluateJavascript(source, (ValueCallback) null); + } else { + webView.loadUrl("javascript:" + source.replaceAll("[\r\n]+", "")); + } + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + final InAppWebView webView = (InAppWebView) view; + webView.isLoading = true; + webView.disposeWebMessageChannels(); + webView.userContentController.resetContentWorlds(); + loadCustomJavaScriptOnPageStarted(webView); + + super.onPageStarted(view, url, favicon); + + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didStartNavigation(url); + } + + if (webView.channelDelegate != null) { + webView.channelDelegate.onLoadStart(url); + } + } + + public void onPageFinished(WebView view, String url) { + final InAppWebView webView = (InAppWebView) view; + webView.isLoading = false; + loadCustomJavaScriptOnPageFinished(webView); + previousAuthRequestFailureCount = 0; + credentialsProposed = null; + + super.onPageFinished(view, url); + + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didFinishNavigation(url); + } + + // WebView not storing cookies reliable to local device storage + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + CookieManager.getInstance().flush(); + } else { + CookieSyncManager.getInstance().sync(); + } + + String js = JavaScriptBridgeJS.PLATFORM_READY_JS_SOURCE; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + webView.evaluateJavascript(js, (ValueCallback) null); + } else { + webView.loadUrl("javascript:" + js.replaceAll("[\r\n]+", "")); + } + + if (webView.channelDelegate != null) { + webView.channelDelegate.onLoadStop(url); + } + } + + @Override + public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) { + super.doUpdateVisitedHistory(view, url, isReload); + + // url argument sometimes doesn't contain the new changed URL, so we get it again from the webview. + url = view.getUrl(); + + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didUpdateVisitedHistory(url); + } + + final InAppWebView webView = (InAppWebView) view; + if (webView.channelDelegate != null) { + webView.channelDelegate.onUpdateVisitedHistory(url, isReload); + } + } + + @RequiresApi(api = Build.VERSION_CODES.M) + @Override + public void onReceivedError(WebView view, @NonNull WebResourceRequest request, @NonNull WebResourceError error) { + final InAppWebView webView = (InAppWebView) view; + + if (request.isForMainFrame()) { + if (webView.customSettings.disableDefaultErrorPage) { + webView.stopLoading(); + webView.loadUrl("about:blank"); + } + + webView.isLoading = false; + previousAuthRequestFailureCount = 0; + credentialsProposed = null; + + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didFailNavigation(request.getUrl().toString(), error.getErrorCode(), error.getDescription().toString()); + } + } + + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedError( + WebResourceRequestExt.fromWebResourceRequest(request), + WebResourceErrorExt.fromWebResourceError(error)); + } + } + + @SuppressLint("RestrictedApi") + @Override + public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { + final InAppWebView webView = (InAppWebView) view; + + if (webView.customSettings.disableDefaultErrorPage) { + webView.stopLoading(); + webView.loadUrl("about:blank"); + } + + webView.isLoading = false; + previousAuthRequestFailureCount = 0; + credentialsProposed = null; + + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didFailNavigation(failingUrl, errorCode, description); + } + + WebResourceRequestExt request = new WebResourceRequestExt( + failingUrl, + null, + false, + false, + true, + "GET"); + + WebResourceErrorExt error = new WebResourceErrorExt( + errorCode, + description + ); + + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedError( + request, + error); + } + + super.onReceivedError(view, errorCode, description, failingUrl); + } + + @RequiresApi(api = Build.VERSION_CODES.M) + @Override + public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) { + super.onReceivedHttpError(view, request, errorResponse); + + final InAppWebView webView = (InAppWebView) view; + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedHttpError( + WebResourceRequestExt.fromWebResourceRequest(request), + WebResourceResponseExt.fromWebResourceResponse(errorResponse)); + } + } + + @Override + public void onReceivedHttpAuthRequest(final WebView view, final HttpAuthHandler handler, final String host, final String realm) { + final String url = view.getUrl(); + String protocol = "https"; + int port = 0; + + if (url != null) { + try { + URI uri = new URI(url); + protocol = uri.getScheme(); + port = uri.getPort(); + } catch (URISyntaxException e) { + Log.e(LOG_TAG, "", e); + } + } + + previousAuthRequestFailureCount++; + + if (credentialsProposed == null) + credentialsProposed = CredentialDatabase.getInstance(view.getContext()).getHttpAuthCredentials(host, protocol, realm, port); + + URLCredential credentialProposed = null; + if (credentialsProposed != null && credentialsProposed.size() > 0) { + credentialProposed = credentialsProposed.get(0); + } + + URLProtectionSpace protectionSpace = new URLProtectionSpace(host, protocol, realm, port, view.getCertificate(), null); + HttpAuthenticationChallenge challenge = new HttpAuthenticationChallenge(protectionSpace, previousAuthRequestFailureCount, credentialProposed); + + final InAppWebView webView = (InAppWebView) view; + final String finalProtocol = protocol; + final int finalPort = port; + final WebViewChannelDelegate.ReceivedHttpAuthRequestCallback callback = new WebViewChannelDelegate.ReceivedHttpAuthRequestCallback() { + @Override + public boolean nonNullSuccess(@NonNull HttpAuthResponse response) { + Integer action = response.getAction(); + if (action != null) { + switch (action) { + case 1: + String username = response.getUsername(); + String password = response.getPassword(); + boolean permanentPersistence = response.isPermanentPersistence(); + if (permanentPersistence) { + CredentialDatabase.getInstance(view.getContext()) + .setHttpAuthCredential(host, finalProtocol, realm, finalPort, username, password); + } + handler.proceed(username, password); + break; + case 2: + if (credentialsProposed.size() > 0) { + URLCredential credential = credentialsProposed.remove(0); + handler.proceed(credential.getUsername(), credential.getPassword()); + } else { + handler.cancel(); + } + // used custom CredentialDatabase! + // handler.useHttpAuthUsernamePassword(); + break; + case 0: + default: + credentialsProposed = null; + previousAuthRequestFailureCount = 0; + handler.cancel(); + } + + return false; + } + + return true; + } + + @Override + public void defaultBehaviour(@Nullable HttpAuthResponse result) { + InAppWebViewClient.super.onReceivedHttpAuthRequest(view, handler, host, realm); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedHttpAuthRequest(challenge, callback); + } else { + callback.defaultBehaviour(null); + } + } + + @Override + public void onReceivedSslError(final WebView view, final SslErrorHandler handler, final SslError sslError) { + final String url = sslError.getUrl(); + String host = ""; + String protocol = "https"; + int port = 0; + + try { + URI uri = new URI(url); + host = uri.getHost(); + protocol = uri.getScheme(); + port = uri.getPort(); + } catch (URISyntaxException e) { + Log.e(LOG_TAG, "", e); + } + + URLProtectionSpace protectionSpace = new URLProtectionSpace(host, protocol, null, port, sslError.getCertificate(), sslError); + ServerTrustChallenge challenge = new ServerTrustChallenge(protectionSpace); + + final InAppWebView webView = (InAppWebView) view; + final WebViewChannelDelegate.ReceivedServerTrustAuthRequestCallback callback = new WebViewChannelDelegate.ReceivedServerTrustAuthRequestCallback() { + @Override + public boolean nonNullSuccess(@NonNull ServerTrustAuthResponse response) { + Integer action = response.getAction(); + if (action != null) { + switch (action) { + case 1: + handler.proceed(); + break; + case 0: + default: + handler.cancel(); + } + + return false; + } + + return true; + } + + @Override + public void defaultBehaviour(@Nullable ServerTrustAuthResponse result) { + InAppWebViewClient.super.onReceivedSslError(view, handler, sslError); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedServerTrustAuthRequest(challenge, callback); + } else { + callback.defaultBehaviour(null); + } + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + public void onReceivedClientCertRequest(final WebView view, final ClientCertRequest request) { + final String url = view.getUrl(); + final String host = request.getHost(); + String protocol = "https"; + final int port = request.getPort(); + + if (url != null) { + try { + URI uri = new URI(url); + protocol = uri.getScheme(); + } catch (URISyntaxException e) { + Log.e(LOG_TAG, "", e); + } + } + + URLProtectionSpace protectionSpace = new URLProtectionSpace(host, protocol, null, port, view.getCertificate(), null); + ClientCertChallenge challenge = new ClientCertChallenge(protectionSpace, request.getPrincipals(), request.getKeyTypes()); + + final InAppWebView webView = (InAppWebView) view; + final WebViewChannelDelegate.ReceivedClientCertRequestCallback callback = new WebViewChannelDelegate.ReceivedClientCertRequestCallback() { + @Override + public boolean nonNullSuccess(@NonNull ClientCertResponse response) { + Integer action = response.getAction(); + if (action != null && webView.plugin != null) { + switch (action) { + case 1: + { + String certificatePath = (String) response.getCertificatePath(); + String certificatePassword = (String) response.getCertificatePassword(); + String keyStoreType = (String) response.getKeyStoreType(); + Util.PrivateKeyAndCertificates privateKeyAndCertificates = + Util.loadPrivateKeyAndCertificate(webView.plugin, certificatePath, certificatePassword, keyStoreType); + if (privateKeyAndCertificates != null) { + request.proceed(privateKeyAndCertificates.privateKey, privateKeyAndCertificates.certificates); + } else { + request.cancel(); + } + } + break; + case 2: + request.ignore(); + break; + case 0: + default: + request.cancel(); + } + + return false; + } + + return true; + } + + @Override + public void defaultBehaviour(@Nullable ClientCertResponse result) { + InAppWebViewClient.super.onReceivedClientCertRequest(view, request); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedClientCertRequest(challenge, callback); + } else { + callback.defaultBehaviour(null); + } + } + + @Override + public void onScaleChanged(WebView view, float oldScale, float newScale) { + super.onScaleChanged(view, oldScale, newScale); + final InAppWebView webView = (InAppWebView) view; + webView.zoomScale = newScale / Util.getPixelDensity(webView.getContext()); + + if (webView.channelDelegate != null) { + webView.channelDelegate.onZoomScaleChanged(oldScale, newScale); + } + } + + @RequiresApi(api = Build.VERSION_CODES.O_MR1) + @Override + public void onSafeBrowsingHit(final WebView view, final WebResourceRequest request, final int threatType, final SafeBrowsingResponse callback) { + final InAppWebView webView = (InAppWebView) view; + final WebViewChannelDelegate.SafeBrowsingHitCallback resultCallback = new WebViewChannelDelegate.SafeBrowsingHitCallback() { + @Override + public boolean nonNullSuccess(@NonNull com.pichillilorenzo.flutter_inappwebview_android.types.SafeBrowsingResponse response) { + Integer action = response.getAction(); + if (action != null) { + boolean report = response.isReport(); + switch (action) { + case 0: + callback.backToSafety(report); + break; + case 1: + callback.proceed(report); + break; + case 2: + default: + callback.showInterstitial(report); + } + + return false; + } + + return true; + } + + @Override + public void defaultBehaviour(@Nullable com.pichillilorenzo.flutter_inappwebview_android.types.SafeBrowsingResponse result) { + InAppWebViewClient.super.onSafeBrowsingHit(view, request, threatType, callback); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.onSafeBrowsingHit(request.getUrl().toString(), threatType, resultCallback); + } else { + resultCallback.defaultBehaviour(null); + } + } + + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequestExt request) { + final InAppWebView webView = (InAppWebView) view; + + if (webView.webViewAssetLoaderExt != null && webView.webViewAssetLoaderExt.loader != null) { + try { + final Uri uri = Uri.parse(request.getUrl()); + WebResourceResponse webResourceResponse = webView.webViewAssetLoaderExt.loader.shouldInterceptRequest(uri); + if (webResourceResponse != null) { + return webResourceResponse; + } + } catch (Exception e) { + Log.e(LOG_TAG, "", e); + } + } + + if (webView.customSettings.useShouldInterceptRequest) { + WebResourceResponseExt response = null; + if (webView.channelDelegate != null) { + try { + response = webView.channelDelegate.shouldInterceptRequest(request); + } catch (InterruptedException e) { + Log.e(LOG_TAG, "", e); + return null; + } + } + + if (response != null) { + String contentType = response.getContentType(); + String contentEncoding = response.getContentEncoding(); + byte[] data = response.getData(); + Map responseHeaders = response.getHeaders(); + Integer statusCode = response.getStatusCode(); + String reasonPhrase = response.getReasonPhrase(); + + ByteArrayInputStream inputStream = (data != null) ? new ByteArrayInputStream(data) : null; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && statusCode != null && reasonPhrase != null) { + return new WebResourceResponse(contentType, contentEncoding, statusCode, reasonPhrase, responseHeaders, inputStream); + } else { + return new WebResourceResponse(contentType, contentEncoding, inputStream); + } + } + + return null; + } + + final String url = request.getUrl(); + String scheme = url.split(":")[0].toLowerCase(); + try { + scheme = Uri.parse(request.getUrl()).getScheme(); + } catch (Exception ignored) {} + + if (webView.customSettings.resourceCustomSchemes != null && webView.customSettings.resourceCustomSchemes.contains(scheme)) { + CustomSchemeResponse customSchemeResponse = null; + if (webView.channelDelegate != null) { + try { + customSchemeResponse = webView.channelDelegate.onLoadResourceWithCustomScheme(request); + } catch (InterruptedException e) { + Log.e(LOG_TAG, "", e); + return null; + } + } + + if (customSchemeResponse != null) { + WebResourceResponse response = null; + try { + response = webView.contentBlockerHandler.checkUrl(webView, request, customSchemeResponse.getContentType()); + } catch (Exception e) { + Log.e(LOG_TAG, "", e); + } + if (response != null) + return response; + return new WebResourceResponse(customSchemeResponse.getContentType(), + customSchemeResponse.getContentType(), + new ByteArrayInputStream(customSchemeResponse.getData())); + } + } + + WebResourceResponse response = null; + if (webView.contentBlockerHandler.getRuleList().size() > 0) { + try { + response = webView.contentBlockerHandler.checkUrl(webView, request); + } catch (Exception e) { + Log.e(LOG_TAG, "", e); + } + } + return response; + } + + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, final String url) { + WebResourceRequestExt requestExt = new WebResourceRequestExt( + url, null, false, + false, true, "GET" + ); + return shouldInterceptRequest(view, requestExt); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { + WebResourceRequestExt requestExt = WebResourceRequestExt.fromWebResourceRequest(request); + return shouldInterceptRequest(view, requestExt); + } + + @Override + public void onFormResubmission(final WebView view, final Message dontResend, final Message resend) { + final InAppWebView webView = (InAppWebView) view; + final WebViewChannelDelegate.FormResubmissionCallback callback = new WebViewChannelDelegate.FormResubmissionCallback() { + @Override + public boolean nonNullSuccess(@NonNull Integer action) { + switch (action) { + case 0: + resend.sendToTarget(); + break; + case 1: + default: + dontResend.sendToTarget(); + } + return false; + } + + @Override + public void defaultBehaviour(@Nullable Integer result) { + InAppWebViewClient.super.onFormResubmission(view, dontResend, resend); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.onFormResubmission(webView.getUrl(), callback); + } else { + callback.defaultBehaviour(null); + } + } + + @Override + public void onPageCommitVisible(WebView view, String url) { + super.onPageCommitVisible(view, url); + + final InAppWebView webView = (InAppWebView) view; + if (webView.channelDelegate != null) { + webView.channelDelegate.onPageCommitVisible(url); + } + } + + @RequiresApi(api = Build.VERSION_CODES.O) + @Override + public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) { + final InAppWebView webView = (InAppWebView) view; + + if (webView.customSettings.useOnRenderProcessGone && webView.channelDelegate != null) { + boolean didCrash = detail.didCrash(); + int rendererPriorityAtExit = detail.rendererPriorityAtExit(); + webView.channelDelegate.onRenderProcessGone(didCrash, rendererPriorityAtExit); + return true; + } + + return super.onRenderProcessGone(view, detail); + } + + @Override + public void onReceivedLoginRequest(WebView view, String realm, String account, String args) { + final InAppWebView webView = (InAppWebView) view; + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedLoginRequest(realm, account, args); + } + } + + @Override + public void onUnhandledKeyEvent(WebView view, KeyEvent event) {} + + public void dispose() { + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate = null; + } + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewClientCompat.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewClientCompat.java new file mode 100755 index 00000000..f00af414 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewClientCompat.java @@ -0,0 +1,840 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.net.Uri; +import android.net.http.SslError; +import android.os.Build; +import android.os.Message; +import android.util.Log; +import android.view.KeyEvent; +import android.webkit.ClientCertRequest; +import android.webkit.CookieManager; +import android.webkit.CookieSyncManager; +import android.webkit.HttpAuthHandler; +import android.webkit.RenderProcessGoneDetail; +import android.webkit.SslErrorHandler; +import android.webkit.ValueCallback; +import android.webkit.WebResourceRequest; +import android.webkit.WebResourceResponse; +import android.webkit.WebView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.webkit.SafeBrowsingResponseCompat; +import androidx.webkit.WebResourceErrorCompat; +import androidx.webkit.WebResourceRequestCompat; +import androidx.webkit.WebViewClientCompat; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.Util; +import com.pichillilorenzo.flutter_inappwebview_android.credential_database.CredentialDatabase; +import com.pichillilorenzo.flutter_inappwebview_android.in_app_browser.InAppBrowserDelegate; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.JavaScriptBridgeJS; +import com.pichillilorenzo.flutter_inappwebview_android.types.ClientCertChallenge; +import com.pichillilorenzo.flutter_inappwebview_android.types.ClientCertResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.CustomSchemeResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.HttpAuthResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.HttpAuthenticationChallenge; +import com.pichillilorenzo.flutter_inappwebview_android.types.NavigationAction; +import com.pichillilorenzo.flutter_inappwebview_android.types.NavigationActionPolicy; +import com.pichillilorenzo.flutter_inappwebview_android.types.ServerTrustAuthResponse; +import com.pichillilorenzo.flutter_inappwebview_android.types.ServerTrustChallenge; +import com.pichillilorenzo.flutter_inappwebview_android.types.URLCredential; +import com.pichillilorenzo.flutter_inappwebview_android.types.URLProtectionSpace; +import com.pichillilorenzo.flutter_inappwebview_android.types.URLRequest; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceErrorExt; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceRequestExt; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceResponseExt; +import com.pichillilorenzo.flutter_inappwebview_android.webview.WebViewChannelDelegate; + +import java.io.ByteArrayInputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; + +public class InAppWebViewClientCompat extends WebViewClientCompat { + + protected static final String LOG_TAG = "IAWebViewClientCompat"; + private InAppBrowserDelegate inAppBrowserDelegate; + private static int previousAuthRequestFailureCount = 0; + private static List credentialsProposed = null; + + public InAppWebViewClientCompat(InAppBrowserDelegate inAppBrowserDelegate) { + super(); + this.inAppBrowserDelegate = inAppBrowserDelegate; + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean shouldOverrideUrlLoading(@NonNull WebView view, @NonNull WebResourceRequest request) { + InAppWebView webView = (InAppWebView) view; + if (webView.customSettings.useShouldOverrideUrlLoading) { + boolean isRedirect = false; + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_REQUEST_IS_REDIRECT)) { + isRedirect = WebResourceRequestCompat.isRedirect(request); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + isRedirect = request.isRedirect(); + } + onShouldOverrideUrlLoading( + webView, + request.getUrl().toString(), + request.getMethod(), + request.getRequestHeaders(), + request.isForMainFrame(), + request.hasGesture(), + isRedirect); + if (webView.regexToCancelSubFramesLoadingCompiled != null) { + if (request.isForMainFrame()) + return true; + else { + Matcher m = webView.regexToCancelSubFramesLoadingCompiled.matcher(request.getUrl().toString()); + return m.matches(); + } + } else { + // There isn't any way to load an URL for a frame that is not the main frame, + // so if the request is not for the main frame, the navigation is allowed. + return request.isForMainFrame(); + } + } + return false; + } + + @Override + public boolean shouldOverrideUrlLoading(WebView webView, String url) { + InAppWebView inAppWebView = (InAppWebView) webView; + if (inAppWebView.customSettings.useShouldOverrideUrlLoading) { + onShouldOverrideUrlLoading(inAppWebView, url, "GET", null,true, false, false); + return true; + } + return false; + } + + private void allowShouldOverrideUrlLoading(WebView webView, String url, + @Nullable Map headers, + boolean isForMainFrame) { + if (isForMainFrame) { + // There isn't any way to load an URL for a frame that is not the main frame, + // so call this only on main frame. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + webView.loadUrl(url, headers); + else + webView.loadUrl(url); + } + } + public void onShouldOverrideUrlLoading(final InAppWebView webView, final String url, + final String method, + @Nullable final Map headers, + final boolean isForMainFrame, boolean hasGesture, + boolean isRedirect) { + URLRequest request = new URLRequest(url, method, null, headers); + NavigationAction navigationAction = new NavigationAction( + request, + isForMainFrame, + hasGesture, + isRedirect + ); + + final WebViewChannelDelegate.ShouldOverrideUrlLoadingCallback callback = new WebViewChannelDelegate.ShouldOverrideUrlLoadingCallback() { + @Override + public boolean nonNullSuccess(@NonNull NavigationActionPolicy result) { + switch (result) { + case ALLOW: + allowShouldOverrideUrlLoading(webView, url, headers, isForMainFrame); + break; + case CANCEL: + default: + break; + } + return false; + } + + @Override + public void defaultBehaviour(@Nullable NavigationActionPolicy result) { + allowShouldOverrideUrlLoading(webView, url, headers, isForMainFrame); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.shouldOverrideUrlLoading(navigationAction, callback); + } else { + callback.defaultBehaviour(null); + } + } + + @SuppressLint("RestrictedApi") + public void loadCustomJavaScriptOnPageStarted(WebView view) { + InAppWebView webView = (InAppWebView) view; + + if (!WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) { + String source = webView.userContentController.generateWrappedCodeForDocumentStart(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + webView.evaluateJavascript(source, (ValueCallback) null); + } else { + webView.loadUrl("javascript:" + source.replaceAll("[\r\n]+", "")); + } + } + } + + public void loadCustomJavaScriptOnPageFinished(WebView view) { + InAppWebView webView = (InAppWebView) view; + + String source = webView.userContentController.generateWrappedCodeForDocumentEnd(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + webView.evaluateJavascript(source, (ValueCallback) null); + } else { + webView.loadUrl("javascript:" + source.replaceAll("[\r\n]+", "")); + } + } + + @Override + public void onPageStarted(WebView view, String url, Bitmap favicon) { + final InAppWebView webView = (InAppWebView) view; + webView.isLoading = true; + webView.disposeWebMessageChannels(); + webView.userContentController.resetContentWorlds(); + loadCustomJavaScriptOnPageStarted(webView); + + super.onPageStarted(view, url, favicon); + + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didStartNavigation(url); + } + + if (webView.channelDelegate != null) { + webView.channelDelegate.onLoadStart(url); + } + } + + public void onPageFinished(WebView view, String url) { + final InAppWebView webView = (InAppWebView) view; + webView.isLoading = false; + loadCustomJavaScriptOnPageFinished(webView); + previousAuthRequestFailureCount = 0; + credentialsProposed = null; + + super.onPageFinished(view, url); + + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didFinishNavigation(url); + } + + // WebView not storing cookies reliable to local device storage + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + CookieManager.getInstance().flush(); + } else { + CookieSyncManager.getInstance().sync(); + } + + String js = JavaScriptBridgeJS.PLATFORM_READY_JS_SOURCE; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + webView.evaluateJavascript(js, (ValueCallback) null); + } else { + webView.loadUrl("javascript:" + js.replaceAll("[\r\n]+", "")); + } + + if (webView.channelDelegate != null) { + webView.channelDelegate.onLoadStop(url); + } + } + + @Override + public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) { + super.doUpdateVisitedHistory(view, url, isReload); + + // url argument sometimes doesn't contain the new changed URL, so we get it again from the webview. + url = view.getUrl(); + + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didUpdateVisitedHistory(url); + } + + final InAppWebView webView = (InAppWebView) view; + if (webView.channelDelegate != null) { + webView.channelDelegate.onUpdateVisitedHistory(url, isReload); + } + } + + @RequiresApi(api = Build.VERSION_CODES.M) + @Override + public void onReceivedError(@NonNull WebView view, + @NonNull WebResourceRequest request, + @NonNull WebResourceErrorCompat error) { + final InAppWebView webView = (InAppWebView) view; + + if (request.isForMainFrame()) { + if (webView.customSettings.disableDefaultErrorPage) { + webView.stopLoading(); + webView.loadUrl("about:blank"); + } + + webView.isLoading = false; + previousAuthRequestFailureCount = 0; + credentialsProposed = null; + + if (inAppBrowserDelegate != null) { + int type = -1; + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE)) { + type = error.getErrorCode(); + } + String description = ""; + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_DESCRIPTION)) { + description = error.getDescription().toString(); + } + inAppBrowserDelegate.didFailNavigation(request.getUrl().toString(), type, description); + } + } + + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedError( + WebResourceRequestExt.fromWebResourceRequest(request), + WebResourceErrorExt.fromWebResourceError(error)); + } + } + + @SuppressLint("RestrictedApi") + @Override + public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { + final InAppWebView webView = (InAppWebView) view; + + if (webView.customSettings.disableDefaultErrorPage) { + webView.stopLoading(); + webView.loadUrl("about:blank"); + } + + webView.isLoading = false; + previousAuthRequestFailureCount = 0; + credentialsProposed = null; + + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate.didFailNavigation(failingUrl, errorCode, description); + } + + WebResourceRequestExt request = new WebResourceRequestExt( + failingUrl, + null, + false, + false, + true, + "GET"); + + WebResourceErrorExt error = new WebResourceErrorExt( + errorCode, + description + ); + + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedError( + request, + error); + } + + super.onReceivedError(view, errorCode, description, failingUrl); + } + + @RequiresApi(api = Build.VERSION_CODES.M) + @Override + public void onReceivedHttpError(@NonNull WebView view, + @NonNull WebResourceRequest request, + @NonNull WebResourceResponse errorResponse) { + super.onReceivedHttpError(view, request, errorResponse); + + final InAppWebView webView = (InAppWebView) view; + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedHttpError( + WebResourceRequestExt.fromWebResourceRequest(request), + WebResourceResponseExt.fromWebResourceResponse(errorResponse)); + } + } + + @Override + public void onReceivedHttpAuthRequest(final WebView view, final HttpAuthHandler handler, final String host, final String realm) { + final String url = view.getUrl(); + String protocol = "https"; + int port = 0; + + if (url != null) { + try { + URI uri = new URI(url); + protocol = uri.getScheme(); + port = uri.getPort(); + } catch (URISyntaxException e) { + Log.e(LOG_TAG, "", e); + } + } + + previousAuthRequestFailureCount++; + + if (credentialsProposed == null) + credentialsProposed = CredentialDatabase.getInstance(view.getContext()).getHttpAuthCredentials(host, protocol, realm, port); + + URLCredential credentialProposed = null; + if (credentialsProposed != null && credentialsProposed.size() > 0) { + credentialProposed = credentialsProposed.get(0); + } + + URLProtectionSpace protectionSpace = new URLProtectionSpace(host, protocol, realm, port, view.getCertificate(), null); + HttpAuthenticationChallenge challenge = new HttpAuthenticationChallenge(protectionSpace, previousAuthRequestFailureCount, credentialProposed); + + final InAppWebView webView = (InAppWebView) view; + final String finalProtocol = protocol; + final int finalPort = port; + final WebViewChannelDelegate.ReceivedHttpAuthRequestCallback callback = new WebViewChannelDelegate.ReceivedHttpAuthRequestCallback() { + @Override + public boolean nonNullSuccess(@NonNull HttpAuthResponse response) { + Integer action = response.getAction(); + if (action != null) { + switch (action) { + case 1: + String username = response.getUsername(); + String password = response.getPassword(); + boolean permanentPersistence = response.isPermanentPersistence(); + if (permanentPersistence) { + CredentialDatabase.getInstance(view.getContext()) + .setHttpAuthCredential(host, finalProtocol, realm, finalPort, username, password); + } + handler.proceed(username, password); + break; + case 2: + if (credentialsProposed.size() > 0) { + URLCredential credential = credentialsProposed.remove(0); + handler.proceed(credential.getUsername(), credential.getPassword()); + } else { + handler.cancel(); + } + // used custom CredentialDatabase! + // handler.useHttpAuthUsernamePassword(); + break; + case 0: + default: + credentialsProposed = null; + previousAuthRequestFailureCount = 0; + handler.cancel(); + } + + return false; + } + + return true; + } + + @Override + public void defaultBehaviour(@Nullable HttpAuthResponse result) { + InAppWebViewClientCompat.super.onReceivedHttpAuthRequest(view, handler, host, realm); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedHttpAuthRequest(challenge, callback); + } else { + callback.defaultBehaviour(null); + } + } + + @Override + public void onReceivedSslError(final WebView view, final SslErrorHandler handler, final SslError sslError) { + final String url = sslError.getUrl(); + String host = ""; + String protocol = "https"; + int port = 0; + + try { + URI uri = new URI(url); + host = uri.getHost(); + protocol = uri.getScheme(); + port = uri.getPort(); + } catch (URISyntaxException e) { + Log.e(LOG_TAG, "", e); + } + + URLProtectionSpace protectionSpace = new URLProtectionSpace(host, protocol, null, port, sslError.getCertificate(), sslError); + ServerTrustChallenge challenge = new ServerTrustChallenge(protectionSpace); + + final InAppWebView webView = (InAppWebView) view; + final WebViewChannelDelegate.ReceivedServerTrustAuthRequestCallback callback = new WebViewChannelDelegate.ReceivedServerTrustAuthRequestCallback() { + @Override + public boolean nonNullSuccess(@NonNull ServerTrustAuthResponse response) { + Integer action = response.getAction(); + if (action != null) { + switch (action) { + case 1: + handler.proceed(); + break; + case 0: + default: + handler.cancel(); + } + + return false; + } + + return true; + } + + @Override + public void defaultBehaviour(@Nullable ServerTrustAuthResponse result) { + InAppWebViewClientCompat.super.onReceivedSslError(view, handler, sslError); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedServerTrustAuthRequest(challenge, callback); + } else { + callback.defaultBehaviour(null); + } + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + public void onReceivedClientCertRequest(final WebView view, final ClientCertRequest request) { + final String url = view.getUrl(); + final String host = request.getHost(); + String protocol = "https"; + final int port = request.getPort(); + + if (url != null) { + try { + URI uri = new URI(url); + protocol = uri.getScheme(); + } catch (URISyntaxException e) { + Log.e(LOG_TAG, "", e); + } + } + + URLProtectionSpace protectionSpace = new URLProtectionSpace(host, protocol, null, port, view.getCertificate(), null); + ClientCertChallenge challenge = new ClientCertChallenge(protectionSpace, request.getPrincipals(), request.getKeyTypes()); + + final InAppWebView webView = (InAppWebView) view; + final WebViewChannelDelegate.ReceivedClientCertRequestCallback callback = new WebViewChannelDelegate.ReceivedClientCertRequestCallback() { + @Override + public boolean nonNullSuccess(@NonNull ClientCertResponse response) { + Integer action = response.getAction(); + if (action != null && webView.plugin != null) { + switch (action) { + case 1: + { + String certificatePath = (String) response.getCertificatePath(); + String certificatePassword = (String) response.getCertificatePassword(); + String keyStoreType = (String) response.getKeyStoreType(); + Util.PrivateKeyAndCertificates privateKeyAndCertificates = + Util.loadPrivateKeyAndCertificate(webView.plugin, certificatePath, certificatePassword, keyStoreType); + if (privateKeyAndCertificates != null) { + request.proceed(privateKeyAndCertificates.privateKey, privateKeyAndCertificates.certificates); + } else { + request.cancel(); + } + } + break; + case 2: + request.ignore(); + break; + case 0: + default: + request.cancel(); + } + + return false; + } + + return true; + } + + @Override + public void defaultBehaviour(@Nullable ClientCertResponse result) { + InAppWebViewClientCompat.super.onReceivedClientCertRequest(view, request); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedClientCertRequest(challenge, callback); + } else { + callback.defaultBehaviour(null); + } + } + + @Override + public void onScaleChanged(WebView view, float oldScale, float newScale) { + super.onScaleChanged(view, oldScale, newScale); + final InAppWebView webView = (InAppWebView) view; + webView.zoomScale = newScale / Util.getPixelDensity(webView.getContext()); + + if (webView.channelDelegate != null) { + webView.channelDelegate.onZoomScaleChanged(oldScale, newScale); + } + } + + @RequiresApi(api = Build.VERSION_CODES.O_MR1) + @Override + public void onSafeBrowsingHit(@NonNull final WebView view, + @NonNull final WebResourceRequest request, + final int threatType, + @NonNull final SafeBrowsingResponseCompat callback) { + final InAppWebView webView = (InAppWebView) view; + final WebViewChannelDelegate.SafeBrowsingHitCallback resultCallback = new WebViewChannelDelegate.SafeBrowsingHitCallback() { + @Override + public boolean nonNullSuccess(@NonNull com.pichillilorenzo.flutter_inappwebview_android.types.SafeBrowsingResponse response) { + Integer action = response.getAction(); + if (action != null) { + boolean report = response.isReport(); + switch (action) { + case 0: + if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_RESPONSE_BACK_TO_SAFETY)) { + callback.backToSafety(report); + } else { + return true; + } + break; + case 1: + if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_RESPONSE_PROCEED)) { + callback.proceed(report); + } else { + return true; + } + break; + case 2: + default: + if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_RESPONSE_SHOW_INTERSTITIAL)) { + callback.showInterstitial(report); + } else { + return true; + } + } + + return false; + } + + return true; + } + + @Override + public void defaultBehaviour(@Nullable com.pichillilorenzo.flutter_inappwebview_android.types.SafeBrowsingResponse result) { + InAppWebViewClientCompat.super.onSafeBrowsingHit(view, request, threatType, callback); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.onSafeBrowsingHit(request.getUrl().toString(), threatType, resultCallback); + } else { + resultCallback.defaultBehaviour(null); + } + } + + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequestExt request) { + final InAppWebView webView = (InAppWebView) view; + + if (webView.webViewAssetLoaderExt != null && webView.webViewAssetLoaderExt.loader != null) { + try { + final Uri uri = Uri.parse(request.getUrl()); + WebResourceResponse webResourceResponse = webView.webViewAssetLoaderExt.loader.shouldInterceptRequest(uri); + if (webResourceResponse != null) { + return webResourceResponse; + } + } catch (Exception e) { + Log.e(LOG_TAG, "", e); + } + } + + if (webView.customSettings.useShouldInterceptRequest) { + WebResourceResponseExt response = null; + if (webView.channelDelegate != null) { + try { + response = webView.channelDelegate.shouldInterceptRequest(request); + } catch (InterruptedException e) { + Log.e(LOG_TAG, "", e); + return null; + } + } + + if (response != null) { + String contentType = response.getContentType(); + String contentEncoding = response.getContentEncoding(); + byte[] data = response.getData(); + Map responseHeaders = response.getHeaders(); + Integer statusCode = response.getStatusCode(); + String reasonPhrase = response.getReasonPhrase(); + + ByteArrayInputStream inputStream = (data != null) ? new ByteArrayInputStream(data) : null; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && statusCode != null && reasonPhrase != null) { + return new WebResourceResponse(contentType, contentEncoding, statusCode, reasonPhrase, responseHeaders, inputStream); + } else { + return new WebResourceResponse(contentType, contentEncoding, inputStream); + } + } + + return null; + } + + final String url = request.getUrl(); + String scheme = url.split(":")[0].toLowerCase(); + try { + scheme = Uri.parse(request.getUrl()).getScheme(); + } catch (Exception ignored) {} + + if (webView.customSettings.resourceCustomSchemes != null && webView.customSettings.resourceCustomSchemes.contains(scheme)) { + CustomSchemeResponse customSchemeResponse = null; + if (webView.channelDelegate != null) { + try { + customSchemeResponse = webView.channelDelegate.onLoadResourceWithCustomScheme(request); + } catch (InterruptedException e) { + Log.e(LOG_TAG, "", e); + return null; + } + } + + if (customSchemeResponse != null) { + WebResourceResponse response = null; + try { + response = webView.contentBlockerHandler.checkUrl(webView, request, customSchemeResponse.getContentType()); + } catch (Exception e) { + Log.e(LOG_TAG, "", e); + } + if (response != null) + return response; + return new WebResourceResponse(customSchemeResponse.getContentType(), + customSchemeResponse.getContentType(), + new ByteArrayInputStream(customSchemeResponse.getData())); + } + } + + WebResourceResponse response = null; + if (webView.contentBlockerHandler.getRuleList().size() > 0) { + try { + response = webView.contentBlockerHandler.checkUrl(webView, request); + } catch (Exception e) { + Log.e(LOG_TAG, "", e); + } + } + return response; + } + + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, final String url) { + WebResourceRequestExt requestExt = new WebResourceRequestExt( + url, null, false, + false, true, "GET" + ); + return shouldInterceptRequest(view, requestExt); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + @Override + public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { + WebResourceRequestExt requestExt = WebResourceRequestExt.fromWebResourceRequest(request); + return shouldInterceptRequest(view, requestExt); + } + + @Override + public void onFormResubmission(final WebView view, final Message dontResend, final Message resend) { + final InAppWebView webView = (InAppWebView) view; + final WebViewChannelDelegate.FormResubmissionCallback callback = new WebViewChannelDelegate.FormResubmissionCallback() { + @Override + public boolean nonNullSuccess(@NonNull Integer action) { + switch (action) { + case 0: + resend.sendToTarget(); + break; + case 1: + default: + dontResend.sendToTarget(); + } + return false; + } + + @Override + public void defaultBehaviour(@Nullable Integer result) { + InAppWebViewClientCompat.super.onFormResubmission(view, dontResend, resend); + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.onFormResubmission(webView.getUrl(), callback); + } else { + callback.defaultBehaviour(null); + } + } + + @Override + public void onPageCommitVisible(@NonNull WebView view, @NonNull String url) { + super.onPageCommitVisible(view, url); + + final InAppWebView webView = (InAppWebView) view; + if (webView.channelDelegate != null) { + webView.channelDelegate.onPageCommitVisible(url); + } + } + + @RequiresApi(api = Build.VERSION_CODES.O) + @Override + public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) { + final InAppWebView webView = (InAppWebView) view; + + if (webView.customSettings.useOnRenderProcessGone && webView.channelDelegate != null) { + boolean didCrash = detail.didCrash(); + int rendererPriorityAtExit = detail.rendererPriorityAtExit(); + webView.channelDelegate.onRenderProcessGone(didCrash, rendererPriorityAtExit); + return true; + } + + return super.onRenderProcessGone(view, detail); + } + + @Override + public void onReceivedLoginRequest(WebView view, String realm, String account, String args) { + final InAppWebView webView = (InAppWebView) view; + if (webView.channelDelegate != null) { + webView.channelDelegate.onReceivedLoginRequest(realm, account, args); + } + } + + @Override + public void onUnhandledKeyEvent(WebView view, KeyEvent event) {} + + public void dispose() { + if (inAppBrowserDelegate != null) { + inAppBrowserDelegate = null; + } + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewRenderProcessClient.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewRenderProcessClient.java new file mode 100644 index 00000000..1fe8c509 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewRenderProcessClient.java @@ -0,0 +1,99 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview; + +import android.util.Log; +import android.webkit.WebView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.WebViewFeature; +import androidx.webkit.WebViewRenderProcess; +import androidx.webkit.WebViewRenderProcessClient; + +import com.pichillilorenzo.flutter_inappwebview_android.webview.WebViewChannelDelegate; + +public class InAppWebViewRenderProcessClient extends WebViewRenderProcessClient { + + protected static final String LOG_TAG = "IAWRenderProcessClient"; + + public InAppWebViewRenderProcessClient() { + super(); + } + + @Override + public void onRenderProcessUnresponsive(@NonNull WebView view, @Nullable final WebViewRenderProcess renderer) { + final InAppWebView webView = (InAppWebView) view; + final WebViewChannelDelegate.RenderProcessUnresponsiveCallback callback = new WebViewChannelDelegate.RenderProcessUnresponsiveCallback() { + @Override + public boolean nonNullSuccess(@NonNull Integer action) { + if (renderer != null) { + switch (action) { + case 0: + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_VIEW_RENDERER_TERMINATE)) + renderer.terminate(); + break; + } + return false; + } + return true; + } + + @Override + public void defaultBehaviour(@Nullable Integer result) { + + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.onRenderProcessUnresponsive(webView.getUrl(), callback); + } else { + callback.defaultBehaviour(null); + } + } + + @Override + public void onRenderProcessResponsive(@NonNull WebView view, @Nullable final WebViewRenderProcess renderer) { + final InAppWebView webView = (InAppWebView) view; + final WebViewChannelDelegate.RenderProcessResponsiveCallback callback = new WebViewChannelDelegate.RenderProcessResponsiveCallback() { + @Override + public boolean nonNullSuccess(@NonNull Integer action) { + if (renderer != null) { + switch (action) { + case 0: + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_VIEW_RENDERER_TERMINATE)) + renderer.terminate(); + break; + } + return false; + } + return true; + } + + @Override + public void defaultBehaviour(@Nullable Integer result) { + + } + + @Override + public void error(String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + Log.e(LOG_TAG, errorCode + ", " + ((errorMessage != null) ? errorMessage : "")); + defaultBehaviour(null); + } + }; + + if (webView.channelDelegate != null) { + webView.channelDelegate.onRenderProcessResponsive(webView.getUrl(), callback); + } else { + callback.defaultBehaviour(null); + } + } + + void dispose() { + + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewSettings.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewSettings.java new file mode 100755 index 00000000..512c60fd --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewSettings.java @@ -0,0 +1,651 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview; + +import static android.webkit.WebSettings.LayoutAlgorithm.NARROW_COLUMNS; +import static android.webkit.WebSettings.LayoutAlgorithm.NORMAL; + +import android.annotation.SuppressLint; +import android.os.Build; +import android.view.View; +import android.webkit.WebSettings; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.WebSettingsCompat; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.ISettings; +import com.pichillilorenzo.flutter_inappwebview_android.types.PreferredContentModeOptionType; +import com.pichillilorenzo.flutter_inappwebview_android.webview.InAppWebViewInterface; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class InAppWebViewSettings implements ISettings { + + public String gopeedProfileId = ""; + public static final String LOG_TAG = "InAppWebViewSettings"; + + public Boolean useShouldOverrideUrlLoading = false; + public Boolean useOnLoadResource = false; + public Boolean useOnDownloadStart = false; + /** + * @deprecated + */ + @Deprecated + public Boolean clearCache = false; + public String userAgent = ""; + public String applicationNameForUserAgent = ""; + public Boolean javaScriptEnabled = true; + public Boolean javaScriptCanOpenWindowsAutomatically = false; + public Boolean mediaPlaybackRequiresUserGesture = true; + public Integer minimumFontSize = 8; + public Boolean verticalScrollBarEnabled = true; + public Boolean horizontalScrollBarEnabled = true; + public List resourceCustomSchemes = new ArrayList<>(); + public List>> contentBlockers = new ArrayList<>(); + public Integer preferredContentMode = PreferredContentModeOptionType.RECOMMENDED.toValue(); + public Boolean useShouldInterceptAjaxRequest = false; + public Boolean interceptOnlyAsyncAjaxRequests = true; + public Boolean useShouldInterceptFetchRequest = false; + public Boolean incognito = false; + public Boolean cacheEnabled = true; + public Boolean transparentBackground = false; + public Boolean disableVerticalScroll = false; + public Boolean disableHorizontalScroll = false; + public Boolean disableContextMenu = false; + public Boolean supportZoom = true; + public Boolean allowFileAccessFromFileURLs = false; + public Boolean allowUniversalAccessFromFileURLs = false; + public Boolean allowBackgroundAudioPlaying = false; + public Integer textZoom = 100; + /** + * @deprecated + */ + @Deprecated + public Boolean clearSessionCache = false; + public Boolean builtInZoomControls = true; + public Boolean displayZoomControls = false; + public Boolean databaseEnabled = false; + public Boolean domStorageEnabled = true; + public Boolean useWideViewPort = true; + public Boolean safeBrowsingEnabled = true; + public Integer mixedContentMode; + public Boolean allowContentAccess = true; + public Boolean allowFileAccess = true; + public String appCachePath; + public Boolean blockNetworkImage = false; + public Boolean blockNetworkLoads = false; + public Integer cacheMode = WebSettings.LOAD_DEFAULT; + public String cursiveFontFamily = "cursive"; + public Integer defaultFixedFontSize = 16; + public Integer defaultFontSize = 16; + public String defaultTextEncodingName = "UTF-8"; + public Integer disabledActionModeMenuItems; + public String fantasyFontFamily = "fantasy"; + public String fixedFontFamily = "monospace"; + public Integer forceDark = 0; // WebSettingsCompat.FORCE_DARK_OFF + public Integer forceDarkStrategy = WebSettingsCompat.DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING; + public Boolean geolocationEnabled = true; + public WebSettings.LayoutAlgorithm layoutAlgorithm; + public Boolean loadWithOverviewMode = true; + public Boolean loadsImagesAutomatically = true; + public Integer minimumLogicalFontSize = 8; + public Integer initialScale = 0; + public Boolean needInitialFocus = true; + public Boolean offscreenPreRaster = false; + public String sansSerifFontFamily = "sans-serif"; + public String serifFontFamily = "sans-serif"; + public String standardFontFamily = "sans-serif"; + public Boolean saveFormData = true; + public Boolean thirdPartyCookiesEnabled = true; + public Boolean hardwareAcceleration = true; + public Boolean supportMultipleWindows = false; + public String regexToCancelSubFramesLoading; + public Integer overScrollMode = View.OVER_SCROLL_IF_CONTENT_SCROLLS; + public Boolean networkAvailable = null; + public Integer scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY; + public Integer verticalScrollbarPosition = View.SCROLLBAR_POSITION_DEFAULT; + public Integer scrollBarDefaultDelayBeforeFade = null; + public Boolean scrollbarFadingEnabled = true; + public Integer scrollBarFadeDuration = null; + @Nullable + public Map rendererPriorityPolicy = null; + public Boolean useShouldInterceptRequest = false; + public Boolean useOnRenderProcessGone = false; + public Boolean disableDefaultErrorPage = false; + public Boolean useHybridComposition = true; + @Nullable + public String verticalScrollbarThumbColor; + @Nullable + public String verticalScrollbarTrackColor; + @Nullable + public String horizontalScrollbarThumbColor; + @Nullable + public String horizontalScrollbarTrackColor; + public Boolean algorithmicDarkeningAllowed = false; + public Boolean enterpriseAuthenticationAppLinkPolicyEnabled = true; + @Nullable + public Map webViewAssetLoader; + @Nullable + public byte[] defaultVideoPoster; + @Nullable + public Set requestedWithHeaderOriginAllowList; + + @NonNull + @Override + public InAppWebViewSettings parse(@NonNull Map settings) { + for (Map.Entry pair : settings.entrySet()) { + String key = pair.getKey(); + Object value = pair.getValue(); + if (value == null) { + continue; + } + + switch (key) { + case "useShouldOverrideUrlLoading": + useShouldOverrideUrlLoading = (Boolean) value; + break; + case "useOnLoadResource": + useOnLoadResource = (Boolean) value; + break; + case "useOnDownloadStart": + useOnDownloadStart = (Boolean) value; + break; + case "clearCache": + clearCache = (Boolean) value; + break; + case "gopeedProfileId": + gopeedProfileId = (String) value; + break; + case "userAgent": + userAgent = (String) value; + break; + case "applicationNameForUserAgent": + applicationNameForUserAgent = (String) value; + break; + case "javaScriptEnabled": + javaScriptEnabled = (Boolean) value; + break; + case "javaScriptCanOpenWindowsAutomatically": + javaScriptCanOpenWindowsAutomatically = (Boolean) value; + break; + case "mediaPlaybackRequiresUserGesture": + mediaPlaybackRequiresUserGesture = (Boolean) value; + break; + case "minimumFontSize": + minimumFontSize = (Integer) value; + break; + case "verticalScrollBarEnabled": + verticalScrollBarEnabled = (Boolean) value; + break; + case "horizontalScrollBarEnabled": + horizontalScrollBarEnabled = (Boolean) value; + break; + case "resourceCustomSchemes": + resourceCustomSchemes = (List) value; + break; + case "contentBlockers": + contentBlockers = (List>>) value; + break; + case "preferredContentMode": + preferredContentMode = (Integer) value; + break; + case "useShouldInterceptAjaxRequest": + useShouldInterceptAjaxRequest = (Boolean) value; + break; + case "interceptOnlyAsyncAjaxRequests": + interceptOnlyAsyncAjaxRequests = (Boolean) value; + break; + case "useShouldInterceptFetchRequest": + useShouldInterceptFetchRequest = (Boolean) value; + break; + case "incognito": + incognito = (Boolean) value; + break; + case "cacheEnabled": + cacheEnabled = (Boolean) value; + break; + case "transparentBackground": + transparentBackground = (Boolean) value; + break; + case "disableVerticalScroll": + disableVerticalScroll = (Boolean) value; + break; + case "disableHorizontalScroll": + disableHorizontalScroll = (Boolean) value; + break; + case "disableContextMenu": + disableContextMenu = (Boolean) value; + break; + case "textZoom": + textZoom = (Integer) value; + break; + case "clearSessionCache": + clearSessionCache = (Boolean) value; + break; + case "builtInZoomControls": + builtInZoomControls = (Boolean) value; + break; + case "displayZoomControls": + displayZoomControls = (Boolean) value; + break; + case "supportZoom": + supportZoom = (Boolean) value; + break; + case "databaseEnabled": + databaseEnabled = (Boolean) value; + break; + case "domStorageEnabled": + domStorageEnabled = (Boolean) value; + break; + case "useWideViewPort": + useWideViewPort = (Boolean) value; + break; + case "safeBrowsingEnabled": + safeBrowsingEnabled = (Boolean) value; + break; + case "mixedContentMode": + mixedContentMode = (Integer) value; + break; + case "allowContentAccess": + allowContentAccess = (Boolean) value; + break; + case "allowFileAccess": + allowFileAccess = (Boolean) value; + break; + case "allowFileAccessFromFileURLs": + allowFileAccessFromFileURLs = (Boolean) value; + break; + case "allowUniversalAccessFromFileURLs": + allowUniversalAccessFromFileURLs = (Boolean) value; + break; + case "appCachePath": + appCachePath = (String) value; + break; + case "blockNetworkImage": + blockNetworkImage = (Boolean) value; + break; + case "blockNetworkLoads": + blockNetworkLoads = (Boolean) value; + break; + case "cacheMode": + cacheMode = (Integer) value; + break; + case "cursiveFontFamily": + cursiveFontFamily = (String) value; + break; + case "defaultFixedFontSize": + defaultFixedFontSize = (Integer) value; + break; + case "defaultFontSize": + defaultFontSize = (Integer) value; + break; + case "defaultTextEncodingName": + defaultTextEncodingName = (String) value; + break; + case "disabledActionModeMenuItems": + disabledActionModeMenuItems = (Integer) value; + break; + case "fantasyFontFamily": + fantasyFontFamily = (String) value; + break; + case "fixedFontFamily": + fixedFontFamily = (String) value; + break; + case "forceDark": + forceDark = (Integer) value; + break; + case "forceDarkStrategy": + forceDarkStrategy = (Integer) value; + break; + case "geolocationEnabled": + geolocationEnabled = (Boolean) value; + break; + case "layoutAlgorithm": + setLayoutAlgorithm((String) value); + break; + case "loadWithOverviewMode": + loadWithOverviewMode = (Boolean) value; + break; + case "loadsImagesAutomatically": + loadsImagesAutomatically = (Boolean) value; + break; + case "minimumLogicalFontSize": + minimumLogicalFontSize = (Integer) value; + break; + case "initialScale": + initialScale = (Integer) value; + break; + case "needInitialFocus": + needInitialFocus = (Boolean) value; + break; + case "offscreenPreRaster": + offscreenPreRaster = (Boolean) value; + break; + case "sansSerifFontFamily": + sansSerifFontFamily = (String) value; + break; + case "serifFontFamily": + serifFontFamily = (String) value; + break; + case "standardFontFamily": + standardFontFamily = (String) value; + break; + case "saveFormData": + saveFormData = (Boolean) value; + break; + case "thirdPartyCookiesEnabled": + thirdPartyCookiesEnabled = (Boolean) value; + break; + case "hardwareAcceleration": + hardwareAcceleration = (Boolean) value; + break; + case "supportMultipleWindows": + supportMultipleWindows = (Boolean) value; + break; + case "regexToCancelSubFramesLoading": + regexToCancelSubFramesLoading = (String) value; + break; + case "overScrollMode": + overScrollMode = (Integer) value; + break; + case "networkAvailable": + networkAvailable = (Boolean) value; + break; + case "scrollBarStyle": + scrollBarStyle = (Integer) value; + break; + case "verticalScrollbarPosition": + verticalScrollbarPosition = (Integer) value; + break; + case "scrollBarDefaultDelayBeforeFade": + scrollBarDefaultDelayBeforeFade = (Integer) value; + break; + case "scrollbarFadingEnabled": + scrollbarFadingEnabled = (Boolean) value; + break; + case "scrollBarFadeDuration": + scrollBarFadeDuration = (Integer) value; + break; + case "rendererPriorityPolicy": + rendererPriorityPolicy = (Map) value; + break; + case "useShouldInterceptRequest": + useShouldInterceptRequest = (Boolean) value; + break; + case "useOnRenderProcessGone": + useOnRenderProcessGone = (Boolean) value; + break; + case "disableDefaultErrorPage": + disableDefaultErrorPage = (Boolean) value; + break; + case "useHybridComposition": + useHybridComposition = (Boolean) value; + break; + case "verticalScrollbarThumbColor": + verticalScrollbarThumbColor = (String) value; + break; + case "verticalScrollbarTrackColor": + verticalScrollbarTrackColor = (String) value; + break; + case "horizontalScrollbarThumbColor": + horizontalScrollbarThumbColor = (String) value; + break; + case "horizontalScrollbarTrackColor": + horizontalScrollbarTrackColor = (String) value; + break; + case "algorithmicDarkeningAllowed": + algorithmicDarkeningAllowed = (Boolean) value; + break; + case "enterpriseAuthenticationAppLinkPolicyEnabled": + enterpriseAuthenticationAppLinkPolicyEnabled = (Boolean) value; + break; + case "allowBackgroundAudioPlaying": + allowBackgroundAudioPlaying = (Boolean) value; + break; + case "webViewAssetLoader": + webViewAssetLoader = (Map) value; + break; + case "defaultVideoPoster": + defaultVideoPoster = (byte[]) value; + break; + case "requestedWithHeaderOriginAllowList": + requestedWithHeaderOriginAllowList = new HashSet<>((List) value); + break; + } + } + + return this; + } + + @NonNull + @Override + public Map toMap() { + Map settings = new HashMap<>(); + settings.put("useShouldOverrideUrlLoading", useShouldOverrideUrlLoading); + settings.put("useOnLoadResource", useOnLoadResource); + settings.put("useOnDownloadStart", useOnDownloadStart); + settings.put("clearCache", clearCache); + settings.put("userAgent", userAgent); + settings.put("applicationNameForUserAgent", applicationNameForUserAgent); + settings.put("javaScriptEnabled", javaScriptEnabled); + settings.put("javaScriptCanOpenWindowsAutomatically", javaScriptCanOpenWindowsAutomatically); + settings.put("mediaPlaybackRequiresUserGesture", mediaPlaybackRequiresUserGesture); + settings.put("minimumFontSize", minimumFontSize); + settings.put("verticalScrollBarEnabled", verticalScrollBarEnabled); + settings.put("horizontalScrollBarEnabled", horizontalScrollBarEnabled); + settings.put("resourceCustomSchemes", resourceCustomSchemes); + settings.put("contentBlockers", contentBlockers); + settings.put("preferredContentMode", preferredContentMode); + settings.put("useShouldInterceptAjaxRequest", useShouldInterceptAjaxRequest); + settings.put("interceptOnlyAsyncAjaxRequests", interceptOnlyAsyncAjaxRequests); + settings.put("useShouldInterceptFetchRequest", useShouldInterceptFetchRequest); + settings.put("incognito", incognito); + settings.put("cacheEnabled", cacheEnabled); + settings.put("transparentBackground", transparentBackground); + settings.put("disableVerticalScroll", disableVerticalScroll); + settings.put("disableHorizontalScroll", disableHorizontalScroll); + settings.put("disableContextMenu", disableContextMenu); + settings.put("textZoom", textZoom); + settings.put("clearSessionCache", clearSessionCache); + settings.put("builtInZoomControls", builtInZoomControls); + settings.put("displayZoomControls", displayZoomControls); + settings.put("supportZoom", supportZoom); + settings.put("databaseEnabled", databaseEnabled); + settings.put("domStorageEnabled", domStorageEnabled); + settings.put("useWideViewPort", useWideViewPort); + settings.put("safeBrowsingEnabled", safeBrowsingEnabled); + settings.put("mixedContentMode", mixedContentMode); + settings.put("allowContentAccess", allowContentAccess); + settings.put("allowFileAccess", allowFileAccess); + settings.put("allowFileAccessFromFileURLs", allowFileAccessFromFileURLs); + settings.put("allowUniversalAccessFromFileURLs", allowUniversalAccessFromFileURLs); + settings.put("appCachePath", appCachePath); + settings.put("blockNetworkImage", blockNetworkImage); + settings.put("blockNetworkLoads", blockNetworkLoads); + settings.put("cacheMode", cacheMode); + settings.put("cursiveFontFamily", cursiveFontFamily); + settings.put("defaultFixedFontSize", defaultFixedFontSize); + settings.put("defaultFontSize", defaultFontSize); + settings.put("defaultTextEncodingName", defaultTextEncodingName); + settings.put("disabledActionModeMenuItems", disabledActionModeMenuItems); + settings.put("fantasyFontFamily", fantasyFontFamily); + settings.put("fixedFontFamily", fixedFontFamily); + settings.put("forceDark", forceDark); + settings.put("forceDarkStrategy", forceDarkStrategy); + settings.put("geolocationEnabled", geolocationEnabled); + settings.put("layoutAlgorithm", getLayoutAlgorithm()); + settings.put("loadWithOverviewMode", loadWithOverviewMode); + settings.put("loadsImagesAutomatically", loadsImagesAutomatically); + settings.put("minimumLogicalFontSize", minimumLogicalFontSize); + settings.put("initialScale", initialScale); + settings.put("needInitialFocus", needInitialFocus); + settings.put("offscreenPreRaster", offscreenPreRaster); + settings.put("sansSerifFontFamily", sansSerifFontFamily); + settings.put("serifFontFamily", serifFontFamily); + settings.put("standardFontFamily", standardFontFamily); + settings.put("saveFormData", saveFormData); + settings.put("thirdPartyCookiesEnabled", thirdPartyCookiesEnabled); + settings.put("hardwareAcceleration", hardwareAcceleration); + settings.put("supportMultipleWindows", supportMultipleWindows); + settings.put("regexToCancelSubFramesLoading", regexToCancelSubFramesLoading); + settings.put("overScrollMode", overScrollMode); + settings.put("networkAvailable", networkAvailable); + settings.put("scrollBarStyle", scrollBarStyle); + settings.put("verticalScrollbarPosition", verticalScrollbarPosition); + settings.put("scrollBarDefaultDelayBeforeFade", scrollBarDefaultDelayBeforeFade); + settings.put("scrollbarFadingEnabled", scrollbarFadingEnabled); + settings.put("scrollBarFadeDuration", scrollBarFadeDuration); + settings.put("rendererPriorityPolicy", rendererPriorityPolicy); + settings.put("useShouldInterceptRequest", useShouldInterceptRequest); + settings.put("useOnRenderProcessGone", useOnRenderProcessGone); + settings.put("disableDefaultErrorPage", disableDefaultErrorPage); + settings.put("useHybridComposition", useHybridComposition); + settings.put("verticalScrollbarThumbColor", verticalScrollbarThumbColor); + settings.put("verticalScrollbarTrackColor", verticalScrollbarTrackColor); + settings.put("horizontalScrollbarThumbColor", horizontalScrollbarThumbColor); + settings.put("horizontalScrollbarTrackColor", horizontalScrollbarTrackColor); + settings.put("algorithmicDarkeningAllowed", algorithmicDarkeningAllowed); + settings.put("enterpriseAuthenticationAppLinkPolicyEnabled", enterpriseAuthenticationAppLinkPolicyEnabled); + settings.put("allowBackgroundAudioPlaying", allowBackgroundAudioPlaying); + settings.put("defaultVideoPoster", defaultVideoPoster); + settings.put("requestedWithHeaderOriginAllowList", + requestedWithHeaderOriginAllowList != null ? new ArrayList<>(requestedWithHeaderOriginAllowList) : null); + return settings; + } + + @SuppressLint("RestrictedApi") + @NonNull + @Override + public Map getRealSettings(@NonNull InAppWebViewInterface inAppWebView) { + Map realSettings = toMap(); + if (inAppWebView instanceof InAppWebView) { + InAppWebView webView = (InAppWebView) inAppWebView; + WebSettings settings = webView.getSettings(); + realSettings.put("userAgent", settings.getUserAgentString()); + realSettings.put("javaScriptEnabled", settings.getJavaScriptEnabled()); + realSettings.put("javaScriptCanOpenWindowsAutomatically", settings.getJavaScriptCanOpenWindowsAutomatically()); + realSettings.put("mediaPlaybackRequiresUserGesture", settings.getMediaPlaybackRequiresUserGesture()); + realSettings.put("minimumFontSize", settings.getMinimumFontSize()); + realSettings.put("verticalScrollBarEnabled", webView.isVerticalScrollBarEnabled()); + realSettings.put("horizontalScrollBarEnabled", webView.isHorizontalScrollBarEnabled()); + realSettings.put("textZoom", settings.getTextZoom()); + realSettings.put("builtInZoomControls", settings.getBuiltInZoomControls()); + realSettings.put("supportZoom", settings.supportZoom()); + realSettings.put("displayZoomControls", settings.getDisplayZoomControls()); + realSettings.put("databaseEnabled", settings.getDatabaseEnabled()); + realSettings.put("domStorageEnabled", settings.getDomStorageEnabled()); + realSettings.put("useWideViewPort", settings.getUseWideViewPort()); + if (WebViewFeature.isFeatureSupported(WebViewFeature.SAFE_BROWSING_ENABLE)) { + realSettings.put("safeBrowsingEnabled", WebSettingsCompat.getSafeBrowsingEnabled(settings)); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + realSettings.put("safeBrowsingEnabled", settings.getSafeBrowsingEnabled()); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + realSettings.put("mixedContentMode", settings.getMixedContentMode()); + } + realSettings.put("allowContentAccess", settings.getAllowContentAccess()); + realSettings.put("allowFileAccess", settings.getAllowFileAccess()); + realSettings.put("allowFileAccessFromFileURLs", settings.getAllowFileAccessFromFileURLs()); + realSettings.put("allowUniversalAccessFromFileURLs", settings.getAllowUniversalAccessFromFileURLs()); + realSettings.put("blockNetworkImage", settings.getBlockNetworkImage()); + realSettings.put("blockNetworkLoads", settings.getBlockNetworkLoads()); + realSettings.put("cacheMode", settings.getCacheMode()); + realSettings.put("cursiveFontFamily", settings.getCursiveFontFamily()); + realSettings.put("defaultFixedFontSize", settings.getDefaultFixedFontSize()); + realSettings.put("defaultFontSize", settings.getDefaultFontSize()); + realSettings.put("defaultTextEncodingName", settings.getDefaultTextEncodingName()); + if (WebViewFeature.isFeatureSupported(WebViewFeature.DISABLED_ACTION_MODE_MENU_ITEMS)) { + realSettings.put("disabledActionModeMenuItems", WebSettingsCompat.getDisabledActionModeMenuItems(settings)); + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + realSettings.put("disabledActionModeMenuItems", settings.getDisabledActionModeMenuItems()); + } + realSettings.put("fantasyFontFamily", settings.getFantasyFontFamily()); + realSettings.put("fixedFontFamily", settings.getFixedFontFamily()); + if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { + realSettings.put("forceDark", WebSettingsCompat.getForceDark(settings)); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + realSettings.put("forceDark", settings.getForceDark()); + } + if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) { + realSettings.put("forceDarkStrategy", WebSettingsCompat.getForceDarkStrategy(settings)); + } + realSettings.put("layoutAlgorithm", settings.getLayoutAlgorithm().name()); + realSettings.put("loadWithOverviewMode", settings.getLoadWithOverviewMode()); + realSettings.put("loadsImagesAutomatically", settings.getLoadsImagesAutomatically()); + realSettings.put("minimumLogicalFontSize", settings.getMinimumLogicalFontSize()); + if (WebViewFeature.isFeatureSupported(WebViewFeature.OFF_SCREEN_PRERASTER)) { + realSettings.put("offscreenPreRaster", WebSettingsCompat.getOffscreenPreRaster(settings)); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + realSettings.put("offscreenPreRaster", settings.getOffscreenPreRaster()); + } + realSettings.put("sansSerifFontFamily", settings.getSansSerifFontFamily()); + realSettings.put("serifFontFamily", settings.getSerifFontFamily()); + realSettings.put("standardFontFamily", settings.getStandardFontFamily()); + realSettings.put("saveFormData", settings.getSaveFormData()); + realSettings.put("supportMultipleWindows", settings.supportMultipleWindows()); + realSettings.put("overScrollMode", webView.getOverScrollMode()); + realSettings.put("scrollBarStyle", webView.getScrollBarStyle()); + realSettings.put("verticalScrollbarPosition", webView.getVerticalScrollbarPosition()); + realSettings.put("scrollBarDefaultDelayBeforeFade", webView.getScrollBarDefaultDelayBeforeFade()); + realSettings.put("scrollbarFadingEnabled", webView.isScrollbarFadingEnabled()); + realSettings.put("scrollBarFadeDuration", webView.getScrollBarFadeDuration()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Map rendererPriorityPolicy = new HashMap<>(); + rendererPriorityPolicy.put("rendererRequestedPriority", webView.getRendererRequestedPriority()); + rendererPriorityPolicy.put("waivedWhenNotVisible", webView.getRendererPriorityWaivedWhenNotVisible()); + realSettings.put("rendererPriorityPolicy", rendererPriorityPolicy); + } + if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + realSettings.put("algorithmicDarkeningAllowed", WebSettingsCompat.isAlgorithmicDarkeningAllowed(settings)); + } + if (WebViewFeature.isFeatureSupported(WebViewFeature.ENTERPRISE_AUTHENTICATION_APP_LINK_POLICY)) { + realSettings.put("enterpriseAuthenticationAppLinkPolicyEnabled", WebSettingsCompat.getEnterpriseAuthenticationAppLinkPolicyEnabled(settings)); + } + if (WebViewFeature.isFeatureSupported(WebViewFeature.REQUESTED_WITH_HEADER_ALLOW_LIST)) { + realSettings.put("requestedWithHeaderOriginAllowList", new ArrayList<>(WebSettingsCompat.getRequestedWithHeaderOriginAllowList(settings))); + } + } + return realSettings; + } + + private void setLayoutAlgorithm(String value) { + if (value != null) { + switch (value) { + case "NARROW_COLUMNS": + layoutAlgorithm = NARROW_COLUMNS; + case "NORMAL": + layoutAlgorithm = NORMAL; + case "TEXT_AUTOSIZING": + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + layoutAlgorithm = WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING; + } else { + layoutAlgorithm = NORMAL; + } + break; + } + } + } + + private String getLayoutAlgorithm() { + if (layoutAlgorithm != null) { + switch (layoutAlgorithm) { + case NORMAL: + return "NORMAL"; + case TEXT_AUTOSIZING: + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + return "TEXT_AUTOSIZING"; + } else { + return "NORMAL"; + } + case NARROW_COLUMNS: + return "NARROW_COLUMNS"; + } + } + return null; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InputAwareWebView.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InputAwareWebView.java new file mode 100755 index 00000000..e4c5e96d --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InputAwareWebView.java @@ -0,0 +1,273 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview; + +import static android.content.Context.INPUT_METHOD_SERVICE; + +import android.content.Context; +import android.graphics.Rect; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.webkit.WebView; +import android.widget.ListPopupWindow; + +import androidx.annotation.Nullable; + +/** + * A WebView subclass that mirrors the same implementation hacks that the system WebView does in + * order to correctly create an InputConnection. + * + * These hacks are only needed in Android versions below N and exist to create an InputConnection + * on the WebView's dedicated input, or IME, thread. The majority of this proxying logic is in + * https://github.com/flutter/plugins/blob/main/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java + */ +public class InputAwareWebView extends WebView { + private static final String LOG_TAG = "InputAwareWebView"; + @Nullable + public View containerView; + private View threadedInputConnectionProxyView; + private ThreadedInputConnectionProxyAdapterView proxyAdapterView; + private boolean useHybridComposition = false; + + public InputAwareWebView(Context context, @Nullable View containerView, Boolean useHybridComposition) { + super(context); + this.containerView = containerView; + this.useHybridComposition = useHybridComposition == null ? false : useHybridComposition; + } + + public InputAwareWebView(Context context, AttributeSet attrs) { + super(context, attrs); + this.containerView = null; + } + + public InputAwareWebView(Context context) { + super(context); + this.containerView = null; + } + + public InputAwareWebView(Context context, AttributeSet attrs, int defaultStyle) { + super(context, attrs, defaultStyle); + this.containerView = null; + } + + public void setContainerView(View containerView) { + this.containerView = containerView; + + if (proxyAdapterView == null) { + return; + } + + Log.w(LOG_TAG, "The containerView has changed while the proxyAdapterView exists."); + if (containerView != null) { + setInputConnectionTarget(proxyAdapterView); + } + } + + /** + * Set our proxy adapter view to use its cached input connection instead of creating new ones. + * + *

This is used to avoid losing our input connection when the virtual display is resized. + */ + public void lockInputConnection() { + if (proxyAdapterView == null) { + return; + } + + proxyAdapterView.setLocked(true); + } + + /** Sets the proxy adapter view back to its default behavior. */ + public void unlockInputConnection() { + if (proxyAdapterView == null) { + return; + } + + proxyAdapterView.setLocked(false); + } + + /** Restore the original InputConnection, if needed. */ + void dispose() { + if (useHybridComposition) { + return; + } + resetInputConnection(); + } + + /** + * Creates an InputConnection from the IME thread when needed. + * + *

We only need to create a {@link ThreadedInputConnectionProxyAdapterView} and create an + * InputConnectionProxy on the IME thread when WebView is doing the same thing. So we rely on the + * system calling this method for WebView's proxy view in order to know when we need to create our + * own. + * + *

This method would normally be called for any View that used the InputMethodManager. We rely + * on flutter/engine filtering the calls we receive down to the ones in our hierarchy and the + * system WebView in order to know whether or not the system WebView expects an InputConnection on + * the IME thread. + */ + @Override + public boolean checkInputConnectionProxy(final View view) { + if (useHybridComposition) { + return super.checkInputConnectionProxy(view); + } + // Check to see if the view param is WebView's ThreadedInputConnectionProxyView. + View previousProxy = threadedInputConnectionProxyView; + threadedInputConnectionProxyView = view; + if (previousProxy == view) { + // This isn't a new ThreadedInputConnectionProxyView. Ignore it. + return super.checkInputConnectionProxy(view); + } + if (containerView == null) { + Log.e( + LOG_TAG, + "Can't create a proxy view because there's no container view. Text input may not work."); + return super.checkInputConnectionProxy(view); + } + + // We've never seen this before, so we make the assumption that this is WebView's + // ThreadedInputConnectionProxyView. We are making the assumption that the only view that could + // possibly be interacting with the IMM here is WebView's ThreadedInputConnectionProxyView. + proxyAdapterView = + new ThreadedInputConnectionProxyAdapterView( + /*containerView=*/ containerView, + /*targetView=*/ view, + /*imeHandler=*/ view.getHandler()); + setInputConnectionTarget(/*targetView=*/ proxyAdapterView); + return super.checkInputConnectionProxy(view); + } + + /** + * Ensure that input creation happens back on {@link #containerView}'s thread once this view no + * longer has focus. + * + *

The logic in {@link #checkInputConnectionProxy} forces input creation to happen on Webview's + * thread for all connections. We undo it here so users will be able to go back to typing in + * Flutter UIs as expected. + */ + @Override + public void clearFocus() { + super.clearFocus(); + + if (useHybridComposition) { + return; + } + resetInputConnection(); + } + + /** + * Ensure that input creation happens back on {@link #containerView}. + * + *

The logic in {@link #checkInputConnectionProxy} forces input creation to happen on Webview's + * thread for all connections. We undo it here so users will be able to go back to typing in + * Flutter UIs as expected. + */ + private void resetInputConnection() { + if (proxyAdapterView == null) { + // No need to reset the InputConnection to the default thread if we've never changed it. + return; + } + if (containerView == null) { + Log.e(LOG_TAG, "Can't reset the input connection to the container view because there is none."); + return; + } + setInputConnectionTarget(/*targetView=*/ containerView); + } + + /** + * This is the crucial trick that gets the InputConnection creation to happen on the correct + * thread pre Android N. + * https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnectionFactory.java?l=169&rcl=f0698ee3e4483fad5b0c34159276f71cfaf81f3a + * + *

{@code targetView} should have a {@link View#getHandler} method with the thread that future + * InputConnections should be created on. + */ + private void setInputConnectionTarget(final View targetView) { + if (containerView == null) { + Log.e( + LOG_TAG, + "Can't set the input connection target because there is no containerView to use as a handler."); + return; + } + + targetView.requestFocus(); + containerView.post( + new Runnable() { + @Override + public void run() { + if (containerView == null) { + Log.e( + LOG_TAG, + "Can't set the input connection target because there is no containerView to use as a handler."); + return; + } + + InputMethodManager imm = + (InputMethodManager) getContext().getSystemService(INPUT_METHOD_SERVICE); + // This is a hack to make InputMethodManager believe that the target view now has focus. + // As a result, InputMethodManager will think that targetView is focused, and will call + // getHandler() of the view when creating input connection. + + // Step 1: Set targetView as InputMethodManager#mNextServedView. This does not affect + // the real window focus. + targetView.onWindowFocusChanged(true); + + // Step 2: Have InputMethodManager focus in on targetView. As a result, IMM will call + // onCreateInputConnection() on targetView on the same thread as + // targetView.getHandler(). It will also call subsequent InputConnection methods on this + // thread. This is the IME thread in cases where targetView is our proxyAdapterView. + + // TODO (ALexVincent525): Currently only prompt has been tested, still needs more test cases. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + imm.isActive(containerView); + } + } + }); + } + + @Override + protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { + if (useHybridComposition) { + super.onFocusChanged(focused, direction, previouslyFocusedRect); + return; + } + // This works around a crash when old (<67.0.3367.0) Chromium versions are used. + + // Prior to Chromium 67.0.3367 the following sequence happens when a select drop down is shown + // on tablets: + // + // - WebView is calling ListPopupWindow#show + // - buildDropDown is invoked, which sets mDropDownList to a DropDownListView. + // - showAsDropDown is invoked - resulting in mDropDownList being added to the window and is + // also synchronously performing the following sequence: + // - WebView's focus change listener is loosing focus (as mDropDownList got it) + // - WebView is hiding all popups (as it lost focus) + // - WebView's SelectPopupDropDown#hide is invoked. + // - DropDownPopupWindow#dismiss is invoked setting mDropDownList to null. + // - mDropDownList#setSelection is invoked and is throwing a NullPointerException (as we just set mDropDownList to null). + // + // To workaround this, we drop the problematic focus lost call. + // See more details on: https://github.com/flutter/flutter/issues/54164 + // + // We don't do this after Android P as it shipped with a new enough WebView version, and it's + // better to not do this on all future Android versions in case DropDownListView's code changes. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P + && isCalledFromListPopupWindowShow() + && !focused) { + return; + } + super.onFocusChanged(focused, direction, previouslyFocusedRect); + } + + private boolean isCalledFromListPopupWindowShow() { + StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); + for (StackTraceElement stackTraceElement : stackTraceElements) { + if (stackTraceElement.getClassName().equals(ListPopupWindow.class.getCanonicalName()) + && stackTraceElement.getMethodName().equals("show")) { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/ThreadedInputConnectionProxyAdapterView.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/ThreadedInputConnectionProxyAdapterView.java new file mode 100755 index 00000000..91a45465 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/ThreadedInputConnectionProxyAdapterView.java @@ -0,0 +1,99 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview; + +import android.os.Handler; +import android.os.IBinder; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; + +/** + * A fake View only exposed to InputMethodManager. + * + * https://github.com/flutter/plugins/blob/main/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java + */ +final class ThreadedInputConnectionProxyAdapterView extends View { + final Handler imeHandler; + final IBinder windowToken; + final View containerView; + final View rootView; + final View targetView; + + private boolean triggerDelayed = true; + private boolean isLocked = false; + private InputConnection cachedConnection; + + ThreadedInputConnectionProxyAdapterView(View containerView, View targetView, Handler imeHandler) { + super(containerView.getContext()); + this.imeHandler = imeHandler; + this.containerView = containerView; + this.targetView = targetView; + windowToken = containerView.getWindowToken(); + rootView = containerView.getRootView(); + setFocusable(true); + setFocusableInTouchMode(true); + setVisibility(VISIBLE); + } + + /** Returns whether or not this is currently asynchronously acquiring an input connection. */ + boolean isTriggerDelayed() { + return triggerDelayed; + } + + /** Sets whether or not this should use its previously cached input connection. */ + void setLocked(boolean locked) { + isLocked = locked; + } + + /** + * This is expected to be called on the IME thread. See the setup required for this in {@link + * InputAwareWebView#checkInputConnectionProxy(View)}. + * + *

Delegates to ThreadedInputConnectionProxyView to get WebView's input connection. + */ + @Override + public InputConnection onCreateInputConnection(final EditorInfo outAttrs) { + triggerDelayed = false; + InputConnection inputConnection = + (isLocked) ? cachedConnection : targetView.onCreateInputConnection(outAttrs); + triggerDelayed = true; + cachedConnection = inputConnection; + return inputConnection; + } + + @Override + public boolean checkInputConnectionProxy(View view) { + return true; + } + + @Override + public boolean hasWindowFocus() { + // None of our views here correctly report they have window focus because of how we're embedding + // the platform view inside of a virtual display. + return true; + } + + @Override + public View getRootView() { + return rootView; + } + + @Override + public boolean onCheckIsTextEditor() { + return true; + } + + @Override + public boolean isFocused() { + return true; + } + + @Override + public IBinder getWindowToken() { + return windowToken; + } + + @Override + public Handler getHandler() { + return imeHandler; + } +} \ No newline at end of file diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageChannel.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageChannel.java new file mode 100644 index 00000000..b7d059c2 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageChannel.java @@ -0,0 +1,166 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.web_message; + +import android.webkit.ValueCallback; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.WebMessageCompat; +import androidx.webkit.WebMessagePortCompat; +import androidx.webkit.WebViewCompat; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.JavaScriptBridgeJS; +import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebMessageCompatExt; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebMessagePortCompatExt; +import com.pichillilorenzo.flutter_inappwebview_android.webview.InAppWebViewInterface; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebMessagePort; +import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebView; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.flutter.plugin.common.MethodChannel; + +public class WebMessageChannel implements Disposable { + protected static final String LOG_TAG = "WebMessageChannel"; + public static final String METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_web_message_channel_"; + + @NonNull + public String id; + @Nullable + public WebMessageChannelChannelDelegate channelDelegate; + public final List compatPorts; + public final List ports; + @Nullable + public InAppWebViewInterface webView; + + public WebMessageChannel(@NonNull String id, @NonNull InAppWebViewInterface webView) { + this.id = id; + final MethodChannel channel = new MethodChannel(webView.getPlugin().messenger, METHOD_CHANNEL_NAME_PREFIX + id); + this.channelDelegate = new WebMessageChannelChannelDelegate(this, channel); + if (webView instanceof InAppWebView) { + this.compatPorts = new ArrayList<>(Arrays.asList(WebViewCompat.createWebMessageChannel((InAppWebView) webView))); + this.ports = new ArrayList<>(); + } else { + this.ports = Arrays.asList(new WebMessagePort("port1", this), new WebMessagePort("port2", this)); + this.compatPorts = new ArrayList<>(); + } + this.webView = webView; + } + + public void initJsInstance(InAppWebViewInterface webView, final ValueCallback callback) { + if (webView != null) { + final WebMessageChannel webMessageChannel = this; + webView.evaluateJavascript("(function() {" + + JavaScriptBridgeJS.WEB_MESSAGE_CHANNELS_VARIABLE_NAME + "['" + webMessageChannel.id + "'] = new MessageChannel();" + + "})();", null, new ValueCallback() { + @Override + public void onReceiveValue(String value) { + callback.onReceiveValue(webMessageChannel); + } + }); + } else { + callback.onReceiveValue(this); + } + } + + public void setWebMessageCallbackForInAppWebView(final int index, @NonNull MethodChannel.Result result) { + if (webView != null && compatPorts.size() > 0 && + WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_PORT_SET_MESSAGE_CALLBACK)) { + final WebMessagePortCompat webMessagePort = compatPorts.get(index); + final WebMessageChannel webMessageChannel = this; + try { + webMessagePort.setWebMessageCallback(new WebMessagePortCompat.WebMessageCallbackCompat() { + @Override + public void onMessage(@NonNull WebMessagePortCompat port, @Nullable WebMessageCompat message) { + super.onMessage(port, message); + webMessageChannel.onMessage(index, message != null ? WebMessageCompatExt.fromMapWebMessageCompat(message) : null); + } + }); + result.success(true); + } catch (Exception e) { + result.error(LOG_TAG, e.getMessage(), null); + } + } else { + result.success(true); + } + } + + public void postMessageForInAppWebView(final Integer index, @NonNull WebMessageCompatExt message, @NonNull MethodChannel.Result result) { + if (webView != null && compatPorts.size() > 0 && + WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_PORT_POST_MESSAGE)) { + WebMessagePortCompat port = compatPorts.get(index); + List webMessagePorts = new ArrayList<>(); + List portsExt = message.getPorts(); + if (portsExt != null) { + for (WebMessagePortCompatExt portExt : portsExt) { + WebMessageChannel webMessageChannel = webView.getWebMessageChannels().get(portExt.getWebMessageChannelId()); + if (webMessageChannel != null) { + webMessagePorts.add(webMessageChannel.compatPorts.get(portExt.getIndex())); + } + } + } + Object data = message.getData(); + try { + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER) && data != null && + message.getType() == WebMessageCompat.TYPE_ARRAY_BUFFER) { + port.postMessage(new WebMessageCompat((byte[]) data, webMessagePorts.toArray(new WebMessagePortCompat[0]))); + } else { + port.postMessage(new WebMessageCompat(data != null ? data.toString() : null, webMessagePorts.toArray(new WebMessagePortCompat[0]))); + } + result.success(true); + } catch (Exception e) { + result.error(LOG_TAG, e.getMessage(), null); + } + } else { + result.success(true); + } + } + + public void closeForInAppWebView(Integer index, @NonNull MethodChannel.Result result) { + if (webView != null && compatPorts.size() > 0 && + WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_PORT_CLOSE)) { + WebMessagePortCompat port = compatPorts.get(index); + try { + port.close(); + result.success(true); + } catch (Exception e) { + result.error(LOG_TAG, e.getMessage(), null); + } + } else { + result.success(true); + } + } + + public void onMessage(int index, @Nullable WebMessageCompatExt message) { + if (channelDelegate != null) { + channelDelegate.onMessage(index, message); + } + } + + public Map toMap() { + Map webMessageChannelMap = new HashMap<>(); + webMessageChannelMap.put("id", id); + return webMessageChannelMap; + } + + public void dispose() { + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_PORT_CLOSE)) { + for (WebMessagePortCompat port : compatPorts) { + try { + port.close(); + } catch (Exception ignored) {} + } + } + if (channelDelegate != null) { + channelDelegate.dispose(); + channelDelegate = null; + } + compatPorts.clear(); + webView = null; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageChannelChannelDelegate.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageChannelChannelDelegate.java new file mode 100644 index 00000000..f801a82d --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageChannelChannelDelegate.java @@ -0,0 +1,72 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.web_message; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebMessageCompatExt; +import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebView; + +import java.util.HashMap; +import java.util.Map; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class WebMessageChannelChannelDelegate extends ChannelDelegateImpl { + @Nullable + private WebMessageChannel webMessageChannel; + + public WebMessageChannelChannelDelegate(@NonNull WebMessageChannel webMessageChannel, @NonNull MethodChannel channel) { + super(channel); + this.webMessageChannel = webMessageChannel; + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + switch (call.method) { + case "setWebMessageCallback": + if (webMessageChannel != null && webMessageChannel.webView instanceof InAppWebView) { + final Integer index = (Integer) call.argument("index"); + webMessageChannel.setWebMessageCallbackForInAppWebView(index, result); + } else { + result.success(false); + } + break; + case "postMessage": + if (webMessageChannel != null && webMessageChannel.webView instanceof InAppWebView) { + final Integer index = (Integer) call.argument("index"); + WebMessageCompatExt message = WebMessageCompatExt.fromMap((Map) call.argument("message")); + webMessageChannel.postMessageForInAppWebView(index, message, result); + } else { + result.success(false); + } + break; + case "close": + if (webMessageChannel != null && webMessageChannel.webView instanceof InAppWebView) { + Integer index = (Integer) call.argument("index"); + webMessageChannel.closeForInAppWebView(index, result); + } else { + result.success(false); + } + break; + default: + result.notImplemented(); + } + } + + public void onMessage(int index, @Nullable WebMessageCompatExt message) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("index", index); + obj.put("message", message != null ? message.toMap() : null); + channel.invokeMethod("onMessage", obj); + } + + @Override + public void dispose() { + super.dispose(); + webMessageChannel = null; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageListener.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageListener.java new file mode 100644 index 00000000..fe7326f7 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageListener.java @@ -0,0 +1,241 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.web_message; + +import android.net.Uri; +import android.text.TextUtils; +import android.webkit.WebView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.webkit.JavaScriptReplyProxy; +import androidx.webkit.WebMessageCompat; +import androidx.webkit.WebViewCompat; +import androidx.webkit.WebViewFeature; + +import com.pichillilorenzo.flutter_inappwebview_android.Util; +import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.JavaScriptBridgeJS; +import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebMessageCompatExt; +import com.pichillilorenzo.flutter_inappwebview_android.webview.InAppWebViewInterface; +import com.pichillilorenzo.flutter_inappwebview_android.types.PluginScript; +import com.pichillilorenzo.flutter_inappwebview_android.types.UserScriptInjectionTime; +import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebView; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodChannel; + +public class WebMessageListener implements Disposable { + protected static final String LOG_TAG = "WebMessageListener"; + public static final String METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_web_message_listener_"; + + @NonNull + public String id; + public String jsObjectName; + public Set allowedOriginRules; + public WebViewCompat.WebMessageListener listener; + public JavaScriptReplyProxy replyProxy; + @Nullable + public InAppWebViewInterface webView; + @Nullable + public WebMessageListenerChannelDelegate channelDelegate; + + public WebMessageListener(@NonNull String id, + @NonNull InAppWebViewInterface webView, @NonNull BinaryMessenger messenger, + @NonNull String jsObjectName, @NonNull Set allowedOriginRules) { + this.id = id; + this.webView = webView; + this.jsObjectName = jsObjectName; + this.allowedOriginRules = allowedOriginRules; + final MethodChannel channel = new MethodChannel(messenger, METHOD_CHANNEL_NAME_PREFIX + this.id + "_" + this.jsObjectName); + this.channelDelegate = new WebMessageListenerChannelDelegate(this, channel); + + if (this.webView instanceof InAppWebView) { + this.listener = new WebViewCompat.WebMessageListener() { + @Override + public void onPostMessage(@NonNull WebView view, @NonNull WebMessageCompat message, @NonNull Uri sourceOrigin, + boolean isMainFrame, @NonNull JavaScriptReplyProxy javaScriptReplyProxy) { + replyProxy = javaScriptReplyProxy; + if (channelDelegate != null) { + channelDelegate.onPostMessage(WebMessageCompatExt.fromMapWebMessageCompat(message), + sourceOrigin.toString().equals("null") ? null : sourceOrigin.toString(), + isMainFrame); + } + } + }; + } + } + + public void initJsInstance() { + if (webView != null) { + String jsObjectNameEscaped = Util.replaceAll(jsObjectName, "\'", "\\'"); + List allowedOriginRulesStringList = new ArrayList<>(); + for (String allowedOriginRule : allowedOriginRules) { + if ("*".equals(allowedOriginRule)) { + allowedOriginRulesStringList.add("'*'"); + } else { + Uri rule = Uri.parse(allowedOriginRule); + String host = rule.getHost() != null ? "'" + Util.replaceAll(rule.getHost(), "\'", "\\'") + "'" : "null"; + allowedOriginRulesStringList.add("{scheme: '" + rule.getScheme() + "', host: " + host + ", port: " + (rule.getPort() != -1 ? rule.getPort() : "null") + "}"); + } + } + String allowedOriginRulesString = TextUtils.join(", ", allowedOriginRulesStringList); + + String source = "(function() {" + + " var allowedOriginRules = [" + allowedOriginRulesString + "];" + + " var isPageBlank = window.location.href === 'about:blank';" + + " var scheme = !isPageBlank ? window.location.protocol.replace(':', '') : null;" + + " var host = !isPageBlank ? window.location.hostname : null;" + + " var port = !isPageBlank ? window.location.port : null;" + + " if (window." + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "._isOriginAllowed(allowedOriginRules, scheme, host, port)) {" + + " window['" + jsObjectNameEscaped + "'] = new FlutterInAppWebViewWebMessageListener('" + jsObjectNameEscaped + "');" + + " }" + + "})();"; + webView.getUserContentController().addPluginScript(new PluginScript( + "WebMessageListener-" + jsObjectName, + source, + UserScriptInjectionTime.AT_DOCUMENT_START, + null, + false, + null + )); + } + } + + @Nullable + public static WebMessageListener fromMap(@NonNull InAppWebViewInterface webView, @NonNull BinaryMessenger messenger, @Nullable Map map) { + if (map == null) { + return null; + } + String id = (String) map.get("id"); + String jsObjectName = (String) map.get("jsObjectName"); + assert jsObjectName != null; + List allowedOriginRuleList = (List) map.get("allowedOriginRules"); + assert allowedOriginRuleList != null; + Set allowedOriginRules = new HashSet<>(allowedOriginRuleList); + return new WebMessageListener(id, webView, messenger, jsObjectName, allowedOriginRules); + } + + public void assertOriginRulesValid() throws Exception { + int index = 0; + for (String originRule : allowedOriginRules) { + if (originRule == null) { + throw new Exception("allowedOriginRules[" + index + "] is null"); + } + if (originRule.isEmpty()) { + throw new Exception("allowedOriginRules[" + index + "] is empty"); + } + if ("*".equals(originRule)) { + continue; + } + Uri url = Uri.parse(originRule); + String scheme = url.getScheme(); + String host = url.getHost(); + String path = url.getPath(); + int port = url.getPort(); + if (scheme == null) { + throw new Exception("allowedOriginRules " + originRule + " is invalid"); + } + if (("http".equals(scheme) || "https".equals(scheme)) && (host == null || host.isEmpty())) { + throw new Exception("allowedOriginRules " + originRule + " is invalid"); + } + if (!"http".equals(scheme) && !"https".equals(scheme) && (host != null || port != -1)) { + throw new Exception("allowedOriginRules " + originRule + " is invalid"); + } + if ((host == null || host.isEmpty()) && port != -1) { + throw new Exception("allowedOriginRules " + originRule + " is invalid"); + } + if (!path.isEmpty()) { + throw new Exception("allowedOriginRules " + originRule + " is invalid"); + } + if (host != null) { + int distance = host.indexOf("*"); + if (distance != 0 || (distance == 0 && !host.startsWith("*."))) { + throw new Exception("allowedOriginRules " + originRule + " is invalid"); + } + if (host.startsWith("[")) { + if (!host.endsWith("]")) { + throw new Exception("allowedOriginRules " + originRule + " is invalid"); + } + String ipv6 = host.substring(1, host.length() - 1); + if (!Util.isIPv6(ipv6)) { + throw new Exception("allowedOriginRules " + originRule + " is invalid"); + } + } + } + index++; + } + } + + public void postMessageForInAppWebView(WebMessageCompatExt message, @NonNull MethodChannel.Result result) { + if (replyProxy != null && WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { + Object data = message.getData(); + if (data != null) { + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER) && message.getType() == WebMessageCompat.TYPE_ARRAY_BUFFER) { + replyProxy.postMessage((byte[]) data); + } else { + replyProxy.postMessage(data.toString()); + } + } + } + result.success(true); + } + + public boolean isOriginAllowed(String scheme, String host, int port) { + for (String allowedOriginRule : allowedOriginRules) { + if ("*".equals(allowedOriginRule)) { + return true; + } + if (scheme == null || scheme.isEmpty()) { + continue; + } + if ((scheme == null || scheme.isEmpty()) && (host == null || host.isEmpty()) && (port == 0 || port == -1)) { + continue; + } + Uri rule = Uri.parse(allowedOriginRule); + int rulePort = rule.getPort() == -1 || rule.getPort() == 0 ? ("https".equals(rule.getScheme()) ? 443 : 80) : rule.getPort(); + int currentPort = port == 0 || port == -1 ? ("https".equals(scheme) ? 443 : 80) : port; + String IPv6 = null; + if (rule.getHost() != null && rule.getHost().startsWith("[")) { + try { + IPv6 = Util.normalizeIPv6(rule.getHost().substring(1, rule.getHost().length() - 1)); + } catch (Exception ignored) { + } + } + String hostIPv6 = null; + try { + hostIPv6 = Util.normalizeIPv6(host); + } catch (Exception ignored) { + } + + boolean schemeAllowed = rule.getScheme().equals(scheme); + + boolean hostAllowed = rule.getHost() == null || + rule.getHost().isEmpty() || + rule.getHost().equals(host) || + (rule.getHost().startsWith("*") && host != null && host.contains(rule.getHost().split("\\*")[1])) || + (hostIPv6 != null && IPv6 != null && hostIPv6.equals(IPv6)); + + boolean portAllowed = rulePort == currentPort; + + if (schemeAllowed && hostAllowed && portAllowed) { + return true; + } + } + return false; + } + + public void dispose() { + if (channelDelegate != null) { + channelDelegate.dispose(); + channelDelegate = null; + } + listener = null; + replyProxy = null; + webView = null; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageListenerChannelDelegate.java b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageListenerChannelDelegate.java new file mode 100644 index 00000000..92c7aaae --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/web_message/WebMessageListenerChannelDelegate.java @@ -0,0 +1,56 @@ +package com.pichillilorenzo.flutter_inappwebview_android.webview.web_message; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl; +import com.pichillilorenzo.flutter_inappwebview_android.types.WebMessageCompatExt; +import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebView; + +import java.util.HashMap; +import java.util.Map; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +public class WebMessageListenerChannelDelegate extends ChannelDelegateImpl { + @Nullable + private WebMessageListener webMessageListener; + + public WebMessageListenerChannelDelegate(@NonNull WebMessageListener webMessageListener, @NonNull MethodChannel channel) { + super(channel); + this.webMessageListener = webMessageListener; + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + switch (call.method) { + case "postMessage": + if (webMessageListener != null && webMessageListener.webView instanceof InAppWebView) { + WebMessageCompatExt message = WebMessageCompatExt.fromMap((Map) call.argument("message")); + webMessageListener.postMessageForInAppWebView(message, result); + } else { + result.success(false); + } + break; + default: + result.notImplemented(); + } + } + + public void onPostMessage(@Nullable WebMessageCompatExt message, String sourceOrigin, boolean isMainFrame) { + MethodChannel channel = getChannel(); + if (channel == null) return; + Map obj = new HashMap<>(); + obj.put("message", message != null ? message.toMap() : null); + obj.put("sourceOrigin", sourceOrigin); + obj.put("isMainFrame", isMainFrame); + channel.invokeMethod("onPostMessage", obj); + } + + @Override + public void dispose() { + super.dispose(); + webMessageListener = null; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/drawable/floating_action_mode_shape.xml b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/drawable/floating_action_mode_shape.xml new file mode 100644 index 00000000..9ad086b9 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/drawable/floating_action_mode_shape.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/layout/activity_web_view.xml b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/layout/activity_web_view.xml new file mode 100755 index 00000000..d7d7e21a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/layout/activity_web_view.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/layout/chrome_custom_tabs_layout.xml b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/layout/chrome_custom_tabs_layout.xml new file mode 100755 index 00000000..f9504c9a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/layout/chrome_custom_tabs_layout.xml @@ -0,0 +1,6 @@ + + + + diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/layout/floating_action_mode.xml b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/layout/floating_action_mode.xml new file mode 100644 index 00000000..b58cc7cb --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/layout/floating_action_mode.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/layout/floating_action_mode_item.xml b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/layout/floating_action_mode_item.xml new file mode 100644 index 00000000..3ce3c111 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/layout/floating_action_mode_item.xml @@ -0,0 +1,12 @@ + + \ No newline at end of file diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/menu/menu_main.xml b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/menu/menu_main.xml new file mode 100755 index 00000000..2bff50bf --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/menu/menu_main.xml @@ -0,0 +1,49 @@ + +

+ + + + + + + + + + + + + + diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/values/strings.xml b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/values/strings.xml new file mode 100755 index 00000000..9301a93e --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/values/strings.xml @@ -0,0 +1,9 @@ + + + Go Back + Go Forward + Reload + Share + Close + Search + diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/values/styles.xml b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/values/styles.xml new file mode 100755 index 00000000..13b55bde --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/values/styles.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/xml/provider_paths.xml b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/xml/provider_paths.xml new file mode 100644 index 00000000..ffa74ab5 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/android/src/main/res/xml/provider_paths.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/flutter_inappwebview_android.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/flutter_inappwebview_android.dart new file mode 100644 index 00000000..dd86c9e0 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/flutter_inappwebview_android.dart @@ -0,0 +1,3 @@ +library flutter_inappwebview_android; + +export 'src/main.dart'; diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/chrome_safari_browser/chrome_safari_browser.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/chrome_safari_browser/chrome_safari_browser.dart new file mode 100755 index 00000000..18336a4f --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/chrome_safari_browser/chrome_safari_browser.dart @@ -0,0 +1,376 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:typed_data'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidChromeSafariBrowser]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformChromeSafariBrowserCreationParams] for +/// more information. +@immutable +class AndroidChromeSafariBrowserCreationParams + extends PlatformChromeSafariBrowserCreationParams { + /// Creates a new [AndroidChromeSafariBrowserCreationParams] instance. + const AndroidChromeSafariBrowserCreationParams(); + + /// Creates a [AndroidChromeSafariBrowserCreationParams] instance based on [PlatformChromeSafariBrowserCreationParams]. + factory AndroidChromeSafariBrowserCreationParams.fromPlatformChromeSafariBrowserCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformChromeSafariBrowserCreationParams params) { + return AndroidChromeSafariBrowserCreationParams(); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformChromeSafariBrowser} +class AndroidChromeSafariBrowser extends PlatformChromeSafariBrowser + with ChannelController { + @override + final String id = IdGenerator.generate(); + + /// Constructs a [AndroidChromeSafariBrowser]. + AndroidChromeSafariBrowser(PlatformChromeSafariBrowserCreationParams params) + : super.implementation( + params is AndroidChromeSafariBrowserCreationParams + ? params + : AndroidChromeSafariBrowserCreationParams + .fromPlatformChromeSafariBrowserCreationParams(params), + ); + + static final AndroidChromeSafariBrowser _staticValue = + AndroidChromeSafariBrowser(AndroidChromeSafariBrowserCreationParams()); + + /// Provide static access. + factory AndroidChromeSafariBrowser.static() { + return _staticValue; + } + + ChromeSafariBrowserActionButton? _actionButton; + Map _menuItems = new HashMap(); + ChromeSafariBrowserSecondaryToolbar? _secondaryToolbar; + bool _isOpened = false; + static const MethodChannel _staticChannel = + const MethodChannel('com.pichillilorenzo/flutter_chromesafaribrowser'); + + _init() { + channel = + MethodChannel('com.pichillilorenzo/flutter_chromesafaribrowser_$id'); + handler = _handleMethod; + initMethodCallHandler(); + } + + _debugLog(String method, dynamic args) { + debugLog( + className: this.runtimeType.toString(), + id: id, + debugLoggingSettings: PlatformChromeSafariBrowser.debugLoggingSettings, + method: method, + args: args); + } + + Future _handleMethod(MethodCall call) async { + _debugLog(call.method, call.arguments); + + switch (call.method) { + case "onServiceConnected": + eventHandler?.onServiceConnected(); + break; + case "onOpened": + eventHandler?.onOpened(); + break; + case "onCompletedInitialLoad": + final bool? didLoadSuccessfully = call.arguments["didLoadSuccessfully"]; + eventHandler?.onCompletedInitialLoad(didLoadSuccessfully); + break; + case "onNavigationEvent": + final navigationEvent = CustomTabsNavigationEventType.fromNativeValue( + call.arguments["navigationEvent"]); + eventHandler?.onNavigationEvent(navigationEvent); + break; + case "onRelationshipValidationResult": + final relation = + CustomTabsRelationType.fromNativeValue(call.arguments["relation"]); + final requestedOrigin = call.arguments["requestedOrigin"] != null + ? WebUri(call.arguments["requestedOrigin"]) + : null; + final bool result = call.arguments["result"]; + eventHandler?.onRelationshipValidationResult( + relation, requestedOrigin, result); + break; + case "onClosed": + _isOpened = false; + final onClosed = eventHandler?.onClosed; + dispose(); + onClosed?.call(); + break; + case "onItemActionPerform": + String url = call.arguments["url"]; + String title = call.arguments["title"]; + int id = call.arguments["id"].toInt(); + if (this._actionButton?.id == id) { + if (this._actionButton?.action != null) { + this._actionButton?.action!(url, title); + } + if (this._actionButton?.onClick != null) { + this._actionButton?.onClick!(WebUri(url), title); + } + } else if (this._menuItems[id] != null) { + if (this._menuItems[id]?.action != null) { + this._menuItems[id]?.action!(url, title); + } + if (this._menuItems[id]?.onClick != null) { + this._menuItems[id]?.onClick!(WebUri(url), title); + } + } + break; + case "onSecondaryItemActionPerform": + final clickableIDs = this._secondaryToolbar?.clickableIDs; + if (clickableIDs != null) { + WebUri? url = call.arguments["url"] != null + ? WebUri(call.arguments["url"]) + : null; + String name = call.arguments["name"]; + for (final clickable in clickableIDs) { + var clickableFullname = clickable.id.name; + if (clickable.id.defType != null && + !clickableFullname.contains("/")) { + clickableFullname = "${clickable.id.defType}/$clickableFullname"; + } + if (clickable.id.defPackage != null && + !clickableFullname.contains(":")) { + clickableFullname = + "${clickable.id.defPackage}:$clickableFullname"; + } + if (clickableFullname == name) { + if (clickable.onClick != null) { + clickable.onClick!(url); + } + break; + } + } + } + break; + case "onMessageChannelReady": + eventHandler?.onMessageChannelReady(); + break; + case "onPostMessage": + final String message = call.arguments["message"]; + eventHandler?.onPostMessage(message); + break; + case "onVerticalScrollEvent": + final bool isDirectionUp = call.arguments["isDirectionUp"]; + eventHandler?.onVerticalScrollEvent(isDirectionUp); + break; + case "onGreatestScrollPercentageIncreased": + final int scrollPercentage = call.arguments["scrollPercentage"]; + eventHandler?.onGreatestScrollPercentageIncreased(scrollPercentage); + break; + case "onSessionEnded": + final bool didUserInteract = call.arguments["didUserInteract"]; + eventHandler?.onSessionEnded(didUserInteract); + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + } + + @override + Future open( + {WebUri? url, + Map? headers, + List? otherLikelyURLs, + WebUri? referrer, + @Deprecated('Use settings instead') + // ignore: deprecated_member_use_from_same_package + ChromeSafariBrowserClassOptions? options, + ChromeSafariBrowserSettings? settings}) async { + assert(!_isOpened, 'The browser is already opened.'); + _isOpened = true; + + if (Util.isIOS) { + assert(url != null, 'The specified URL must not be null on iOS.'); + assert(['http', 'https'].contains(url!.scheme), + 'The specified URL has an unsupported scheme. Only HTTP and HTTPS URLs are supported on iOS.'); + } + if (url != null) { + assert(url.toString().isNotEmpty, 'The specified URL must not be empty.'); + } + + _init(); + + List> menuItemList = []; + _menuItems.forEach((key, value) { + menuItemList.add(value.toMap()); + }); + + var initialSettings = settings?.toMap() ?? + options?.toMap() ?? + ChromeSafariBrowserSettings().toMap(); + + Map args = {}; + args.putIfAbsent('id', () => id); + args.putIfAbsent('url', () => url?.toString()); + args.putIfAbsent('headers', () => headers); + args.putIfAbsent('otherLikelyURLs', + () => otherLikelyURLs?.map((e) => e.toString()).toList()); + args.putIfAbsent('referrer', () => referrer?.toString()); + args.putIfAbsent('settings', () => initialSettings); + args.putIfAbsent('actionButton', () => _actionButton?.toMap()); + args.putIfAbsent('secondaryToolbar', () => _secondaryToolbar?.toMap()); + args.putIfAbsent('menuItemList', () => menuItemList); + await _staticChannel.invokeMethod('open', args); + } + + @override + Future launchUrl({ + required WebUri url, + Map? headers, + List? otherLikelyURLs, + WebUri? referrer, + }) async { + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + args.putIfAbsent('headers', () => headers); + args.putIfAbsent('otherLikelyURLs', + () => otherLikelyURLs?.map((e) => e.toString()).toList()); + args.putIfAbsent('referrer', () => referrer?.toString()); + await channel?.invokeMethod("launchUrl", args); + } + + @override + Future mayLaunchUrl( + {WebUri? url, List? otherLikelyURLs}) async { + Map args = {}; + args.putIfAbsent('url', () => url?.toString()); + args.putIfAbsent('otherLikelyURLs', + () => otherLikelyURLs?.map((e) => e.toString()).toList()); + return await channel?.invokeMethod("mayLaunchUrl", args) ?? false; + } + + @override + Future validateRelationship( + {required CustomTabsRelationType relation, + required WebUri origin}) async { + Map args = {}; + args.putIfAbsent('relation', () => relation.toNativeValue()); + args.putIfAbsent('origin', () => origin.toString()); + return await channel?.invokeMethod("validateRelationship", args) ?? + false; + } + + @override + Future close() async { + Map args = {}; + await channel?.invokeMethod("close", args); + } + + @override + void setActionButton(ChromeSafariBrowserActionButton actionButton) { + this._actionButton = actionButton; + } + + @override + Future updateActionButton( + {required Uint8List icon, required String description}) async { + Map args = {}; + args.putIfAbsent('icon', () => icon); + args.putIfAbsent('description', () => description); + await channel?.invokeMethod("updateActionButton", args); + _actionButton?.icon = icon; + _actionButton?.description = description; + } + + @override + void setSecondaryToolbar( + ChromeSafariBrowserSecondaryToolbar secondaryToolbar) { + this._secondaryToolbar = secondaryToolbar; + } + + @override + Future updateSecondaryToolbar( + ChromeSafariBrowserSecondaryToolbar secondaryToolbar) async { + Map args = {}; + args.putIfAbsent('secondaryToolbar', () => secondaryToolbar.toMap()); + await channel?.invokeMethod("updateSecondaryToolbar", args); + this._secondaryToolbar = secondaryToolbar; + } + + @override + void addMenuItem(ChromeSafariBrowserMenuItem menuItem) { + this._menuItems[menuItem.id] = menuItem; + } + + @override + void addMenuItems(List menuItems) { + menuItems.forEach((menuItem) { + this._menuItems[menuItem.id] = menuItem; + }); + } + + @override + Future requestPostMessageChannel( + {required WebUri sourceOrigin, WebUri? targetOrigin}) async { + Map args = {}; + args.putIfAbsent("sourceOrigin", () => sourceOrigin.toString()); + args.putIfAbsent("targetOrigin", () => targetOrigin.toString()); + return await channel?.invokeMethod( + "requestPostMessageChannel", args) ?? + false; + } + + @override + Future postMessage(String message) async { + Map args = {}; + args.putIfAbsent("message", () => message); + return CustomTabsPostMessageResultType.fromNativeValue( + await channel?.invokeMethod("postMessage", args)) ?? + CustomTabsPostMessageResultType.FAILURE_MESSAGING_ERROR; + } + + @override + Future isEngagementSignalsApiAvailable() async { + Map args = {}; + return await channel?.invokeMethod( + "isEngagementSignalsApiAvailable", args) ?? + false; + } + + @override + Future isAvailable() async { + Map args = {}; + return await _staticChannel.invokeMethod("isAvailable", args) ?? + false; + } + + @override + Future getMaxToolbarItems() async { + Map args = {}; + return await _staticChannel.invokeMethod("getMaxToolbarItems", args) ?? + 0; + } + + @override + Future getPackageName( + {List? packages, bool ignoreDefault = false}) async { + Map args = {}; + args.putIfAbsent("packages", () => packages); + args.putIfAbsent("ignoreDefault", () => ignoreDefault); + return await _staticChannel.invokeMethod("getPackageName", args); + } + + @override + bool isOpened() { + return _isOpened; + } + + @override + @mustCallSuper + void dispose() { + super.dispose(); + disposeChannel(); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/chrome_safari_browser/main.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/chrome_safari_browser/main.dart new file mode 100644 index 00000000..9a6238b7 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/chrome_safari_browser/main.dart @@ -0,0 +1 @@ +export 'chrome_safari_browser.dart'; diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/cookie_manager.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/cookie_manager.dart new file mode 100755 index 00000000..c0e2fba5 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/cookie_manager.dart @@ -0,0 +1,220 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidCookieManager]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformCookieManagerCreationParams] for +/// more information. +@immutable +class AndroidCookieManagerCreationParams + extends PlatformCookieManagerCreationParams { + /// Creates a new [AndroidCookieManagerCreationParams] instance. + const AndroidCookieManagerCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformCookieManagerCreationParams params, + ) : super(); + + /// Creates a [AndroidCookieManagerCreationParams] instance based on [PlatformCookieManagerCreationParams]. + factory AndroidCookieManagerCreationParams.fromPlatformCookieManagerCreationParams( + PlatformCookieManagerCreationParams params) { + return AndroidCookieManagerCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformCookieManager} +class AndroidCookieManager extends PlatformCookieManager + with ChannelController { + /// Creates a new [AndroidCookieManager]. + AndroidCookieManager(PlatformCookieManagerCreationParams params) + : super.implementation( + params is AndroidCookieManagerCreationParams + ? params + : AndroidCookieManagerCreationParams + .fromPlatformCookieManagerCreationParams(params), + ) { + channel = const MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_cookiemanager'); + handler = handleMethod; + initMethodCallHandler(); + } + + static AndroidCookieManager? _instance; + + ///Gets the [AndroidCookieManager] shared instance. + static AndroidCookieManager instance() { + return (_instance != null) ? _instance! : _init(); + } + + static AndroidCookieManager _init() { + _instance = AndroidCookieManager(AndroidCookieManagerCreationParams( + const PlatformCookieManagerCreationParams())); + return _instance!; + } + + Future _handleMethod(MethodCall call) async {} + + @override + Future setCookie( + {required WebUri url, + required String name, + required String value, + String path = "/", + String? domain, + int? expiresDate, + int? maxAge, + bool? isSecure, + bool? isHttpOnly, + HTTPCookieSameSitePolicy? sameSite, + @Deprecated("Use webViewController instead") + PlatformInAppWebViewController? iosBelow11WebViewController, + PlatformInAppWebViewController? webViewController}) async { + assert(url.toString().isNotEmpty); + assert(name.isNotEmpty); + assert(value.isNotEmpty); + assert(path.isNotEmpty); + + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + args.putIfAbsent('name', () => name); + args.putIfAbsent('value', () => value); + args.putIfAbsent('domain', () => domain); + args.putIfAbsent('path', () => path); + args.putIfAbsent('expiresDate', () => expiresDate?.toString()); + args.putIfAbsent('maxAge', () => maxAge); + args.putIfAbsent('isSecure', () => isSecure); + args.putIfAbsent('isHttpOnly', () => isHttpOnly); + args.putIfAbsent('sameSite', () => sameSite?.toNativeValue()); + + return await channel?.invokeMethod('setCookie', args) ?? false; + } + + @override + Future> getCookies( + {required WebUri url, + @Deprecated("Use webViewController instead") + PlatformInAppWebViewController? iosBelow11WebViewController, + PlatformInAppWebViewController? webViewController}) async { + assert(url.toString().isNotEmpty); + + List cookies = []; + + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + List cookieListMap = + await channel?.invokeMethod('getCookies', args) ?? []; + cookieListMap = cookieListMap.cast>(); + + cookieListMap.forEach((cookieMap) { + cookies.add(Cookie( + name: cookieMap["name"], + value: cookieMap["value"], + expiresDate: cookieMap["expiresDate"], + isSessionOnly: cookieMap["isSessionOnly"], + domain: cookieMap["domain"], + sameSite: + HTTPCookieSameSitePolicy.fromNativeValue(cookieMap["sameSite"]), + isSecure: cookieMap["isSecure"], + isHttpOnly: cookieMap["isHttpOnly"], + path: cookieMap["path"])); + }); + return cookies; + } + + @override + Future getCookie( + {required WebUri url, + required String name, + @Deprecated("Use webViewController instead") + PlatformInAppWebViewController? iosBelow11WebViewController, + PlatformInAppWebViewController? webViewController}) async { + assert(url.toString().isNotEmpty); + assert(name.isNotEmpty); + + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + List cookies = + await channel?.invokeMethod('getCookies', args) ?? []; + cookies = cookies.cast>(); + for (var i = 0; i < cookies.length; i++) { + cookies[i] = cookies[i].cast(); + if (cookies[i]["name"] == name) + return Cookie( + name: cookies[i]["name"], + value: cookies[i]["value"], + expiresDate: cookies[i]["expiresDate"], + isSessionOnly: cookies[i]["isSessionOnly"], + domain: cookies[i]["domain"], + sameSite: HTTPCookieSameSitePolicy.fromNativeValue( + cookies[i]["sameSite"]), + isSecure: cookies[i]["isSecure"], + isHttpOnly: cookies[i]["isHttpOnly"], + path: cookies[i]["path"]); + } + return null; + } + + @override + Future deleteCookie( + {required WebUri url, + required String name, + String path = "/", + String? domain, + @Deprecated("Use webViewController instead") + PlatformInAppWebViewController? iosBelow11WebViewController, + PlatformInAppWebViewController? webViewController}) async { + assert(url.toString().isNotEmpty); + assert(name.isNotEmpty); + + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + args.putIfAbsent('name', () => name); + args.putIfAbsent('domain', () => domain); + args.putIfAbsent('path', () => path); + return await channel?.invokeMethod('deleteCookie', args) ?? false; + } + + @override + Future deleteCookies( + {required WebUri url, + String path = "/", + String? domain, + @Deprecated("Use webViewController instead") + PlatformInAppWebViewController? iosBelow11WebViewController, + PlatformInAppWebViewController? webViewController}) async { + assert(url.toString().isNotEmpty); + + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + args.putIfAbsent('domain', () => domain); + args.putIfAbsent('path', () => path); + return await channel?.invokeMethod('deleteCookies', args) ?? false; + } + + @override + Future deleteAllCookies() async { + Map args = {}; + return await channel?.invokeMethod('deleteAllCookies', args) ?? false; + } + + @override + Future removeSessionCookies() async { + Map args = {}; + return await channel?.invokeMethod('removeSessionCookies', args) ?? + false; + } + + @override + void dispose() { + // empty + } +} + +extension InternalCookieManager on AndroidCookieManager { + get handleMethod => _handleMethod; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/find_interaction/find_interaction_controller.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/find_interaction/find_interaction_controller.dart new file mode 100644 index 00000000..943176a5 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/find_interaction/find_interaction_controller.dart @@ -0,0 +1,125 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidFindInteractionController]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformFindInteractionControllerCreationParams] for +/// more information. +@immutable +class AndroidFindInteractionControllerCreationParams + extends PlatformFindInteractionControllerCreationParams { + /// Creates a new [AndroidFindInteractionControllerCreationParams] instance. + const AndroidFindInteractionControllerCreationParams( + {super.onFindResultReceived}); + + /// Creates a [AndroidFindInteractionControllerCreationParams] instance based on [PlatformFindInteractionControllerCreationParams]. + factory AndroidFindInteractionControllerCreationParams.fromPlatformFindInteractionControllerCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformFindInteractionControllerCreationParams params) { + return AndroidFindInteractionControllerCreationParams( + onFindResultReceived: params.onFindResultReceived); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController} +class AndroidFindInteractionController extends PlatformFindInteractionController + with ChannelController { + /// Constructs a [AndroidFindInteractionController]. + AndroidFindInteractionController( + PlatformFindInteractionControllerCreationParams params) + : super.implementation( + params is AndroidFindInteractionControllerCreationParams + ? params + : AndroidFindInteractionControllerCreationParams + .fromPlatformFindInteractionControllerCreationParams(params), + ); + + _debugLog(String method, dynamic args) { + debugLog( + className: this.runtimeType.toString(), + debugLoggingSettings: + PlatformFindInteractionController.debugLoggingSettings, + method: method, + args: args); + } + + Future _handleMethod(MethodCall call) async { + _debugLog(call.method, call.arguments); + + switch (call.method) { + case "onFindResultReceived": + if (onFindResultReceived != null) { + int activeMatchOrdinal = call.arguments["activeMatchOrdinal"]; + int numberOfMatches = call.arguments["numberOfMatches"]; + bool isDoneCounting = call.arguments["isDoneCounting"]; + onFindResultReceived!( + this, activeMatchOrdinal, numberOfMatches, isDoneCounting); + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.findAll} + Future findAll({String? find}) async { + Map args = {}; + args.putIfAbsent('find', () => find); + await channel?.invokeMethod('findAll', args); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.findNext} + Future findNext({bool forward = true}) async { + Map args = {}; + args.putIfAbsent('forward', () => forward); + await channel?.invokeMethod('findNext', args); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.clearMatches} + Future clearMatches() async { + Map args = {}; + await channel?.invokeMethod('clearMatches', args); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.setSearchText} + Future setSearchText(String? searchText) async { + Map args = {}; + args.putIfAbsent('searchText', () => searchText); + await channel?.invokeMethod('setSearchText', args); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.getSearchText} + Future getSearchText() async { + Map args = {}; + return await channel?.invokeMethod('getSearchText', args); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.getActiveFindSession} + Future getActiveFindSession() async { + Map args = {}; + Map? result = + (await channel?.invokeMethod('getActiveFindSession', args)) + ?.cast(); + return FindSession.fromMap(result); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.dispose} + @override + void dispose({bool isKeepAlive = false}) { + disposeChannel(removeMethodCallHandler: !isKeepAlive); + } +} + +extension InternalFindInteractionController + on AndroidFindInteractionController { + void init(dynamic id) { + channel = MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_find_interaction_$id'); + handler = _handleMethod; + initMethodCallHandler(); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/find_interaction/main.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/find_interaction/main.dart new file mode 100644 index 00000000..a7adaacf --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/find_interaction/main.dart @@ -0,0 +1,2 @@ +export 'find_interaction_controller.dart' + hide InternalFindInteractionController; diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/http_auth_credentials_database.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/http_auth_credentials_database.dart new file mode 100755 index 00000000..06ec37ac --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/http_auth_credentials_database.dart @@ -0,0 +1,155 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidHttpAuthCredentialDatabase]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformHttpAuthCredentialDatabaseCreationParams] for +/// more information. +@immutable +class AndroidHttpAuthCredentialDatabaseCreationParams + extends PlatformHttpAuthCredentialDatabaseCreationParams { + /// Creates a new [AndroidHttpAuthCredentialDatabaseCreationParams] instance. + const AndroidHttpAuthCredentialDatabaseCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformHttpAuthCredentialDatabaseCreationParams params, + ) : super(); + + /// Creates a [AndroidHttpAuthCredentialDatabaseCreationParams] instance based on [PlatformHttpAuthCredentialDatabaseCreationParams]. + factory AndroidHttpAuthCredentialDatabaseCreationParams.fromPlatformHttpAuthCredentialDatabaseCreationParams( + PlatformHttpAuthCredentialDatabaseCreationParams params) { + return AndroidHttpAuthCredentialDatabaseCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformHttpAuthCredentialDatabase} +class AndroidHttpAuthCredentialDatabase + extends PlatformHttpAuthCredentialDatabase with ChannelController { + /// Creates a new [AndroidHttpAuthCredentialDatabase]. + AndroidHttpAuthCredentialDatabase( + PlatformHttpAuthCredentialDatabaseCreationParams params) + : super.implementation( + params is AndroidHttpAuthCredentialDatabaseCreationParams + ? params + : AndroidHttpAuthCredentialDatabaseCreationParams + .fromPlatformHttpAuthCredentialDatabaseCreationParams(params), + ) { + channel = const MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_credential_database'); + handler = handleMethod; + initMethodCallHandler(); + } + + static AndroidHttpAuthCredentialDatabase? _instance; + + ///Gets the database shared instance. + static AndroidHttpAuthCredentialDatabase instance() { + return (_instance != null) ? _instance! : _init(); + } + + static AndroidHttpAuthCredentialDatabase _init() { + _instance = AndroidHttpAuthCredentialDatabase( + AndroidHttpAuthCredentialDatabaseCreationParams( + const PlatformHttpAuthCredentialDatabaseCreationParams())); + return _instance!; + } + + Future _handleMethod(MethodCall call) async {} + + @override + Future> + getAllAuthCredentials() async { + Map args = {}; + List allCredentials = + await channel?.invokeMethod('getAllAuthCredentials', args) ?? []; + + List result = []; + + for (Map map in allCredentials) { + var element = URLProtectionSpaceHttpAuthCredentials.fromMap( + map.cast()); + if (element != null) { + result.add(element); + } + } + return result; + } + + @override + Future> getHttpAuthCredentials( + {required URLProtectionSpace protectionSpace}) async { + Map args = {}; + args.putIfAbsent("host", () => protectionSpace.host); + args.putIfAbsent("protocol", () => protectionSpace.protocol); + args.putIfAbsent("realm", () => protectionSpace.realm); + args.putIfAbsent("port", () => protectionSpace.port); + List credentialList = + await channel?.invokeMethod('getHttpAuthCredentials', args) ?? []; + List credentials = []; + for (Map map in credentialList) { + var credential = URLCredential.fromMap(map.cast()); + if (credential != null) { + credentials.add(credential); + } + } + return credentials; + } + + @override + Future setHttpAuthCredential( + {required URLProtectionSpace protectionSpace, + required URLCredential credential}) async { + Map args = {}; + args.putIfAbsent("host", () => protectionSpace.host); + args.putIfAbsent("protocol", () => protectionSpace.protocol); + args.putIfAbsent("realm", () => protectionSpace.realm); + args.putIfAbsent("port", () => protectionSpace.port); + args.putIfAbsent("username", () => credential.username); + args.putIfAbsent("password", () => credential.password); + await channel?.invokeMethod('setHttpAuthCredential', args); + } + + @override + Future removeHttpAuthCredential( + {required URLProtectionSpace protectionSpace, + required URLCredential credential}) async { + Map args = {}; + args.putIfAbsent("host", () => protectionSpace.host); + args.putIfAbsent("protocol", () => protectionSpace.protocol); + args.putIfAbsent("realm", () => protectionSpace.realm); + args.putIfAbsent("port", () => protectionSpace.port); + args.putIfAbsent("username", () => credential.username); + args.putIfAbsent("password", () => credential.password); + await channel?.invokeMethod('removeHttpAuthCredential', args); + } + + @override + Future removeHttpAuthCredentials( + {required URLProtectionSpace protectionSpace}) async { + Map args = {}; + args.putIfAbsent("host", () => protectionSpace.host); + args.putIfAbsent("protocol", () => protectionSpace.protocol); + args.putIfAbsent("realm", () => protectionSpace.realm); + args.putIfAbsent("port", () => protectionSpace.port); + await channel?.invokeMethod('removeHttpAuthCredentials', args); + } + + @override + Future clearAllAuthCredentials() async { + Map args = {}; + await channel?.invokeMethod('clearAllAuthCredentials', args); + } + + @override + void dispose() { + // empty + } +} + +extension InternalHttpAuthCredentialDatabase + on AndroidHttpAuthCredentialDatabase { + get handleMethod => _handleMethod; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/in_app_browser/in_app_browser.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/in_app_browser/in_app_browser.dart new file mode 100755 index 00000000..1c317c65 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/in_app_browser/in_app_browser.dart @@ -0,0 +1,375 @@ +import 'dart:async'; +import 'dart:collection'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +import '../find_interaction/find_interaction_controller.dart'; +import '../in_app_webview/in_app_webview_controller.dart'; +import '../pull_to_refresh/pull_to_refresh_controller.dart'; + +/// Object specifying creation parameters for creating a [AndroidInAppBrowser]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformInAppBrowserCreationParams] for +/// more information. +class AndroidInAppBrowserCreationParams + extends PlatformInAppBrowserCreationParams { + /// Creates a new [AndroidInAppBrowserCreationParams] instance. + AndroidInAppBrowserCreationParams( + {super.contextMenu, + this.pullToRefreshController, + this.findInteractionController, + super.initialUserScripts, + super.windowId}); + + /// Creates a [AndroidInAppBrowserCreationParams] instance based on [PlatformInAppBrowserCreationParams]. + factory AndroidInAppBrowserCreationParams.fromPlatformInAppBrowserCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformInAppBrowserCreationParams params) { + return AndroidInAppBrowserCreationParams( + contextMenu: params.contextMenu, + pullToRefreshController: + params.pullToRefreshController as AndroidPullToRefreshController?, + findInteractionController: params.findInteractionController + as AndroidFindInteractionController?, + initialUserScripts: params.initialUserScripts, + windowId: params.windowId); + } + + @override + final AndroidFindInteractionController? findInteractionController; + + @override + final AndroidPullToRefreshController? pullToRefreshController; +} + +///{@macro flutter_inappwebview_platform_interface.PlatformInAppBrowser} +class AndroidInAppBrowser extends PlatformInAppBrowser with ChannelController { + @override + final String id = IdGenerator.generate(); + + /// Constructs a [AndroidInAppBrowser]. + AndroidInAppBrowser(PlatformInAppBrowserCreationParams params) + : super.implementation( + params is AndroidInAppBrowserCreationParams + ? params + : AndroidInAppBrowserCreationParams + .fromPlatformInAppBrowserCreationParams(params), + ) { + _contextMenu = params.contextMenu; + } + + static final AndroidInAppBrowser _staticValue = + AndroidInAppBrowser(AndroidInAppBrowserCreationParams()); + + /// Provide static access. + factory AndroidInAppBrowser.static() { + return _staticValue; + } + + AndroidInAppBrowserCreationParams get _androidParams => + params as AndroidInAppBrowserCreationParams; + + static const MethodChannel _staticChannel = + const MethodChannel('com.pichillilorenzo/flutter_inappbrowser'); + + ContextMenu? _contextMenu; + + @override + ContextMenu? get contextMenu => _contextMenu; + + Map _menuItems = HashMap(); + bool _isOpened = false; + AndroidInAppWebViewController? _webViewController; + + @override + AndroidInAppWebViewController? get webViewController { + return _isOpened ? _webViewController : null; + } + + _init() { + channel = MethodChannel('com.pichillilorenzo/flutter_inappbrowser_$id'); + handler = _handleMethod; + initMethodCallHandler(); + + _webViewController = AndroidInAppWebViewController.fromInAppBrowser( + AndroidInAppWebViewControllerCreationParams(id: id), + channel!, + this, + this.initialUserScripts); + _androidParams.pullToRefreshController?.init(id); + _androidParams.findInteractionController?.init(id); + } + + _debugLog(String method, dynamic args) { + debugLog( + className: this.runtimeType.toString(), + id: id, + debugLoggingSettings: PlatformInAppBrowser.debugLoggingSettings, + method: method, + args: args); + } + + Future _handleMethod(MethodCall call) async { + switch (call.method) { + case "onBrowserCreated": + _debugLog(call.method, call.arguments); + eventHandler?.onBrowserCreated(); + break; + case "onMenuItemClicked": + _debugLog(call.method, call.arguments); + int id = call.arguments["id"].toInt(); + if (this._menuItems[id] != null) { + if (this._menuItems[id]?.onClick != null) { + this._menuItems[id]?.onClick!(); + } + } + break; + case "onExit": + _debugLog(call.method, call.arguments); + _isOpened = false; + final onExit = eventHandler?.onExit; + dispose(); + onExit?.call(); + break; + default: + return _webViewController?.handleMethod(call); + } + } + + Map _prepareOpenRequest( + {@Deprecated('Use settings instead') InAppBrowserClassOptions? options, + InAppBrowserClassSettings? settings}) { + assert(!_isOpened, 'The browser is already opened.'); + _isOpened = true; + _init(); + + var initialSettings = settings?.toMap() ?? + options?.toMap() ?? + InAppBrowserClassSettings().toMap(); + + Map pullToRefreshSettings = + pullToRefreshController?.settings.toMap() ?? + pullToRefreshController?.options.toMap() ?? + PullToRefreshSettings(enabled: false).toMap(); + + List> menuItemList = []; + _menuItems.forEach((key, value) { + menuItemList.add(value.toMap()); + }); + + Map args = {}; + args.putIfAbsent('id', () => id); + args.putIfAbsent('settings', () => initialSettings); + args.putIfAbsent('contextMenu', () => contextMenu?.toMap() ?? {}); + args.putIfAbsent('windowId', () => windowId); + args.putIfAbsent('initialUserScripts', + () => initialUserScripts?.map((e) => e.toMap()).toList() ?? []); + args.putIfAbsent('pullToRefreshSettings', () => pullToRefreshSettings); + args.putIfAbsent('menuItems', () => menuItemList); + return args; + } + + @override + Future openUrlRequest( + {required URLRequest urlRequest, + // ignore: deprecated_member_use_from_same_package + @Deprecated('Use settings instead') InAppBrowserClassOptions? options, + InAppBrowserClassSettings? settings}) async { + assert(urlRequest.url != null && urlRequest.url.toString().isNotEmpty); + + Map args = + _prepareOpenRequest(options: options, settings: settings); + args.putIfAbsent('urlRequest', () => urlRequest.toMap()); + await _staticChannel.invokeMethod('open', args); + } + + @override + Future openFile( + {required String assetFilePath, + // ignore: deprecated_member_use_from_same_package + @Deprecated('Use settings instead') InAppBrowserClassOptions? options, + InAppBrowserClassSettings? settings}) async { + assert(assetFilePath.isNotEmpty); + + Map args = + _prepareOpenRequest(options: options, settings: settings); + args.putIfAbsent('assetFilePath', () => assetFilePath); + await _staticChannel.invokeMethod('open', args); + } + + @override + Future openData( + {required String data, + String mimeType = "text/html", + String encoding = "utf8", + WebUri? baseUrl, + @Deprecated("Use historyUrl instead") Uri? androidHistoryUrl, + WebUri? historyUrl, + // ignore: deprecated_member_use_from_same_package + @Deprecated('Use settings instead') InAppBrowserClassOptions? options, + InAppBrowserClassSettings? settings}) async { + Map args = + _prepareOpenRequest(options: options, settings: settings); + args.putIfAbsent('data', () => data); + args.putIfAbsent('mimeType', () => mimeType); + args.putIfAbsent('encoding', () => encoding); + args.putIfAbsent('baseUrl', () => baseUrl?.toString() ?? "about:blank"); + args.putIfAbsent('historyUrl', + () => (historyUrl ?? androidHistoryUrl)?.toString() ?? "about:blank"); + await _staticChannel.invokeMethod('open', args); + } + + @override + Future openWithSystemBrowser({required WebUri url}) async { + assert(url.toString().isNotEmpty); + + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + return await _staticChannel.invokeMethod('openWithSystemBrowser', args); + } + + @override + void addMenuItem(InAppBrowserMenuItem menuItem) { + _menuItems[menuItem.id] = menuItem; + } + + @override + void addMenuItems(List menuItems) { + menuItems.forEach((menuItem) { + _menuItems[menuItem.id] = menuItem; + }); + } + + @override + bool removeMenuItem(InAppBrowserMenuItem menuItem) { + return _menuItems.remove(menuItem.id) != null; + } + + @override + void removeMenuItems(List menuItems) { + for (final menuItem in menuItems) { + removeMenuItem(menuItem); + } + } + + @override + void removeAllMenuItem() { + _menuItems.clear(); + } + + @override + bool hasMenuItem(InAppBrowserMenuItem menuItem) { + return _menuItems.containsKey(menuItem.id); + } + + @override + Future show() async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + await channel?.invokeMethod('show', args); + } + + @override + Future hide() async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + await channel?.invokeMethod('hide', args); + } + + @override + Future close() async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + await channel?.invokeMethod('close', args); + } + + @override + Future isHidden() async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + return await channel?.invokeMethod('isHidden', args) ?? false; + } + + @override + @Deprecated('Use setSettings instead') + Future setOptions({required InAppBrowserClassOptions options}) async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + args.putIfAbsent('settings', () => options.toMap()); + await channel?.invokeMethod('setSettings', args); + } + + @override + @Deprecated('Use getSettings instead') + Future getOptions() async { + assert(_isOpened, 'The browser is not opened.'); + Map args = {}; + + Map? options = + await channel?.invokeMethod('getSettings', args); + if (options != null) { + options = options.cast(); + return InAppBrowserClassOptions.fromMap(options as Map); + } + + return null; + } + + @override + Future setSettings( + {required InAppBrowserClassSettings settings}) async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + args.putIfAbsent('settings', () => settings.toMap()); + await channel?.invokeMethod('setSettings', args); + } + + @override + Future getSettings() async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + + Map? settings = + await channel?.invokeMethod('getSettings', args); + if (settings != null) { + settings = settings.cast(); + return InAppBrowserClassSettings.fromMap( + settings as Map); + } + + return null; + } + + @override + bool isOpened() { + return this._isOpened; + } + + @override + @mustCallSuper + void dispose() { + super.dispose(); + disposeChannel(); + _webViewController?.dispose(); + _webViewController = null; + pullToRefreshController?.dispose(); + findInteractionController?.dispose(); + } +} + +extension InternalInAppBrowser on AndroidInAppBrowser { + void setContextMenu(ContextMenu? contextMenu) { + _contextMenu = contextMenu; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/in_app_browser/main.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/in_app_browser/main.dart new file mode 100644 index 00000000..e11eb8b1 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/in_app_browser/main.dart @@ -0,0 +1 @@ +export 'in_app_browser.dart' hide InternalInAppBrowser; diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/in_app_webview/_static_channel.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/in_app_webview/_static_channel.dart new file mode 100644 index 00000000..beb7de70 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/in_app_webview/_static_channel.dart @@ -0,0 +1,4 @@ +import 'package:flutter/services.dart'; + +const IN_APP_WEBVIEW_STATIC_CHANNEL = + MethodChannel('com.pichillilorenzo/flutter_inappwebview_manager'); diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/in_app_webview/headless_in_app_webview.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/in_app_webview/headless_in_app_webview.dart new file mode 100644 index 00000000..c53a36d5 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/in_app_webview/headless_in_app_webview.dart @@ -0,0 +1,440 @@ +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; +import '../find_interaction/find_interaction_controller.dart'; +import '../pull_to_refresh/pull_to_refresh_controller.dart'; +import 'in_app_webview_controller.dart'; + +/// Object specifying creation parameters for creating a [AndroidHeadlessInAppWebView]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformHeadlessInAppWebViewCreationParams] for +/// more information. +@immutable +class AndroidHeadlessInAppWebViewCreationParams + extends PlatformHeadlessInAppWebViewCreationParams { + /// Creates a new [AndroidHeadlessInAppWebViewCreationParams] instance. + AndroidHeadlessInAppWebViewCreationParams( + {super.controllerFromPlatform, + super.initialSize, + super.windowId, + super.onWebViewCreated, + super.onLoadStart, + super.onLoadStop, + @Deprecated('Use onReceivedError instead') super.onLoadError, + super.onReceivedError, + @Deprecated("Use onReceivedHttpError instead") super.onLoadHttpError, + super.onReceivedHttpError, + super.onProgressChanged, + super.onConsoleMessage, + super.shouldOverrideUrlLoading, + super.onLoadResource, + super.onScrollChanged, + @Deprecated('Use onDownloadStartRequest instead') super.onDownloadStart, + super.onDownloadStartRequest, + @Deprecated('Use onLoadResourceWithCustomScheme instead') + super.onLoadResourceCustomScheme, + super.onLoadResourceWithCustomScheme, + super.onCreateWindow, + super.onCloseWindow, + super.onJsAlert, + super.onJsConfirm, + super.onJsPrompt, + super.onReceivedHttpAuthRequest, + super.onReceivedServerTrustAuthRequest, + super.onReceivedClientCertRequest, + @Deprecated('Use FindInteractionController.onFindResultReceived instead') + super.onFindResultReceived, + super.shouldInterceptAjaxRequest, + super.onAjaxReadyStateChange, + super.onAjaxProgress, + super.shouldInterceptFetchRequest, + super.onUpdateVisitedHistory, + @Deprecated("Use onPrintRequest instead") super.onPrint, + super.onPrintRequest, + super.onLongPressHitTestResult, + super.onEnterFullscreen, + super.onExitFullscreen, + super.onPageCommitVisible, + super.onTitleChanged, + super.onWindowFocus, + super.onWindowBlur, + super.onOverScrolled, + super.onZoomScaleChanged, + @Deprecated('Use onSafeBrowsingHit instead') + super.androidOnSafeBrowsingHit, + super.onSafeBrowsingHit, + @Deprecated('Use onPermissionRequest instead') + super.androidOnPermissionRequest, + super.onPermissionRequest, + @Deprecated('Use onGeolocationPermissionsShowPrompt instead') + super.androidOnGeolocationPermissionsShowPrompt, + super.onGeolocationPermissionsShowPrompt, + @Deprecated('Use onGeolocationPermissionsHidePrompt instead') + super.androidOnGeolocationPermissionsHidePrompt, + super.onGeolocationPermissionsHidePrompt, + @Deprecated('Use shouldInterceptRequest instead') + super.androidShouldInterceptRequest, + super.shouldInterceptRequest, + @Deprecated('Use onRenderProcessGone instead') + super.androidOnRenderProcessGone, + super.onRenderProcessGone, + @Deprecated('Use onRenderProcessResponsive instead') + super.androidOnRenderProcessResponsive, + super.onRenderProcessResponsive, + @Deprecated('Use onRenderProcessUnresponsive instead') + super.androidOnRenderProcessUnresponsive, + super.onRenderProcessUnresponsive, + @Deprecated('Use onFormResubmission instead') + super.androidOnFormResubmission, + super.onFormResubmission, + @Deprecated('Use onZoomScaleChanged instead') super.androidOnScaleChanged, + @Deprecated('Use onReceivedIcon instead') super.androidOnReceivedIcon, + super.onReceivedIcon, + @Deprecated('Use onReceivedTouchIconUrl instead') + super.androidOnReceivedTouchIconUrl, + super.onReceivedTouchIconUrl, + @Deprecated('Use onJsBeforeUnload instead') super.androidOnJsBeforeUnload, + super.onJsBeforeUnload, + @Deprecated('Use onReceivedLoginRequest instead') + super.androidOnReceivedLoginRequest, + super.onReceivedLoginRequest, + super.onPermissionRequestCanceled, + super.onRequestFocus, + @Deprecated('Use onWebContentProcessDidTerminate instead') + super.iosOnWebContentProcessDidTerminate, + super.onWebContentProcessDidTerminate, + @Deprecated( + 'Use onDidReceiveServerRedirectForProvisionalNavigation instead') + super.iosOnDidReceiveServerRedirectForProvisionalNavigation, + super.onDidReceiveServerRedirectForProvisionalNavigation, + @Deprecated('Use onNavigationResponse instead') + super.iosOnNavigationResponse, + super.onNavigationResponse, + @Deprecated('Use shouldAllowDeprecatedTLS instead') + super.iosShouldAllowDeprecatedTLS, + super.shouldAllowDeprecatedTLS, + super.onCameraCaptureStateChanged, + super.onMicrophoneCaptureStateChanged, + super.onContentSizeChanged, + super.initialUrlRequest, + super.initialFile, + super.initialData, + @Deprecated('Use initialSettings instead') super.initialOptions, + super.initialSettings, + super.contextMenu, + super.initialUserScripts, + this.pullToRefreshController, + this.findInteractionController}); + + /// Creates a [AndroidHeadlessInAppWebViewCreationParams] instance based on [PlatformHeadlessInAppWebViewCreationParams]. + AndroidHeadlessInAppWebViewCreationParams.fromPlatformHeadlessInAppWebViewCreationParams( + PlatformHeadlessInAppWebViewCreationParams params) + : this( + controllerFromPlatform: params.controllerFromPlatform, + initialSize: params.initialSize, + windowId: params.windowId, + onWebViewCreated: params.onWebViewCreated, + onLoadStart: params.onLoadStart, + onLoadStop: params.onLoadStop, + onLoadError: params.onLoadError, + onReceivedError: params.onReceivedError, + onLoadHttpError: params.onLoadHttpError, + onReceivedHttpError: params.onReceivedHttpError, + onProgressChanged: params.onProgressChanged, + onConsoleMessage: params.onConsoleMessage, + shouldOverrideUrlLoading: params.shouldOverrideUrlLoading, + onLoadResource: params.onLoadResource, + onScrollChanged: params.onScrollChanged, + onDownloadStart: params.onDownloadStart, + onDownloadStartRequest: params.onDownloadStartRequest, + onLoadResourceCustomScheme: params.onLoadResourceCustomScheme, + onLoadResourceWithCustomScheme: + params.onLoadResourceWithCustomScheme, + onCreateWindow: params.onCreateWindow, + onCloseWindow: params.onCloseWindow, + onJsAlert: params.onJsAlert, + onJsConfirm: params.onJsConfirm, + onJsPrompt: params.onJsPrompt, + onReceivedHttpAuthRequest: params.onReceivedHttpAuthRequest, + onReceivedServerTrustAuthRequest: + params.onReceivedServerTrustAuthRequest, + onReceivedClientCertRequest: params.onReceivedClientCertRequest, + onFindResultReceived: params.onFindResultReceived, + shouldInterceptAjaxRequest: params.shouldInterceptAjaxRequest, + onAjaxReadyStateChange: params.onAjaxReadyStateChange, + onAjaxProgress: params.onAjaxProgress, + shouldInterceptFetchRequest: params.shouldInterceptFetchRequest, + onUpdateVisitedHistory: params.onUpdateVisitedHistory, + onPrint: params.onPrint, + onPrintRequest: params.onPrintRequest, + onLongPressHitTestResult: params.onLongPressHitTestResult, + onEnterFullscreen: params.onEnterFullscreen, + onExitFullscreen: params.onExitFullscreen, + onPageCommitVisible: params.onPageCommitVisible, + onTitleChanged: params.onTitleChanged, + onWindowFocus: params.onWindowFocus, + onWindowBlur: params.onWindowBlur, + onOverScrolled: params.onOverScrolled, + onZoomScaleChanged: params.onZoomScaleChanged, + androidOnSafeBrowsingHit: params.androidOnSafeBrowsingHit, + onSafeBrowsingHit: params.onSafeBrowsingHit, + androidOnPermissionRequest: params.androidOnPermissionRequest, + onPermissionRequest: params.onPermissionRequest, + androidOnGeolocationPermissionsShowPrompt: + params.androidOnGeolocationPermissionsShowPrompt, + onGeolocationPermissionsShowPrompt: + params.onGeolocationPermissionsShowPrompt, + androidOnGeolocationPermissionsHidePrompt: + params.androidOnGeolocationPermissionsHidePrompt, + onGeolocationPermissionsHidePrompt: + params.onGeolocationPermissionsHidePrompt, + androidShouldInterceptRequest: params.androidShouldInterceptRequest, + shouldInterceptRequest: params.shouldInterceptRequest, + androidOnRenderProcessGone: params.androidOnRenderProcessGone, + onRenderProcessGone: params.onRenderProcessGone, + androidOnRenderProcessResponsive: + params.androidOnRenderProcessResponsive, + onRenderProcessResponsive: params.onRenderProcessResponsive, + androidOnRenderProcessUnresponsive: + params.androidOnRenderProcessUnresponsive, + onRenderProcessUnresponsive: params.onRenderProcessUnresponsive, + androidOnFormResubmission: params.androidOnFormResubmission, + onFormResubmission: params.onFormResubmission, + androidOnScaleChanged: params.androidOnScaleChanged, + androidOnReceivedIcon: params.androidOnReceivedIcon, + onReceivedIcon: params.onReceivedIcon, + androidOnReceivedTouchIconUrl: params.androidOnReceivedTouchIconUrl, + onReceivedTouchIconUrl: params.onReceivedTouchIconUrl, + androidOnJsBeforeUnload: params.androidOnJsBeforeUnload, + onJsBeforeUnload: params.onJsBeforeUnload, + androidOnReceivedLoginRequest: params.androidOnReceivedLoginRequest, + onReceivedLoginRequest: params.onReceivedLoginRequest, + onPermissionRequestCanceled: params.onPermissionRequestCanceled, + onRequestFocus: params.onRequestFocus, + iosOnWebContentProcessDidTerminate: + params.iosOnWebContentProcessDidTerminate, + onWebContentProcessDidTerminate: + params.onWebContentProcessDidTerminate, + iosOnDidReceiveServerRedirectForProvisionalNavigation: + params.iosOnDidReceiveServerRedirectForProvisionalNavigation, + onDidReceiveServerRedirectForProvisionalNavigation: + params.onDidReceiveServerRedirectForProvisionalNavigation, + iosOnNavigationResponse: params.iosOnNavigationResponse, + onNavigationResponse: params.onNavigationResponse, + iosShouldAllowDeprecatedTLS: params.iosShouldAllowDeprecatedTLS, + shouldAllowDeprecatedTLS: params.shouldAllowDeprecatedTLS, + onCameraCaptureStateChanged: params.onCameraCaptureStateChanged, + onMicrophoneCaptureStateChanged: + params.onMicrophoneCaptureStateChanged, + onContentSizeChanged: params.onContentSizeChanged, + initialUrlRequest: params.initialUrlRequest, + initialFile: params.initialFile, + initialData: params.initialData, + initialOptions: params.initialOptions, + initialSettings: params.initialSettings, + contextMenu: params.contextMenu, + initialUserScripts: params.initialUserScripts, + pullToRefreshController: params.pullToRefreshController + as AndroidPullToRefreshController?, + findInteractionController: params.findInteractionController + as AndroidFindInteractionController?); + + @override + final AndroidFindInteractionController? findInteractionController; + + @override + final AndroidPullToRefreshController? pullToRefreshController; +} + +///{@macro flutter_inappwebview_platform_interface.PlatformHeadlessInAppWebView} +class AndroidHeadlessInAppWebView extends PlatformHeadlessInAppWebView + with ChannelController { + @override + late final String id; + + bool _started = false; + bool _running = false; + + static const MethodChannel _sharedChannel = + const MethodChannel('com.pichillilorenzo/flutter_headless_inappwebview'); + + AndroidInAppWebViewController? _webViewController; + + /// Constructs a [AndroidHeadlessInAppWebView]. + AndroidHeadlessInAppWebView(PlatformHeadlessInAppWebViewCreationParams params) + : super.implementation( + params is AndroidHeadlessInAppWebViewCreationParams + ? params + : AndroidHeadlessInAppWebViewCreationParams + .fromPlatformHeadlessInAppWebViewCreationParams(params), + ) { + id = IdGenerator.generate(); + } + + @override + AndroidInAppWebViewController? get webViewController => _webViewController; + + dynamic _controllerFromPlatform; + + AndroidHeadlessInAppWebViewCreationParams get _androidParams => + params as AndroidHeadlessInAppWebViewCreationParams; + + _init() { + _webViewController = AndroidInAppWebViewController( + AndroidInAppWebViewControllerCreationParams( + id: id, webviewParams: params), + ); + _controllerFromPlatform = + params.controllerFromPlatform?.call(_webViewController!) ?? + _webViewController!; + _androidParams.pullToRefreshController?.init(id); + _androidParams.findInteractionController?.init(id); + channel = + MethodChannel('com.pichillilorenzo/flutter_headless_inappwebview_$id'); + handler = _handleMethod; + initMethodCallHandler(); + } + + Future _handleMethod(MethodCall call) async { + switch (call.method) { + case "onWebViewCreated": + if (params.onWebViewCreated != null && _webViewController != null) { + params.onWebViewCreated!(_controllerFromPlatform); + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + Future run() async { + if (_started) { + return; + } + _started = true; + _init(); + + final initialSettings = params.initialSettings ?? InAppWebViewSettings(); + _inferInitialSettings(initialSettings); + + Map settingsMap = + (params.initialSettings != null ? initialSettings.toMap() : null) ?? + params.initialOptions?.toMap() ?? + initialSettings.toMap(); + + Map pullToRefreshSettings = + _androidParams.pullToRefreshController?.params.settings.toMap() ?? + _androidParams.pullToRefreshController?.params.options.toMap() ?? + PullToRefreshSettings(enabled: false).toMap(); + + Map args = {}; + args.putIfAbsent('id', () => id); + args.putIfAbsent( + 'params', + () => { + 'initialUrlRequest': params.initialUrlRequest?.toMap(), + 'initialFile': params.initialFile, + 'initialData': params.initialData?.toMap(), + 'initialSettings': settingsMap, + 'contextMenu': params.contextMenu?.toMap() ?? {}, + 'windowId': params.windowId, + 'initialUserScripts': + params.initialUserScripts?.map((e) => e.toMap()).toList() ?? + [], + 'pullToRefreshSettings': pullToRefreshSettings, + 'initialSize': params.initialSize.toMap() + }); + await _sharedChannel.invokeMethod('run', args); + _running = true; + } + + void _inferInitialSettings(InAppWebViewSettings settings) { + if (params.shouldOverrideUrlLoading != null && + settings.useShouldOverrideUrlLoading == null) { + settings.useShouldOverrideUrlLoading = true; + } + if (params.onLoadResource != null && settings.useOnLoadResource == null) { + settings.useOnLoadResource = true; + } + if (params.onDownloadStartRequest != null && + settings.useOnDownloadStart == null) { + settings.useOnDownloadStart = true; + } + if (params.shouldInterceptAjaxRequest != null && + settings.useShouldInterceptAjaxRequest == null) { + settings.useShouldInterceptAjaxRequest = true; + } + if (params.shouldInterceptFetchRequest != null && + settings.useShouldInterceptFetchRequest == null) { + settings.useShouldInterceptFetchRequest = true; + } + if (params.shouldInterceptRequest != null && + settings.useShouldInterceptRequest == null) { + settings.useShouldInterceptRequest = true; + } + if (params.onRenderProcessGone != null && + settings.useOnRenderProcessGone == null) { + settings.useOnRenderProcessGone = true; + } + if (params.onNavigationResponse != null && + settings.useOnNavigationResponse == null) { + settings.useOnNavigationResponse = true; + } + } + + @override + bool isRunning() { + return _running; + } + + @override + Future setSize(Size size) async { + if (!_running) { + return; + } + + Map args = {}; + args.putIfAbsent('size', () => size.toMap()); + await channel?.invokeMethod('setSize', args); + } + + @override + Future getSize() async { + if (!_running) { + return null; + } + + Map args = {}; + Map sizeMap = + (await channel?.invokeMethod('getSize', args))?.cast(); + return MapSize.fromMap(sizeMap); + } + + @override + Future dispose() async { + if (!_running) { + return; + } + Map args = {}; + await channel?.invokeMethod('dispose', args); + disposeChannel(); + _started = false; + _running = false; + _webViewController?.dispose(); + _webViewController = null; + _controllerFromPlatform = null; + _androidParams.pullToRefreshController?.dispose(); + _androidParams.findInteractionController?.dispose(); + } +} + +extension InternalHeadlessInAppWebView on AndroidHeadlessInAppWebView { + Future internalDispose() async { + _started = false; + _running = false; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/in_app_webview/in_app_webview.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/in_app_webview/in_app_webview.dart new file mode 100755 index 00000000..e96a92b7 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/in_app_webview/in_app_webview.dart @@ -0,0 +1,475 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; +import 'headless_in_app_webview.dart'; + +import '../find_interaction/find_interaction_controller.dart'; +import 'in_app_webview_controller.dart'; +import '../pull_to_refresh/main.dart'; +import '../pull_to_refresh/pull_to_refresh_controller.dart'; + +/// Object specifying creation parameters for creating a [PlatformInAppWebViewWidget]. +/// +/// Platform specific implementations can add additional fields by extending +/// this class. +class AndroidInAppWebViewWidgetCreationParams + extends PlatformInAppWebViewWidgetCreationParams { + AndroidInAppWebViewWidgetCreationParams( + {super.controllerFromPlatform, + super.key, + super.layoutDirection, + super.gestureRecognizers, + super.headlessWebView, + super.keepAlive, + super.preventGestureDelay, + super.windowId, + super.onWebViewCreated, + super.onLoadStart, + super.onLoadStop, + @Deprecated('Use onReceivedError instead') super.onLoadError, + super.onReceivedError, + @Deprecated("Use onReceivedHttpError instead") super.onLoadHttpError, + super.onReceivedHttpError, + super.onProgressChanged, + super.onConsoleMessage, + super.shouldOverrideUrlLoading, + super.onLoadResource, + super.onScrollChanged, + @Deprecated('Use onDownloadStartRequest instead') super.onDownloadStart, + super.onDownloadStartRequest, + @Deprecated('Use onLoadResourceWithCustomScheme instead') + super.onLoadResourceCustomScheme, + super.onLoadResourceWithCustomScheme, + super.onCreateWindow, + super.onCloseWindow, + super.onJsAlert, + super.onJsConfirm, + super.onJsPrompt, + super.onReceivedHttpAuthRequest, + super.onReceivedServerTrustAuthRequest, + super.onReceivedClientCertRequest, + @Deprecated('Use FindInteractionController.onFindResultReceived instead') + super.onFindResultReceived, + super.shouldInterceptAjaxRequest, + super.onAjaxReadyStateChange, + super.onAjaxProgress, + super.shouldInterceptFetchRequest, + super.onUpdateVisitedHistory, + @Deprecated("Use onPrintRequest instead") super.onPrint, + super.onPrintRequest, + super.onLongPressHitTestResult, + super.onEnterFullscreen, + super.onExitFullscreen, + super.onPageCommitVisible, + super.onTitleChanged, + super.onWindowFocus, + super.onWindowBlur, + super.onOverScrolled, + super.onZoomScaleChanged, + @Deprecated('Use onSafeBrowsingHit instead') + super.androidOnSafeBrowsingHit, + super.onSafeBrowsingHit, + @Deprecated('Use onPermissionRequest instead') + super.androidOnPermissionRequest, + super.onPermissionRequest, + @Deprecated('Use onGeolocationPermissionsShowPrompt instead') + super.androidOnGeolocationPermissionsShowPrompt, + super.onGeolocationPermissionsShowPrompt, + @Deprecated('Use onGeolocationPermissionsHidePrompt instead') + super.androidOnGeolocationPermissionsHidePrompt, + super.onGeolocationPermissionsHidePrompt, + @Deprecated('Use shouldInterceptRequest instead') + super.androidShouldInterceptRequest, + super.shouldInterceptRequest, + @Deprecated('Use onRenderProcessGone instead') + super.androidOnRenderProcessGone, + super.onRenderProcessGone, + @Deprecated('Use onRenderProcessResponsive instead') + super.androidOnRenderProcessResponsive, + super.onRenderProcessResponsive, + @Deprecated('Use onRenderProcessUnresponsive instead') + super.androidOnRenderProcessUnresponsive, + super.onRenderProcessUnresponsive, + @Deprecated('Use onFormResubmission instead') + super.androidOnFormResubmission, + super.onFormResubmission, + @Deprecated('Use onZoomScaleChanged instead') super.androidOnScaleChanged, + @Deprecated('Use onReceivedIcon instead') super.androidOnReceivedIcon, + super.onReceivedIcon, + @Deprecated('Use onReceivedTouchIconUrl instead') + super.androidOnReceivedTouchIconUrl, + super.onReceivedTouchIconUrl, + @Deprecated('Use onJsBeforeUnload instead') super.androidOnJsBeforeUnload, + super.onJsBeforeUnload, + @Deprecated('Use onReceivedLoginRequest instead') + super.androidOnReceivedLoginRequest, + super.onReceivedLoginRequest, + super.onPermissionRequestCanceled, + super.onRequestFocus, + @Deprecated('Use onWebContentProcessDidTerminate instead') + super.iosOnWebContentProcessDidTerminate, + super.onWebContentProcessDidTerminate, + @Deprecated( + 'Use onDidReceiveServerRedirectForProvisionalNavigation instead') + super.iosOnDidReceiveServerRedirectForProvisionalNavigation, + super.onDidReceiveServerRedirectForProvisionalNavigation, + @Deprecated('Use onNavigationResponse instead') + super.iosOnNavigationResponse, + super.onNavigationResponse, + @Deprecated('Use shouldAllowDeprecatedTLS instead') + super.iosShouldAllowDeprecatedTLS, + super.shouldAllowDeprecatedTLS, + super.onCameraCaptureStateChanged, + super.onMicrophoneCaptureStateChanged, + super.onContentSizeChanged, + super.initialUrlRequest, + super.initialFile, + super.initialData, + @Deprecated('Use initialSettings instead') super.initialOptions, + super.initialSettings, + super.contextMenu, + super.initialUserScripts, + this.pullToRefreshController, + this.findInteractionController}); + + /// Constructs a [AndroidInAppWebViewWidgetCreationParams] using a + /// [PlatformInAppWebViewWidgetCreationParams]. + AndroidInAppWebViewWidgetCreationParams.fromPlatformInAppWebViewWidgetCreationParams( + PlatformInAppWebViewWidgetCreationParams params) + : this( + controllerFromPlatform: params.controllerFromPlatform, + key: params.key, + layoutDirection: params.layoutDirection, + gestureRecognizers: params.gestureRecognizers, + headlessWebView: params.headlessWebView, + keepAlive: params.keepAlive, + preventGestureDelay: params.preventGestureDelay, + windowId: params.windowId, + onWebViewCreated: params.onWebViewCreated, + onLoadStart: params.onLoadStart, + onLoadStop: params.onLoadStop, + onLoadError: params.onLoadError, + onReceivedError: params.onReceivedError, + onLoadHttpError: params.onLoadHttpError, + onReceivedHttpError: params.onReceivedHttpError, + onProgressChanged: params.onProgressChanged, + onConsoleMessage: params.onConsoleMessage, + shouldOverrideUrlLoading: params.shouldOverrideUrlLoading, + onLoadResource: params.onLoadResource, + onScrollChanged: params.onScrollChanged, + onDownloadStart: params.onDownloadStart, + onDownloadStartRequest: params.onDownloadStartRequest, + onLoadResourceCustomScheme: params.onLoadResourceCustomScheme, + onLoadResourceWithCustomScheme: + params.onLoadResourceWithCustomScheme, + onCreateWindow: params.onCreateWindow, + onCloseWindow: params.onCloseWindow, + onJsAlert: params.onJsAlert, + onJsConfirm: params.onJsConfirm, + onJsPrompt: params.onJsPrompt, + onReceivedHttpAuthRequest: params.onReceivedHttpAuthRequest, + onReceivedServerTrustAuthRequest: + params.onReceivedServerTrustAuthRequest, + onReceivedClientCertRequest: params.onReceivedClientCertRequest, + onFindResultReceived: params.onFindResultReceived, + shouldInterceptAjaxRequest: params.shouldInterceptAjaxRequest, + onAjaxReadyStateChange: params.onAjaxReadyStateChange, + onAjaxProgress: params.onAjaxProgress, + shouldInterceptFetchRequest: params.shouldInterceptFetchRequest, + onUpdateVisitedHistory: params.onUpdateVisitedHistory, + onPrint: params.onPrint, + onPrintRequest: params.onPrintRequest, + onLongPressHitTestResult: params.onLongPressHitTestResult, + onEnterFullscreen: params.onEnterFullscreen, + onExitFullscreen: params.onExitFullscreen, + onPageCommitVisible: params.onPageCommitVisible, + onTitleChanged: params.onTitleChanged, + onWindowFocus: params.onWindowFocus, + onWindowBlur: params.onWindowBlur, + onOverScrolled: params.onOverScrolled, + onZoomScaleChanged: params.onZoomScaleChanged, + androidOnSafeBrowsingHit: params.androidOnSafeBrowsingHit, + onSafeBrowsingHit: params.onSafeBrowsingHit, + androidOnPermissionRequest: params.androidOnPermissionRequest, + onPermissionRequest: params.onPermissionRequest, + androidOnGeolocationPermissionsShowPrompt: + params.androidOnGeolocationPermissionsShowPrompt, + onGeolocationPermissionsShowPrompt: + params.onGeolocationPermissionsShowPrompt, + androidOnGeolocationPermissionsHidePrompt: + params.androidOnGeolocationPermissionsHidePrompt, + onGeolocationPermissionsHidePrompt: + params.onGeolocationPermissionsHidePrompt, + androidShouldInterceptRequest: params.androidShouldInterceptRequest, + shouldInterceptRequest: params.shouldInterceptRequest, + androidOnRenderProcessGone: params.androidOnRenderProcessGone, + onRenderProcessGone: params.onRenderProcessGone, + androidOnRenderProcessResponsive: + params.androidOnRenderProcessResponsive, + onRenderProcessResponsive: params.onRenderProcessResponsive, + androidOnRenderProcessUnresponsive: + params.androidOnRenderProcessUnresponsive, + onRenderProcessUnresponsive: params.onRenderProcessUnresponsive, + androidOnFormResubmission: params.androidOnFormResubmission, + onFormResubmission: params.onFormResubmission, + androidOnScaleChanged: params.androidOnScaleChanged, + androidOnReceivedIcon: params.androidOnReceivedIcon, + onReceivedIcon: params.onReceivedIcon, + androidOnReceivedTouchIconUrl: params.androidOnReceivedTouchIconUrl, + onReceivedTouchIconUrl: params.onReceivedTouchIconUrl, + androidOnJsBeforeUnload: params.androidOnJsBeforeUnload, + onJsBeforeUnload: params.onJsBeforeUnload, + androidOnReceivedLoginRequest: params.androidOnReceivedLoginRequest, + onReceivedLoginRequest: params.onReceivedLoginRequest, + onPermissionRequestCanceled: params.onPermissionRequestCanceled, + onRequestFocus: params.onRequestFocus, + iosOnWebContentProcessDidTerminate: + params.iosOnWebContentProcessDidTerminate, + onWebContentProcessDidTerminate: + params.onWebContentProcessDidTerminate, + iosOnDidReceiveServerRedirectForProvisionalNavigation: + params.iosOnDidReceiveServerRedirectForProvisionalNavigation, + onDidReceiveServerRedirectForProvisionalNavigation: + params.onDidReceiveServerRedirectForProvisionalNavigation, + iosOnNavigationResponse: params.iosOnNavigationResponse, + onNavigationResponse: params.onNavigationResponse, + iosShouldAllowDeprecatedTLS: params.iosShouldAllowDeprecatedTLS, + shouldAllowDeprecatedTLS: params.shouldAllowDeprecatedTLS, + onCameraCaptureStateChanged: params.onCameraCaptureStateChanged, + onMicrophoneCaptureStateChanged: + params.onMicrophoneCaptureStateChanged, + onContentSizeChanged: params.onContentSizeChanged, + initialUrlRequest: params.initialUrlRequest, + initialFile: params.initialFile, + initialData: params.initialData, + initialOptions: params.initialOptions, + initialSettings: params.initialSettings, + contextMenu: params.contextMenu, + initialUserScripts: params.initialUserScripts, + pullToRefreshController: params.pullToRefreshController + as AndroidPullToRefreshController?, + findInteractionController: params.findInteractionController + as AndroidFindInteractionController?); + + @override + final AndroidFindInteractionController? findInteractionController; + + @override + final AndroidPullToRefreshController? pullToRefreshController; +} + +///{@macro flutter_inappwebview_platform_interface.PlatformInAppWebViewWidget} +class AndroidInAppWebViewWidget extends PlatformInAppWebViewWidget { + /// Constructs a [AndroidInAppWebViewWidget]. + /// + ///{@macro flutter_inappwebview_platform_interface.PlatformInAppWebViewWidget} + AndroidInAppWebViewWidget(PlatformInAppWebViewWidgetCreationParams params) + : super.implementation( + params is AndroidInAppWebViewWidgetCreationParams + ? params + : AndroidInAppWebViewWidgetCreationParams + .fromPlatformInAppWebViewWidgetCreationParams(params), + ); + + AndroidInAppWebViewWidgetCreationParams get _androidParams => + params as AndroidInAppWebViewWidgetCreationParams; + + AndroidInAppWebViewController? _controller; + + AndroidHeadlessInAppWebView? get _androidHeadlessInAppWebView => + params.headlessWebView as AndroidHeadlessInAppWebView?; + + @override + Widget build(BuildContext context) { + final initialSettings = params.initialSettings ?? InAppWebViewSettings(); + _inferInitialSettings(initialSettings); + + Map settingsMap = + (params.initialSettings != null ? initialSettings.toMap() : null) ?? + // ignore: deprecated_member_use_from_same_package + params.initialOptions?.toMap() ?? + initialSettings.toMap(); + + Map pullToRefreshSettings = + params.pullToRefreshController?.params.settings.toMap() ?? + // ignore: deprecated_member_use_from_same_package + params.pullToRefreshController?.params.options.toMap() ?? + PullToRefreshSettings(enabled: false).toMap(); + + if ((params.headlessWebView?.isRunning() ?? false) && + params.keepAlive != null) { + final headlessId = params.headlessWebView?.id; + if (headlessId != null) { + // force keep alive id to match headless webview id + params.keepAlive?.id = headlessId; + } + } + + var useHybridComposition = (params.initialSettings != null + ? initialSettings.useHybridComposition + : params.initialOptions?.android.useHybridComposition) ?? + true; + + return PlatformViewLink( + key: params.key, + viewType: 'com.pichillilorenzo/flutter_inappwebview', + surfaceFactory: ( + BuildContext context, + PlatformViewController controller, + ) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: params.gestureRecognizers ?? + const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + onCreatePlatformView: (PlatformViewCreationParams params) { + return _createAndroidViewController( + hybridComposition: useHybridComposition, + id: params.id, + viewType: 'com.pichillilorenzo/flutter_inappwebview', + layoutDirection: this.params.layoutDirection ?? + Directionality.maybeOf(context) ?? + TextDirection.rtl, + creationParams: { + 'initialUrlRequest': this.params.initialUrlRequest?.toMap(), + 'initialFile': this.params.initialFile, + 'initialData': this.params.initialData?.toMap(), + 'initialSettings': settingsMap, + 'contextMenu': this.params.contextMenu?.toMap() ?? {}, + 'windowId': this.params.windowId, + 'headlessWebViewId': + this.params.headlessWebView?.isRunning() ?? false + ? this.params.headlessWebView?.id + : null, + 'initialUserScripts': this + .params + .initialUserScripts + ?.map((e) => e.toMap()) + .toList() ?? + [], + 'pullToRefreshSettings': pullToRefreshSettings, + 'keepAliveId': this.params.keepAlive?.id + }, + ) + ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) + ..addOnPlatformViewCreatedListener((id) => _onPlatformViewCreated(id)) + ..create(); + }, + ); + } + + AndroidViewController _createAndroidViewController({ + required bool hybridComposition, + required int id, + required String viewType, + required TextDirection layoutDirection, + required Map creationParams, + }) { + if (hybridComposition) { + return PlatformViewsService.initExpensiveAndroidView( + id: id, + viewType: viewType, + layoutDirection: layoutDirection, + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + ); + } + return PlatformViewsService.initSurfaceAndroidView( + id: id, + viewType: viewType, + layoutDirection: layoutDirection, + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + ); + } + + void _onPlatformViewCreated(int id) { + dynamic viewId = id; + if (params.headlessWebView?.isRunning() ?? false) { + viewId = params.headlessWebView?.id; + } + viewId = params.keepAlive?.id ?? viewId ?? id; + _androidHeadlessInAppWebView?.internalDispose(); + _controller = AndroidInAppWebViewController( + PlatformInAppWebViewControllerCreationParams( + id: viewId, webviewParams: params)); + _androidParams.pullToRefreshController?.init(viewId); + _androidParams.findInteractionController?.init(viewId); + debugLog( + className: runtimeType.toString(), + id: viewId?.toString(), + debugLoggingSettings: + PlatformInAppWebViewController.debugLoggingSettings, + method: "onWebViewCreated", + args: []); + if (params.onWebViewCreated != null) { + params.onWebViewCreated!( + params.controllerFromPlatform?.call(_controller!) ?? _controller!); + } + } + + void _inferInitialSettings(InAppWebViewSettings settings) { + if (params.shouldOverrideUrlLoading != null && + settings.useShouldOverrideUrlLoading == null) { + settings.useShouldOverrideUrlLoading = true; + } + if (params.onLoadResource != null && settings.useOnLoadResource == null) { + settings.useOnLoadResource = true; + } + if (params.onDownloadStartRequest != null && + settings.useOnDownloadStart == null) { + settings.useOnDownloadStart = true; + } + if ((params.shouldInterceptAjaxRequest != null || + params.onAjaxProgress != null || + params.onAjaxReadyStateChange != null) && + settings.useShouldInterceptAjaxRequest == null) { + settings.useShouldInterceptAjaxRequest = true; + } + if (params.shouldInterceptFetchRequest != null && + settings.useShouldInterceptFetchRequest == null) { + settings.useShouldInterceptFetchRequest = true; + } + if (params.shouldInterceptRequest != null && + settings.useShouldInterceptRequest == null) { + settings.useShouldInterceptRequest = true; + } + if (params.onRenderProcessGone != null && + settings.useOnRenderProcessGone == null) { + settings.useOnRenderProcessGone = true; + } + if (params.onNavigationResponse != null && + settings.useOnNavigationResponse == null) { + settings.useOnNavigationResponse = true; + } + } + + @override + void dispose() { + dynamic viewId = _controller?.getViewId(); + debugLog( + className: runtimeType.toString(), + id: viewId?.toString(), + debugLoggingSettings: + PlatformInAppWebViewController.debugLoggingSettings, + method: "dispose", + args: []); + final isKeepAlive = params.keepAlive != null; + _controller?.dispose(isKeepAlive: isKeepAlive); + _controller = null; + params.pullToRefreshController?.dispose(isKeepAlive: isKeepAlive); + params.findInteractionController?.dispose(isKeepAlive: isKeepAlive); + } + + @override + T controllerFromPlatform(PlatformInAppWebViewController controller) { + // unused + throw UnimplementedError(); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/in_app_webview/in_app_webview_controller.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/in_app_webview/in_app_webview_controller.dart new file mode 100644 index 00000000..ceb4bc9a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/in_app_webview/in_app_webview_controller.dart @@ -0,0 +1,2779 @@ +import 'dart:io'; +import 'dart:collection'; +import 'dart:convert'; +import 'dart:core'; +import 'dart:developer' as developer; +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +import '../web_message/main.dart'; + +import '../in_app_browser/in_app_browser.dart'; +import '../web_storage/web_storage.dart'; + +import 'headless_in_app_webview.dart'; +import '_static_channel.dart'; + +import '../print_job/main.dart'; + +///List of forbidden names for JavaScript handlers. +// ignore: non_constant_identifier_names +final _JAVASCRIPT_HANDLER_FORBIDDEN_NAMES = UnmodifiableListView([ + "onLoadResource", + "shouldInterceptAjaxRequest", + "onAjaxReadyStateChange", + "onAjaxProgress", + "shouldInterceptFetchRequest", + "onPrintRequest", + "onWindowFocus", + "onWindowBlur", + "callAsyncJavaScript", + "evaluateJavaScriptWithContentWorld" +]); + +/// Object specifying creation parameters for creating a [AndroidInAppWebViewController]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformInAppWebViewControllerCreationParams] for +/// more information. +@immutable +class AndroidInAppWebViewControllerCreationParams + extends PlatformInAppWebViewControllerCreationParams { + /// Creates a new [AndroidInAppWebViewControllerCreationParams] instance. + const AndroidInAppWebViewControllerCreationParams( + {required super.id, super.webviewParams}); + + /// Creates a [AndroidInAppWebViewControllerCreationParams] instance based on [PlatformInAppWebViewControllerCreationParams]. + factory AndroidInAppWebViewControllerCreationParams.fromPlatformInAppWebViewControllerCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformInAppWebViewControllerCreationParams params) { + return AndroidInAppWebViewControllerCreationParams( + id: params.id, webviewParams: params.webviewParams); + } +} + +///Controls a WebView, such as an [InAppWebView] widget instance, a [AndroidHeadlessInAppWebView] instance or [AndroidInAppBrowser] WebView instance. +/// +///If you are using the [InAppWebView] widget, an [InAppWebViewController] instance can be obtained by setting the [InAppWebView.onWebViewCreated] +///callback. Instead, if you are using an [AndroidInAppBrowser] instance, you can get it through the [AndroidInAppBrowser.webViewController] attribute. +class AndroidInAppWebViewController extends PlatformInAppWebViewController + with ChannelController { + static final MethodChannel _staticChannel = IN_APP_WEBVIEW_STATIC_CHANNEL; + + // List of properties to be saved and restored for keep alive feature + Map _javaScriptHandlersMap = + HashMap(); + Map> _userScripts = { + UserScriptInjectionTime.AT_DOCUMENT_START: [], + UserScriptInjectionTime.AT_DOCUMENT_END: [] + }; + Set _webMessageListenerObjNames = Set(); + Map _injectedScriptsFromURL = {}; + Set _webMessageChannels = Set(); + Set _webMessageListeners = Set(); + + // static map that contains the properties to be saved and restored for keep alive feature + static final Map + _keepAliveMap = {}; + + AndroidInAppBrowser? _inAppBrowser; + + PlatformInAppBrowserEvents? get _inAppBrowserEventHandler => + _inAppBrowser?.eventHandler; + + dynamic _controllerFromPlatform; + + @override + late AndroidWebStorage webStorage; + + AndroidInAppWebViewController( + PlatformInAppWebViewControllerCreationParams params) + : super.implementation(params + is AndroidInAppWebViewControllerCreationParams + ? params + : AndroidInAppWebViewControllerCreationParams + .fromPlatformInAppWebViewControllerCreationParams(params)) { + channel = MethodChannel('com.pichillilorenzo/flutter_inappwebview_$id'); + handler = handleMethod; + initMethodCallHandler(); + + final initialUserScripts = webviewParams?.initialUserScripts; + if (initialUserScripts != null) { + for (final userScript in initialUserScripts) { + if (userScript.injectionTime == + UserScriptInjectionTime.AT_DOCUMENT_START) { + this + ._userScripts[UserScriptInjectionTime.AT_DOCUMENT_START] + ?.add(userScript); + } else { + this + ._userScripts[UserScriptInjectionTime.AT_DOCUMENT_END] + ?.add(userScript); + } + } + } + + this._init(params); + } + + static final AndroidInAppWebViewController _staticValue = + AndroidInAppWebViewController( + AndroidInAppWebViewControllerCreationParams(id: null)); + + factory AndroidInAppWebViewController.static() { + return _staticValue; + } + + AndroidInAppWebViewController.fromInAppBrowser( + PlatformInAppWebViewControllerCreationParams params, + MethodChannel channel, + AndroidInAppBrowser inAppBrowser, + UnmodifiableListView? initialUserScripts) + : super.implementation( + params is AndroidInAppWebViewControllerCreationParams + ? params + : AndroidInAppWebViewControllerCreationParams + .fromPlatformInAppWebViewControllerCreationParams(params)) { + this.channel = channel; + this._inAppBrowser = inAppBrowser; + + if (initialUserScripts != null) { + for (final userScript in initialUserScripts) { + if (userScript.injectionTime == + UserScriptInjectionTime.AT_DOCUMENT_START) { + this + ._userScripts[UserScriptInjectionTime.AT_DOCUMENT_START] + ?.add(userScript); + } else { + this + ._userScripts[UserScriptInjectionTime.AT_DOCUMENT_END] + ?.add(userScript); + } + } + } + this._init(params); + } + + void _init(PlatformInAppWebViewControllerCreationParams params) { + _controllerFromPlatform = + params.webviewParams?.controllerFromPlatform?.call(this) ?? this; + + webStorage = AndroidWebStorage(AndroidWebStorageCreationParams( + localStorage: AndroidLocalStorage.defaultStorage(controller: this), + sessionStorage: + AndroidSessionStorage.defaultStorage(controller: this))); + + if (params.webviewParams is PlatformInAppWebViewWidgetCreationParams) { + final keepAlive = + (params.webviewParams as PlatformInAppWebViewWidgetCreationParams) + .keepAlive; + if (keepAlive != null) { + InAppWebViewControllerKeepAliveProps? props = _keepAliveMap[keepAlive]; + if (props == null) { + // save controller properties to restore it later + _keepAliveMap[keepAlive] = InAppWebViewControllerKeepAliveProps( + injectedScriptsFromURL: _injectedScriptsFromURL, + javaScriptHandlersMap: _javaScriptHandlersMap, + userScripts: _userScripts, + webMessageListenerObjNames: _webMessageListenerObjNames, + webMessageChannels: _webMessageChannels, + webMessageListeners: _webMessageListeners); + } else { + // restore controller properties + _injectedScriptsFromURL = props.injectedScriptsFromURL; + _javaScriptHandlersMap = props.javaScriptHandlersMap; + _userScripts = props.userScripts; + _webMessageListenerObjNames = props.webMessageListenerObjNames; + _webMessageChannels = + props.webMessageChannels as Set; + _webMessageListeners = + props.webMessageListeners as Set; + } + } + } + } + + _debugLog(String method, dynamic args) { + debugLog( + className: this.runtimeType.toString(), + name: _inAppBrowser == null + ? "WebView" + : _inAppBrowser.runtimeType.toString(), + id: (getViewId() ?? _inAppBrowser?.id).toString(), + debugLoggingSettings: + PlatformInAppWebViewController.debugLoggingSettings, + method: method, + args: args); + } + + Future _handleMethod(MethodCall call) async { + if (PlatformInAppWebViewController.debugLoggingSettings.enabled && + call.method != "onCallJsHandler") { + _debugLog(call.method, call.arguments); + } + + switch (call.method) { + case "onLoadStart": + _injectedScriptsFromURL.clear(); + if ((webviewParams != null && webviewParams!.onLoadStart != null) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + WebUri? uri = url != null ? WebUri(url) : null; + if (webviewParams != null && webviewParams!.onLoadStart != null) + webviewParams!.onLoadStart!(_controllerFromPlatform, uri); + else + _inAppBrowserEventHandler!.onLoadStart(uri); + } + break; + case "onLoadStop": + if ((webviewParams != null && webviewParams!.onLoadStop != null) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + WebUri? uri = url != null ? WebUri(url) : null; + if (webviewParams != null && webviewParams!.onLoadStop != null) + webviewParams!.onLoadStop!(_controllerFromPlatform, uri); + else + _inAppBrowserEventHandler!.onLoadStop(uri); + } + break; + case "onReceivedError": + if ((webviewParams != null && + (webviewParams!.onReceivedError != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.onLoadError != null)) || + _inAppBrowserEventHandler != null) { + WebResourceRequest request = WebResourceRequest.fromMap( + call.arguments["request"].cast())!; + WebResourceError error = WebResourceError.fromMap( + call.arguments["error"].cast())!; + var isForMainFrame = request.isForMainFrame ?? false; + + if (webviewParams != null) { + if (webviewParams!.onReceivedError != null) + webviewParams!.onReceivedError!( + _controllerFromPlatform, request, error); + else if (isForMainFrame) { + // ignore: deprecated_member_use_from_same_package + webviewParams!.onLoadError!(_controllerFromPlatform, request.url, + error.type.toNativeValue() ?? -1, error.description); + } + } else { + if (isForMainFrame) { + _inAppBrowserEventHandler!.onLoadError(request.url, + error.type.toNativeValue() ?? -1, error.description); + } + _inAppBrowserEventHandler!.onReceivedError(request, error); + } + } + break; + case "onReceivedHttpError": + if ((webviewParams != null && + (webviewParams!.onReceivedHttpError != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.onLoadHttpError != null)) || + _inAppBrowserEventHandler != null) { + WebResourceRequest request = WebResourceRequest.fromMap( + call.arguments["request"].cast())!; + WebResourceResponse errorResponse = WebResourceResponse.fromMap( + call.arguments["errorResponse"].cast())!; + var isForMainFrame = request.isForMainFrame ?? false; + + if (webviewParams != null) { + if (webviewParams!.onReceivedHttpError != null) + webviewParams!.onReceivedHttpError!( + _controllerFromPlatform, request, errorResponse); + else if (isForMainFrame) { + // ignore: deprecated_member_use_from_same_package + webviewParams!.onLoadHttpError!( + _controllerFromPlatform, + request.url, + errorResponse.statusCode ?? -1, + errorResponse.reasonPhrase ?? ''); + } + } else { + if (isForMainFrame) { + _inAppBrowserEventHandler!.onLoadHttpError( + request.url, + errorResponse.statusCode ?? -1, + errorResponse.reasonPhrase ?? ''); + } + _inAppBrowserEventHandler! + .onReceivedHttpError(request, errorResponse); + } + } + break; + case "onProgressChanged": + if ((webviewParams != null && + webviewParams!.onProgressChanged != null) || + _inAppBrowserEventHandler != null) { + int progress = call.arguments["progress"]; + if (webviewParams != null && webviewParams!.onProgressChanged != null) + webviewParams!.onProgressChanged!( + _controllerFromPlatform, progress); + else + _inAppBrowserEventHandler!.onProgressChanged(progress); + } + break; + case "shouldOverrideUrlLoading": + if ((webviewParams != null && + webviewParams!.shouldOverrideUrlLoading != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + NavigationAction navigationAction = + NavigationAction.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.shouldOverrideUrlLoading != null) + return (await webviewParams!.shouldOverrideUrlLoading!( + _controllerFromPlatform, navigationAction)) + ?.toNativeValue(); + return (await _inAppBrowserEventHandler! + .shouldOverrideUrlLoading(navigationAction)) + ?.toNativeValue(); + } + break; + case "onConsoleMessage": + if ((webviewParams != null && + webviewParams!.onConsoleMessage != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + ConsoleMessage consoleMessage = ConsoleMessage.fromMap(arguments)!; + if (webviewParams != null && webviewParams!.onConsoleMessage != null) + webviewParams!.onConsoleMessage!( + _controllerFromPlatform, consoleMessage); + else + _inAppBrowserEventHandler!.onConsoleMessage(consoleMessage); + } + break; + case "onScrollChanged": + if ((webviewParams != null && webviewParams!.onScrollChanged != null) || + _inAppBrowserEventHandler != null) { + int x = call.arguments["x"]; + int y = call.arguments["y"]; + if (webviewParams != null && webviewParams!.onScrollChanged != null) + webviewParams!.onScrollChanged!(_controllerFromPlatform, x, y); + else + _inAppBrowserEventHandler!.onScrollChanged(x, y); + } + break; + case "onDownloadStartRequest": + if ((webviewParams != null && + // ignore: deprecated_member_use_from_same_package + (webviewParams!.onDownloadStart != null || + webviewParams!.onDownloadStartRequest != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + DownloadStartRequest downloadStartRequest = + DownloadStartRequest.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.onDownloadStartRequest != null) + webviewParams!.onDownloadStartRequest!( + _controllerFromPlatform, downloadStartRequest); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.onDownloadStart!( + _controllerFromPlatform, downloadStartRequest.url); + } + } else { + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler! + .onDownloadStart(downloadStartRequest.url); + _inAppBrowserEventHandler! + .onDownloadStartRequest(downloadStartRequest); + } + } + break; + case "onLoadResourceWithCustomScheme": + if ((webviewParams != null && + (webviewParams!.onLoadResourceWithCustomScheme != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.onLoadResourceCustomScheme != null)) || + _inAppBrowserEventHandler != null) { + Map requestMap = + call.arguments["request"].cast(); + WebResourceRequest request = WebResourceRequest.fromMap(requestMap)!; + + if (webviewParams != null) { + if (webviewParams!.onLoadResourceWithCustomScheme != null) + return (await webviewParams!.onLoadResourceWithCustomScheme!( + _controllerFromPlatform, request)) + ?.toMap(); + else { + return (await params + .webviewParams! + // ignore: deprecated_member_use_from_same_package + .onLoadResourceCustomScheme!( + _controllerFromPlatform, request.url)) + ?.toMap(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onLoadResourceWithCustomScheme(request)) ?? + (await _inAppBrowserEventHandler! + .onLoadResourceCustomScheme(request.url))) + ?.toMap(); + } + } + break; + case "onCreateWindow": + if ((webviewParams != null && webviewParams!.onCreateWindow != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + CreateWindowAction createWindowAction = + CreateWindowAction.fromMap(arguments)!; + + if (webviewParams != null && webviewParams!.onCreateWindow != null) + return await webviewParams!.onCreateWindow!( + _controllerFromPlatform, createWindowAction); + else + return await _inAppBrowserEventHandler! + .onCreateWindow(createWindowAction); + } + break; + case "onCloseWindow": + if (webviewParams != null && webviewParams!.onCloseWindow != null) + webviewParams!.onCloseWindow!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onCloseWindow(); + break; + case "onTitleChanged": + if ((webviewParams != null && webviewParams!.onTitleChanged != null) || + _inAppBrowserEventHandler != null) { + String? title = call.arguments["title"]; + if (webviewParams != null && webviewParams!.onTitleChanged != null) + webviewParams!.onTitleChanged!(_controllerFromPlatform, title); + else + _inAppBrowserEventHandler!.onTitleChanged(title); + } + break; + case "onGeolocationPermissionsShowPrompt": + if ((webviewParams != null && + (webviewParams!.onGeolocationPermissionsShowPrompt != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnGeolocationPermissionsShowPrompt != + null)) || + _inAppBrowserEventHandler != null) { + String origin = call.arguments["origin"]; + + if (webviewParams != null) { + if (webviewParams!.onGeolocationPermissionsShowPrompt != null) + return (await webviewParams!.onGeolocationPermissionsShowPrompt!( + _controllerFromPlatform, origin)) + ?.toMap(); + else { + return (await params + .webviewParams! + // ignore: deprecated_member_use_from_same_package + .androidOnGeolocationPermissionsShowPrompt!( + _controllerFromPlatform, origin)) + ?.toMap(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onGeolocationPermissionsShowPrompt(origin)) ?? + (await _inAppBrowserEventHandler! + .androidOnGeolocationPermissionsShowPrompt(origin))) + ?.toMap(); + } + } + break; + case "onGeolocationPermissionsHidePrompt": + if (webviewParams != null && + (webviewParams!.onGeolocationPermissionsHidePrompt != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnGeolocationPermissionsHidePrompt != + null)) { + if (webviewParams!.onGeolocationPermissionsHidePrompt != null) + webviewParams! + .onGeolocationPermissionsHidePrompt!(_controllerFromPlatform); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnGeolocationPermissionsHidePrompt!( + _controllerFromPlatform); + } + } else if (_inAppBrowserEventHandler != null) { + _inAppBrowserEventHandler!.onGeolocationPermissionsHidePrompt(); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler! + .androidOnGeolocationPermissionsHidePrompt(); + } + break; + case "shouldInterceptRequest": + if ((webviewParams != null && + (webviewParams!.shouldInterceptRequest != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidShouldInterceptRequest != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + WebResourceRequest request = WebResourceRequest.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.shouldInterceptRequest != null) + return (await webviewParams!.shouldInterceptRequest!( + _controllerFromPlatform, request)) + ?.toMap(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.androidShouldInterceptRequest!( + _controllerFromPlatform, request)) + ?.toMap(); + } + } else { + return ((await _inAppBrowserEventHandler! + .shouldInterceptRequest(request)) ?? + (await _inAppBrowserEventHandler! + .androidShouldInterceptRequest(request))) + ?.toMap(); + } + } + break; + case "onRenderProcessUnresponsive": + if ((webviewParams != null && + (webviewParams!.onRenderProcessUnresponsive != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnRenderProcessUnresponsive != + null)) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + WebUri? uri = url != null ? WebUri(url) : null; + + if (webviewParams != null) { + if (webviewParams!.onRenderProcessUnresponsive != null) + return (await webviewParams!.onRenderProcessUnresponsive!( + _controllerFromPlatform, uri)) + ?.toNativeValue(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.androidOnRenderProcessUnresponsive!( + _controllerFromPlatform, uri)) + ?.toNativeValue(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onRenderProcessUnresponsive(uri)) ?? + (await _inAppBrowserEventHandler! + .androidOnRenderProcessUnresponsive(uri))) + ?.toNativeValue(); + } + } + break; + case "onRenderProcessResponsive": + if ((webviewParams != null && + (webviewParams!.onRenderProcessResponsive != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnRenderProcessResponsive != null)) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + WebUri? uri = url != null ? WebUri(url) : null; + + if (webviewParams != null) { + if (webviewParams!.onRenderProcessResponsive != null) + return (await webviewParams!.onRenderProcessResponsive!( + _controllerFromPlatform, uri)) + ?.toNativeValue(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.androidOnRenderProcessResponsive!( + _controllerFromPlatform, uri)) + ?.toNativeValue(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onRenderProcessResponsive(uri)) ?? + (await _inAppBrowserEventHandler! + .androidOnRenderProcessResponsive(uri))) + ?.toNativeValue(); + } + } + break; + case "onRenderProcessGone": + if ((webviewParams != null && + (webviewParams!.onRenderProcessGone != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnRenderProcessGone != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + RenderProcessGoneDetail detail = + RenderProcessGoneDetail.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.onRenderProcessGone != null) + webviewParams!.onRenderProcessGone!( + _controllerFromPlatform, detail); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnRenderProcessGone!( + _controllerFromPlatform, detail); + } + } else if (_inAppBrowserEventHandler != null) { + _inAppBrowserEventHandler!.onRenderProcessGone(detail); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler!.androidOnRenderProcessGone(detail); + } + } + break; + case "onFormResubmission": + if ((webviewParams != null && + (webviewParams!.onFormResubmission != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnFormResubmission != null)) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + WebUri? uri = url != null ? WebUri(url) : null; + + if (webviewParams != null) { + if (webviewParams!.onFormResubmission != null) + return (await webviewParams!.onFormResubmission!( + _controllerFromPlatform, uri)) + ?.toNativeValue(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.androidOnFormResubmission!( + _controllerFromPlatform, uri)) + ?.toNativeValue(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onFormResubmission(uri)) ?? + // ignore: deprecated_member_use_from_same_package + (await _inAppBrowserEventHandler! + .androidOnFormResubmission(uri))) + ?.toNativeValue(); + } + } + break; + case "onZoomScaleChanged": + if ((webviewParams != null && + // ignore: deprecated_member_use_from_same_package + (webviewParams!.androidOnScaleChanged != null || + webviewParams!.onZoomScaleChanged != null)) || + _inAppBrowserEventHandler != null) { + double oldScale = call.arguments["oldScale"]; + double newScale = call.arguments["newScale"]; + + if (webviewParams != null) { + if (webviewParams!.onZoomScaleChanged != null) + webviewParams!.onZoomScaleChanged!( + _controllerFromPlatform, oldScale, newScale); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnScaleChanged!( + _controllerFromPlatform, oldScale, newScale); + } + } else { + _inAppBrowserEventHandler!.onZoomScaleChanged(oldScale, newScale); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler! + .androidOnScaleChanged(oldScale, newScale); + } + } + break; + case "onReceivedIcon": + if ((webviewParams != null && + (webviewParams!.onReceivedIcon != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnReceivedIcon != null)) || + _inAppBrowserEventHandler != null) { + Uint8List icon = + Uint8List.fromList(call.arguments["icon"].cast()); + + if (webviewParams != null) { + if (webviewParams!.onReceivedIcon != null) + webviewParams!.onReceivedIcon!(_controllerFromPlatform, icon); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnReceivedIcon!( + _controllerFromPlatform, icon); + } + } else { + _inAppBrowserEventHandler!.onReceivedIcon(icon); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler!.androidOnReceivedIcon(icon); + } + } + break; + case "onReceivedTouchIconUrl": + if ((webviewParams != null && + (webviewParams!.onReceivedTouchIconUrl != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnReceivedTouchIconUrl != null)) || + _inAppBrowserEventHandler != null) { + String url = call.arguments["url"]; + bool precomposed = call.arguments["precomposed"]; + WebUri uri = WebUri(url); + + if (webviewParams != null) { + if (webviewParams!.onReceivedTouchIconUrl != null) + webviewParams!.onReceivedTouchIconUrl!( + _controllerFromPlatform, uri, precomposed); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnReceivedTouchIconUrl!( + _controllerFromPlatform, uri, precomposed); + } + } else { + _inAppBrowserEventHandler!.onReceivedTouchIconUrl(uri, precomposed); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler! + .androidOnReceivedTouchIconUrl(uri, precomposed); + } + } + break; + case "onJsAlert": + if ((webviewParams != null && webviewParams!.onJsAlert != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + JsAlertRequest jsAlertRequest = JsAlertRequest.fromMap(arguments)!; + + if (webviewParams != null && webviewParams!.onJsAlert != null) + return (await webviewParams!.onJsAlert!( + _controllerFromPlatform, jsAlertRequest)) + ?.toMap(); + else + return (await _inAppBrowserEventHandler!.onJsAlert(jsAlertRequest)) + ?.toMap(); + } + break; + case "onJsConfirm": + if ((webviewParams != null && webviewParams!.onJsConfirm != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + JsConfirmRequest jsConfirmRequest = + JsConfirmRequest.fromMap(arguments)!; + + if (webviewParams != null && webviewParams!.onJsConfirm != null) + return (await webviewParams!.onJsConfirm!( + _controllerFromPlatform, jsConfirmRequest)) + ?.toMap(); + else + return (await _inAppBrowserEventHandler! + .onJsConfirm(jsConfirmRequest)) + ?.toMap(); + } + break; + case "onJsPrompt": + if ((webviewParams != null && webviewParams!.onJsPrompt != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + JsPromptRequest jsPromptRequest = JsPromptRequest.fromMap(arguments)!; + + if (webviewParams != null && webviewParams!.onJsPrompt != null) + return (await webviewParams!.onJsPrompt!( + _controllerFromPlatform, jsPromptRequest)) + ?.toMap(); + else + return (await _inAppBrowserEventHandler! + .onJsPrompt(jsPromptRequest)) + ?.toMap(); + } + break; + case "onJsBeforeUnload": + if ((webviewParams != null && + (webviewParams!.onJsBeforeUnload != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnJsBeforeUnload != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + JsBeforeUnloadRequest jsBeforeUnloadRequest = + JsBeforeUnloadRequest.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.onJsBeforeUnload != null) + return (await webviewParams!.onJsBeforeUnload!( + _controllerFromPlatform, jsBeforeUnloadRequest)) + ?.toMap(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.androidOnJsBeforeUnload!( + _controllerFromPlatform, jsBeforeUnloadRequest)) + ?.toMap(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onJsBeforeUnload(jsBeforeUnloadRequest)) ?? + (await _inAppBrowserEventHandler! + .androidOnJsBeforeUnload(jsBeforeUnloadRequest))) + ?.toMap(); + } + } + break; + case "onSafeBrowsingHit": + if ((webviewParams != null && + (webviewParams!.onSafeBrowsingHit != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnSafeBrowsingHit != null)) || + _inAppBrowserEventHandler != null) { + String url = call.arguments["url"]; + SafeBrowsingThreat? threatType = + SafeBrowsingThreat.fromNativeValue(call.arguments["threatType"]); + WebUri uri = WebUri(url); + + if (webviewParams != null) { + if (webviewParams!.onSafeBrowsingHit != null) + return (await webviewParams!.onSafeBrowsingHit!( + _controllerFromPlatform, uri, threatType)) + ?.toMap(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.androidOnSafeBrowsingHit!( + _controllerFromPlatform, uri, threatType)) + ?.toMap(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onSafeBrowsingHit(uri, threatType)) ?? + (await _inAppBrowserEventHandler! + .androidOnSafeBrowsingHit(uri, threatType))) + ?.toMap(); + } + } + break; + case "onReceivedLoginRequest": + if ((webviewParams != null && + (webviewParams!.onReceivedLoginRequest != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnReceivedLoginRequest != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + LoginRequest loginRequest = LoginRequest.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.onReceivedLoginRequest != null) + webviewParams!.onReceivedLoginRequest!( + _controllerFromPlatform, loginRequest); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnReceivedLoginRequest!( + _controllerFromPlatform, loginRequest); + } + } else { + _inAppBrowserEventHandler!.onReceivedLoginRequest(loginRequest); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler! + .androidOnReceivedLoginRequest(loginRequest); + } + } + break; + case "onPermissionRequestCanceled": + if ((webviewParams != null && + webviewParams!.onPermissionRequestCanceled != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + PermissionRequest permissionRequest = + PermissionRequest.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onPermissionRequestCanceled != null) + webviewParams!.onPermissionRequestCanceled!( + _controllerFromPlatform, permissionRequest); + else + _inAppBrowserEventHandler! + .onPermissionRequestCanceled(permissionRequest); + } + break; + case "onRequestFocus": + if ((webviewParams != null && webviewParams!.onRequestFocus != null) || + _inAppBrowserEventHandler != null) { + if (webviewParams != null && webviewParams!.onRequestFocus != null) + webviewParams!.onRequestFocus!(_controllerFromPlatform); + else + _inAppBrowserEventHandler!.onRequestFocus(); + } + break; + case "onReceivedHttpAuthRequest": + if ((webviewParams != null && + webviewParams!.onReceivedHttpAuthRequest != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + HttpAuthenticationChallenge challenge = + HttpAuthenticationChallenge.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onReceivedHttpAuthRequest != null) + return (await webviewParams!.onReceivedHttpAuthRequest!( + _controllerFromPlatform, challenge)) + ?.toMap(); + else + return (await _inAppBrowserEventHandler! + .onReceivedHttpAuthRequest(challenge)) + ?.toMap(); + } + break; + case "onReceivedServerTrustAuthRequest": + if ((webviewParams != null && + webviewParams!.onReceivedServerTrustAuthRequest != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + ServerTrustChallenge challenge = + ServerTrustChallenge.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onReceivedServerTrustAuthRequest != null) + return (await webviewParams!.onReceivedServerTrustAuthRequest!( + _controllerFromPlatform, challenge)) + ?.toMap(); + else + return (await _inAppBrowserEventHandler! + .onReceivedServerTrustAuthRequest(challenge)) + ?.toMap(); + } + break; + case "onReceivedClientCertRequest": + if ((webviewParams != null && + webviewParams!.onReceivedClientCertRequest != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + ClientCertChallenge challenge = + ClientCertChallenge.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onReceivedClientCertRequest != null) + return (await webviewParams!.onReceivedClientCertRequest!( + _controllerFromPlatform, challenge)) + ?.toMap(); + else + return (await _inAppBrowserEventHandler! + .onReceivedClientCertRequest(challenge)) + ?.toMap(); + } + break; + case "onFindResultReceived": + if ((webviewParams != null && + (webviewParams!.onFindResultReceived != null || + (webviewParams!.findInteractionController != null && + webviewParams!.findInteractionController!.params + .onFindResultReceived != + null))) || + _inAppBrowserEventHandler != null) { + int activeMatchOrdinal = call.arguments["activeMatchOrdinal"]; + int numberOfMatches = call.arguments["numberOfMatches"]; + bool isDoneCounting = call.arguments["isDoneCounting"]; + if (webviewParams != null) { + if (webviewParams!.findInteractionController != null && + webviewParams!.findInteractionController!.params + .onFindResultReceived != + null) + webviewParams! + .findInteractionController!.params.onFindResultReceived!( + webviewParams!.findInteractionController!, + activeMatchOrdinal, + numberOfMatches, + isDoneCounting); + else + webviewParams!.onFindResultReceived!(_controllerFromPlatform, + activeMatchOrdinal, numberOfMatches, isDoneCounting); + } else { + if (_inAppBrowser!.findInteractionController != null && + _inAppBrowser! + .findInteractionController!.onFindResultReceived != + null) + _inAppBrowser!.findInteractionController!.onFindResultReceived!( + webviewParams!.findInteractionController!, + activeMatchOrdinal, + numberOfMatches, + isDoneCounting); + else + _inAppBrowserEventHandler!.onFindResultReceived( + activeMatchOrdinal, numberOfMatches, isDoneCounting); + } + } + break; + case "onPermissionRequest": + if ((webviewParams != null && + (webviewParams!.onPermissionRequest != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnPermissionRequest != null)) || + _inAppBrowserEventHandler != null) { + String origin = call.arguments["origin"]; + List resources = call.arguments["resources"].cast(); + + Map arguments = + call.arguments.cast(); + PermissionRequest permissionRequest = + PermissionRequest.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.onPermissionRequest != null) + return (await webviewParams!.onPermissionRequest!( + _controllerFromPlatform, permissionRequest)) + ?.toMap(); + else { + return (await webviewParams!.androidOnPermissionRequest!( + _controllerFromPlatform, origin, resources)) + ?.toMap(); + } + } else { + return (await _inAppBrowserEventHandler! + .onPermissionRequest(permissionRequest)) + ?.toMap() ?? + (await _inAppBrowserEventHandler! + .androidOnPermissionRequest(origin, resources)) + ?.toMap(); + } + } + break; + case "onUpdateVisitedHistory": + if ((webviewParams != null && + webviewParams!.onUpdateVisitedHistory != null) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + bool? isReload = call.arguments["isReload"]; + WebUri? uri = url != null ? WebUri(url) : null; + if (webviewParams != null && + webviewParams!.onUpdateVisitedHistory != null) + webviewParams!.onUpdateVisitedHistory!( + _controllerFromPlatform, uri, isReload); + else + _inAppBrowserEventHandler!.onUpdateVisitedHistory(uri, isReload); + } + break; + case "onWebContentProcessDidTerminate": + if (webviewParams != null && + (webviewParams!.onWebContentProcessDidTerminate != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.iosOnWebContentProcessDidTerminate != null)) { + if (webviewParams!.onWebContentProcessDidTerminate != null) + webviewParams! + .onWebContentProcessDidTerminate!(_controllerFromPlatform); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams! + .iosOnWebContentProcessDidTerminate!(_controllerFromPlatform); + } + } else if (_inAppBrowserEventHandler != null) { + _inAppBrowserEventHandler!.onWebContentProcessDidTerminate(); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler!.iosOnWebContentProcessDidTerminate(); + } + break; + case "onPageCommitVisible": + if ((webviewParams != null && + webviewParams!.onPageCommitVisible != null) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + WebUri? uri = url != null ? WebUri(url) : null; + if (webviewParams != null && + webviewParams!.onPageCommitVisible != null) + webviewParams!.onPageCommitVisible!(_controllerFromPlatform, uri); + else + _inAppBrowserEventHandler!.onPageCommitVisible(uri); + } + break; + case "onDidReceiveServerRedirectForProvisionalNavigation": + if (webviewParams != null && + (webviewParams! + .onDidReceiveServerRedirectForProvisionalNavigation != + null || + params + .webviewParams! + // ignore: deprecated_member_use_from_same_package + .iosOnDidReceiveServerRedirectForProvisionalNavigation != + null)) { + if (webviewParams! + .onDidReceiveServerRedirectForProvisionalNavigation != + null) + webviewParams!.onDidReceiveServerRedirectForProvisionalNavigation!( + _controllerFromPlatform); + else { + params + .webviewParams! + // ignore: deprecated_member_use_from_same_package + .iosOnDidReceiveServerRedirectForProvisionalNavigation!( + _controllerFromPlatform); + } + } else if (_inAppBrowserEventHandler != null) { + _inAppBrowserEventHandler! + .onDidReceiveServerRedirectForProvisionalNavigation(); + _inAppBrowserEventHandler! + .iosOnDidReceiveServerRedirectForProvisionalNavigation(); + } + break; + case "onNavigationResponse": + if ((webviewParams != null && + (webviewParams!.onNavigationResponse != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.iosOnNavigationResponse != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + // ignore: deprecated_member_use_from_same_package + IOSWKNavigationResponse iosOnNavigationResponse = + // ignore: deprecated_member_use_from_same_package + IOSWKNavigationResponse.fromMap(arguments)!; + + NavigationResponse navigationResponse = + NavigationResponse.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.onNavigationResponse != null) + return (await webviewParams!.onNavigationResponse!( + _controllerFromPlatform, navigationResponse)) + ?.toNativeValue(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.iosOnNavigationResponse!( + _controllerFromPlatform, iosOnNavigationResponse)) + ?.toNativeValue(); + } + } else { + return (await _inAppBrowserEventHandler! + .onNavigationResponse(navigationResponse)) + ?.toNativeValue() ?? + (await _inAppBrowserEventHandler! + .iosOnNavigationResponse(iosOnNavigationResponse)) + ?.toNativeValue(); + } + } + break; + case "shouldAllowDeprecatedTLS": + if ((webviewParams != null && + (webviewParams!.shouldAllowDeprecatedTLS != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.iosShouldAllowDeprecatedTLS != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + URLAuthenticationChallenge challenge = + URLAuthenticationChallenge.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.shouldAllowDeprecatedTLS != null) + return (await webviewParams!.shouldAllowDeprecatedTLS!( + _controllerFromPlatform, challenge)) + ?.toNativeValue(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.iosShouldAllowDeprecatedTLS!( + _controllerFromPlatform, challenge)) + ?.toNativeValue(); + } + } else { + return (await _inAppBrowserEventHandler! + .shouldAllowDeprecatedTLS(challenge)) + ?.toNativeValue() ?? + // ignore: deprecated_member_use_from_same_package + (await _inAppBrowserEventHandler! + .iosShouldAllowDeprecatedTLS(challenge)) + ?.toNativeValue(); + } + } + break; + case "onLongPressHitTestResult": + if ((webviewParams != null && + webviewParams!.onLongPressHitTestResult != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + InAppWebViewHitTestResult hitTestResult = + InAppWebViewHitTestResult.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onLongPressHitTestResult != null) + webviewParams!.onLongPressHitTestResult!( + _controllerFromPlatform, hitTestResult); + else + _inAppBrowserEventHandler!.onLongPressHitTestResult(hitTestResult); + } + break; + case "onCreateContextMenu": + ContextMenu? contextMenu; + if (webviewParams != null && webviewParams!.contextMenu != null) { + contextMenu = webviewParams!.contextMenu; + } else if (_inAppBrowserEventHandler != null && + _inAppBrowser!.contextMenu != null) { + contextMenu = _inAppBrowser!.contextMenu; + } + + if (contextMenu != null && contextMenu.onCreateContextMenu != null) { + Map arguments = + call.arguments.cast(); + InAppWebViewHitTestResult hitTestResult = + InAppWebViewHitTestResult.fromMap(arguments)!; + + contextMenu.onCreateContextMenu!(hitTestResult); + } + break; + case "onHideContextMenu": + ContextMenu? contextMenu; + if (webviewParams != null && webviewParams!.contextMenu != null) { + contextMenu = webviewParams!.contextMenu; + } else if (_inAppBrowserEventHandler != null && + _inAppBrowser!.contextMenu != null) { + contextMenu = _inAppBrowser!.contextMenu; + } + + if (contextMenu != null && contextMenu.onHideContextMenu != null) { + contextMenu.onHideContextMenu!(); + } + break; + case "onContextMenuActionItemClicked": + ContextMenu? contextMenu; + if (webviewParams != null && webviewParams!.contextMenu != null) { + contextMenu = webviewParams!.contextMenu; + } else if (_inAppBrowserEventHandler != null && + _inAppBrowser!.contextMenu != null) { + contextMenu = _inAppBrowser!.contextMenu; + } + + if (contextMenu != null) { + int? androidId = call.arguments["androidId"]; + String? iosId = call.arguments["iosId"]; + dynamic id = call.arguments["id"]; + String title = call.arguments["title"]; + + ContextMenuItem menuItemClicked = ContextMenuItem( + id: id, + // ignore: deprecated_member_use_from_same_package + androidId: androidId, + // ignore: deprecated_member_use_from_same_package + iosId: iosId, + title: title, + action: null); + + for (var menuItem in contextMenu.menuItems) { + if (menuItem.id == id) { + menuItemClicked = menuItem; + if (menuItem.action != null) { + menuItem.action!(); + } + break; + } + } + + if (contextMenu.onContextMenuActionItemClicked != null) { + contextMenu.onContextMenuActionItemClicked!(menuItemClicked); + } + } + break; + case "onEnterFullscreen": + if (webviewParams != null && webviewParams!.onEnterFullscreen != null) + webviewParams!.onEnterFullscreen!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onEnterFullscreen(); + break; + case "onExitFullscreen": + if (webviewParams != null && webviewParams!.onExitFullscreen != null) + webviewParams!.onExitFullscreen!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onExitFullscreen(); + break; + case "onOverScrolled": + if ((webviewParams != null && webviewParams!.onOverScrolled != null) || + _inAppBrowserEventHandler != null) { + int x = call.arguments["x"]; + int y = call.arguments["y"]; + bool clampedX = call.arguments["clampedX"]; + bool clampedY = call.arguments["clampedY"]; + + if (webviewParams != null && webviewParams!.onOverScrolled != null) + webviewParams!.onOverScrolled!( + _controllerFromPlatform, x, y, clampedX, clampedY); + else + _inAppBrowserEventHandler!.onOverScrolled(x, y, clampedX, clampedY); + } + break; + case "onWindowFocus": + if (webviewParams != null && webviewParams!.onWindowFocus != null) + webviewParams!.onWindowFocus!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onWindowFocus(); + break; + case "onWindowBlur": + if (webviewParams != null && webviewParams!.onWindowBlur != null) + webviewParams!.onWindowBlur!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onWindowBlur(); + break; + case "onPrintRequest": + if ((webviewParams != null && + (webviewParams!.onPrintRequest != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.onPrint != null)) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + String? printJobId = call.arguments["printJobId"]; + WebUri? uri = url != null ? WebUri(url) : null; + AndroidPrintJobController? printJob = printJobId != null + ? AndroidPrintJobController( + AndroidPrintJobControllerCreationParams(id: printJobId)) + : null; + + if (webviewParams != null) { + if (webviewParams!.onPrintRequest != null) + return await webviewParams!.onPrintRequest!( + _controllerFromPlatform, uri, printJob); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.onPrint!(_controllerFromPlatform, uri); + return false; + } + } else { + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler!.onPrint(uri); + return await _inAppBrowserEventHandler! + .onPrintRequest(uri, printJob); + } + } + break; + case "onInjectedScriptLoaded": + String id = call.arguments[0]; + var onLoadCallback = _injectedScriptsFromURL[id]?.onLoad; + if ((webviewParams != null || _inAppBrowserEventHandler != null) && + onLoadCallback != null) { + onLoadCallback(); + } + break; + case "onInjectedScriptError": + String id = call.arguments[0]; + var onErrorCallback = _injectedScriptsFromURL[id]?.onError; + if ((webviewParams != null || _inAppBrowserEventHandler != null) && + onErrorCallback != null) { + onErrorCallback(); + } + break; + case "onCameraCaptureStateChanged": + if ((webviewParams != null && + webviewParams!.onCameraCaptureStateChanged != null) || + _inAppBrowserEventHandler != null) { + var oldState = + MediaCaptureState.fromNativeValue(call.arguments["oldState"]); + var newState = + MediaCaptureState.fromNativeValue(call.arguments["newState"]); + + if (webviewParams != null && + webviewParams!.onCameraCaptureStateChanged != null) + webviewParams!.onCameraCaptureStateChanged!( + _controllerFromPlatform, oldState, newState); + else + _inAppBrowserEventHandler! + .onCameraCaptureStateChanged(oldState, newState); + } + break; + case "onMicrophoneCaptureStateChanged": + if ((webviewParams != null && + webviewParams!.onMicrophoneCaptureStateChanged != null) || + _inAppBrowserEventHandler != null) { + var oldState = + MediaCaptureState.fromNativeValue(call.arguments["oldState"]); + var newState = + MediaCaptureState.fromNativeValue(call.arguments["newState"]); + + if (webviewParams != null && + webviewParams!.onMicrophoneCaptureStateChanged != null) + webviewParams!.onMicrophoneCaptureStateChanged!( + _controllerFromPlatform, oldState, newState); + else + _inAppBrowserEventHandler! + .onMicrophoneCaptureStateChanged(oldState, newState); + } + break; + case "onContentSizeChanged": + if ((webviewParams != null && + webviewParams!.onContentSizeChanged != null) || + _inAppBrowserEventHandler != null) { + var oldContentSize = MapSize.fromMap( + call.arguments["oldContentSize"]?.cast())!; + var newContentSize = MapSize.fromMap( + call.arguments["newContentSize"]?.cast())!; + + if (webviewParams != null && + webviewParams!.onContentSizeChanged != null) + webviewParams!.onContentSizeChanged!( + _controllerFromPlatform, oldContentSize, newContentSize); + else + _inAppBrowserEventHandler! + .onContentSizeChanged(oldContentSize, newContentSize); + } + break; + case "onCallJsHandler": + String handlerName = call.arguments["handlerName"]; + // decode args to json + List args = jsonDecode(call.arguments["args"]); + + _debugLog(handlerName, args); + + switch (handlerName) { + case "onLoadResource": + if ((webviewParams != null && + webviewParams!.onLoadResource != null) || + _inAppBrowserEventHandler != null) { + Map arguments = args[0].cast(); + arguments["startTime"] = arguments["startTime"] is int + ? arguments["startTime"].toDouble() + : arguments["startTime"]; + arguments["duration"] = arguments["duration"] is int + ? arguments["duration"].toDouble() + : arguments["duration"]; + + var response = LoadedResource.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onLoadResource != null) + webviewParams!.onLoadResource!( + _controllerFromPlatform, response); + else + _inAppBrowserEventHandler!.onLoadResource(response); + } + return null; + case "shouldInterceptAjaxRequest": + if ((webviewParams != null && + webviewParams!.shouldInterceptAjaxRequest != null) || + _inAppBrowserEventHandler != null) { + Map arguments = args[0].cast(); + AjaxRequest request = AjaxRequest.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.shouldInterceptAjaxRequest != null) + return jsonEncode( + await params.webviewParams!.shouldInterceptAjaxRequest!( + _controllerFromPlatform, request)); + else + return jsonEncode(await _inAppBrowserEventHandler! + .shouldInterceptAjaxRequest(request)); + } + return null; + case "onAjaxReadyStateChange": + if ((webviewParams != null && + webviewParams!.onAjaxReadyStateChange != null) || + _inAppBrowserEventHandler != null) { + Map arguments = args[0].cast(); + AjaxRequest request = AjaxRequest.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onAjaxReadyStateChange != null) + return (await webviewParams!.onAjaxReadyStateChange!( + _controllerFromPlatform, request)) + ?.toNativeValue(); + else + return (await _inAppBrowserEventHandler! + .onAjaxReadyStateChange(request)) + ?.toNativeValue(); + } + return null; + case "onAjaxProgress": + if ((webviewParams != null && + webviewParams!.onAjaxProgress != null) || + _inAppBrowserEventHandler != null) { + Map arguments = args[0].cast(); + AjaxRequest request = AjaxRequest.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onAjaxProgress != null) + return (await webviewParams!.onAjaxProgress!( + _controllerFromPlatform, request)) + ?.toNativeValue(); + else + return (await _inAppBrowserEventHandler! + .onAjaxProgress(request)) + ?.toNativeValue(); + } + return null; + case "shouldInterceptFetchRequest": + if ((webviewParams != null && + webviewParams!.shouldInterceptFetchRequest != null) || + _inAppBrowserEventHandler != null) { + Map arguments = args[0].cast(); + FetchRequest request = FetchRequest.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.shouldInterceptFetchRequest != null) + return jsonEncode( + await webviewParams!.shouldInterceptFetchRequest!( + _controllerFromPlatform, request)); + else + return jsonEncode(await _inAppBrowserEventHandler! + .shouldInterceptFetchRequest(request)); + } + return null; + case "onWindowFocus": + if (webviewParams != null && webviewParams!.onWindowFocus != null) + webviewParams!.onWindowFocus!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onWindowFocus(); + return null; + case "onWindowBlur": + if (webviewParams != null && webviewParams!.onWindowBlur != null) + webviewParams!.onWindowBlur!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onWindowBlur(); + return null; + case "onInjectedScriptLoaded": + String id = args[0]; + var onLoadCallback = _injectedScriptsFromURL[id]?.onLoad; + if ((webviewParams != null || _inAppBrowserEventHandler != null) && + onLoadCallback != null) { + onLoadCallback(); + } + return null; + case "onInjectedScriptError": + String id = args[0]; + var onErrorCallback = _injectedScriptsFromURL[id]?.onError; + if ((webviewParams != null || _inAppBrowserEventHandler != null) && + onErrorCallback != null) { + onErrorCallback(); + } + return null; + } + + if (_javaScriptHandlersMap.containsKey(handlerName)) { + // convert result to json + try { + return jsonEncode(await _javaScriptHandlersMap[handlerName]!(args)); + } catch (error, stacktrace) { + developer.log(error.toString() + '\n' + stacktrace.toString(), + name: 'JavaScript Handler "$handlerName"'); + throw Exception(error.toString().replaceFirst('Exception: ', '')); + } + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + @override + Future getUrl() async { + Map args = {}; + String? url = await channel?.invokeMethod('getUrl', args); + return url != null ? WebUri(url) : null; + } + + @override + Future getTitle() async { + Map args = {}; + return await channel?.invokeMethod('getTitle', args); + } + + @override + Future getProgress() async { + Map args = {}; + return await channel?.invokeMethod('getProgress', args); + } + + @override + Future getHtml() async { + String? html; + + InAppWebViewSettings? settings = await getSettings(); + if (settings != null && settings.javaScriptEnabled == true) { + html = await evaluateJavascript( + source: "window.document.getElementsByTagName('html')[0].outerHTML;"); + if (html != null && html.isNotEmpty) return html; + } + + var webviewUrl = await getUrl(); + if (webviewUrl == null) { + return html; + } + + if (webviewUrl.isScheme("file")) { + var assetPathSplit = webviewUrl.toString().split("/flutter_assets/"); + var assetPath = assetPathSplit[assetPathSplit.length - 1]; + try { + var bytes = await rootBundle.load(assetPath); + html = utf8.decode(bytes.buffer.asUint8List()); + } catch (e) {} + } else { + try { + HttpClient client = HttpClient(); + var htmlRequest = await client.getUrl(webviewUrl); + html = + await (await htmlRequest.close()).transform(Utf8Decoder()).join(); + } catch (e) { + developer.log(e.toString(), name: this.runtimeType.toString()); + } + } + + return html; + } + + @override + Future> getFavicons() async { + List favicons = []; + + var webviewUrl = await getUrl(); + + if (webviewUrl == null) { + return favicons; + } + + String? manifestUrl; + + var html = await getHtml(); + if (html == null || html.isEmpty) { + return favicons; + } + var assetPathBase; + + if (webviewUrl.isScheme("file")) { + var assetPathSplit = webviewUrl.toString().split("/flutter_assets/"); + assetPathBase = assetPathSplit[0] + "/flutter_assets/"; + } + + InAppWebViewSettings? settings = await getSettings(); + if (settings != null && settings.javaScriptEnabled == true) { + List> links = (await evaluateJavascript(source: """ +(function() { + var linkNodes = document.head.getElementsByTagName("link"); + var links = []; + for (var i = 0; i < linkNodes.length; i++) { + var linkNode = linkNodes[i]; + if (linkNode.rel === 'manifest') { + links.push( + { + rel: linkNode.rel, + href: linkNode.href, + sizes: null + } + ); + } else if (linkNode.rel != null && linkNode.rel.indexOf('icon') >= 0) { + links.push( + { + rel: linkNode.rel, + href: linkNode.href, + sizes: linkNode.sizes != null && linkNode.sizes.value != "" ? linkNode.sizes.value : null + } + ); + } + } + return links; +})(); +"""))?.cast>() ?? []; + for (var link in links) { + if (link["rel"] == "manifest") { + manifestUrl = link["href"]; + if (!_isUrlAbsolute(manifestUrl!)) { + if (manifestUrl.startsWith("/")) { + manifestUrl = manifestUrl.substring(1); + } + manifestUrl = ((assetPathBase == null) + ? webviewUrl.scheme + "://" + webviewUrl.host + "/" + : assetPathBase) + + manifestUrl; + } + continue; + } + favicons.addAll(_createFavicons(webviewUrl, assetPathBase, link["href"], + link["rel"], link["sizes"], false)); + } + } + + // try to get /favicon.ico + try { + HttpClient client = HttpClient(); + var faviconUrl = + webviewUrl.scheme + "://" + webviewUrl.host + "/favicon.ico"; + var faviconUri = WebUri(faviconUrl); + var headRequest = await client.headUrl(faviconUri); + var headResponse = await headRequest.close(); + if (headResponse.statusCode == 200) { + favicons.add(Favicon(url: faviconUri, rel: "shortcut icon")); + } + } catch (e) { + developer.log("/favicon.ico file not found: " + e.toString(), + name: this.runtimeType.toString()); + } + + // try to get the manifest file + HttpClientRequest? manifestRequest; + HttpClientResponse? manifestResponse; + bool manifestFound = false; + if (manifestUrl == null) { + manifestUrl = + webviewUrl.scheme + "://" + webviewUrl.host + "/manifest.json"; + } + try { + HttpClient client = HttpClient(); + manifestRequest = await client.getUrl(Uri.parse(manifestUrl)); + manifestResponse = await manifestRequest.close(); + manifestFound = manifestResponse.statusCode == 200 && + manifestResponse.headers.contentType?.mimeType == "application/json"; + } catch (e) { + developer.log("Manifest file not found: " + e.toString(), + name: runtimeType.toString()); + } + + if (manifestFound) { + try { + Map manifest = json + .decode(await manifestResponse!.transform(Utf8Decoder()).join()); + if (manifest.containsKey("icons")) { + for (Map icon in manifest["icons"]) { + favicons.addAll(_createFavicons(webviewUrl, assetPathBase, + icon["src"], icon["rel"], icon["sizes"], true)); + } + } + } catch (e) { + developer.log( + "Cannot get favicons from Manifest file. It might not have a valid format: " + + e.toString(), + error: e, + name: runtimeType.toString()); + } + } + + return favicons; + } + + bool _isUrlAbsolute(String url) { + return url.startsWith("http://") || url.startsWith("https://"); + } + + List _createFavicons(WebUri url, String? assetPathBase, + String urlIcon, String? rel, String? sizes, bool isManifest) { + List favicons = []; + + List urlSplit = urlIcon.split("/"); + if (!_isUrlAbsolute(urlIcon)) { + if (urlIcon.startsWith("/")) { + urlIcon = urlIcon.substring(1); + } + urlIcon = ((assetPathBase == null) + ? url.scheme + "://" + url.host + "/" + : assetPathBase) + + urlIcon; + } + if (isManifest) { + rel = (sizes != null) + ? urlSplit[urlSplit.length - 1] + .replaceFirst("-" + sizes, "") + .split(" ")[0] + .split(".")[0] + : null; + } + if (sizes != null && sizes.isNotEmpty && sizes != "any") { + List sizesSplit = sizes.split(" "); + for (String size in sizesSplit) { + int width = int.parse(size.split("x")[0]); + int height = int.parse(size.split("x")[1]); + favicons.add(Favicon( + url: WebUri(urlIcon), rel: rel, width: width, height: height)); + } + } else { + favicons.add( + Favicon(url: WebUri(urlIcon), rel: rel, width: null, height: null)); + } + + return favicons; + } + + @override + Future loadUrl( + {required URLRequest urlRequest, + @Deprecated('Use allowingReadAccessTo instead') + Uri? iosAllowingReadAccessTo, + WebUri? allowingReadAccessTo}) async { + assert(urlRequest.url != null && urlRequest.url.toString().isNotEmpty); + assert( + allowingReadAccessTo == null || allowingReadAccessTo.isScheme("file")); + assert(iosAllowingReadAccessTo == null || + iosAllowingReadAccessTo.isScheme("file")); + + Map args = {}; + args.putIfAbsent('urlRequest', () => urlRequest.toMap()); + args.putIfAbsent( + 'allowingReadAccessTo', + () => + allowingReadAccessTo?.toString() ?? + iosAllowingReadAccessTo?.toString()); + await channel?.invokeMethod('loadUrl', args); + } + + @override + Future postUrl( + {required WebUri url, required Uint8List postData}) async { + assert(url.toString().isNotEmpty); + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + args.putIfAbsent('postData', () => postData); + await channel?.invokeMethod('postUrl', args); + } + + @override + Future loadData( + {required String data, + String mimeType = "text/html", + String encoding = "utf8", + WebUri? baseUrl, + @Deprecated('Use historyUrl instead') Uri? androidHistoryUrl, + WebUri? historyUrl, + @Deprecated('Use allowingReadAccessTo instead') + Uri? iosAllowingReadAccessTo, + WebUri? allowingReadAccessTo}) async { + assert( + allowingReadAccessTo == null || allowingReadAccessTo.isScheme("file")); + assert(iosAllowingReadAccessTo == null || + iosAllowingReadAccessTo.isScheme("file")); + + Map args = {}; + args.putIfAbsent('data', () => data); + args.putIfAbsent('mimeType', () => mimeType); + args.putIfAbsent('encoding', () => encoding); + args.putIfAbsent('baseUrl', () => baseUrl?.toString() ?? "about:blank"); + args.putIfAbsent( + 'historyUrl', + () => + historyUrl?.toString() ?? + androidHistoryUrl?.toString() ?? + "about:blank"); + args.putIfAbsent( + 'allowingReadAccessTo', + () => + allowingReadAccessTo?.toString() ?? + iosAllowingReadAccessTo?.toString()); + await channel?.invokeMethod('loadData', args); + } + + @override + Future loadFile({required String assetFilePath}) async { + assert(assetFilePath.isNotEmpty); + Map args = {}; + args.putIfAbsent('assetFilePath', () => assetFilePath); + await channel?.invokeMethod('loadFile', args); + } + + @override + Future reload() async { + Map args = {}; + await channel?.invokeMethod('reload', args); + } + + @override + Future goBack() async { + Map args = {}; + await channel?.invokeMethod('goBack', args); + } + + @override + Future canGoBack() async { + Map args = {}; + return await channel?.invokeMethod('canGoBack', args) ?? false; + } + + @override + Future goForward() async { + Map args = {}; + await channel?.invokeMethod('goForward', args); + } + + @override + Future canGoForward() async { + Map args = {}; + return await channel?.invokeMethod('canGoForward', args) ?? false; + } + + @override + Future goBackOrForward({required int steps}) async { + Map args = {}; + args.putIfAbsent('steps', () => steps); + await channel?.invokeMethod('goBackOrForward', args); + } + + @override + Future canGoBackOrForward({required int steps}) async { + Map args = {}; + args.putIfAbsent('steps', () => steps); + return await channel?.invokeMethod('canGoBackOrForward', args) ?? + false; + } + + @override + Future goTo({required WebHistoryItem historyItem}) async { + var steps = historyItem.offset; + if (steps != null) { + await goBackOrForward(steps: steps); + } + } + + @override + Future isLoading() async { + Map args = {}; + return await channel?.invokeMethod('isLoading', args) ?? false; + } + + @override + Future stopLoading() async { + Map args = {}; + await channel?.invokeMethod('stopLoading', args); + } + + @override + Future evaluateJavascript( + {required String source, ContentWorld? contentWorld}) async { + Map args = {}; + args.putIfAbsent('source', () => source); + args.putIfAbsent('contentWorld', () => contentWorld?.toMap()); + var data = await channel?.invokeMethod('evaluateJavascript', args); + if (data != null) { + try { + // try to json decode the data coming from JavaScript + // otherwise return it as it is. + data = json.decode(data); + } catch (e) {} + } + return data; + } + + @override + Future injectJavascriptFileFromUrl( + {required WebUri urlFile, + ScriptHtmlTagAttributes? scriptHtmlTagAttributes}) async { + assert(urlFile.toString().isNotEmpty); + var id = scriptHtmlTagAttributes?.id; + if (scriptHtmlTagAttributes != null && id != null) { + _injectedScriptsFromURL[id] = scriptHtmlTagAttributes; + } + Map args = {}; + args.putIfAbsent('urlFile', () => urlFile.toString()); + args.putIfAbsent( + 'scriptHtmlTagAttributes', () => scriptHtmlTagAttributes?.toMap()); + await channel?.invokeMethod('injectJavascriptFileFromUrl', args); + } + + @override + Future injectJavascriptFileFromAsset( + {required String assetFilePath}) async { + String source = await rootBundle.loadString(assetFilePath); + return await evaluateJavascript(source: source); + } + + @override + Future injectCSSCode({required String source}) async { + Map args = {}; + args.putIfAbsent('source', () => source); + await channel?.invokeMethod('injectCSSCode', args); + } + + @override + Future injectCSSFileFromUrl( + {required WebUri urlFile, + CSSLinkHtmlTagAttributes? cssLinkHtmlTagAttributes}) async { + assert(urlFile.toString().isNotEmpty); + Map args = {}; + args.putIfAbsent('urlFile', () => urlFile.toString()); + args.putIfAbsent( + 'cssLinkHtmlTagAttributes', () => cssLinkHtmlTagAttributes?.toMap()); + await channel?.invokeMethod('injectCSSFileFromUrl', args); + } + + @override + Future injectCSSFileFromAsset({required String assetFilePath}) async { + String source = await rootBundle.loadString(assetFilePath); + await injectCSSCode(source: source); + } + + @override + void addJavaScriptHandler( + {required String handlerName, + required JavaScriptHandlerCallback callback}) { + assert(!_JAVASCRIPT_HANDLER_FORBIDDEN_NAMES.contains(handlerName), + '"$handlerName" is a forbidden name!'); + this._javaScriptHandlersMap[handlerName] = (callback); + } + + @override + JavaScriptHandlerCallback? removeJavaScriptHandler( + {required String handlerName}) { + return this._javaScriptHandlersMap.remove(handlerName); + } + + @override + bool hasJavaScriptHandler({required String handlerName}) { + return this._javaScriptHandlersMap.containsKey(handlerName); + } + + @override + Future takeScreenshot( + {ScreenshotConfiguration? screenshotConfiguration}) async { + Map args = {}; + args.putIfAbsent( + 'screenshotConfiguration', () => screenshotConfiguration?.toMap()); + return await channel?.invokeMethod('takeScreenshot', args); + } + + @override + @Deprecated('Use setSettings instead') + Future setOptions({required InAppWebViewGroupOptions options}) async { + InAppWebViewSettings settings = + InAppWebViewSettings.fromMap(options.toMap()) ?? InAppWebViewSettings(); + await setSettings(settings: settings); + } + + @override + @Deprecated('Use getSettings instead') + Future getOptions() async { + InAppWebViewSettings? settings = await getSettings(); + + Map? options = settings?.toMap(); + if (options != null) { + options = options.cast(); + return InAppWebViewGroupOptions.fromMap(options as Map); + } + + return null; + } + + @override + Future setSettings({required InAppWebViewSettings settings}) async { + Map args = {}; + + args.putIfAbsent('settings', () => settings.toMap()); + await channel?.invokeMethod('setSettings', args); + } + + @override + Future getSettings() async { + Map args = {}; + + Map? settings = + await channel?.invokeMethod('getSettings', args); + if (settings != null) { + settings = settings.cast(); + return InAppWebViewSettings.fromMap(settings as Map); + } + + return null; + } + + @override + Future getCopyBackForwardList() async { + Map args = {}; + Map? result = + (await channel?.invokeMethod('getCopyBackForwardList', args)) + ?.cast(); + return WebHistory.fromMap(result); + } + + @override + @Deprecated("Use InAppWebViewController.clearAllCache instead") + Future clearCache() async { + Map args = {}; + await channel?.invokeMethod('clearCache', args); + } + + @override + @Deprecated("Use FindInteractionController.findAll instead") + Future findAllAsync({required String find}) async { + Map args = {}; + args.putIfAbsent('find', () => find); + await channel?.invokeMethod('findAll', args); + } + + @override + @Deprecated("Use FindInteractionController.findNext instead") + Future findNext({required bool forward}) async { + Map args = {}; + args.putIfAbsent('forward', () => forward); + await channel?.invokeMethod('findNext', args); + } + + @override + @Deprecated("Use FindInteractionController.clearMatches instead") + Future clearMatches() async { + Map args = {}; + await channel?.invokeMethod('clearMatches', args); + } + + @override + @Deprecated("Use tRexRunnerHtml instead") + Future getTRexRunnerHtml() async { + return await tRexRunnerHtml; + } + + @override + @Deprecated("Use tRexRunnerCss instead") + Future getTRexRunnerCss() async { + return await tRexRunnerCss; + } + + @override + Future scrollTo( + {required int x, required int y, bool animated = false}) async { + Map args = {}; + args.putIfAbsent('x', () => x); + args.putIfAbsent('y', () => y); + args.putIfAbsent('animated', () => animated); + await channel?.invokeMethod('scrollTo', args); + } + + @override + Future scrollBy( + {required int x, required int y, bool animated = false}) async { + Map args = {}; + args.putIfAbsent('x', () => x); + args.putIfAbsent('y', () => y); + args.putIfAbsent('animated', () => animated); + await channel?.invokeMethod('scrollBy', args); + } + + @override + Future pauseTimers() async { + Map args = {}; + await channel?.invokeMethod('pauseTimers', args); + } + + @override + Future resumeTimers() async { + Map args = {}; + await channel?.invokeMethod('resumeTimers', args); + } + + @override + Future printCurrentPage( + {PrintJobSettings? settings}) async { + Map args = {}; + args.putIfAbsent("settings", () => settings?.toMap()); + String? jobId = + await channel?.invokeMethod('printCurrentPage', args); + if (jobId != null) { + return AndroidPrintJobController( + PlatformPrintJobControllerCreationParams(id: jobId)); + } + return null; + } + + @override + Future getContentHeight() async { + Map args = {}; + var height = await channel?.invokeMethod('getContentHeight', args); + if (height == null || height == 0) { + // try to use javascript + var scrollHeight = await evaluateJavascript( + source: "document.documentElement.scrollHeight;"); + if (scrollHeight != null && scrollHeight is num) { + height = scrollHeight.toInt(); + } + } + return height; + } + + @override + Future getContentWidth() async { + Map args = {}; + var height = await channel?.invokeMethod('getContentWidth', args); + if (height == null || height == 0) { + // try to use javascript + var scrollHeight = await evaluateJavascript( + source: "document.documentElement.scrollWidth;"); + if (scrollHeight != null && scrollHeight is num) { + height = scrollHeight.toInt(); + } + } + return height; + } + + @override + Future zoomBy( + {required double zoomFactor, + @Deprecated('Use animated instead') bool? iosAnimated, + bool animated = false}) async { + assert(zoomFactor > 0.01 && zoomFactor <= 100.0); + + Map args = {}; + args.putIfAbsent('zoomFactor', () => zoomFactor); + args.putIfAbsent('animated', () => iosAnimated ?? animated); + return await channel?.invokeMethod('zoomBy', args); + } + + @override + Future getOriginalUrl() async { + Map args = {}; + String? url = await channel?.invokeMethod('getOriginalUrl', args); + return url != null ? WebUri(url) : null; + } + + @override + Future getZoomScale() async { + Map args = {}; + return await channel?.invokeMethod('getZoomScale', args); + } + + @override + @Deprecated('Use getZoomScale instead') + Future getScale() async { + return await getZoomScale(); + } + + @override + Future getSelectedText() async { + Map args = {}; + return await channel?.invokeMethod('getSelectedText', args); + } + + @override + Future getHitTestResult() async { + Map args = {}; + Map? hitTestResultMap = + await channel?.invokeMethod('getHitTestResult', args); + + if (hitTestResultMap == null) { + return null; + } + + hitTestResultMap = hitTestResultMap.cast(); + + InAppWebViewHitTestResultType? type = + InAppWebViewHitTestResultType.fromNativeValue( + hitTestResultMap["type"]?.toInt()); + String? extra = hitTestResultMap["extra"]; + return InAppWebViewHitTestResult(type: type, extra: extra); + } + + @override + Future clearFocus() async { + Map args = {}; + return await channel?.invokeMethod('clearFocus', args); + } + + @override + Future setContextMenu(ContextMenu? contextMenu) async { + Map args = {}; + args.putIfAbsent("contextMenu", () => contextMenu?.toMap()); + await channel?.invokeMethod('setContextMenu', args); + _inAppBrowser?.setContextMenu(contextMenu); + } + + @override + Future requestFocusNodeHref() async { + Map args = {}; + Map? result = + await channel?.invokeMethod('requestFocusNodeHref', args); + return result != null + ? RequestFocusNodeHrefResult( + url: result['url'] != null ? WebUri(result['url']) : null, + title: result['title'], + src: result['src'], + ) + : null; + } + + @override + Future requestImageRef() async { + Map args = {}; + Map? result = + await channel?.invokeMethod('requestImageRef', args); + return result != null + ? RequestImageRefResult( + url: result['url'] != null ? WebUri(result['url']) : null, + ) + : null; + } + + @override + Future> getMetaTags() async { + List metaTags = []; + + List>? metaTagList = + (await evaluateJavascript(source: """ +(function() { + var metaTags = []; + var metaTagNodes = document.head.getElementsByTagName('meta'); + for (var i = 0; i < metaTagNodes.length; i++) { + var metaTagNode = metaTagNodes[i]; + + var otherAttributes = metaTagNode.getAttributeNames(); + var nameIndex = otherAttributes.indexOf("name"); + if (nameIndex !== -1) otherAttributes.splice(nameIndex, 1); + var contentIndex = otherAttributes.indexOf("content"); + if (contentIndex !== -1) otherAttributes.splice(contentIndex, 1); + + var attrs = []; + for (var j = 0; j < otherAttributes.length; j++) { + var otherAttribute = otherAttributes[j]; + attrs.push( + { + name: otherAttribute, + value: metaTagNode.getAttribute(otherAttribute) + } + ); + } + + metaTags.push( + { + name: metaTagNode.name, + content: metaTagNode.content, + attrs: attrs + } + ); + } + return metaTags; +})(); + """))?.cast>(); + + if (metaTagList == null) { + return metaTags; + } + + for (var metaTag in metaTagList) { + var attrs = []; + + for (var metaTagAttr in metaTag["attrs"]) { + attrs.add(MetaTagAttribute( + name: metaTagAttr["name"], value: metaTagAttr["value"])); + } + + metaTags.add(MetaTag( + name: metaTag["name"], content: metaTag["content"], attrs: attrs)); + } + + return metaTags; + } + + @override + Future getMetaThemeColor() async { + Color? themeColor; + + try { + Map args = {}; + themeColor = UtilColor.fromStringRepresentation( + await channel?.invokeMethod('getMetaThemeColor', args)); + return themeColor; + } catch (e) { + // not implemented + } + + // try using javascript + var metaTags = await getMetaTags(); + MetaTag? metaTagThemeColor; + + for (var metaTag in metaTags) { + if (metaTag.name == "theme-color") { + metaTagThemeColor = metaTag; + break; + } + } + + if (metaTagThemeColor == null) { + return null; + } + + var colorValue = metaTagThemeColor.content; + + themeColor = colorValue != null + ? UtilColor.fromStringRepresentation(colorValue) + : null; + + return themeColor; + } + + @override + Future getScrollX() async { + Map args = {}; + return await channel?.invokeMethod('getScrollX', args); + } + + @override + Future getScrollY() async { + Map args = {}; + return await channel?.invokeMethod('getScrollY', args); + } + + @override + Future getCertificate() async { + Map args = {}; + Map? sslCertificateMap = + (await channel?.invokeMethod('getCertificate', args)) + ?.cast(); + return SslCertificate.fromMap(sslCertificateMap); + } + + @override + Future addUserScript({required UserScript userScript}) async { + Map args = {}; + args.putIfAbsent('userScript', () => userScript.toMap()); + if (!(_userScripts[userScript.injectionTime]?.contains(userScript) ?? + false)) { + _userScripts[userScript.injectionTime]?.add(userScript); + await channel?.invokeMethod('addUserScript', args); + } + } + + @override + Future addUserScripts({required List userScripts}) async { + for (var i = 0; i < userScripts.length; i++) { + await addUserScript(userScript: userScripts[i]); + } + } + + @override + Future removeUserScript({required UserScript userScript}) async { + var index = _userScripts[userScript.injectionTime]?.indexOf(userScript); + if (index == null || index == -1) { + return false; + } + + _userScripts[userScript.injectionTime]?.remove(userScript); + Map args = {}; + args.putIfAbsent('userScript', () => userScript.toMap()); + args.putIfAbsent('index', () => index); + await channel?.invokeMethod('removeUserScript', args); + + return true; + } + + @override + Future removeUserScriptsByGroupName({required String groupName}) async { + final List userScriptsAtDocumentStart = List.from( + _userScripts[UserScriptInjectionTime.AT_DOCUMENT_START] ?? []); + for (final userScript in userScriptsAtDocumentStart) { + if (userScript.groupName == groupName) { + _userScripts[userScript.injectionTime]?.remove(userScript); + } + } + + final List userScriptsAtDocumentEnd = + List.from(_userScripts[UserScriptInjectionTime.AT_DOCUMENT_END] ?? []); + for (final userScript in userScriptsAtDocumentEnd) { + if (userScript.groupName == groupName) { + _userScripts[userScript.injectionTime]?.remove(userScript); + } + } + + Map args = {}; + args.putIfAbsent('groupName', () => groupName); + await channel?.invokeMethod('removeUserScriptsByGroupName', args); + } + + @override + Future removeUserScripts( + {required List userScripts}) async { + for (final userScript in userScripts) { + await removeUserScript(userScript: userScript); + } + } + + @override + Future removeAllUserScripts() async { + _userScripts[UserScriptInjectionTime.AT_DOCUMENT_START]?.clear(); + _userScripts[UserScriptInjectionTime.AT_DOCUMENT_END]?.clear(); + + Map args = {}; + await channel?.invokeMethod('removeAllUserScripts', args); + } + + @override + bool hasUserScript({required UserScript userScript}) { + return _userScripts[userScript.injectionTime]?.contains(userScript) ?? + false; + } + + @override + Future callAsyncJavaScript( + {required String functionBody, + Map arguments = const {}, + ContentWorld? contentWorld}) async { + Map args = {}; + args.putIfAbsent('functionBody', () => functionBody); + args.putIfAbsent('arguments', () => arguments); + args.putIfAbsent('contentWorld', () => contentWorld?.toMap()); + var data = await channel?.invokeMethod('callAsyncJavaScript', args); + if (data == null) { + return null; + } + data = json.decode(data); + return CallAsyncJavaScriptResult( + value: data["value"], error: data["error"]); + } + + @override + Future saveWebArchive( + {required String filePath, bool autoname = false}) async { + if (!autoname) { + assert(filePath.endsWith("." + WebArchiveFormat.MHT.toNativeValue())); + } + + Map args = {}; + args.putIfAbsent("filePath", () => filePath); + args.putIfAbsent("autoname", () => autoname); + return await channel?.invokeMethod('saveWebArchive', args); + } + + @override + Future isSecureContext() async { + Map args = {}; + return await channel?.invokeMethod('isSecureContext', args) ?? false; + } + + @override + Future createWebMessageChannel() async { + Map args = {}; + Map? result = + (await channel?.invokeMethod('createWebMessageChannel', args)) + ?.cast(); + final webMessageChannel = AndroidWebMessageChannel.static().fromMap(result); + if (webMessageChannel != null) { + _webMessageChannels.add(webMessageChannel); + } + return webMessageChannel; + } + + @override + Future postWebMessage( + {required WebMessage message, WebUri? targetOrigin}) async { + if (targetOrigin == null) { + targetOrigin = WebUri(''); + } + Map args = {}; + args.putIfAbsent('message', () => message.toMap()); + args.putIfAbsent('targetOrigin', () => targetOrigin.toString()); + await channel?.invokeMethod('postWebMessage', args); + } + + @override + Future addWebMessageListener( + PlatformWebMessageListener webMessageListener) async { + assert(!_webMessageListeners.contains(webMessageListener), + "${webMessageListener} was already added."); + assert( + !_webMessageListenerObjNames + .contains(webMessageListener.params.jsObjectName), + "jsObjectName ${webMessageListener.params.jsObjectName} was already added."); + _webMessageListeners.add(webMessageListener as AndroidWebMessageListener); + _webMessageListenerObjNames.add(webMessageListener.params.jsObjectName); + + Map args = {}; + args.putIfAbsent('webMessageListener', () => webMessageListener.toMap()); + await channel?.invokeMethod('addWebMessageListener', args); + } + + @override + bool hasWebMessageListener(PlatformWebMessageListener webMessageListener) { + return _webMessageListeners.contains(webMessageListener) || + _webMessageListenerObjNames + .contains(webMessageListener.params.jsObjectName); + } + + @override + Future canScrollVertically() async { + Map args = {}; + return await channel?.invokeMethod('canScrollVertically', args) ?? + false; + } + + @override + Future canScrollHorizontally() async { + Map args = {}; + return await channel?.invokeMethod('canScrollHorizontally', args) ?? + false; + } + + @override + Future startSafeBrowsing() async { + Map args = {}; + return await channel?.invokeMethod('startSafeBrowsing', args) ?? + false; + } + + @override + Future clearSslPreferences() async { + Map args = {}; + await channel?.invokeMethod('clearSslPreferences', args); + } + + @override + Future pause() async { + Map args = {}; + await channel?.invokeMethod('pause', args); + } + + @override + Future resume() async { + Map args = {}; + await channel?.invokeMethod('resume', args); + } + + @override + Future pageDown({required bool bottom}) async { + Map args = {}; + args.putIfAbsent("bottom", () => bottom); + return await channel?.invokeMethod('pageDown', args) ?? false; + } + + @override + Future pageUp({required bool top}) async { + Map args = {}; + args.putIfAbsent("top", () => top); + return await channel?.invokeMethod('pageUp', args) ?? false; + } + + @override + Future zoomIn() async { + Map args = {}; + return await channel?.invokeMethod('zoomIn', args) ?? false; + } + + @override + Future zoomOut() async { + Map args = {}; + return await channel?.invokeMethod('zoomOut', args) ?? false; + } + + @override + Future clearHistory() async { + Map args = {}; + return await channel?.invokeMethod('clearHistory', args); + } + + @override + Future isInFullscreen() async { + Map args = {}; + return await channel?.invokeMethod('isInFullscreen', args) ?? false; + } + + @override + Future clearFormData() async { + Map args = {}; + return await channel?.invokeMethod('clearFormData', args); + } + + @override + Future getDefaultUserAgent() async { + Map args = {}; + return await _staticChannel.invokeMethod( + 'getDefaultUserAgent', args) ?? + ''; + } + + @override + Future clearClientCertPreferences() async { + Map args = {}; + await _staticChannel.invokeMethod('clearClientCertPreferences', args); + } + + @override + Future getSafeBrowsingPrivacyPolicyUrl() async { + Map args = {}; + String? url = await _staticChannel.invokeMethod( + 'getSafeBrowsingPrivacyPolicyUrl', args); + return url != null ? WebUri(url) : null; + } + + @override + @Deprecated("Use setSafeBrowsingAllowlist instead") + Future setSafeBrowsingWhitelist({required List hosts}) async { + return await setSafeBrowsingAllowlist(hosts: hosts); + } + + @override + Future setSafeBrowsingAllowlist({required List hosts}) async { + Map args = {}; + args.putIfAbsent('hosts', () => hosts); + return await _staticChannel.invokeMethod( + 'setSafeBrowsingAllowlist', args) ?? + false; + } + + @override + Future getCurrentWebViewPackage() async { + Map args = {}; + Map? packageInfo = + (await _staticChannel.invokeMethod('getCurrentWebViewPackage', args)) + ?.cast(); + return WebViewPackageInfo.fromMap(packageInfo); + } + + @override + Future setWebContentsDebuggingEnabled(bool debuggingEnabled) async { + Map args = {}; + args.putIfAbsent('debuggingEnabled', () => debuggingEnabled); + return await _staticChannel.invokeMethod( + 'setWebContentsDebuggingEnabled', args); + } + + @override + Future getVariationsHeader() async { + Map args = {}; + return await _staticChannel.invokeMethod( + 'getVariationsHeader', args); + } + + @override + Future isMultiProcessEnabled() async { + Map args = {}; + return await _staticChannel.invokeMethod( + 'isMultiProcessEnabled', args) ?? + false; + } + + @override + Future disableWebView() async { + Map args = {}; + await _staticChannel.invokeMethod('disableWebView', args); + } + + @override + Future disposeKeepAlive(InAppWebViewKeepAlive keepAlive) async { + Map args = {}; + args.putIfAbsent('keepAliveId', () => keepAlive.id); + await _staticChannel.invokeMethod('disposeKeepAlive', args); + _keepAliveMap[keepAlive] = null; + } + + @override + Future clearAllCache({bool includeDiskFiles = true}) async { + Map args = {}; + args.putIfAbsent('includeDiskFiles', () => includeDiskFiles); + await _staticChannel.invokeMethod('clearAllCache', args); + } + + @override + Future get tRexRunnerHtml async => await rootBundle.loadString( + 'packages/flutter_inappwebview/assets/t_rex_runner/t-rex.html'); + + @override + Future get tRexRunnerCss async => await rootBundle.loadString( + 'packages/flutter_inappwebview/assets/t_rex_runner/t-rex.css'); + + @override + dynamic getViewId() { + return id; + } + + @override + void dispose({bool isKeepAlive = false}) { + disposeChannel(removeMethodCallHandler: !isKeepAlive); + _inAppBrowser = null; + webStorage.dispose(); + if (!isKeepAlive) { + _controllerFromPlatform = null; + _javaScriptHandlersMap.clear(); + _userScripts.clear(); + _webMessageListenerObjNames.clear(); + _injectedScriptsFromURL.clear(); + for (final webMessageChannel in _webMessageChannels) { + webMessageChannel.dispose(); + } + _webMessageChannels.clear(); + for (final webMessageListener in _webMessageListeners) { + webMessageListener.dispose(); + } + _webMessageListeners.clear(); + } + } +} + +extension InternalInAppWebViewController on AndroidInAppWebViewController { + get handleMethod => _handleMethod; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/in_app_webview/main.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/in_app_webview/main.dart new file mode 100644 index 00000000..b83b0611 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/in_app_webview/main.dart @@ -0,0 +1,3 @@ +export 'in_app_webview_controller.dart' hide InternalInAppWebViewController; +export 'in_app_webview.dart'; +export 'headless_in_app_webview.dart' hide InternalHeadlessInAppWebView; diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/inappwebview_platform.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/inappwebview_platform.dart new file mode 100644 index 00000000..aa5c4207 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/inappwebview_platform.dart @@ -0,0 +1,376 @@ +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +import 'chrome_safari_browser/chrome_safari_browser.dart'; +import 'cookie_manager.dart'; +import 'http_auth_credentials_database.dart'; +import 'find_interaction/main.dart'; +import 'in_app_browser/in_app_browser.dart'; +import 'in_app_webview/main.dart'; +import 'print_job/main.dart'; +import 'pull_to_refresh/main.dart'; +import 'web_message/main.dart'; +import 'web_storage/main.dart'; +import 'process_global_config.dart'; +import 'proxy_controller.dart'; +import 'service_worker_controller.dart'; +import 'tracing_controller.dart'; +import 'webview_asset_loader.dart'; +import 'webview_feature.dart' as wv; + +/// Implementation of [InAppWebViewPlatform] using the WebView API. +class AndroidInAppWebViewPlatform extends InAppWebViewPlatform { + /// Registers this class as the default instance of [InAppWebViewPlatform]. + static void registerWith() { + InAppWebViewPlatform.instance = AndroidInAppWebViewPlatform(); + } + + /// Creates a new [AndroidCookieManager]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [CookieManager] in `flutter_inappwebview` instead. + @override + AndroidCookieManager createPlatformCookieManager( + PlatformCookieManagerCreationParams params, + ) { + return AndroidCookieManager(params); + } + + /// Creates a new [AndroidInAppWebViewController]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [InAppWebViewController] in `flutter_inappwebview` instead. + @override + AndroidInAppWebViewController createPlatformInAppWebViewController( + PlatformInAppWebViewControllerCreationParams params, + ) { + return AndroidInAppWebViewController(params); + } + + /// Creates a new empty [AndroidInAppWebViewController] to access static methods. + /// + /// This function should only be called by the app-facing package. + /// Look at using [InAppWebViewController] in `flutter_inappwebview` instead. + @override + AndroidInAppWebViewController createPlatformInAppWebViewControllerStatic() { + return AndroidInAppWebViewController.static(); + } + + /// Creates a new [AndroidInAppWebViewWidget]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [InAppWebView] in `flutter_inappwebview` instead. + @override + AndroidInAppWebViewWidget createPlatformInAppWebViewWidget( + PlatformInAppWebViewWidgetCreationParams params, + ) { + return AndroidInAppWebViewWidget(params); + } + + /// Creates a new [AndroidFindInteractionController]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [FindInteractionController] in `flutter_inappwebview` instead. + @override + AndroidFindInteractionController createPlatformFindInteractionController( + PlatformFindInteractionControllerCreationParams params, + ) { + return AndroidFindInteractionController(params); + } + + /// Creates a new [AndroidPrintJobController]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [PrintJobController] in `flutter_inappwebview` instead. + @override + AndroidPrintJobController createPlatformPrintJobController( + PlatformPrintJobControllerCreationParams params, + ) { + return AndroidPrintJobController(params); + } + + /// Creates a new [AndroidPullToRefreshController]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [PullToRefreshController] in `flutter_inappwebview` instead. + @override + AndroidPullToRefreshController createPlatformPullToRefreshController( + PlatformPullToRefreshControllerCreationParams params, + ) { + return AndroidPullToRefreshController(params); + } + + /// Creates a new [AndroidWebMessageChannel]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebMessageChannel] in `flutter_inappwebview` instead. + @override + AndroidWebMessageChannel createPlatformWebMessageChannel( + PlatformWebMessageChannelCreationParams params, + ) { + return AndroidWebMessageChannel(params); + } + + /// Creates a new empty [AndroidWebMessageChannel] to access static methods. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebMessageChannel] in `flutter_inappwebview` instead. + @override + AndroidWebMessageChannel createPlatformWebMessageChannelStatic() { + return AndroidWebMessageChannel.static(); + } + + /// Creates a new [AndroidWebMessageListener]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebMessageListener] in `flutter_inappwebview` instead. + @override + AndroidWebMessageListener createPlatformWebMessageListener( + PlatformWebMessageListenerCreationParams params, + ) { + return AndroidWebMessageListener(params); + } + + /// Creates a new [AndroidJavaScriptReplyProxy]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [JavaScriptReplyProxy] in `flutter_inappwebview` instead. + @override + AndroidJavaScriptReplyProxy createPlatformJavaScriptReplyProxy( + PlatformJavaScriptReplyProxyCreationParams params, + ) { + return AndroidJavaScriptReplyProxy(params); + } + + /// Creates a new [AndroidWebMessagePort]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebMessagePort] in `flutter_inappwebview` instead. + @override + AndroidWebMessagePort createPlatformWebMessagePort( + PlatformWebMessagePortCreationParams params, + ) { + return AndroidWebMessagePort(params); + } + + /// Creates a new [AndroidWebStorage]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebStorage] in `flutter_inappwebview` instead. + @override + AndroidWebStorage createPlatformWebStorage( + PlatformWebStorageCreationParams params, + ) { + return AndroidWebStorage(params); + } + + /// Creates a new [AndroidLocalStorage]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [LocalStorage] in `flutter_inappwebview` instead. + @override + AndroidLocalStorage createPlatformLocalStorage( + PlatformLocalStorageCreationParams params, + ) { + return AndroidLocalStorage(params); + } + + /// Creates a new [AndroidSessionStorage]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [SessionStorage] in `flutter_inappwebview` instead. + @override + AndroidSessionStorage createPlatformSessionStorage( + PlatformSessionStorageCreationParams params, + ) { + return AndroidSessionStorage(params); + } + + /// Creates a new [AndroidHeadlessInAppWebView]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [HeadlessInAppWebView] in `flutter_inappwebview` instead. + @override + AndroidHeadlessInAppWebView createPlatformHeadlessInAppWebView( + PlatformHeadlessInAppWebViewCreationParams params, + ) { + return AndroidHeadlessInAppWebView(params); + } + + /// Creates a new [AndroidHttpAuthCredentialDatabase]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [HttpAuthCredentialDatabase] in `flutter_inappwebview` instead. + @override + AndroidHttpAuthCredentialDatabase createPlatformHttpAuthCredentialDatabase( + PlatformHttpAuthCredentialDatabaseCreationParams params, + ) { + return AndroidHttpAuthCredentialDatabase(params); + } + + /// Creates a new [AndroidInAppBrowser]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [InAppBrowser] in `flutter_inappwebview` instead. + @override + AndroidInAppBrowser createPlatformInAppBrowser( + PlatformInAppBrowserCreationParams params, + ) { + return AndroidInAppBrowser(params); + } + + /// Creates a new empty [AndroidInAppBrowser] to access static methods. + /// + /// This function should only be called by the app-facing package. + /// Look at using [InAppBrowser] in `flutter_inappwebview` instead. + @override + AndroidInAppBrowser createPlatformInAppBrowserStatic() { + return AndroidInAppBrowser.static(); + } + + /// Creates a new [AndroidProcessGlobalConfig]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [ProcessGlobalConfig] in `flutter_inappwebview` instead. + @override + AndroidProcessGlobalConfig createPlatformProcessGlobalConfig( + PlatformProcessGlobalConfigCreationParams params, + ) { + return AndroidProcessGlobalConfig(params); + } + + /// Creates a new [AndroidProxyController]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [ProxyController] in `flutter_inappwebview` instead. + @override + AndroidProxyController createPlatformProxyController( + PlatformProxyControllerCreationParams params, + ) { + return AndroidProxyController(params); + } + + /// Creates a new [AndroidServiceWorkerController]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [ServiceWorkerController] in `flutter_inappwebview` instead. + @override + AndroidServiceWorkerController createPlatformServiceWorkerController( + PlatformServiceWorkerControllerCreationParams params, + ) { + return AndroidServiceWorkerController(params); + } + + /// Creates a new empty [AndroidServiceWorkerController] to access static methods. + /// + /// This function should only be called by the app-facing package. + /// Look at using [ServiceWorkerController] in `flutter_inappwebview` instead. + @override + AndroidServiceWorkerController createPlatformServiceWorkerControllerStatic() { + return AndroidServiceWorkerController.static(); + } + + /// Creates a new [AndroidTracingController]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [TracingController] in `flutter_inappwebview` instead. + @override + AndroidTracingController createPlatformTracingController( + PlatformTracingControllerCreationParams params, + ) { + return AndroidTracingController(params); + } + + /// Creates a new [AndroidAssetsPathHandler]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [AssetsPathHandler] in `flutter_inappwebview` instead. + @override + AndroidAssetsPathHandler createPlatformAssetsPathHandler( + PlatformAssetsPathHandlerCreationParams params, + ) { + return AndroidAssetsPathHandler(params); + } + + /// Creates a new [AndroidResourcesPathHandler]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [ResourcesPathHandler] in `flutter_inappwebview` instead. + @override + AndroidResourcesPathHandler createPlatformResourcesPathHandler( + PlatformResourcesPathHandlerCreationParams params, + ) { + return AndroidResourcesPathHandler(params); + } + + /// Creates a new [AndroidInternalStoragePathHandler]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [InternalStoragePathHandler] in `flutter_inappwebview` instead. + @override + AndroidInternalStoragePathHandler createPlatformInternalStoragePathHandler( + PlatformInternalStoragePathHandlerCreationParams params, + ) { + return AndroidInternalStoragePathHandler(params); + } + + /// Creates a new [AndroidCustomPathHandler]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [CustomPathHandler] in `flutter_inappwebview` instead. + @override + AndroidCustomPathHandler createPlatformCustomPathHandler( + PlatformCustomPathHandlerCreationParams params, + ) { + return AndroidCustomPathHandler(params); + } + + /// Creates a new [wv.AndroidWebViewFeature]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebViewFeature] in `flutter_inappwebview` instead. + @override + wv.AndroidWebViewFeature createPlatformWebViewFeature( + PlatformWebViewFeatureCreationParams params, + ) { + return wv.AndroidWebViewFeature(params); + } + + /// Creates a new empty [wv.AndroidWebViewFeature] to access static methods. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebViewFeature] in `flutter_inappwebview` instead. + @override + wv.AndroidWebViewFeature createPlatformWebViewFeatureStatic() { + return wv.AndroidWebViewFeature.static(); + } + + /// Creates a new [AndroidChromeSafariBrowser]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [ChromeSafariBrowser] in `flutter_inappwebview` instead. + @override + AndroidChromeSafariBrowser createPlatformChromeSafariBrowser( + PlatformChromeSafariBrowserCreationParams params, + ) { + return AndroidChromeSafariBrowser(params); + } + + /// Creates a new empty [AndroidChromeSafariBrowser] to access static methods. + /// + /// This function should only be called by the app-facing package. + /// Look at using [ChromeSafariBrowser] in `flutter_inappwebview` instead. + @override + AndroidChromeSafariBrowser createPlatformChromeSafariBrowserStatic() { + return AndroidChromeSafariBrowser.static(); + } + + /// Creates a new empty [AndroidWebStorageManager] to access static methods. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebStorageManager] in `flutter_inappwebview` instead. + @override + AndroidWebStorageManager createPlatformWebStorageManager( + PlatformWebStorageManagerCreationParams params) { + return AndroidWebStorageManager(params); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/main.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/main.dart new file mode 100644 index 00000000..b49714ad --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/main.dart @@ -0,0 +1,18 @@ +export 'inappwebview_platform.dart'; +export 'in_app_webview/main.dart'; +export 'in_app_browser/main.dart'; +export 'chrome_safari_browser/main.dart'; +export 'web_storage/main.dart'; +export 'cookie_manager.dart' hide InternalCookieManager; +export 'http_auth_credentials_database.dart' + hide InternalHttpAuthCredentialDatabase; +export 'pull_to_refresh/main.dart'; +export 'web_message/main.dart'; +export 'print_job/main.dart'; +export 'find_interaction/main.dart'; +export 'service_worker_controller.dart'; +export 'webview_feature.dart' hide InternalWebViewFeature; +export 'proxy_controller.dart' hide InternalProxyController; +export 'webview_asset_loader.dart'; +export 'tracing_controller.dart' hide InternalTracingController; +export 'process_global_config.dart' hide InternalProcessGlobalConfig; diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/platform_util.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/platform_util.dart new file mode 100644 index 00000000..27e96478 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/platform_util.dart @@ -0,0 +1,64 @@ +import 'package:flutter/services.dart'; + +///Platform native utilities +class PlatformUtil { + static PlatformUtil? _instance; + static const MethodChannel _channel = + MethodChannel('com.pichillilorenzo/flutter_inappwebview_platformutil'); + + PlatformUtil._(); + + ///Get [PlatformUtil] instance. + static PlatformUtil instance() { + return (_instance != null) ? _instance! : _init(); + } + + static PlatformUtil _init() { + _channel.setMethodCallHandler((call) async { + try { + return await _handleMethod(call); + } on Error catch (e) { + print(e); + print(e.stackTrace); + } + }); + _instance = PlatformUtil._(); + return _instance!; + } + + static Future _handleMethod(MethodCall call) async {} + + String? _cachedSystemVersion; + + ///Get current platform system version. + Future getSystemVersion() async { + if (_cachedSystemVersion != null) { + return _cachedSystemVersion!; + } + Map args = {}; + _cachedSystemVersion = + await _channel.invokeMethod('getSystemVersion', args); + return _cachedSystemVersion!; + } + + ///Format date. + Future formatDate( + {required DateTime date, + required String format, + String locale = "en_US", + String timezone = "UTC"}) async { + Map args = {}; + args.putIfAbsent('date', () => date.millisecondsSinceEpoch); + args.putIfAbsent('format', () => format); + args.putIfAbsent('locale', () => locale); + args.putIfAbsent('timezone', () => timezone); + return await _channel.invokeMethod('formatDate', args); + } + + ///Get cookie expiration date used by Web platform. + Future getWebCookieExpirationDate({required DateTime date}) async { + Map args = {}; + args.putIfAbsent('date', () => date.millisecondsSinceEpoch); + return await _channel.invokeMethod('getWebCookieExpirationDate', args); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/print_job/main.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/print_job/main.dart new file mode 100644 index 00000000..4e70ad94 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/print_job/main.dart @@ -0,0 +1 @@ +export 'print_job_controller.dart'; diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/print_job/print_job_controller.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/print_job/print_job_controller.dart new file mode 100644 index 00000000..76831154 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/print_job/print_job_controller.dart @@ -0,0 +1,77 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidPrintJobController]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformPrintJobControllerCreationParams] for +/// more information. +@immutable +class AndroidPrintJobControllerCreationParams + extends PlatformPrintJobControllerCreationParams { + /// Creates a new [AndroidPrintJobControllerCreationParams] instance. + const AndroidPrintJobControllerCreationParams( + {required super.id, super.onComplete}); + + /// Creates a [AndroidPrintJobControllerCreationParams] instance based on [PlatformPrintJobControllerCreationParams]. + factory AndroidPrintJobControllerCreationParams.fromPlatformPrintJobControllerCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformPrintJobControllerCreationParams params) { + return AndroidPrintJobControllerCreationParams( + id: params.id, onComplete: params.onComplete); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformPrintJobController} +class AndroidPrintJobController extends PlatformPrintJobController + with ChannelController { + /// Constructs a [AndroidPrintJobController]. + AndroidPrintJobController(PlatformPrintJobControllerCreationParams params) + : super.implementation( + params is AndroidPrintJobControllerCreationParams + ? params + : AndroidPrintJobControllerCreationParams + .fromPlatformPrintJobControllerCreationParams(params), + ) { + channel = MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_printjobcontroller_${params.id}'); + handler = _handleMethod; + initMethodCallHandler(); + } + + Future _handleMethod(MethodCall call) async { + switch (call.method) { + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + } + + @override + Future cancel() async { + Map args = {}; + await channel?.invokeMethod('cancel', args); + } + + @override + Future restart() async { + Map args = {}; + await channel?.invokeMethod('restart', args); + } + + @override + Future getInfo() async { + Map args = {}; + Map? infoMap = + (await channel?.invokeMethod('getInfo', args))?.cast(); + return PrintJobInfo.fromMap(infoMap); + } + + @override + Future dispose() async { + Map args = {}; + await channel?.invokeMethod('dispose', args); + disposeChannel(); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/process_global_config.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/process_global_config.dart new file mode 100644 index 00000000..1f4d6fe0 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/process_global_config.dart @@ -0,0 +1,76 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidProcessGlobalConfig]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformProcessGlobalConfigCreationParams] for +/// more information. +@immutable +class AndroidProcessGlobalConfigCreationParams + extends PlatformProcessGlobalConfigCreationParams { + /// Creates a new [AndroidProcessGlobalConfigCreationParams] instance. + const AndroidProcessGlobalConfigCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformProcessGlobalConfigCreationParams params, + ) : super(); + + /// Creates a [AndroidProcessGlobalConfigCreationParams] instance based on [PlatformProcessGlobalConfigCreationParams]. + factory AndroidProcessGlobalConfigCreationParams.fromPlatformProcessGlobalConfigCreationParams( + PlatformProcessGlobalConfigCreationParams params) { + return AndroidProcessGlobalConfigCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformProcessGlobalConfig} +class AndroidProcessGlobalConfig extends PlatformProcessGlobalConfig + with ChannelController { + /// Creates a new [AndroidProcessGlobalConfig]. + AndroidProcessGlobalConfig(PlatformProcessGlobalConfigCreationParams params) + : super.implementation( + params is AndroidProcessGlobalConfigCreationParams + ? params + : AndroidProcessGlobalConfigCreationParams + .fromPlatformProcessGlobalConfigCreationParams(params), + ) { + channel = const MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_processglobalconfig'); + handler = handleMethod; + initMethodCallHandler(); + } + + static AndroidProcessGlobalConfig? _instance; + + ///Gets the [AndroidProcessGlobalConfig] shared instance. + static AndroidProcessGlobalConfig instance() { + return (_instance != null) ? _instance! : _init(); + } + + static AndroidProcessGlobalConfig _init() { + _instance = AndroidProcessGlobalConfig( + AndroidProcessGlobalConfigCreationParams( + const PlatformProcessGlobalConfigCreationParams())); + return _instance!; + } + + Future _handleMethod(MethodCall call) async {} + + @override + Future apply({required ProcessGlobalConfigSettings settings}) async { + Map args = {}; + args.putIfAbsent("settings", () => settings.toMap()); + await channel?.invokeMethod('apply', args); + } + + @override + void dispose() { + // empty + } +} + +extension InternalProcessGlobalConfig on AndroidProcessGlobalConfig { + get handleMethod => _handleMethod; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/proxy_controller.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/proxy_controller.dart new file mode 100644 index 00000000..07649ac8 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/proxy_controller.dart @@ -0,0 +1,81 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidProxyController]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformProxyControllerCreationParams] for +/// more information. +@immutable +class AndroidProxyControllerCreationParams + extends PlatformProxyControllerCreationParams { + /// Creates a new [AndroidProxyControllerCreationParams] instance. + const AndroidProxyControllerCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformProxyControllerCreationParams params, + ) : super(); + + /// Creates a [AndroidProxyControllerCreationParams] instance based on [PlatformProxyControllerCreationParams]. + factory AndroidProxyControllerCreationParams.fromPlatformProxyControllerCreationParams( + PlatformProxyControllerCreationParams params) { + return AndroidProxyControllerCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformProxyController} +class AndroidProxyController extends PlatformProxyController + with ChannelController { + /// Creates a new [AndroidProxyController]. + AndroidProxyController(PlatformProxyControllerCreationParams params) + : super.implementation( + params is AndroidProxyControllerCreationParams + ? params + : AndroidProxyControllerCreationParams + .fromPlatformProxyControllerCreationParams(params), + ) { + channel = const MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_proxycontroller'); + handler = handleMethod; + initMethodCallHandler(); + } + + static AndroidProxyController? _instance; + + ///Gets the [AndroidProxyController] shared instance. + static AndroidProxyController instance() { + return (_instance != null) ? _instance! : _init(); + } + + static AndroidProxyController _init() { + _instance = AndroidProxyController(AndroidProxyControllerCreationParams( + const PlatformProxyControllerCreationParams())); + return _instance!; + } + + Future _handleMethod(MethodCall call) async {} + + @override + Future setProxyOverride({required ProxySettings settings}) async { + Map args = {}; + args.putIfAbsent("settings", () => settings.toMap()); + await channel?.invokeMethod('setProxyOverride', args); + } + + @override + Future clearProxyOverride() async { + Map args = {}; + await channel?.invokeMethod('clearProxyOverride', args); + } + + @override + void dispose() { + // empty + } +} + +extension InternalProxyController on AndroidProxyController { + get handleMethod => _handleMethod; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/pull_to_refresh/main.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/pull_to_refresh/main.dart new file mode 100644 index 00000000..586af9d8 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/pull_to_refresh/main.dart @@ -0,0 +1 @@ +export 'pull_to_refresh_controller.dart' hide InternalPullToRefreshController; diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/pull_to_refresh/pull_to_refresh_controller.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/pull_to_refresh/pull_to_refresh_controller.dart new file mode 100644 index 00000000..693c98a1 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/pull_to_refresh/pull_to_refresh_controller.dart @@ -0,0 +1,163 @@ +import 'dart:ui'; + +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidPullToRefreshController]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformPullToRefreshControllerCreationParams] for +/// more information. +class AndroidPullToRefreshControllerCreationParams + extends PlatformPullToRefreshControllerCreationParams { + /// Creates a new [AndroidPullToRefreshControllerCreationParams] instance. + AndroidPullToRefreshControllerCreationParams( + {super.onRefresh, super.options, super.settings}); + + /// Creates a [AndroidPullToRefreshControllerCreationParams] instance based on [PlatformPullToRefreshControllerCreationParams]. + factory AndroidPullToRefreshControllerCreationParams.fromPlatformPullToRefreshControllerCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformPullToRefreshControllerCreationParams params) { + return AndroidPullToRefreshControllerCreationParams( + onRefresh: params.onRefresh, + options: params.options, + settings: params.settings); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformPullToRefreshController} +class AndroidPullToRefreshController extends PlatformPullToRefreshController + with ChannelController { + /// Constructs a [AndroidPullToRefreshController]. + AndroidPullToRefreshController( + PlatformPullToRefreshControllerCreationParams params) + : super.implementation( + params is AndroidPullToRefreshControllerCreationParams + ? params + : AndroidPullToRefreshControllerCreationParams + .fromPlatformPullToRefreshControllerCreationParams(params), + ); + + _debugLog(String method, dynamic args) { + debugLog( + className: this.runtimeType.toString(), + debugLoggingSettings: + PlatformPullToRefreshController.debugLoggingSettings, + method: method, + args: args); + } + + Future _handleMethod(MethodCall call) async { + _debugLog(call.method, call.arguments); + + switch (call.method) { + case "onRefresh": + if (params.onRefresh != null) params.onRefresh!(); + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + @override + Future setEnabled(bool enabled) async { + Map args = {}; + args.putIfAbsent('enabled', () => enabled); + await channel?.invokeMethod('setEnabled', args); + } + + @override + Future isEnabled() async { + Map args = {}; + return await channel?.invokeMethod('isEnabled', args) ?? false; + } + + Future _setRefreshing(bool refreshing) async { + Map args = {}; + args.putIfAbsent('refreshing', () => refreshing); + await channel?.invokeMethod('setRefreshing', args); + } + + @override + Future beginRefreshing() async { + return await _setRefreshing(true); + } + + @override + Future endRefreshing() async { + await _setRefreshing(false); + } + + @override + Future isRefreshing() async { + Map args = {}; + return await channel?.invokeMethod('isRefreshing', args) ?? false; + } + + @override + Future setColor(Color color) async { + Map args = {}; + args.putIfAbsent('color', () => color.toHex()); + await channel?.invokeMethod('setColor', args); + } + + @override + Future setBackgroundColor(Color color) async { + Map args = {}; + args.putIfAbsent('color', () => color.toHex()); + await channel?.invokeMethod('setBackgroundColor', args); + } + + @override + Future setDistanceToTriggerSync(int distanceToTriggerSync) async { + Map args = {}; + args.putIfAbsent('distanceToTriggerSync', () => distanceToTriggerSync); + await channel?.invokeMethod('setDistanceToTriggerSync', args); + } + + @override + Future setSlingshotDistance(int slingshotDistance) async { + Map args = {}; + args.putIfAbsent('slingshotDistance', () => slingshotDistance); + await channel?.invokeMethod('setSlingshotDistance', args); + } + + @override + Future getDefaultSlingshotDistance() async { + Map args = {}; + return await channel?.invokeMethod( + 'getDefaultSlingshotDistance', args) ?? + 0; + } + + @Deprecated("Use setIndicatorSize instead") + @override + Future setSize(AndroidPullToRefreshSize size) async { + Map args = {}; + args.putIfAbsent('size', () => size.toNativeValue()); + await channel?.invokeMethod('setSize', args); + } + + @override + Future setIndicatorSize(PullToRefreshSize size) async { + Map args = {}; + args.putIfAbsent('size', () => size.toNativeValue()); + await channel?.invokeMethod('setSize', args); + } + + @override + void dispose({bool isKeepAlive = false}) { + disposeChannel(removeMethodCallHandler: !isKeepAlive); + } +} + +extension InternalPullToRefreshController on AndroidPullToRefreshController { + void init(dynamic id) { + channel = MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_pull_to_refresh_$id'); + handler = _handleMethod; + initMethodCallHandler(); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/service_worker_controller.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/service_worker_controller.dart new file mode 100644 index 00000000..646b958a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/service_worker_controller.dart @@ -0,0 +1,161 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidServiceWorkerController]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformServiceWorkerControllerCreationParams] for +/// more information. +@immutable +class AndroidServiceWorkerControllerCreationParams + extends PlatformServiceWorkerControllerCreationParams { + /// Creates a new [AndroidServiceWorkerControllerCreationParams] instance. + const AndroidServiceWorkerControllerCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformServiceWorkerControllerCreationParams params, + ) : super(); + + /// Creates a [AndroidServiceWorkerControllerCreationParams] instance based on [PlatformServiceWorkerControllerCreationParams]. + factory AndroidServiceWorkerControllerCreationParams.fromPlatformServiceWorkerControllerCreationParams( + PlatformServiceWorkerControllerCreationParams params) { + return AndroidServiceWorkerControllerCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformServiceWorkerController} +class AndroidServiceWorkerController extends PlatformServiceWorkerController + with ChannelController { + /// Creates a new [AndroidServiceWorkerController]. + AndroidServiceWorkerController( + PlatformServiceWorkerControllerCreationParams params) + : super.implementation( + params is AndroidServiceWorkerControllerCreationParams + ? params + : AndroidServiceWorkerControllerCreationParams + .fromPlatformServiceWorkerControllerCreationParams(params), + ) { + channel = const MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_serviceworkercontroller'); + handler = handleMethod; + initMethodCallHandler(); + } + + factory AndroidServiceWorkerController.static() { + return instance(); + } + + static AndroidServiceWorkerController? _instance; + + ///Gets the [AndroidServiceWorkerController] shared instance. + static AndroidServiceWorkerController instance() { + return (_instance != null) ? _instance! : _init(); + } + + static AndroidServiceWorkerController _init() { + _instance = AndroidServiceWorkerController( + AndroidServiceWorkerControllerCreationParams( + const PlatformServiceWorkerControllerCreationParams())); + return _instance!; + } + + ServiceWorkerClient? _serviceWorkerClient; + + @override + ServiceWorkerClient? get serviceWorkerClient => _serviceWorkerClient; + + @override + Future setServiceWorkerClient(ServiceWorkerClient? value) async { + Map args = {}; + args.putIfAbsent('isNull', () => value == null); + await channel?.invokeMethod("setServiceWorkerClient", args); + _serviceWorkerClient = value; + } + + Future _handleMethod(MethodCall call) async { + switch (call.method) { + case "shouldInterceptRequest": + if (serviceWorkerClient != null && + serviceWorkerClient!.shouldInterceptRequest != null) { + Map arguments = + call.arguments.cast(); + WebResourceRequest request = WebResourceRequest.fromMap(arguments)!; + + return (await serviceWorkerClient!.shouldInterceptRequest!(request)) + ?.toMap(); + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + + return null; + } + + @override + Future getAllowContentAccess() async { + Map args = {}; + return await channel?.invokeMethod('getAllowContentAccess', args) ?? + false; + } + + @override + Future getAllowFileAccess() async { + Map args = {}; + return await channel?.invokeMethod('getAllowFileAccess', args) ?? + false; + } + + @override + Future getBlockNetworkLoads() async { + Map args = {}; + return await channel?.invokeMethod('getBlockNetworkLoads', args) ?? + false; + } + + @override + Future getCacheMode() async { + Map args = {}; + return CacheMode.fromNativeValue( + await channel?.invokeMethod('getCacheMode', args)); + } + + @override + Future setAllowContentAccess(bool allow) async { + Map args = {}; + args.putIfAbsent("allow", () => allow); + await channel?.invokeMethod('setAllowContentAccess', args); + } + + @override + Future setAllowFileAccess(bool allow) async { + Map args = {}; + args.putIfAbsent("allow", () => allow); + await channel?.invokeMethod('setAllowFileAccess', args); + } + + @override + Future setBlockNetworkLoads(bool flag) async { + Map args = {}; + args.putIfAbsent("flag", () => flag); + await channel?.invokeMethod('setBlockNetworkLoads', args); + } + + @override + Future setCacheMode(CacheMode mode) async { + Map args = {}; + args.putIfAbsent("mode", () => mode.toNativeValue()); + await channel?.invokeMethod('setCacheMode', args); + } + + @override + void dispose() { + // empty + } +} + +extension InternalServiceWorkerController on AndroidServiceWorkerController { + get handleMethod => _handleMethod; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/tracing_controller.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/tracing_controller.dart new file mode 100644 index 00000000..9d619f07 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/tracing_controller.dart @@ -0,0 +1,88 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidTracingController]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformTracingControllerCreationParams] for +/// more information. +@immutable +class AndroidTracingControllerCreationParams + extends PlatformTracingControllerCreationParams { + /// Creates a new [AndroidTracingControllerCreationParams] instance. + const AndroidTracingControllerCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformTracingControllerCreationParams params, + ) : super(); + + /// Creates a [AndroidTracingControllerCreationParams] instance based on [PlatformTracingControllerCreationParams]. + factory AndroidTracingControllerCreationParams.fromPlatformTracingControllerCreationParams( + PlatformTracingControllerCreationParams params) { + return AndroidTracingControllerCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformTracingController} +class AndroidTracingController extends PlatformTracingController + with ChannelController { + /// Creates a new [AndroidTracingController]. + AndroidTracingController(PlatformTracingControllerCreationParams params) + : super.implementation( + params is AndroidTracingControllerCreationParams + ? params + : AndroidTracingControllerCreationParams + .fromPlatformTracingControllerCreationParams(params), + ) { + channel = const MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_tracingcontroller'); + handler = handleMethod; + initMethodCallHandler(); + } + + static AndroidTracingController? _instance; + + ///Gets the [AndroidTracingController] shared instance. + static AndroidTracingController instance() { + return (_instance != null) ? _instance! : _init(); + } + + static AndroidTracingController _init() { + _instance = AndroidTracingController(AndroidTracingControllerCreationParams( + const PlatformTracingControllerCreationParams())); + return _instance!; + } + + Future _handleMethod(MethodCall call) async {} + + @override + Future start({required TracingSettings settings}) async { + Map args = {}; + args.putIfAbsent("settings", () => settings.toMap()); + await channel?.invokeMethod('start', args); + } + + @override + Future stop({String? filePath}) async { + Map args = {}; + args.putIfAbsent("filePath", () => filePath); + return await channel?.invokeMethod('stop', args) ?? false; + } + + @override + Future isTracing() async { + Map args = {}; + return await channel?.invokeMethod('isTracing', args) ?? false; + } + + @override + void dispose() { + // empty + } +} + +extension InternalTracingController on AndroidTracingController { + get handleMethod => _handleMethod; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/web_message/main.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/web_message/main.dart new file mode 100644 index 00000000..d41e30c7 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/web_message/main.dart @@ -0,0 +1,3 @@ +export 'web_message_port.dart' hide InternalWebMessagePort; +export 'web_message_channel.dart' hide InternalWebMessageChannel; +export 'web_message_listener.dart'; diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/web_message/web_message_channel.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/web_message/web_message_channel.dart new file mode 100644 index 00000000..9af87309 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/web_message/web_message_channel.dart @@ -0,0 +1,120 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; +import 'web_message_port.dart'; + +/// Object specifying creation parameters for creating a [AndroidWebMessageChannel]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebMessageChannelCreationParams] for +/// more information. +@immutable +class AndroidWebMessageChannelCreationParams + extends PlatformWebMessageChannelCreationParams { + /// Creates a new [AndroidWebMessageChannelCreationParams] instance. + const AndroidWebMessageChannelCreationParams( + {required super.id, required super.port1, required super.port2}); + + /// Creates a [AndroidWebMessageChannelCreationParams] instance based on [PlatformWebMessageChannelCreationParams]. + factory AndroidWebMessageChannelCreationParams.fromPlatformWebMessageChannelCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformWebMessageChannelCreationParams params) { + return AndroidWebMessageChannelCreationParams( + id: params.id, port1: params.port1, port2: params.port2); + } + + @override + String toString() { + return 'AndroidWebMessageChannelCreationParams{id: $id, port1: $port1, port2: $port2}'; + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformWebMessageChannel} +class AndroidWebMessageChannel extends PlatformWebMessageChannel + with ChannelController { + /// Constructs a [AndroidWebMessageChannel]. + AndroidWebMessageChannel(PlatformWebMessageChannelCreationParams params) + : super.implementation( + params is AndroidWebMessageChannelCreationParams + ? params + : AndroidWebMessageChannelCreationParams + .fromPlatformWebMessageChannelCreationParams(params), + ) { + channel = MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_web_message_channel_${params.id}'); + handler = _handleMethod; + initMethodCallHandler(); + } + + static final AndroidWebMessageChannel _staticValue = AndroidWebMessageChannel( + AndroidWebMessageChannelCreationParams( + id: '', + port1: AndroidWebMessagePort( + AndroidWebMessagePortCreationParams(index: 0)), + port2: AndroidWebMessagePort( + AndroidWebMessagePortCreationParams(index: 1)))); + + /// Provide static access. + factory AndroidWebMessageChannel.static() { + return _staticValue; + } + + AndroidWebMessagePort get _androidPort1 => port1 as AndroidWebMessagePort; + + AndroidWebMessagePort get _androidPort2 => port2 as AndroidWebMessagePort; + + static AndroidWebMessageChannel? _fromMap(Map? map) { + if (map == null) { + return null; + } + var webMessageChannel = AndroidWebMessageChannel( + AndroidWebMessageChannelCreationParams( + id: map["id"], + port1: AndroidWebMessagePort( + AndroidWebMessagePortCreationParams(index: 0)), + port2: AndroidWebMessagePort( + AndroidWebMessagePortCreationParams(index: 1)))); + webMessageChannel._androidPort1.webMessageChannel = webMessageChannel; + webMessageChannel._androidPort2.webMessageChannel = webMessageChannel; + return webMessageChannel; + } + + Future _handleMethod(MethodCall call) async { + switch (call.method) { + case "onMessage": + int index = call.arguments["index"]; + var port = index == 0 ? _androidPort1 : _androidPort2; + if (port.onMessage != null) { + WebMessage? message = call.arguments["message"] != null + ? WebMessage.fromMap( + call.arguments["message"].cast()) + : null; + port.onMessage!(message); + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + @override + AndroidWebMessageChannel? fromMap(Map? map) { + return _fromMap(map); + } + + @override + void dispose() { + disposeChannel(); + } + + @override + String toString() { + return 'AndroidWebMessageChannel{id: $id, port1: $port1, port2: $port2}'; + } +} + +extension InternalWebMessageChannel on AndroidWebMessageChannel { + MethodChannel? get internalChannel => channel; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/web_message/web_message_listener.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/web_message/web_message_listener.dart new file mode 100644 index 00000000..00307c9a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/web_message/web_message_listener.dart @@ -0,0 +1,164 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidWebMessageListener]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebMessageListenerCreationParams] for +/// more information. +@immutable +class AndroidWebMessageListenerCreationParams + extends PlatformWebMessageListenerCreationParams { + /// Creates a new [AndroidWebMessageListenerCreationParams] instance. + const AndroidWebMessageListenerCreationParams( + {required this.allowedOriginRules, + required super.jsObjectName, + super.onPostMessage}); + + /// Creates a [AndroidWebMessageListenerCreationParams] instance based on [PlatformWebMessageListenerCreationParams]. + factory AndroidWebMessageListenerCreationParams.fromPlatformWebMessageListenerCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformWebMessageListenerCreationParams params) { + return AndroidWebMessageListenerCreationParams( + allowedOriginRules: params.allowedOriginRules ?? Set.from(["*"]), + jsObjectName: params.jsObjectName, + onPostMessage: params.onPostMessage); + } + + @override + final Set allowedOriginRules; + + @override + String toString() { + return 'AndroidWebMessageListenerCreationParams{jsObjectName: $jsObjectName, allowedOriginRules: $allowedOriginRules, onPostMessage: $onPostMessage}'; + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformWebMessageListener} +class AndroidWebMessageListener extends PlatformWebMessageListener + with ChannelController { + /// Constructs a [AndroidWebMessageListener]. + AndroidWebMessageListener(PlatformWebMessageListenerCreationParams params) + : super.implementation( + params is AndroidWebMessageListenerCreationParams + ? params + : AndroidWebMessageListenerCreationParams + .fromPlatformWebMessageListenerCreationParams(params), + ) { + assert(!this._androidParams.allowedOriginRules.contains(""), + "allowedOriginRules cannot contain empty strings"); + channel = MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_web_message_listener_${_id}_${params.jsObjectName}'); + handler = _handleMethod; + initMethodCallHandler(); + } + + ///Message Listener ID used internally. + final String _id = IdGenerator.generate(); + + AndroidJavaScriptReplyProxy? _replyProxy; + + AndroidWebMessageListenerCreationParams get _androidParams => + params as AndroidWebMessageListenerCreationParams; + + Future _handleMethod(MethodCall call) async { + switch (call.method) { + case "onPostMessage": + if (_replyProxy == null) { + _replyProxy = AndroidJavaScriptReplyProxy( + PlatformJavaScriptReplyProxyCreationParams( + webMessageListener: this)); + } + if (onPostMessage != null) { + WebMessage? message = call.arguments["message"] != null + ? WebMessage.fromMap( + call.arguments["message"].cast()) + : null; + WebUri? sourceOrigin = call.arguments["sourceOrigin"] != null + ? WebUri(call.arguments["sourceOrigin"]) + : null; + bool isMainFrame = call.arguments["isMainFrame"]; + onPostMessage!(message, sourceOrigin, isMainFrame, _replyProxy!); + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + @override + void dispose() { + disposeChannel(); + } + + @override + Map toMap() { + return { + "id": _id, + "jsObjectName": params.jsObjectName, + "allowedOriginRules": _androidParams.allowedOriginRules.toList(), + }; + } + + @override + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return 'AndroidWebMessageListener{id: ${_id}, jsObjectName: ${params.jsObjectName}, allowedOriginRules: ${params.allowedOriginRules}, replyProxy: $_replyProxy}'; + } +} + +/// Object specifying creation parameters for creating a [AndroidJavaScriptReplyProxy]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformJavaScriptReplyProxyCreationParams] for +/// more information. +@immutable +class AndroidJavaScriptReplyProxyCreationParams + extends PlatformJavaScriptReplyProxyCreationParams { + /// Creates a new [AndroidJavaScriptReplyProxyCreationParams] instance. + const AndroidJavaScriptReplyProxyCreationParams( + {required super.webMessageListener}); + + /// Creates a [AndroidJavaScriptReplyProxyCreationParams] instance based on [PlatformJavaScriptReplyProxyCreationParams]. + factory AndroidJavaScriptReplyProxyCreationParams.fromPlatformJavaScriptReplyProxyCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformJavaScriptReplyProxyCreationParams params) { + return AndroidJavaScriptReplyProxyCreationParams( + webMessageListener: params.webMessageListener); + } +} + +///{@macro flutter_inappwebview_platform_interface.JavaScriptReplyProxy} +class AndroidJavaScriptReplyProxy extends PlatformJavaScriptReplyProxy { + /// Constructs a [AndroidWebMessageListener]. + AndroidJavaScriptReplyProxy(PlatformJavaScriptReplyProxyCreationParams params) + : super.implementation( + params is AndroidJavaScriptReplyProxyCreationParams + ? params + : AndroidJavaScriptReplyProxyCreationParams + .fromPlatformJavaScriptReplyProxyCreationParams(params), + ); + + AndroidWebMessageListener get _androidWebMessageListener => + params.webMessageListener as AndroidWebMessageListener; + + @override + Future postMessage(WebMessage message) async { + Map args = {}; + args.putIfAbsent('message', () => message.toMap()); + await _androidWebMessageListener.channel?.invokeMethod('postMessage', args); + } + + @override + String toString() { + return 'AndroidJavaScriptReplyProxy{}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/web_message/web_message_port.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/web_message/web_message_port.dart new file mode 100644 index 00000000..f8979f5c --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/web_message/web_message_port.dart @@ -0,0 +1,95 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +import 'web_message_channel.dart'; + +/// Object specifying creation parameters for creating a [AndroidWebMessagePort]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebMessagePortCreationParams] for +/// more information. +@immutable +class AndroidWebMessagePortCreationParams + extends PlatformWebMessagePortCreationParams { + /// Creates a new [AndroidWebMessagePortCreationParams] instance. + const AndroidWebMessagePortCreationParams({required super.index}); + + /// Creates a [AndroidWebMessagePortCreationParams] instance based on [PlatformWebMessagePortCreationParams]. + factory AndroidWebMessagePortCreationParams.fromPlatformWebMessagePortCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformWebMessagePortCreationParams params) { + return AndroidWebMessagePortCreationParams(index: params.index); + } + + @override + String toString() { + return 'AndroidWebMessagePortCreationParams{index: $index}'; + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformWebMessagePort} +class AndroidWebMessagePort extends PlatformWebMessagePort { + WebMessageCallback? _onMessage; + late AndroidWebMessageChannel _webMessageChannel; + + /// Constructs a [AndroidWebMessagePort]. + AndroidWebMessagePort(PlatformWebMessagePortCreationParams params) + : super.implementation( + params is AndroidWebMessagePortCreationParams + ? params + : AndroidWebMessagePortCreationParams + .fromPlatformWebMessagePortCreationParams(params), + ); + + @override + Future setWebMessageCallback(WebMessageCallback? onMessage) async { + Map args = {}; + args.putIfAbsent('index', () => params.index); + await _webMessageChannel.internalChannel + ?.invokeMethod('setWebMessageCallback', args); + this._onMessage = onMessage; + } + + @override + Future postMessage(WebMessage message) async { + Map args = {}; + args.putIfAbsent('index', () => params.index); + args.putIfAbsent('message', () => message.toMap()); + await _webMessageChannel.internalChannel?.invokeMethod('postMessage', args); + } + + @override + Future close() async { + Map args = {}; + args.putIfAbsent('index', () => params.index); + await _webMessageChannel.internalChannel?.invokeMethod('close', args); + } + + @override + Map toMap() { + return { + "index": params.index, + "webMessageChannelId": this._webMessageChannel.params.id + }; + } + + @override + Map toJson() { + return toMap(); + } + + @override + String toString() { + return 'AndroidWebMessagePort{index: ${params.index}}'; + } +} + +extension InternalWebMessagePort on AndroidWebMessagePort { + WebMessageCallback? get onMessage => _onMessage; + void set onMessage(WebMessageCallback? value) => _onMessage = value; + + AndroidWebMessageChannel get webMessageChannel => _webMessageChannel; + void set webMessageChannel(AndroidWebMessageChannel value) => + _webMessageChannel = value; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/web_storage/main.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/web_storage/main.dart new file mode 100644 index 00000000..7265ae52 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/web_storage/main.dart @@ -0,0 +1,2 @@ +export 'web_storage.dart'; +export 'web_storage_manager.dart' hide InternalWebStorageManager; diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/web_storage/web_storage.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/web_storage/web_storage.dart new file mode 100644 index 00000000..ad2a3002 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/web_storage/web_storage.dart @@ -0,0 +1,257 @@ +import 'dart:convert'; + +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +import '../in_app_webview/in_app_webview_controller.dart'; + +/// Object specifying creation parameters for creating a [AndroidWebStorage]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebStorageCreationParams] for +/// more information. +class AndroidWebStorageCreationParams extends PlatformWebStorageCreationParams { + /// Creates a new [AndroidWebStorageCreationParams] instance. + AndroidWebStorageCreationParams( + {required super.localStorage, required super.sessionStorage}); + + /// Creates a [AndroidWebStorageCreationParams] instance based on [PlatformWebStorageCreationParams]. + factory AndroidWebStorageCreationParams.fromPlatformWebStorageCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformWebStorageCreationParams params) { + return AndroidWebStorageCreationParams( + localStorage: params.localStorage, + sessionStorage: params.sessionStorage); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformWebStorage} +class AndroidWebStorage extends PlatformWebStorage { + /// Constructs a [AndroidWebStorage]. + AndroidWebStorage(PlatformWebStorageCreationParams params) + : super.implementation( + params is AndroidWebStorageCreationParams + ? params + : AndroidWebStorageCreationParams + .fromPlatformWebStorageCreationParams(params), + ); + + @override + PlatformLocalStorage get localStorage => params.localStorage; + + @override + PlatformSessionStorage get sessionStorage => params.sessionStorage; + + @override + void dispose() { + localStorage.dispose(); + sessionStorage.dispose(); + } +} + +/// Object specifying creation parameters for creating a [AndroidStorage]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformStorageCreationParams] for +/// more information. +class AndroidStorageCreationParams extends PlatformStorageCreationParams { + /// Creates a new [AndroidStorageCreationParams] instance. + AndroidStorageCreationParams( + {required super.controller, required super.webStorageType}); + + /// Creates a [AndroidStorageCreationParams] instance based on [PlatformStorageCreationParams]. + factory AndroidStorageCreationParams.fromPlatformStorageCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformStorageCreationParams params) { + return AndroidStorageCreationParams( + controller: params.controller, webStorageType: params.webStorageType); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformStorage} +abstract mixin class AndroidStorage implements PlatformStorage { + @override + AndroidInAppWebViewController? controller; + + @override + Future length() async { + var result = await controller?.evaluateJavascript(source: """ + window.$webStorageType.length; + """); + return result != null ? int.parse(json.decode(result)) : null; + } + + @override + Future setItem({required String key, required dynamic value}) async { + var encodedValue = json.encode(value); + await controller?.evaluateJavascript(source: """ + window.$webStorageType.setItem("$key", ${value is String ? encodedValue : "JSON.stringify($encodedValue)"}); + """); + } + + @override + Future getItem({required String key}) async { + var itemValue = await controller?.evaluateJavascript(source: """ + window.$webStorageType.getItem("$key"); + """); + + if (itemValue == null) { + return null; + } + + try { + return json.decode(itemValue); + } catch (e) {} + + return itemValue; + } + + @override + Future removeItem({required String key}) async { + await controller?.evaluateJavascript(source: """ + window.$webStorageType.removeItem("$key"); + """); + } + + @override + Future> getItems() async { + var webStorageItems = []; + + List>? items = + (await controller?.evaluateJavascript(source: """ +(function() { + var webStorageItems = []; + for(var i = 0; i < window.$webStorageType.length; i++){ + var key = window.$webStorageType.key(i); + webStorageItems.push( + { + key: key, + value: window.$webStorageType.getItem(key) + } + ); + } + return webStorageItems; +})(); + """))?.cast>(); + + if (items == null) { + return webStorageItems; + } + + for (var item in items) { + webStorageItems + .add(WebStorageItem(key: item["key"], value: item["value"])); + } + + return webStorageItems; + } + + @override + Future clear() async { + await controller?.evaluateJavascript(source: """ + window.$webStorageType.clear(); + """); + } + + @override + Future key({required int index}) async { + var result = await controller?.evaluateJavascript(source: """ + window.$webStorageType.key($index); + """); + return result != null ? json.decode(result) : null; + } + + @override + void dispose() { + controller = null; + } +} + +/// Object specifying creation parameters for creating a [AndroidLocalStorage]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformLocalStorageCreationParams] for +/// more information. +class AndroidLocalStorageCreationParams + extends PlatformLocalStorageCreationParams { + /// Creates a new [AndroidLocalStorageCreationParams] instance. + AndroidLocalStorageCreationParams(super.params); + + /// Creates a [AndroidLocalStorageCreationParams] instance based on [PlatformLocalStorageCreationParams]. + factory AndroidLocalStorageCreationParams.fromPlatformLocalStorageCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformLocalStorageCreationParams params) { + return AndroidLocalStorageCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformLocalStorage} +class AndroidLocalStorage extends PlatformLocalStorage with AndroidStorage { + /// Constructs a [AndroidLocalStorage]. + AndroidLocalStorage(PlatformLocalStorageCreationParams params) + : super.implementation( + params is AndroidLocalStorageCreationParams + ? params + : AndroidLocalStorageCreationParams + .fromPlatformLocalStorageCreationParams(params), + ); + + /// Default storage + factory AndroidLocalStorage.defaultStorage( + {required PlatformInAppWebViewController? controller}) { + return AndroidLocalStorage(AndroidLocalStorageCreationParams( + PlatformLocalStorageCreationParams(PlatformStorageCreationParams( + controller: controller, + webStorageType: WebStorageType.LOCAL_STORAGE)))); + } + + @override + AndroidInAppWebViewController? get controller => + params.controller as AndroidInAppWebViewController?; +} + +/// Object specifying creation parameters for creating a [AndroidSessionStorage]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformSessionStorageCreationParams] for +/// more information. +class AndroidSessionStorageCreationParams + extends PlatformSessionStorageCreationParams { + /// Creates a new [AndroidSessionStorageCreationParams] instance. + AndroidSessionStorageCreationParams(super.params); + + /// Creates a [AndroidSessionStorageCreationParams] instance based on [PlatformSessionStorageCreationParams]. + factory AndroidSessionStorageCreationParams.fromPlatformSessionStorageCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformSessionStorageCreationParams params) { + return AndroidSessionStorageCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformSessionStorage} +class AndroidSessionStorage extends PlatformSessionStorage with AndroidStorage { + /// Constructs a [AndroidSessionStorage]. + AndroidSessionStorage(PlatformSessionStorageCreationParams params) + : super.implementation( + params is AndroidSessionStorageCreationParams + ? params + : AndroidSessionStorageCreationParams + .fromPlatformSessionStorageCreationParams(params), + ); + + /// Default storage + factory AndroidSessionStorage.defaultStorage( + {required PlatformInAppWebViewController? controller}) { + return AndroidSessionStorage(AndroidSessionStorageCreationParams( + PlatformSessionStorageCreationParams(PlatformStorageCreationParams( + controller: controller, + webStorageType: WebStorageType.SESSION_STORAGE)))); + } + + @override + AndroidInAppWebViewController? get controller => + params.controller as AndroidInAppWebViewController?; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/web_storage/web_storage_manager.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/web_storage/web_storage_manager.dart new file mode 100755 index 00000000..d5fc4562 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/web_storage/web_storage_manager.dart @@ -0,0 +1,116 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidWebStorageManager]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebStorageManagerCreationParams] for +/// more information. +@immutable +class AndroidWebStorageManagerCreationParams + extends PlatformWebStorageManagerCreationParams { + /// Creates a new [AndroidWebStorageManagerCreationParams] instance. + const AndroidWebStorageManagerCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformWebStorageManagerCreationParams params, + ) : super(); + + /// Creates a [AndroidWebStorageManagerCreationParams] instance based on [PlatformWebStorageManagerCreationParams]. + factory AndroidWebStorageManagerCreationParams.fromPlatformWebStorageManagerCreationParams( + PlatformWebStorageManagerCreationParams params) { + return AndroidWebStorageManagerCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformWebStorageManager} +class AndroidWebStorageManager extends PlatformWebStorageManager + with ChannelController { + /// Creates a new [AndroidWebStorageManager]. + AndroidWebStorageManager(PlatformWebStorageManagerCreationParams params) + : super.implementation( + params is AndroidWebStorageManagerCreationParams + ? params + : AndroidWebStorageManagerCreationParams + .fromPlatformWebStorageManagerCreationParams(params), + ) { + channel = const MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_webstoragemanager'); + handler = handleMethod; + initMethodCallHandler(); + } + + static AndroidWebStorageManager? _instance; + + ///Gets the WebStorage manager shared instance. + static AndroidWebStorageManager instance() { + return (_instance != null) ? _instance! : _init(); + } + + static AndroidWebStorageManager _init() { + _instance = AndroidWebStorageManager(AndroidWebStorageManagerCreationParams( + const PlatformWebStorageManagerCreationParams())); + return _instance!; + } + + Future _handleMethod(MethodCall call) async {} + + @override + Future> getOrigins() async { + List originsList = []; + + Map args = {}; + List> origins = + (await channel?.invokeMethod('getOrigins', args)) + ?.cast>() ?? + []; + + for (var origin in origins) { + originsList.add(WebStorageOrigin( + origin: origin["origin"], + quota: origin["quota"], + usage: origin["usage"])); + } + + return originsList; + } + + @override + Future deleteAllData() async { + Map args = {}; + await channel?.invokeMethod('deleteAllData', args); + } + + @override + Future deleteOrigin({required String origin}) async { + Map args = {}; + args.putIfAbsent("origin", () => origin); + await channel?.invokeMethod('deleteOrigin', args); + } + + @override + Future getQuotaForOrigin({required String origin}) async { + Map args = {}; + args.putIfAbsent("origin", () => origin); + return await channel?.invokeMethod('getQuotaForOrigin', args) ?? 0; + } + + @override + Future getUsageForOrigin({required String origin}) async { + Map args = {}; + args.putIfAbsent("origin", () => origin); + return await channel?.invokeMethod('getUsageForOrigin', args) ?? 0; + } + + @override + void dispose() { + // empty + } +} + +extension InternalWebStorageManager on AndroidWebStorageManager { + get handleMethod => _handleMethod; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/webview_asset_loader.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/webview_asset_loader.dart new file mode 100644 index 00000000..64ca3ec3 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/webview_asset_loader.dart @@ -0,0 +1,237 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidPathHandler]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformPathHandlerCreationParams] for +/// more information. +@immutable +class AndroidPathHandlerCreationParams + extends PlatformPathHandlerCreationParams { + /// Creates a new [AndroidPathHandlerCreationParams] instance. + AndroidPathHandlerCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformPathHandlerCreationParams params, + ) : super(path: params.path); + + /// Creates a [AndroidPathHandlerCreationParams] instance based on [PlatformPathHandlerCreationParams]. + factory AndroidPathHandlerCreationParams.fromPlatformPathHandlerCreationParams( + PlatformPathHandlerCreationParams params) { + return AndroidPathHandlerCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformPathHandler} +abstract mixin class AndroidPathHandler + implements ChannelController, PlatformPathHandler { + final String _id = IdGenerator.generate(); + + @override + late final PlatformPathHandlerEvents? eventHandler; + + @override + late final String path; + + void _init(PlatformPathHandlerCreationParams params) { + this.path = params.path; + channel = MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_custompathhandler_${_id}'); + handler = _handleMethod; + initMethodCallHandler(); + } + + Future _handleMethod(MethodCall call) async { + switch (call.method) { + case "handle": + String path = call.arguments["path"]; + return (await eventHandler?.handle(path))?.toMap(); + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + } + + @override + Map toMap() { + return {"path": path, "type": type, "id": _id}; + } + + @override + Map toJson() { + return toMap(); + } + + @override + String toString() { + return 'AndroidPathHandler{path: $path, type: $type}'; + } + + @override + void dispose() { + disposeChannel(); + eventHandler = null; + } +} + +/// Object specifying creation parameters for creating a [AndroidAssetsPathHandler]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformAssetsPathHandlerCreationParams] for +/// more information. +@immutable +class AndroidAssetsPathHandlerCreationParams + extends PlatformAssetsPathHandlerCreationParams { + /// Creates a new [AndroidAssetsPathHandlerCreationParams] instance. + AndroidAssetsPathHandlerCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformAssetsPathHandlerCreationParams params, + ) : super(params); + + /// Creates a [AndroidAssetsPathHandlerCreationParams] instance based on [PlatformAssetsPathHandlerCreationParams]. + factory AndroidAssetsPathHandlerCreationParams.fromPlatformAssetsPathHandlerCreationParams( + PlatformAssetsPathHandlerCreationParams params) { + return AndroidAssetsPathHandlerCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformAssetsPathHandler} +class AndroidAssetsPathHandler extends PlatformAssetsPathHandler + with AndroidPathHandler, ChannelController { + /// Constructs a [AndroidAssetsPathHandler]. + AndroidAssetsPathHandler(PlatformAssetsPathHandlerCreationParams params) + : super.implementation( + params is AndroidAssetsPathHandlerCreationParams + ? params + : AndroidAssetsPathHandlerCreationParams + .fromPlatformAssetsPathHandlerCreationParams(params), + ) { + _init(params); + } +} + +/// Object specifying creation parameters for creating a [AndroidResourcesPathHandler]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformResourcesPathHandlerCreationParams] for +/// more information. +@immutable +class AndroidResourcesPathHandlerCreationParams + extends PlatformResourcesPathHandlerCreationParams { + /// Creates a new [AndroidResourcesPathHandlerCreationParams] instance. + AndroidResourcesPathHandlerCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformResourcesPathHandlerCreationParams params, + ) : super(params); + + /// Creates a [AndroidResourcesPathHandlerCreationParams] instance based on [PlatformResourcesPathHandlerCreationParams]. + factory AndroidResourcesPathHandlerCreationParams.fromPlatformResourcesPathHandlerCreationParams( + PlatformResourcesPathHandlerCreationParams params) { + return AndroidResourcesPathHandlerCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformResourcesPathHandler} +class AndroidResourcesPathHandler extends PlatformResourcesPathHandler + with AndroidPathHandler, ChannelController { + /// Constructs a [AndroidResourcesPathHandler]. + AndroidResourcesPathHandler(PlatformResourcesPathHandlerCreationParams params) + : super.implementation( + params is AndroidResourcesPathHandlerCreationParams + ? params + : AndroidResourcesPathHandlerCreationParams + .fromPlatformResourcesPathHandlerCreationParams(params), + ) { + _init(params); + } +} + +/// Object specifying creation parameters for creating a [AndroidInternalStoragePathHandler]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformInternalStoragePathHandlerCreationParams] for +/// more information. +@immutable +class AndroidInternalStoragePathHandlerCreationParams + extends PlatformInternalStoragePathHandlerCreationParams { + /// Creates a new [AndroidInternalStoragePathHandlerCreationParams] instance. + AndroidInternalStoragePathHandlerCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformInternalStoragePathHandlerCreationParams params, + ) : super(params, directory: params.directory); + + /// Creates a [AndroidInternalStoragePathHandlerCreationParams] instance based on [PlatformInternalStoragePathHandlerCreationParams]. + factory AndroidInternalStoragePathHandlerCreationParams.fromPlatformInternalStoragePathHandlerCreationParams( + PlatformInternalStoragePathHandlerCreationParams params) { + return AndroidInternalStoragePathHandlerCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformInternalStoragePathHandler} +class AndroidInternalStoragePathHandler + extends PlatformInternalStoragePathHandler + with AndroidPathHandler, ChannelController { + /// Constructs a [AndroidInternalStoragePathHandler]. + AndroidInternalStoragePathHandler( + PlatformInternalStoragePathHandlerCreationParams params) + : super.implementation( + params is AndroidInternalStoragePathHandlerCreationParams + ? params + : AndroidInternalStoragePathHandlerCreationParams + .fromPlatformInternalStoragePathHandlerCreationParams(params), + ) { + _init(params); + } + + AndroidInternalStoragePathHandlerCreationParams get _internalParams => + params as AndroidInternalStoragePathHandlerCreationParams; + + @override + String get directory => _internalParams.directory; + + @override + Map toMap() { + return {...toMap(), 'directory': directory}; + } +} + +/// Object specifying creation parameters for creating a [AndroidCustomPathHandler]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformCustomPathHandlerCreationParams] for +/// more information. +@immutable +class AndroidCustomPathHandlerCreationParams + extends PlatformCustomPathHandlerCreationParams { + /// Creates a new [AndroidCustomPathHandlerCreationParams] instance. + AndroidCustomPathHandlerCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformCustomPathHandlerCreationParams params, + ) : super(params); + + /// Creates a [AndroidCustomPathHandlerCreationParams] instance based on [PlatformCustomPathHandlerCreationParams]. + factory AndroidCustomPathHandlerCreationParams.fromPlatformCustomPathHandlerCreationParams( + PlatformCustomPathHandlerCreationParams params) { + return AndroidCustomPathHandlerCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformCustomPathHandler} +class AndroidCustomPathHandler extends PlatformCustomPathHandler + with AndroidPathHandler, ChannelController { + /// Constructs a [AndroidCustomPathHandler]. + AndroidCustomPathHandler(PlatformCustomPathHandlerCreationParams params) + : super.implementation( + params is AndroidCustomPathHandlerCreationParams + ? params + : AndroidCustomPathHandlerCreationParams + .fromPlatformCustomPathHandlerCreationParams(params), + ) { + _init(params); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/lib/src/webview_feature.dart b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/webview_feature.dart new file mode 100644 index 00000000..9b149510 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/lib/src/webview_feature.dart @@ -0,0 +1,89 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [AndroidWebViewFeature]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebViewFeatureCreationParams] for +/// more information. +@immutable +class AndroidWebViewFeatureCreationParams + extends PlatformWebViewFeatureCreationParams { + /// Creates a new [AndroidWebViewFeatureCreationParams] instance. + const AndroidWebViewFeatureCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformWebViewFeatureCreationParams params, + ) : super(); + + /// Creates a [AndroidWebViewFeatureCreationParams] instance based on [PlatformWebViewFeatureCreationParams]. + factory AndroidWebViewFeatureCreationParams.fromPlatformWebViewFeatureCreationParams( + PlatformWebViewFeatureCreationParams params) { + return AndroidWebViewFeatureCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformWebViewFeature} +class AndroidWebViewFeature extends PlatformWebViewFeature + with ChannelController { + /// Creates a new [AndroidWebViewFeature]. + AndroidWebViewFeature(PlatformWebViewFeatureCreationParams params) + : super.implementation( + params is AndroidWebViewFeatureCreationParams + ? params + : AndroidWebViewFeatureCreationParams + .fromPlatformWebViewFeatureCreationParams(params), + ) { + channel = const MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_webviewfeature'); + handler = handleMethod; + initMethodCallHandler(); + } + + factory AndroidWebViewFeature.static() { + return instance(); + } + + static AndroidWebViewFeature? _instance; + + ///Gets the [AndroidWebViewFeature] shared instance. + static AndroidWebViewFeature instance() { + return (_instance != null) ? _instance! : _init(); + } + + static AndroidWebViewFeature _init() { + _instance = AndroidWebViewFeature(AndroidWebViewFeatureCreationParams( + const PlatformWebViewFeatureCreationParams())); + return _instance!; + } + + Future _handleMethod(MethodCall call) async {} + + @override + Future isFeatureSupported(WebViewFeature feature) async { + Map args = {}; + args.putIfAbsent("feature", () => feature.toNativeValue()); + return await channel?.invokeMethod('isFeatureSupported', args) ?? + false; + } + + @override + Future isStartupFeatureSupported(WebViewFeature startupFeature) async { + Map args = {}; + args.putIfAbsent("startupFeature", () => startupFeature.toNativeValue()); + return await channel?.invokeMethod( + 'isStartupFeatureSupported', args) ?? + false; + } + + @override + void dispose() { + // empty + } +} + +extension InternalWebViewFeature on AndroidWebViewFeature { + get handleMethod => _handleMethod; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_android/pubspec.yaml b/ui/flutter/third_party/flutter_inappwebview_android/pubspec.yaml new file mode 100644 index 00000000..912f92ee --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_android/pubspec.yaml @@ -0,0 +1,83 @@ +name: flutter_inappwebview_android +description: Android implementation of the flutter_inappwebview plugin. +version: 1.1.3 +homepage: https://inappwebview.dev/ +repository: https://github.com/pichillilorenzo/flutter_inappwebview/tree/master/flutter_inappwebview_android +issue_tracker: https://github.com/pichillilorenzo/flutter_inappwebview/issues +funding: + - https://inappwebview.dev/donate/ +topics: + - html + - webview + - webview-flutter + - inappwebview + - browser + +environment: + sdk: ^3.5.0 + flutter: ">=3.24.0" + +dependencies: + flutter: + sdk: flutter + flutter_inappwebview_platform_interface: ^1.3.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^4.0.0 + plugin_platform_interface: ^2.1.8 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + implements: flutter_inappwebview + platforms: + android: + package: com.pichillilorenzo.flutter_inappwebview_android + pluginClass: InAppWebViewFlutterPlugin + dartPluginClass: AndroidInAppWebViewPlatform + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/CHANGELOG.md b/ui/flutter/third_party/flutter_inappwebview_ios/CHANGELOG.md new file mode 100644 index 00000000..60e2bffd --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/CHANGELOG.md @@ -0,0 +1,90 @@ +## 1.1.2 + +- Updated flutter_inappwebview_platform_interface version to ^1.3.0 + +## 1.1.1 + +- Updated flutter_inappwebview_platform_interface version to ^1.2.0 + +## 1.1.0+3 + +- Updated flutter_inappwebview_platform_interface version + +## ## 1.1.0+2 + +- Updated pubspec.yaml + +## 1.1.0+1 + +- Fixed "v6.1.0 fails to compile on Xcode 15" [#2288](https://github.com/pichillilorenzo/flutter_inappwebview/issues/2288) + +## 1.1.0 + +- Fixed XCode 16 build +- Updates minimum supported SDK version to Flutter 3.24/Dart 3.5. +- Merged "Add privacy manifest for iOS" [#2029](https://github.com/pichillilorenzo/flutter_inappwebview/pull/2029) (thanks to [ueman](https://github.com/ueman)) + +## 1.0.13 + +- Updated `flutter_inappwebview_platform_interface` version dependency to `^1.0.10` + +## 1.0.12 + +- Updated `flutter_inappwebview_platform_interface` version dependency to `^1.0.9` +- Fix typos and other code improvements (thanks to [michalsrutek](https://github.com/michalsrutek)) +- Fixed "runtime issue of SecTrustCopyExceptions 'This method should not be called on the main thread as it may lead to UI unresponsiveness.' when using onReceivedServerTrustAuthRequest" [#1924](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1924) +- Merged "💥 Fix iPad crash due to missing sourceView" [#1933](https://github.com/pichillilorenzo/flutter_inappwebview/pull/1933) (thanks to [michalsrutek](https://github.com/michalsrutek)) +- Merged "💥 Fix crash - remove force unwrapping from dispose method" [#1932](https://github.com/pichillilorenzo/flutter_inappwebview/pull/1932) (thanks to [michalsrutek](https://github.com/michalsrutek)) + +## 1.0.11 + +- Updated `flutter_inappwebview_platform_interface` version dependency to `^1.0.8` + +## 1.0.10 + +- Updated `flutter_inappwebview_platform_interface` version dependency to `^1.0.7` + +## 1.0.9 + +- Implemented `InAppWebViewSettings.interceptOnlyAsyncAjaxRequests` +- Updated `useShouldInterceptAjaxRequest` automatic infer logic +- Updated `CookieManager` methods return value +- Fixed "iOS crash at public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)" [#1912](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1912) + +## 1.0.8 + +- Fixed error in InterceptAjaxRequestJS 'Failed to set responseType property' +- Fixed shouldInterceptAjaxRequest javascript code when overriding XMLHttpRequest.open method parameters + +## 1.0.7 + +- Fixed "getFavicons: _TypeError: type '_Map' is not a subtype of type 'Iterable'" [#1897](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1897) + +## 1.0.6 + +- Possible fix for "iOS Fatal Crash" [#1894](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1894) + +## 1.0.5 + +- Call `super.dispose();` on `InAppBrowser` and `ChromeSafari` implementations + +## 1.0.4 + +- Fixed "Cloudflare Turnstile failure" [#1738](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1738) + +## 1.0.3 + +- Fixed `InAppBrowserMenuItem.iconColor` not working + +## 1.0.2 + +- Added `PlatformPrintJobController.onComplete` setter +- Updated `flutter_inappwebview_platform_interface` version dependency to `1.0.2` + +## 1.0.1 + +- Updated README + +## 1.0.0 + +Initial release. diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/LICENSE b/ui/flutter/third_party/flutter_inappwebview_ios/LICENSE new file mode 100644 index 00000000..6ccd8da4 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/LICENSE @@ -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. diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/README.md b/ui/flutter/third_party/flutter_inappwebview_ios/README.md new file mode 100644 index 00000000..6121ff51 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/README.md @@ -0,0 +1,13 @@ +# flutter\_inappwebview\_ios + +The Apple iOS WKWebView 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. \ No newline at end of file diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/CredentialDatabase.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/CredentialDatabase.swift new file mode 100755 index 00000000..bc660244 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/CredentialDatabase.swift @@ -0,0 +1,171 @@ +// +// CredentialDatabase.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 29/10/2019. +// + +import Foundation + +public class CredentialDatabase: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_credential_database" + static let credentialStore = URLCredentialStorage.shared + + private var plugin: SwiftFlutterPlugin? + + init(plugin: SwiftFlutterPlugin) { + super.init(channel: FlutterMethodChannel(name: CredentialDatabase.METHOD_CHANNEL_NAME, binaryMessenger: plugin.registrar!.messenger())) + self.plugin = plugin + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + switch call.method { + case "getAllAuthCredentials": + var allCredentials: [[String: Any?]] = [] + for (protectionSpace, credentials) in CredentialDatabase.credentialStore.allCredentials { + var crendentials: [[String: Any?]] = [] + for c in credentials { + let credential: [String: Any?] = c.value.toMap() + crendentials.append(credential) + } + if crendentials.count > 0 { + let dict: [String : Any] = [ + "protectionSpace": protectionSpace.toMap(), + "credentials": crendentials + ] + allCredentials.append(dict) + } + } + result(allCredentials) + break + case "getHttpAuthCredentials": + var crendentials: [[String: Any?]] = [] + + let host = arguments!["host"] as! String + let urlProtocol = arguments!["protocol"] as? String + let urlPort = arguments!["port"] as? Int ?? 0 + var realm = arguments!["realm"] as? String + if let r = realm, r.isEmpty { + realm = nil + } + + for (protectionSpace, credentials) in CredentialDatabase.credentialStore.allCredentials { + if protectionSpace.host == host && protectionSpace.realm == realm && + protectionSpace.protocol == urlProtocol && protectionSpace.port == urlPort { + for c in credentials { + crendentials.append(c.value.toMap()) + } + break + } + } + result(crendentials) + break + case "setHttpAuthCredential": + let host = arguments!["host"] as! String + let urlProtocol = arguments!["protocol"] as? String + let urlPort = arguments!["port"] as? Int ?? 0 + var realm = arguments!["realm"] as? String + if let r = realm, r.isEmpty { + realm = nil + } + let username = arguments!["username"] as! String + let password = arguments!["password"] as! String + let credential = URLCredential(user: username, password: password, persistence: .permanent) + CredentialDatabase.credentialStore.set(credential, + for: URLProtectionSpace(host: host, port: urlPort, protocol: urlProtocol, + realm: realm, authenticationMethod: NSURLAuthenticationMethodHTTPBasic)) + result(true) + break + case "removeHttpAuthCredential": + let host = arguments!["host"] as! String + let urlProtocol = arguments!["protocol"] as? String + let urlPort = arguments!["port"] as? Int ?? 0 + var realm = arguments!["realm"] as? String + if let r = realm, r.isEmpty { + realm = nil + } + let username = arguments!["username"] as! String + let password = arguments!["password"] as! String + + var credential: URLCredential? = nil + var protectionSpaceCredential: URLProtectionSpace? = nil + + for (protectionSpace, credentials) in CredentialDatabase.credentialStore.allCredentials { + if protectionSpace.host == host && protectionSpace.realm == realm && + protectionSpace.protocol == urlProtocol && protectionSpace.port == urlPort { + for c in credentials { + if c.value.user == username, c.value.password == password { + credential = c.value + protectionSpaceCredential = protectionSpace + break + } + } + } + if credential != nil { + break + } + } + + if let c = credential, let protectionSpace = protectionSpaceCredential { + CredentialDatabase.credentialStore.remove(c, for: protectionSpace) + } + + result(true) + break + case "removeHttpAuthCredentials": + let host = arguments!["host"] as! String + let urlProtocol = arguments!["protocol"] as? String + let urlPort = arguments!["port"] as? Int ?? 0 + var realm = arguments!["realm"] as? String + if let r = realm, r.isEmpty { + realm = nil + } + + var credentialsToRemove: [URLCredential] = [] + var protectionSpaceCredential: URLProtectionSpace? = nil + + for (protectionSpace, credentials) in CredentialDatabase.credentialStore.allCredentials { + if protectionSpace.host == host && protectionSpace.realm == realm && + protectionSpace.protocol == urlProtocol && protectionSpace.port == urlPort { + protectionSpaceCredential = protectionSpace + for c in credentials { + if let _ = c.value.user, let _ = c.value.password { + credentialsToRemove.append(c.value) + } + } + break + } + } + + if let protectionSpace = protectionSpaceCredential { + for credential in credentialsToRemove { + CredentialDatabase.credentialStore.remove(credential, for: protectionSpace) + } + } + + result(true) + break + case "clearAllAuthCredentials": + for (protectionSpace, credentials) in CredentialDatabase.credentialStore.allCredentials { + for credential in credentials { + CredentialDatabase.credentialStore.remove(credential.value, for: protectionSpace) + } + } + result(true) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public override func dispose() { + super.dispose() + plugin = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/FindInteraction/FindInteractionChannelDelegate.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/FindInteraction/FindInteractionChannelDelegate.swift new file mode 100644 index 00000000..4b1a43aa --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/FindInteraction/FindInteractionChannelDelegate.swift @@ -0,0 +1,158 @@ +// +// FindInteractionChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/10/22. +// + +import Foundation + +public class FindInteractionChannelDelegate: ChannelDelegate { + private weak var findInteractionController: FindInteractionController? + + public init(findInteractionController: FindInteractionController, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.findInteractionController = findInteractionController + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "findAll": + if let findInteractionController = findInteractionController { + let find = arguments!["find"] as! String + findInteractionController.findAll(find: find, completionHandler: {(value, error) in + if error != nil { + result(FlutterError(code: "FindInteractionChannelDelegate", message: error?.localizedDescription, details: nil)) + return + } + result(true) + }) + } else { + result(false) + } + break + case "findNext": + if let findInteractionController = findInteractionController { + let forward = arguments!["forward"] as! Bool + findInteractionController.findNext(forward: forward, completionHandler: {(value, error) in + if error != nil { + result(FlutterError(code: "FindInteractionChannelDelegate", message: error?.localizedDescription, details: nil)) + return + } + result(true) + }) + } else { + result(false) + } + break + case "clearMatches": + if let findInteractionController = findInteractionController { + findInteractionController.clearMatches(completionHandler: {(value, error) in + if error != nil { + result(FlutterError(code: "FindInteractionChannelDelegate", message: error?.localizedDescription, details: nil)) + return + } + result(true) + }) + } else { + result(false) + } + break + case "setSearchText": + if let findInteractionController = findInteractionController { + let searchText = arguments!["searchText"] as? String + findInteractionController.searchText = searchText + result(true) + } else { + result(false) + } + break + case "getSearchText": + result(findInteractionController?.searchText) + break + case "isFindNavigatorVisible": + if #available(iOS 16.0, *) { + if let interaction = findInteractionController?.webView?.findInteraction { + result(interaction.isFindNavigatorVisible) + } else { + result(false) + } + } else { + result(false) + } + break + case "updateResultCount": + if #available(iOS 16.0, *) { + if let interaction = findInteractionController?.webView?.findInteraction { + interaction.updateResultCount() + result(true) + } else { + result(false) + } + } else { + result(false) + } + break + case "presentFindNavigator": + if #available(iOS 16.0, *) { + if let interaction = findInteractionController?.webView?.findInteraction { + interaction.presentFindNavigator(showingReplace: false) + result(true) + } else { + result(false) + } + } else { + result(false) + } + break + case "dismissFindNavigator": + if #available(iOS 16.0, *) { + if let interaction = findInteractionController?.webView?.findInteraction { + interaction.dismissFindNavigator() + result(true) + } else { + result(false) + } + } else { + result(false) + } + break + case "getActiveFindSession": + if let findInteractionController = findInteractionController { + result(findInteractionController.activeFindSession?.toMap()) + } else { + result(nil) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onFindResultReceived(activeMatchOrdinal: Int, numberOfMatches: Int, isDoneCounting: Bool) { + if isDoneCounting, let findInteractionController = findInteractionController { + findInteractionController.activeFindSession = FindSession(resultCount: numberOfMatches, + highlightedResultIndex: activeMatchOrdinal, + searchResultDisplayStyle: 2) // matches UIFindSession.SearchResultDisplayStyle.none + } + + let arguments: [String : Any?] = [ + "activeMatchOrdinal": activeMatchOrdinal, + "numberOfMatches": numberOfMatches, + "isDoneCounting": isDoneCounting + ] + channel?.invokeMethod("onFindResultReceived", arguments: arguments) + } + + public override func dispose() { + super.dispose() + findInteractionController = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/FindInteraction/FindInteractionController.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/FindInteraction/FindInteractionController.swift new file mode 100644 index 00000000..cff9cc9e --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/FindInteraction/FindInteractionController.swift @@ -0,0 +1,162 @@ +// +// FindInteractionController.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/10/22. +// + +import Foundation +import Flutter + +public class FindInteractionController: NSObject, Disposable { + static let METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_find_interaction_" + + var webView: InAppWebView? + var channelDelegate: FindInteractionChannelDelegate? + + private var plugin: SwiftFlutterPlugin? + private var settings: FindInteractionSettings? + + private var _searchText: String? = nil + var searchText: String? { + get { + if #available(iOS 16.0, *), let interaction = webView?.findInteraction { + return interaction.searchText + } + return _searchText + } + set { + if #available(iOS 16.0, *), let interaction = webView?.findInteraction { + interaction.searchText = newValue + } + self._searchText = newValue + } + } + + private var _activeFindSession: FindSession? = nil + var activeFindSession: FindSession? { + get { + if #available(iOS 16.0, *), let interaction = webView?.findInteraction { + if let activeFindSession = interaction.activeFindSession { + return FindSession.fromUIFindSession(uiFindSession: activeFindSession) + } + return nil + } + return _activeFindSession + } + set { + self._activeFindSession = newValue + } + } + + public init(plugin: SwiftFlutterPlugin, id: Any, webView: InAppWebView, settings: FindInteractionSettings?) { + super.init() + self.plugin = plugin + self.webView = webView + self.settings = settings + if let registrar = plugin.registrar { + let channel = FlutterMethodChannel(name: FindInteractionController.METHOD_CHANNEL_NAME_PREFIX + String(describing: id), + binaryMessenger: registrar.messenger()) + self.channelDelegate = FindInteractionChannelDelegate(findInteractionController: self, channel: channel) + } + } + + public func prepare() { +// if let settings = settings { +// +// } + } + + public func findAll(find: String?, completionHandler: ((Any?, Error?) -> Void)?) { + guard let webView else { + if let completionHandler = completionHandler { + completionHandler(nil, nil) + } + return + } + + var find = find + if find == nil { + find = searchText + } else { + // updated searchText + searchText = find + } + + guard let find else { + if let completionHandler = completionHandler { + completionHandler(nil, nil) + } + return + } + + if #available(iOS 16.0, *), webView.isFindInteractionEnabled { + if let interaction = webView.findInteraction { + interaction.searchText = find + interaction.presentFindNavigator(showingReplace: false) + } + if let completionHandler = completionHandler { + completionHandler(nil, nil) + } + } else if find != "" { + let startSearch = "window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsync('\(find)');" + webView.evaluateJavaScript(startSearch, completionHandler: completionHandler) + } + } + + public func findNext(forward: Bool, completionHandler: ((Any?, Error?) -> Void)?) { + guard let webView else { + if let completionHandler = completionHandler { + completionHandler(nil, nil) + } + return + } + if #available(iOS 16.0, *), webView.isFindInteractionEnabled { + if let interaction = webView.findInteraction { + if forward { + interaction.findNext() + } else { + interaction.findPrevious() + } + } + if let completionHandler = completionHandler { + completionHandler(nil, nil) + } + } else { + webView.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._findNext(\(forward ? "true" : "false"));", completionHandler: completionHandler) + } + } + + public func clearMatches(completionHandler: ((Any?, Error?) -> Void)?) { + guard let webView else { + if let completionHandler = completionHandler { + completionHandler(nil, nil) + } + return + } + if #available(iOS 16.0, *), webView.isFindInteractionEnabled { + if let interaction = webView.findInteraction { + interaction.searchText = nil + interaction.dismissFindNavigator() + } + if let completionHandler = completionHandler { + completionHandler(nil, nil) + } + } else { + webView.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatches();", completionHandler: completionHandler) + } + } + + public func dispose() { + channelDelegate?.dispose() + channelDelegate = nil + webView = nil + activeFindSession = nil + plugin = nil + } + + deinit { + debugPrint("FindInteractionControl - dealloc") + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/FindInteraction/FindInteractionSettings.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/FindInteraction/FindInteractionSettings.swift new file mode 100644 index 00000000..3c4ecb14 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/FindInteraction/FindInteractionSettings.swift @@ -0,0 +1,25 @@ +// +// FindInteractionSettings.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/10/22. +// + +import Foundation + +public class FindInteractionSettings: ISettings { + + override init(){ + super.init() + } + + override func parse(settings: [String: Any?]) -> FindInteractionSettings { + let _ = super.parse(settings: settings) + return self + } + + override func getRealSettings(obj: FindInteractionController?) -> [String: Any?] { + let realSettings: [String: Any?] = toMap() + return realSettings + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/GopeedProfiles.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/GopeedProfiles.swift new file mode 100644 index 00000000..bbc16471 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/GopeedProfiles.swift @@ -0,0 +1,29 @@ +// Internal Gopeed host bridge. No extension-facing API. +import Foundation +import WebKit +import Network +import Flutter + +final class GopeedProfiles { + private static var stores: [String: WKWebsiteDataStore] = [:] + static func store(_ identifier: String) -> WKWebsiteDataStore? { stores[identifier] } + + static func prepare(arguments: NSDictionary?, result: @escaping FlutterResult) { + guard #available(macOS 14.0, iOS 17.0, *), + let identifier = arguments?["gopeedProfileId"] as? String, + let uuid = UUID(uuidString: identifier), + let proxyURL = arguments?["proxyUrl"] as? String, + let url = URL(string: proxyURL), url.scheme == "socks5", + let host = url.host, host == "127.0.0.1", + let port = url.port, port > 0 && port <= 65535 else { + result(FlutterError(code: "UNAVAILABLE", message: "Isolated WebView profiles and proxy require macOS 14 / iOS 17 or newer and valid host configuration", details: nil)) + return + } + let store = stores[identifier] ?? WKWebsiteDataStore(forIdentifier: uuid) + let endpoint = nw_endpoint_create_host(host, String(port)) + let proxy = nw_proxy_config_create_socksv5(endpoint) + store.__proxyConfigurations = [proxy] + stores[identifier] = store + result(true) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/HeadlessInAppWebView/HeadlessInAppWebView.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/HeadlessInAppWebView/HeadlessInAppWebView.swift new file mode 100644 index 00000000..5d13086d --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/HeadlessInAppWebView/HeadlessInAppWebView.swift @@ -0,0 +1,99 @@ +// +// HeadlessInAppWebView.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 26/03/21. +// + +import Foundation + +public class HeadlessInAppWebView: Disposable { + static let METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_headless_inappwebview_" + + var id: String + var channelDelegate: HeadlessWebViewChannelDelegate? + var flutterWebView: FlutterWebViewController? + var plugin: SwiftFlutterPlugin? + + public init(plugin: SwiftFlutterPlugin, id: String, flutterWebView: FlutterWebViewController) { + self.id = id + self.flutterWebView = flutterWebView + self.plugin = plugin + let channel = FlutterMethodChannel(name: HeadlessInAppWebView.METHOD_CHANNEL_NAME_PREFIX + id, + binaryMessenger: plugin.registrar!.messenger()) + self.channelDelegate = HeadlessWebViewChannelDelegate(headlessWebView: self, channel: channel) + } + + public func onWebViewCreated() { + channelDelegate?.onWebViewCreated() + } + + public func prepare(params: NSDictionary) { + if let view = flutterWebView?.view() { + view.alpha = 0.01 + let initialSize = params["initialSize"] as? [String: Any?] + if let size = Size2D.fromMap(map: initialSize) { + setSize(size: size) + } else { + view.frame = CGRect(x: 0.0, y: 0.0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height) + } + if let keyWindow = UIApplication.shared.keyWindow { + /// Note: The WKWebView behaves very unreliable when rendering offscreen + /// on a device. This is especially true with JavaScript, which simply + /// won't be executed sometimes. + /// So, add the headless WKWebView to the view hierarchy. + /// This way is also possible to take screenshots. + keyWindow.insertSubview(view, at: 0) + keyWindow.sendSubviewToBack(view) + } + } + } + + public func setSize(size: Size2D) { + if let view = flutterWebView?.view() { + let width = size.width == -1.0 ? UIScreen.main.bounds.width : CGFloat(size.width) + let height = size.height == -1.0 ? UIScreen.main.bounds.height : CGFloat(size.height) + view.frame = CGRect(x: 0.0, y: 0.0, width: width, height: height) + } + } + + public func getSize() -> Size2D? { + if let view = flutterWebView?.view() { + return Size2D(width: Double(view.frame.width), height: Double(view.frame.height)) + } + return nil + } + + public func disposeAndGetFlutterWebView(withFrame frame: CGRect) -> FlutterWebViewController? { + let newFlutterWebView = flutterWebView + if let view = flutterWebView?.view() { + // restore WebView frame and alpha + view.frame = frame + view.alpha = 1.0 + // remove from parent + view.removeFromSuperview() + dispose(disposeWebView: false) + } + return newFlutterWebView + } + + public func dispose(disposeWebView: Bool) { + channelDelegate?.dispose() + channelDelegate = nil + plugin?.headlessInAppWebViewManager?.webViews[id] = nil + if disposeWebView { + flutterWebView?.dispose(removeFromSuperview: true) + } + flutterWebView = nil + plugin = nil + } + + public func dispose() { + dispose(disposeWebView: true) + } + + deinit { + debugPrint("HeadlessInAppWebView - dealloc") + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/HeadlessInAppWebView/HeadlessInAppWebViewManager.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/HeadlessInAppWebView/HeadlessInAppWebViewManager.swift new file mode 100644 index 00000000..36d770c3 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/HeadlessInAppWebView/HeadlessInAppWebViewManager.swift @@ -0,0 +1,71 @@ +// +// HeadlessInAppWebViewManager.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/05/2020. +// + +import Foundation + +import Flutter +import UIKit +import WebKit +import Foundation +import AVFoundation + +public class HeadlessInAppWebViewManager: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_headless_inappwebview" + var plugin: SwiftFlutterPlugin? + var webViews: [String: HeadlessInAppWebView?] = [:] + + init(plugin: SwiftFlutterPlugin) { + super.init(channel: FlutterMethodChannel(name: HeadlessInAppWebViewManager.METHOD_CHANNEL_NAME, binaryMessenger: plugin.registrar!.messenger())) + self.plugin = plugin + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + let id: String = arguments!["id"] as! String + + switch call.method { + case "run": + let params = arguments!["params"] as! [String: Any?] + run(id: id, params: params) + result(true) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func run(id: String, params: [String: Any?]) { + guard let plugin = plugin else { + return + } + let flutterWebView = FlutterWebViewController(plugin: plugin, + withFrame: CGRect.zero, + viewIdentifier: id, + params: params as NSDictionary) + let headlessInAppWebView = HeadlessInAppWebView(plugin: plugin, id: id, flutterWebView: flutterWebView) + webViews[id] = headlessInAppWebView + + headlessInAppWebView.prepare(params: params as NSDictionary) + headlessInAppWebView.onWebViewCreated() + flutterWebView.makeInitialLoad(params: params as NSDictionary) + } + + public override func dispose() { + super.dispose() + let headlessWebViews = webViews.values + headlessWebViews.forEach { (headlessWebView: HeadlessInAppWebView?) in + headlessWebView?.dispose() + } + webViews.removeAll() + plugin = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/HeadlessInAppWebView/HeadlessWebViewChannelDelegate.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/HeadlessInAppWebView/HeadlessWebViewChannelDelegate.swift new file mode 100644 index 00000000..23e12a46 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/HeadlessInAppWebView/HeadlessWebViewChannelDelegate.swift @@ -0,0 +1,63 @@ +// +// HeadlessWebViewChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 05/05/22. +// + +import Foundation + +public class HeadlessWebViewChannelDelegate: ChannelDelegate { + private weak var headlessWebView: HeadlessInAppWebView? + + public init(headlessWebView: HeadlessInAppWebView, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.headlessWebView = headlessWebView + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "dispose": + if let headlessWebView = headlessWebView { + headlessWebView.dispose() + result(true) + } else { + result(false) + } + break + case "setSize": + if let headlessWebView = headlessWebView { + let sizeMap = arguments!["size"] as? [String: Any?] + if let size = Size2D.fromMap(map: sizeMap) { + headlessWebView.setSize(size: size) + } + result(true) + } else { + result(false) + } + break + case "getSize": + result(headlessWebView?.getSize()?.toMap()) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onWebViewCreated() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onWebViewCreated", arguments: arguments) + } + + public override func dispose() { + super.dispose() + headlessWebView = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/ISettings.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/ISettings.swift new file mode 100755 index 00000000..3c536486 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/ISettings.swift @@ -0,0 +1,50 @@ +// +// Options.swift +// flutter_inappwebview +// +// Created by Lorenzo on 26/09/18. +// + +import Foundation + +@objcMembers +public class ISettings: NSObject { + + override init(){ + super.init() + } + + func parse(settings: [String: Any?]) -> ISettings { + for (key, value) in settings { + if !(value is NSNull), value != nil { + if self.responds(to: Selector(key)) { + self.setValue(value, forKey: key) + } else if self.responds(to: Selector("_" + key)) { + self.setValue(value, forKey: "_" + key) + } + } + } + return self + } + + func toMap() -> [String: Any?] { + var settings: [String: Any?] = [:] + var counts = UInt32() + let properties = class_copyPropertyList(object_getClass(self), &counts) + for i in 0.. [String: Any?] { + let realSettings: [String: Any?] = toMap() + return realSettings + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppBrowser/InAppBrowserChannelDelegate.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppBrowser/InAppBrowserChannelDelegate.swift new file mode 100644 index 00000000..01d66ed8 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppBrowser/InAppBrowserChannelDelegate.swift @@ -0,0 +1,35 @@ +// +// InAppBrowserChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 05/05/22. +// + +import Foundation + +public class InAppBrowserChannelDelegate: ChannelDelegate { + public override init(channel: FlutterMethodChannel) { + super.init(channel: channel) + } + + public func onBrowserCreated() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onBrowserCreated", arguments: arguments) + } + + public func onMenuItemClicked(menuItem: InAppBrowserMenuItem) { + let arguments: [String: Any?] = [ + "id": menuItem.id + ] + channel?.invokeMethod("onMenuItemClicked", arguments: arguments) + } + + public func onExit() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onExit", arguments: arguments) + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppBrowser/InAppBrowserDelegate.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppBrowser/InAppBrowserDelegate.swift new file mode 100644 index 00000000..ba013c58 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppBrowser/InAppBrowserDelegate.swift @@ -0,0 +1,17 @@ +// +// InAppBrowserDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 14/02/21. +// + +import Foundation + +public protocol InAppBrowserDelegate { + func didChangeTitle(title: String?) + func didStartNavigation(url: URL?) + func didUpdateVisitedHistory(url: URL?) + func didFinishNavigation(url: URL?) + func didFailNavigation(url: URL?, error: Error) + func didChangeProgress(progress: Double) +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppBrowser/InAppBrowserManager.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppBrowser/InAppBrowserManager.swift new file mode 100755 index 00000000..06219762 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppBrowser/InAppBrowserManager.swift @@ -0,0 +1,153 @@ +// +// InAppBrowserManager.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 18/12/2019. +// + +import Flutter +import UIKit +import WebKit +import Foundation +import AVFoundation + +public class InAppBrowserManager: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappbrowser" + static let WEBVIEW_STORYBOARD = "WebView" + static let WEBVIEW_STORYBOARD_CONTROLLER_ID = "viewController" + static let NAV_STORYBOARD_CONTROLLER_ID = "navController" + var plugin: SwiftFlutterPlugin? + + private var previousStatusBarStyle = -1 + + init(plugin: SwiftFlutterPlugin) { + super.init(channel: FlutterMethodChannel(name: InAppBrowserManager.METHOD_CHANNEL_NAME, binaryMessenger: plugin.registrar!.messenger())) + self.plugin = plugin + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "open": + open(arguments: arguments!) + result(true) + break + case "openWithSystemBrowser": + let url = arguments!["url"] as! String + openWithSystemBrowser(url: url, result: result) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func prepareInAppBrowserWebViewController(settings: [String: Any?]) -> InAppBrowserWebViewController { + if previousStatusBarStyle == -1 { + previousStatusBarStyle = UIApplication.shared.statusBarStyle.rawValue + } + + let browserSettings = InAppBrowserSettings() + let _ = browserSettings.parse(settings: settings) + + let webViewSettings = InAppWebViewSettings() + let _ = webViewSettings.parse(settings: settings) + + let webViewController = InAppBrowserWebViewController() + webViewController.plugin = plugin + webViewController.browserSettings = browserSettings + webViewController.isHidden = browserSettings.hidden + webViewController.webViewSettings = webViewSettings + webViewController.previousStatusBarStyle = previousStatusBarStyle + return webViewController + } + + public func open(arguments: NSDictionary) { + let id = arguments["id"] as! String + let urlRequest = arguments["urlRequest"] as? [String:Any?] + let assetFilePath = arguments["assetFilePath"] as? String + let data = arguments["data"] as? String + let mimeType = arguments["mimeType"] as? String + let encoding = arguments["encoding"] as? String + let baseUrl = arguments["baseUrl"] as? String + let settings = arguments["settings"] as! [String: Any?] + let contextMenu = arguments["contextMenu"] as! [String: Any] + let windowId = arguments["windowId"] as? Int64 + let initialUserScripts = arguments["initialUserScripts"] as? [[String: Any]] + let pullToRefreshInitialSettings = arguments["pullToRefreshSettings"] as! [String: Any?] + let menuItems = arguments["menuItems"] as! [[String: Any?]] + + let webViewController = prepareInAppBrowserWebViewController(settings: settings) + + webViewController.id = id + webViewController.initialUrlRequest = urlRequest != nil ? URLRequest.init(fromPluginMap: urlRequest!) : nil + webViewController.initialFile = assetFilePath + webViewController.initialData = data + webViewController.initialMimeType = mimeType + webViewController.initialEncoding = encoding + webViewController.initialBaseUrl = baseUrl + webViewController.contextMenu = contextMenu + webViewController.windowId = windowId + webViewController.initialUserScripts = initialUserScripts ?? [] + webViewController.pullToRefreshInitialSettings = pullToRefreshInitialSettings + for menuItem in menuItems { + webViewController.menuItems.append(InAppBrowserMenuItem.fromMap(map: menuItem)!) + } + + presentViewController(webViewController: webViewController) + } + + public func presentViewController(webViewController: InAppBrowserWebViewController) { + let storyboard = UIStoryboard(name: InAppBrowserManager.WEBVIEW_STORYBOARD, bundle: Bundle(for: InAppWebViewFlutterPlugin.self)) + let navController = storyboard.instantiateViewController(withIdentifier: InAppBrowserManager.NAV_STORYBOARD_CONTROLLER_ID) as! InAppBrowserNavigationController + webViewController.edgesForExtendedLayout = [] + navController.pushViewController(webViewController, animated: false) + webViewController.prepareNavigationControllerBeforeViewWillAppear() + + var animated = true + if let browserSettings = webViewController.browserSettings, browserSettings.hidden { + animated = false + } + + guard let visibleViewController = UIApplication.shared.visibleViewController else { + assertionFailure("Failure init the visibleViewController!") + return + } + + if let popover = navController.popoverPresentationController { + let sourceView = visibleViewController.view ?? UIView() + + popover.sourceRect = CGRect(x: sourceView.bounds.midX, y: sourceView.bounds.midY, width: 0, height: 0) + popover.permittedArrowDirections = [] + popover.sourceView = sourceView + } + + visibleViewController.present(navController, animated: animated) + } + + public func openWithSystemBrowser(url: String, result: @escaping FlutterResult) { + let absoluteUrl = URL(string: url)!.absoluteURL + if !UIApplication.shared.canOpenURL(absoluteUrl) { + result(FlutterError(code: "InAppBrowserManager", message: url + " cannot be opened!", details: nil)) + return + } + else { + if #available(iOS 10.0, *) { + UIApplication.shared.open(absoluteUrl) + } else { + UIApplication.shared.openURL(absoluteUrl) + } + } + result(true) + } + + public override func dispose() { + super.dispose() + plugin = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppBrowser/InAppBrowserNavigationController.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppBrowser/InAppBrowserNavigationController.swift new file mode 100644 index 00000000..2d7b8c59 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppBrowser/InAppBrowserNavigationController.swift @@ -0,0 +1,19 @@ +// +// InAppBrowserNavigationController.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 14/02/21. +// + +import Foundation + +public class InAppBrowserNavigationController: UINavigationController { + var tmpWindow: UIWindow? + + deinit { + debugPrint("InAppBrowserNavigationController - dealloc") + tmpWindow?.windowLevel = UIWindow.Level(rawValue: 0.0) + tmpWindow = nil + UIApplication.shared.delegate?.window??.makeKeyAndVisible() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppBrowser/InAppBrowserSettings.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppBrowser/InAppBrowserSettings.swift new file mode 100755 index 00000000..54d34244 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppBrowser/InAppBrowserSettings.swift @@ -0,0 +1,62 @@ +// +// InAppBrowserOptions.swift +// flutter_inappwebview +// +// Created by Lorenzo on 17/09/18. +// + +import Foundation + +@objcMembers +public class InAppBrowserSettings: ISettings { + + var hidden = false + var hideToolbarTop = true + var toolbarTopBackgroundColor: String? + var hideUrlBar = false + var hideProgressBar = false + + var toolbarTopTranslucent = true + var toolbarTopBarTintColor: String? + var toolbarTopTintColor: String? + var hideToolbarBottom = true + var toolbarBottomBackgroundColor: String? + var toolbarBottomTintColor: String? + var toolbarBottomTranslucent = true + var closeButtonCaption: String? + var closeButtonColor: String? + var presentationStyle = 0 //fullscreen + var transitionStyle = 0 //crossDissolve + var hideCloseButton = false + var menuButtonColor: String? + + override init(){ + super.init() + } + + override func getRealSettings(obj: InAppBrowserWebViewController?) -> [String: Any?] { + var realOptions: [String: Any?] = toMap() + if let inAppBrowserWebViewController = obj { + realOptions["hidden"] = inAppBrowserWebViewController.isHidden + realOptions["hideUrlBar"] = inAppBrowserWebViewController.searchBar.isHidden + realOptions["hideProgressBar"] = inAppBrowserWebViewController.progressBar.isHidden + realOptions["closeButtonCaption"] = inAppBrowserWebViewController.closeButton.title + realOptions["closeButtonColor"] = inAppBrowserWebViewController.closeButton.tintColor?.hexString + realOptions["menuButtonColor"] = inAppBrowserWebViewController.menuButton?.tintColor?.hexString + if let navController = inAppBrowserWebViewController.navigationController { + realOptions["hideToolbarTop"] = navController.navigationBar.isHidden + realOptions["toolbarTopBackgroundColor"] = navController.navigationBar.backgroundColor?.hexString + realOptions["toolbarTopTranslucent"] = navController.navigationBar.isTranslucent + realOptions["toolbarTopBarTintColor"] = navController.navigationBar.barTintColor?.hexString + realOptions["toolbarTopTintColor"] = navController.navigationBar.tintColor?.hexString + realOptions["hideToolbarBottom"] = navController.toolbar.isHidden + realOptions["toolbarBottomBackgroundColor"] = navController.toolbar.barTintColor?.hexString + realOptions["toolbarBottomTranslucent"] = navController.toolbar.isTranslucent + realOptions["toolbarBottomTintColor"] = navController.toolbar.tintColor?.hexString + realOptions["presentationStyle"] = navController.modalPresentationStyle.rawValue + realOptions["transitionStyle"] = navController.modalTransitionStyle.rawValue + } + } + return realOptions + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift new file mode 100755 index 00000000..c7543478 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppBrowser/InAppBrowserWebViewController.swift @@ -0,0 +1,654 @@ +// +// InAppBrowserWebViewController.swift +// flutter_inappwebview +// +// Created by Lorenzo on 17/09/18. +// + +import Flutter +import UIKit +import WebKit +import Foundation + +public class InAppBrowserWebViewController: UIViewController, InAppBrowserDelegate, UIScrollViewDelegate, UISearchBarDelegate, Disposable { + static let METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappbrowser_" + + var closeButton: UIBarButtonItem! + var reloadButton: UIBarButtonItem! + var backButton: UIBarButtonItem! + var forwardButton: UIBarButtonItem! + var shareButton: UIBarButtonItem! + var searchBar: UISearchBar! + var progressBar: UIProgressView! + var menuButton: UIBarButtonItem? + private var _menu: Any? + @available(iOS 13.0, *) + var menu: UIMenu? { + set { + _menu = newValue + } + get { + return _menu as? UIMenu + } + } + + var tmpWindow: UIWindow? + var id: String = "" + var plugin: SwiftFlutterPlugin? + var windowId: Int64? + var webView: InAppWebView? + var channelDelegate: InAppBrowserChannelDelegate? + var initialUrlRequest: URLRequest? + var initialFile: String? + var contextMenu: [String: Any]? + var browserSettings: InAppBrowserSettings? + var webViewSettings: InAppWebViewSettings? + var initialData: String? + var initialMimeType: String? + var initialEncoding: String? + var initialBaseUrl: String? + var previousStatusBarStyle = -1 + var initialUserScripts: [[String: Any]] = [] + var pullToRefreshInitialSettings: [String: Any?] = [:] + var isHidden = false + var menuItems: [InAppBrowserMenuItem] = [] + + public override func loadView() { + guard let plugin = plugin, let registrar = plugin.registrar else { + return + } + + let channel = FlutterMethodChannel(name: InAppBrowserWebViewController.METHOD_CHANNEL_NAME_PREFIX + id, binaryMessenger: registrar.messenger()) + channelDelegate = InAppBrowserChannelDelegate(channel: channel) + + var userScripts: [UserScript] = [] + for initialUserScript in initialUserScripts { + userScripts.append(UserScript.fromMap(map: initialUserScript, windowId: windowId)!) + } + + let preWebviewConfiguration = InAppWebView.preWKWebViewConfiguration(settings: webViewSettings) + if let wId = windowId, let webViewTransport = plugin.inAppWebViewManager?.windowWebViews[wId] { + webView = webViewTransport.webView + webView!.contextMenu = contextMenu + webView!.initialUserScripts = userScripts + } else { + webView = InAppWebView(id: nil, + plugin: nil, + frame: .zero, + configuration: preWebviewConfiguration, + contextMenu: contextMenu, + userScripts: userScripts) + } + + guard let webView = webView else { + return + } + + webView.inAppBrowserDelegate = self + webView.id = id + webView.plugin = plugin + webView.channelDelegate = WebViewChannelDelegate(webView: webView, channel: channel) + + let pullToRefreshSettings = PullToRefreshSettings() + let _ = pullToRefreshSettings.parse(settings: pullToRefreshInitialSettings) + let pullToRefreshControl = PullToRefreshControl(plugin: plugin, id: id, settings: pullToRefreshSettings) + webView.pullToRefreshControl = pullToRefreshControl + pullToRefreshControl.delegate = webView + pullToRefreshControl.prepare() + + let findInteractionController = FindInteractionController( + plugin: plugin, + id: id, webView: webView, settings: nil) + webView.findInteractionController = findInteractionController + findInteractionController.prepare() + + prepareWebView() + webView.windowCreated = true + + progressBar = UIProgressView(progressViewStyle: .bar) + + view = UIView() + view.addSubview(webView) + view.insertSubview(progressBar, aboveSubview: webView) + } + + public override func viewDidLoad() { + super.viewDidLoad() + + webView?.translatesAutoresizingMaskIntoConstraints = false + progressBar.translatesAutoresizingMaskIntoConstraints = false + + if #available(iOS 9.0, *) { + webView?.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0.0).isActive = true + webView?.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0.0).isActive = true + webView?.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0.0).isActive = true + webView?.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 0.0).isActive = true + + progressBar.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0.0).isActive = true + progressBar.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0.0).isActive = true + progressBar.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 0.0).isActive = true + } else { + if let webView = webView { + view.addConstraints([ + NSLayoutConstraint(item: webView, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1, constant: 0), + NSLayoutConstraint(item: webView, attribute: .bottom, relatedBy: .equal, toItem: view, attribute: .bottom, multiplier: 1, constant: 0), + NSLayoutConstraint(item: webView, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: 0), + NSLayoutConstraint(item: webView, attribute: .right, relatedBy: .equal, toItem: view, attribute: .right, multiplier: 1, constant: 0) + ]) + } + if let progressBar = progressBar { + view.addConstraints([ + NSLayoutConstraint(item: progressBar, attribute: .top, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1, constant: 0), + NSLayoutConstraint(item: progressBar, attribute: .left, relatedBy: .equal, toItem: view, attribute: .left, multiplier: 1, constant: 0), + NSLayoutConstraint(item: progressBar, attribute: .right, relatedBy: .equal, toItem: view, attribute: .right, multiplier: 1, constant: 0) + ]) + } + } + + if windowId != nil { + channelDelegate?.onBrowserCreated() + webView?.runWindowBeforeCreatedCallbacks() + } else { + if #available(iOS 11.0, *) { + if let contentBlockers = webView?.settings?.contentBlockers, contentBlockers.count > 0 { + do { + let jsonData = try JSONSerialization.data(withJSONObject: contentBlockers, options: []) + let blockRules = String(data: jsonData, encoding: .utf8) + WKContentRuleListStore.default().compileContentRuleList( + forIdentifier: "ContentBlockingRules", + encodedContentRuleList: blockRules) { (contentRuleList, error) in + + if let error = error { + print(error.localizedDescription) + return + } + + let configuration = self.webView!.configuration + configuration.userContentController.add(contentRuleList!) + + self.initLoad() + } + return + } catch { + print(error.localizedDescription) + } + } + } + + initLoad() + } + } + + public func initLoad() { + if let initialFile = initialFile { + do { + try webView?.loadFile(assetFilePath: initialFile) + } + catch let error as NSError { + dump(error) + } + } + else if let initialData = initialData { + let baseUrl = URL(string: initialBaseUrl ?? "about:blank")! + var allowingReadAccessToURL: URL? = nil + if let allowingReadAccessTo = webView?.settings?.allowingReadAccessTo, baseUrl.scheme == "file" { + allowingReadAccessToURL = URL(string: allowingReadAccessTo) + if allowingReadAccessToURL?.scheme != "file" { + allowingReadAccessToURL = nil + } + } + webView?.loadData(data: initialData, mimeType: initialMimeType!, encoding: initialEncoding!, baseUrl: baseUrl, allowingReadAccessTo: allowingReadAccessToURL) + } + else if let initialUrlRequest = initialUrlRequest { + var allowingReadAccessToURL: URL? = nil + if let allowingReadAccessTo = webView?.settings?.allowingReadAccessTo, let url = initialUrlRequest.url, url.scheme == "file" { + allowingReadAccessToURL = URL(string: allowingReadAccessTo) + if allowingReadAccessToURL?.scheme != "file" { + allowingReadAccessToURL = nil + } + } + webView?.loadUrl(urlRequest: initialUrlRequest, allowingReadAccessTo: allowingReadAccessToURL) + } + + channelDelegate?.onBrowserCreated() + } + + public override func viewDidDisappear(_ animated: Bool) { + dispose() + super.viewDidDisappear(animated) + } + + public override func viewWillDisappear (_ animated: Bool) { + super.viewWillDisappear(animated) + } + + public func prepareNavigationControllerBeforeViewWillAppear() { + if let browserOptions = browserSettings { + navigationController?.modalPresentationStyle = UIModalPresentationStyle(rawValue: browserOptions.presentationStyle)! + navigationController?.modalTransitionStyle = UIModalTransitionStyle(rawValue: browserOptions.transitionStyle)! + } + } + + public func prepareWebView() { + webView?.settings = webViewSettings + webView?.prepare() + + searchBar = UISearchBar() + searchBar.keyboardType = .URL + searchBar.sizeToFit() + searchBar.delegate = self + navigationItem.titleView = searchBar + + let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + reloadButton = UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(reload)) + shareButton = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(share)) + forwardButton = UIBarButtonItem(title: "\u{203A}", style: .plain, target: self, action: #selector(goForward)) + forwardButton.isEnabled = false + backButton = UIBarButtonItem(title: "\u{2039}", style: .plain, target: self, action: #selector(goBack)) + backButton.isEnabled = false + + toolbarItems = [backButton, spacer, forwardButton, spacer, shareButton, spacer, reloadButton] + + for state: UIControl.State in [.normal, .disabled, .highlighted, .selected] { + forwardButton.setTitleTextAttributes([ + NSAttributedString.Key.font: UIFont.systemFont(ofSize: 50.0), + NSAttributedString.Key.baselineOffset: 2.5 + ], for: state) + backButton.setTitleTextAttributes([ + NSAttributedString.Key.font: UIFont.systemFont(ofSize: 50.0), + NSAttributedString.Key.baselineOffset: 2.5 + ], for: state) + } + + if let browserSettings = browserSettings { + if !browserSettings.hideToolbarTop { + navigationController?.navigationBar.isHidden = false + if browserSettings.hideUrlBar { + searchBar.isHidden = true + } + if let bgColor = browserSettings.toolbarTopBackgroundColor, !bgColor.isEmpty { + navigationController?.navigationBar.backgroundColor = UIColor(hexString: bgColor) + } + if let barTintColor = browserSettings.toolbarTopBarTintColor, !barTintColor.isEmpty { + navigationController?.navigationBar.barTintColor = UIColor(hexString: barTintColor) + } + if let tintColor = browserSettings.toolbarTopTintColor, !tintColor.isEmpty { + navigationController?.navigationBar.tintColor = UIColor(hexString: tintColor) + } + navigationController?.navigationBar.isTranslucent = browserSettings.toolbarTopTranslucent + } + else { + navigationController?.navigationBar.isHidden = true + } + + if !browserSettings.hideToolbarBottom { + navigationController?.isToolbarHidden = false + if let bgColor = browserSettings.toolbarBottomBackgroundColor, !bgColor.isEmpty { + navigationController?.toolbar.barTintColor = UIColor(hexString: bgColor) + } + if let tintColor = browserSettings.toolbarBottomTintColor, !tintColor.isEmpty { + navigationController?.toolbar.tintColor = UIColor(hexString: tintColor) + } + navigationController?.toolbar.isTranslucent = false + } + else { + navigationController?.isToolbarHidden = true + } + + if let closeButtonCaption = browserSettings.closeButtonCaption, !closeButtonCaption.isEmpty { + closeButton = UIBarButtonItem(title: closeButtonCaption, style: .plain, target: self, action: #selector(close)) + } else { + setDefaultCloseButton() + } + + if let closeButtonColor = browserSettings.closeButtonColor, !closeButtonColor.isEmpty { + closeButton.tintColor = UIColor(hexString: closeButtonColor) + } + + if browserSettings.hideProgressBar { + progressBar.isHidden = true + } + + navigationItem.rightBarButtonItems = [] + + if !browserSettings.hideCloseButton { + navigationItem.rightBarButtonItems = [closeButton] + } + + if #available(iOS 14.0, *), !menuItems.isEmpty { + var uiActions: [UIAction] = [] + menuItems = menuItems.sorted(by: {$0.order ?? 0 < $1.order ?? 0}) + for menuItem in menuItems { + let uiAction = UIAction(title: menuItem.title, image: menuItem.icon, handler: {_ in + self.channelDelegate?.onMenuItemClicked(menuItem: menuItem) + }) + if !menuItem.showAsAction { + uiActions.append(uiAction) + } else { + let buttonItem = UIBarButtonItem(primaryAction: uiAction) + buttonItem.tintColor = menuItem.iconColor + navigationItem.rightBarButtonItems?.append(buttonItem) + } + } + if !uiActions.isEmpty { + menu = UIMenu(title: "", options: .displayInline, children: uiActions) + menuButton = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), menu: menu) + if let menuButtonColor = browserSettings.menuButtonColor, !menuButtonColor.isEmpty { + menuButton?.tintColor = UIColor(hexString: menuButtonColor) + } + let index = browserSettings.hideCloseButton ? 0 : 1 + navigationItem.rightBarButtonItems?.insert(menuButton!, at: index) + } + } + } + } + + func setDefaultCloseButton() { + if closeButton != nil { + closeButton.target = nil + closeButton.action = nil + } + var barButtonSystemItem = UIBarButtonItem.SystemItem.cancel + if #available(iOS 13.0, *) { + barButtonSystemItem = UIBarButtonItem.SystemItem.close + } + closeButton = UIBarButtonItem(barButtonSystemItem: barButtonSystemItem, target: self, action: #selector(close)) + } + + public func didChangeTitle(title: String?) { + guard let _ = title else { + return + } + } + + public func didStartNavigation(url: URL?) { + forwardButton.isEnabled = webView?.canGoForward ?? false + backButton.isEnabled = webView?.canGoBack ?? false + progressBar.setProgress(0.0, animated: false) + guard let url = url else { + return + } + searchBar.text = url.absoluteString + } + + public func didUpdateVisitedHistory(url: URL?) { + forwardButton.isEnabled = webView?.canGoForward ?? false + backButton.isEnabled = webView?.canGoBack ?? false + guard let url = url else { + return + } + searchBar.text = url.absoluteString + } + + public func didFinishNavigation(url: URL?) { + forwardButton.isEnabled = webView?.canGoForward ?? false + backButton.isEnabled = webView?.canGoBack ?? false + progressBar.setProgress(0.0, animated: false) + guard let url = url else { + return + } + searchBar.text = url.absoluteString + } + + public func didFailNavigation(url: URL?, error: Error) { + forwardButton.isEnabled = webView?.canGoForward ?? false + backButton.isEnabled = webView?.canGoBack ?? false + progressBar.setProgress(0.0, animated: false) + } + + public func didChangeProgress(progress: Double) { + progressBar.setProgress(Float(progress), animated: true) + } + + public func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { + guard let text = searchBar.text, + let urlEncoded = text.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), + let url = URL(string: urlEncoded) else { + return + } + let request = URLRequest(url: url) + webView?.load(request) + } + + public func show(completion: (() -> Void)? = nil) { + if let navController = navigationController as? InAppBrowserNavigationController, let window = navController.tmpWindow { + isHidden = false + window.alpha = 0.0 + window.isHidden = false + window.makeKeyAndVisible() + UIView.animate(withDuration: 0.2) { + window.alpha = 1.0 + completion?() + } + } + } + + public func hide(completion: (() -> Void)? = nil) { + if let navController = navigationController as? InAppBrowserNavigationController, let window = navController.tmpWindow { + isHidden = true + window.alpha = 1.0 + UIView.animate(withDuration: 0.2) { + window.alpha = 0.0 + } completion: { (finished) in + if finished { + window.isHidden = true + UIApplication.shared.delegate?.window??.makeKeyAndVisible() + completion?() + } + } + } + } + + @objc public func reload() { + webView?.reload() + didUpdateVisitedHistory(url: webView?.url) + } + + @objc public func share() { + let vc = UIActivityViewController(activityItems: [webView?.url?.absoluteString ?? ""], applicationActivities: []) + present(vc, animated: true, completion: nil) + } + + public func close(completion: (() -> Void)? = nil) { + if (navigationController?.responds(to: #selector(getter: navigationController?.presentingViewController)))! { + navigationController?.presentingViewController?.dismiss(animated: true, completion: {() -> Void in + completion?() + }) + } + else { + navigationController?.parent?.dismiss(animated: true, completion: {() -> Void in + completion?() + }) + } + } + + @objc public func close() { + if (navigationController?.responds(to: #selector(getter: navigationController?.presentingViewController)))! { + navigationController?.presentingViewController?.dismiss(animated: true, completion: nil) + } + else { + navigationController?.parent?.dismiss(animated: true, completion: nil) + } + } + + @objc public func goBack() { + if let webView = webView, webView.canGoBack { + webView.goBack() + } + } + + @objc public func goForward() { + if let webView = webView, webView.canGoForward { + webView.goForward() + } + } + + @objc public func goBackOrForward(steps: Int) { + webView?.goBackOrForward(steps: steps) + } + + public func setSettings(newSettings: InAppBrowserSettings, newSettingsMap: [String: Any]) { + let newInAppWebViewSettings = InAppWebViewSettings() + let _ = newInAppWebViewSettings.parse(settings: newSettingsMap) + webView?.setSettings(newSettings: newInAppWebViewSettings, newSettingsMap: newSettingsMap) + + if newSettingsMap["hidden"] != nil, browserSettings?.hidden != newSettings.hidden { + if newSettings.hidden { + hide() + } + else { + show() + } + } + + if newSettingsMap["hideUrlBar"] != nil, browserSettings?.hideUrlBar != newSettings.hideUrlBar { + searchBar.isHidden = newSettings.hideUrlBar + } + + if newSettingsMap["hideToolbarTop"] != nil, browserSettings?.hideToolbarTop != newSettings.hideToolbarTop { + navigationController?.navigationBar.isHidden = newSettings.hideToolbarTop + } + + if newSettingsMap["toolbarTopBackgroundColor"] != nil, browserSettings?.toolbarTopBackgroundColor != newSettings.toolbarTopBackgroundColor { + if let bgColor = newSettings.toolbarTopBackgroundColor, !bgColor.isEmpty { + navigationController?.navigationBar.backgroundColor = UIColor(hexString: bgColor) + } else { + navigationController?.navigationBar.backgroundColor = nil + } + } + + if newSettingsMap["toolbarTopBarTintColor"] != nil, browserSettings?.toolbarTopBarTintColor != newSettings.toolbarTopBarTintColor { + if let barTintColor = newSettings.toolbarTopBarTintColor, !barTintColor.isEmpty { + navigationController?.navigationBar.barTintColor = UIColor(hexString: barTintColor) + } else { + navigationController?.navigationBar.barTintColor = nil + } + } + + if newSettingsMap["toolbarTopTintColor"] != nil, browserSettings?.toolbarTopTintColor != newSettings.toolbarTopTintColor { + if let tintColor = newSettings.toolbarTopTintColor, !tintColor.isEmpty { + navigationController?.navigationBar.tintColor = UIColor(hexString: tintColor) + } else { + navigationController?.navigationBar.tintColor = nil + } + } + + if newSettingsMap["hideToolbarBottom"] != nil, browserSettings?.hideToolbarBottom != newSettings.hideToolbarBottom { + navigationController?.isToolbarHidden = !newSettings.hideToolbarBottom + } + + if newSettingsMap["toolbarBottomBackgroundColor"] != nil, browserSettings?.toolbarBottomBackgroundColor != newSettings.toolbarBottomBackgroundColor { + if let bgColor = newSettings.toolbarBottomBackgroundColor, !bgColor.isEmpty { + navigationController?.toolbar.barTintColor = UIColor(hexString: bgColor) + } else { + navigationController?.toolbar.barTintColor = nil + } + } + + if newSettingsMap["toolbarBottomTintColor"] != nil, browserSettings?.toolbarBottomTintColor != newSettings.toolbarBottomTintColor { + if let tintColor = newSettings.toolbarBottomTintColor, !tintColor.isEmpty { + navigationController?.toolbar.tintColor = UIColor(hexString: tintColor) + } else { + navigationController?.toolbar.tintColor = nil + } + } + + if newSettingsMap["toolbarTopTranslucent"] != nil, browserSettings?.toolbarTopTranslucent != newSettings.toolbarTopTranslucent { + navigationController?.navigationBar.isTranslucent = newSettings.toolbarTopTranslucent + } + + if newSettingsMap["toolbarBottomTranslucent"] != nil, browserSettings?.toolbarBottomTranslucent != newSettings.toolbarBottomTranslucent { + navigationController?.toolbar.isTranslucent = newSettings.toolbarBottomTranslucent + } + + if newSettingsMap["closeButtonCaption"] != nil, browserSettings?.closeButtonCaption != newSettings.closeButtonCaption { + if let closeButtonCaption = newSettings.closeButtonCaption, !closeButtonCaption.isEmpty { + if let oldTitle = closeButton.title, !oldTitle.isEmpty { + closeButton.title = closeButtonCaption + } else { + closeButton.target = nil + closeButton.action = nil + closeButton = UIBarButtonItem(title: closeButtonCaption, style: .plain, target: self, action: #selector(close)) + } + } else { + setDefaultCloseButton() + } + } + + if newSettingsMap["closeButtonColor"] != nil, browserSettings?.closeButtonColor != newSettings.closeButtonColor { + if let tintColor = newSettings.closeButtonColor, !tintColor.isEmpty { + closeButton.tintColor = UIColor(hexString: tintColor) + } else { + closeButton.tintColor = nil + } + } + + if newSettingsMap["hideCloseButton"] != nil, browserSettings?.hideCloseButton != newSettings.hideCloseButton { + if !newSettings.hideCloseButton { + navigationItem.rightBarButtonItems = [closeButton] + } else { + navigationItem.rightBarButtonItems = [] + } + } + + if newSettingsMap["presentationStyle"] != nil, browserSettings?.presentationStyle != newSettings.presentationStyle { + navigationController?.modalPresentationStyle = UIModalPresentationStyle(rawValue: newSettings.presentationStyle)! + } + + if newSettingsMap["transitionStyle"] != nil, browserSettings?.transitionStyle != newSettings.transitionStyle { + navigationController?.modalTransitionStyle = UIModalTransitionStyle(rawValue: newSettings.transitionStyle)! + } + + if newSettingsMap["hideProgressBar"] != nil, browserSettings?.hideProgressBar != newSettings.hideProgressBar { + progressBar.isHidden = newSettings.hideProgressBar + } + + if newSettingsMap["menuButtonColor"] != nil, browserSettings?.menuButtonColor != newSettings.menuButtonColor { + if let tintColor = newSettings.menuButtonColor, !tintColor.isEmpty { + menuButton?.tintColor = UIColor(hexString: tintColor) + } else { + menuButton?.tintColor = nil + } + } + + browserSettings = newSettings + webViewSettings = newInAppWebViewSettings + } + + public func getSettings() -> [String: Any?]? { + let webViewSettingsMap = webView?.getSettings() + if (self.browserSettings == nil || webViewSettingsMap == nil) { + return nil + } + var settingsMap = self.browserSettings!.getRealSettings(obj: self) + settingsMap.merge(webViewSettingsMap!, uniquingKeysWith: { (current, _) in current }) + return settingsMap + } + + public func dispose() { + channelDelegate?.onExit() + channelDelegate?.dispose() + channelDelegate = nil + webView?.dispose() + webView?.removeFromSuperview() + webView = nil + view = nil + if previousStatusBarStyle != -1, let statusBarStyle = UIStatusBarStyle(rawValue: previousStatusBarStyle) { + UIApplication.shared.statusBarStyle = statusBarStyle + } + transitioningDelegate = nil + searchBar?.delegate = nil + closeButton?.target = nil + forwardButton?.target = nil + backButton?.target = nil + reloadButton?.target = nil + shareButton?.target = nil + menuButton?.target = nil + plugin = nil + } + + deinit { + debugPrint("InAppBrowserWebViewController - dealloc") + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/ContextMenuSettings.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/ContextMenuSettings.swift new file mode 100644 index 00000000..9009e536 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/ContextMenuSettings.swift @@ -0,0 +1,17 @@ +// +// ContextMenuOptions.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 30/05/2020. +// + +import Foundation + +public class ContextMenuSettings: ISettings { + + var hideDefaultSystemContextMenuItems = false + + override init(){ + super.init() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/CustomSchemeHandler.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/CustomSchemeHandler.swift new file mode 100755 index 00000000..22abdf24 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/CustomSchemeHandler.swift @@ -0,0 +1,45 @@ +// +// CustomSchemeHandler.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 25/10/2019. +// + +import Flutter +import Foundation +import WebKit + +@available(iOS 11.0, *) +public class CustomSchemeHandler: NSObject, WKURLSchemeHandler { + var schemeHandlers: [Int: WKURLSchemeTask] = [:] + + public func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { + schemeHandlers[urlSchemeTask.hash] = urlSchemeTask + let inAppWebView = webView as! InAppWebView + let request = WebResourceRequest.init(fromURLRequest: urlSchemeTask.request) + let callback = WebViewChannelDelegate.LoadResourceWithCustomSchemeCallback() + callback.nonNullSuccess = { (response: CustomSchemeResponse) in + if (self.schemeHandlers[urlSchemeTask.hash] != nil) { + let urlResponse = URLResponse(url: request.url, mimeType: response.contentType, expectedContentLength: -1, textEncodingName: response.contentEncoding) + urlSchemeTask.didReceive(urlResponse) + urlSchemeTask.didReceive(response.data) + urlSchemeTask.didFinish() + self.schemeHandlers.removeValue(forKey: urlSchemeTask.hash) + } + return false + } + callback.error = { (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + } + + if let channelDelegate = inAppWebView.channelDelegate { + channelDelegate.onLoadResourceWithCustomScheme(request: request, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + public func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) { + schemeHandlers.removeValue(forKey: urlSchemeTask.hash) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/FlutterWebViewController.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/FlutterWebViewController.swift new file mode 100755 index 00000000..9a0d0690 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/FlutterWebViewController.swift @@ -0,0 +1,213 @@ +// +// FlutterWebViewController.swift +// flutter_inappwebview +// +// Created by Lorenzo on 13/11/18. +// + +import Foundation +import WebKit + +public class FlutterWebViewController: NSObject, FlutterPlatformView, Disposable { + + var myView: UIView? + var keepAliveId: String? + + init(plugin: SwiftFlutterPlugin, withFrame frame: CGRect, viewIdentifier viewId: Any, params: NSDictionary) { + super.init() + + myView = UIView(frame: frame) + myView!.clipsToBounds = true + + keepAliveId = params["keepAliveId"] as? String + + let initialSettings = params["initialSettings"] as! [String: Any?] + let contextMenu = params["contextMenu"] as? [String: Any] + let windowId = params["windowId"] as? Int64 + let initialUserScripts = params["initialUserScripts"] as? [[String: Any]] + let pullToRefreshInitialSettings = params["pullToRefreshSettings"] as! [String: Any?] + + var userScripts: [UserScript] = [] + if let initialUserScripts = initialUserScripts { + for initialUserScript in initialUserScripts { + userScripts.append(UserScript.fromMap(map: initialUserScript, windowId: windowId)!) + } + } + + let settings = InAppWebViewSettings() + let _ = settings.parse(settings: initialSettings) + let preWebviewConfiguration = InAppWebView.preWKWebViewConfiguration(settings: settings) + + var webView: InAppWebView? + + if let wId = windowId, let webViewTransport = plugin.inAppWebViewManager?.windowWebViews[wId] { + webView = webViewTransport.webView + webView!.id = viewId + webView!.plugin = plugin + if let registrar = plugin.registrar { + let channel = FlutterMethodChannel(name: InAppWebView.METHOD_CHANNEL_NAME_PREFIX + String(describing: viewId), + binaryMessenger: registrar.messenger()) + webView!.channelDelegate = WebViewChannelDelegate(webView: webView!, channel: channel) + } + webView!.frame = myView!.bounds + webView!.contextMenu = contextMenu + webView!.initialUserScripts = userScripts + } else { + webView = InAppWebView(id: viewId, + plugin: plugin, + frame: myView!.bounds, + configuration: preWebviewConfiguration, + contextMenu: contextMenu, + userScripts: userScripts) + } + + let pullToRefreshSettings = PullToRefreshSettings() + let _ = pullToRefreshSettings.parse(settings: pullToRefreshInitialSettings) + let pullToRefreshControl = PullToRefreshControl(plugin: plugin, id: viewId, settings: pullToRefreshSettings) + webView!.pullToRefreshControl = pullToRefreshControl + pullToRefreshControl.delegate = webView! + pullToRefreshControl.prepare() + + let findInteractionController = FindInteractionController( + plugin: plugin, + id: viewId, webView: webView!, settings: nil) + webView!.findInteractionController = findInteractionController + findInteractionController.prepare() + + webView!.autoresizingMask = [.flexibleWidth, .flexibleHeight] + myView!.autoresizesSubviews = true + myView!.autoresizingMask = [.flexibleWidth, .flexibleHeight] + myView!.addSubview(webView!) + + webView!.settings = settings + webView!.prepare() + webView!.windowCreated = true + } + + public func webView() -> InAppWebView? { + for subview in myView?.subviews ?? [] + { + if let item = subview as? InAppWebView + { + return item + } + } + return nil + } + + public func view() -> UIView { + return myView! + } + + public func makeInitialLoad(params: NSDictionary) { + guard let webView = webView() else { + return + } + + let windowId = params["windowId"] as? Int64 + let initialUrlRequest = params["initialUrlRequest"] as? [String: Any?] + let initialFile = params["initialFile"] as? String + let initialData = params["initialData"] as? [String: String?] + + if windowId == nil { + if #available(iOS 11.0, *) { + webView.configuration.userContentController.removeAllContentRuleLists() + if let contentBlockers = webView.settings?.contentBlockers, contentBlockers.count > 0 { + do { + let jsonData = try JSONSerialization.data(withJSONObject: contentBlockers, options: []) + let blockRules = String(data: jsonData, encoding: .utf8) + WKContentRuleListStore.default().compileContentRuleList( + forIdentifier: "ContentBlockingRules", + encodedContentRuleList: blockRules) { (contentRuleList, error) in + + if let error = error { + print(error.localizedDescription) + return + } + + let configuration = webView.configuration + configuration.userContentController.add(contentRuleList!) + + self.load(initialUrlRequest: initialUrlRequest, initialFile: initialFile, initialData: initialData) + } + return + } catch { + print(error.localizedDescription) + } + } + } + load(initialUrlRequest: initialUrlRequest, initialFile: initialFile, initialData: initialData) + } + else if windowId != nil { + webView.runWindowBeforeCreatedCallbacks() + } + } + + func load(initialUrlRequest: [String:Any?]?, initialFile: String?, initialData: [String: String?]?) { + guard let webView = webView() else { + return + } + + if let initialFile = initialFile { + do { + try webView.loadFile(assetFilePath: initialFile) + } + catch let error as NSError { + dump(error) + } + } + else if let initialData = initialData, let data = initialData["data"]!, + let mimeType = initialData["mimeType"]!, let encoding = initialData["encoding"]!, + let baseUrl = URL(string: initialData["baseUrl"]! ?? "about:blank") { + var allowingReadAccessToURL: URL? = nil + if let allowingReadAccessTo = webView.settings?.allowingReadAccessTo, baseUrl.scheme == "file" { + allowingReadAccessToURL = URL(string: allowingReadAccessTo) + if allowingReadAccessToURL?.scheme != "file" { + allowingReadAccessToURL = nil + } + } + webView.loadData(data: data, + mimeType: mimeType, + encoding: encoding, + baseUrl: baseUrl, + allowingReadAccessTo: allowingReadAccessToURL) + } + else if let initialUrlRequest = initialUrlRequest { + let urlRequest = URLRequest.init(fromPluginMap: initialUrlRequest) + var allowingReadAccessToURL: URL? = nil + if let allowingReadAccessTo = webView.settings?.allowingReadAccessTo, let url = urlRequest.url, url.scheme == "file" { + allowingReadAccessToURL = URL(string: allowingReadAccessTo) + if allowingReadAccessToURL?.scheme != "file" { + allowingReadAccessToURL = nil + } + } + webView.loadUrl(urlRequest: urlRequest, allowingReadAccessTo: allowingReadAccessToURL) + } + } + + // method added to fix: + // https://github.com/pichillilorenzo/flutter_inappwebview/issues/1837 + public func dispose(removeFromSuperview: Bool) { + if keepAliveId == nil { + if let webView = webView() { + webView.dispose() + if removeFromSuperview { + webView.removeFromSuperview() + } + } + if removeFromSuperview { + myView?.removeFromSuperview() + } + myView = nil + } + } + + public func dispose() { + dispose(removeFromSuperview: false) + } + + deinit { + debugPrint("FlutterWebViewController - dealloc") + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/FlutterWebViewFactory.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/FlutterWebViewFactory.swift new file mode 100755 index 00000000..216c06b3 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/FlutterWebViewFactory.swift @@ -0,0 +1,74 @@ +// +// FlutterWebViewFactory.swift +// flutter_inappwebview +// +// Created by Lorenzo on 13/11/18. +// + +import Flutter +import Foundation + +public class FlutterWebViewFactory: NSObject, FlutterPlatformViewFactory { + static let VIEW_TYPE_ID = "com.pichillilorenzo/flutter_inappwebview" + + private var plugin: SwiftFlutterPlugin + + init(plugin: SwiftFlutterPlugin) { + self.plugin = plugin + super.init() + } + + public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol { + return FlutterStandardMessageCodec.sharedInstance() + } + + public func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> FlutterPlatformView { + let arguments = args as? NSDictionary + var flutterWebView: FlutterWebViewController? + var id: Any = viewId + + let keepAliveId = arguments?["keepAliveId"] as? String + let headlessWebViewId = arguments?["headlessWebViewId"] as? String + let preventGestureDelay = arguments?["preventGestureDelay"] as? Bool ?? false + + if let headlessWebViewId = headlessWebViewId, + let headlessWebView = plugin.headlessInAppWebViewManager?.webViews[headlessWebViewId], + let platformView = headlessWebView?.disposeAndGetFlutterWebView(withFrame: frame) { + flutterWebView = platformView + flutterWebView?.keepAliveId = keepAliveId + } + + if let keepAliveId = keepAliveId, + flutterWebView == nil, + let keepAliveWebView = plugin.inAppWebViewManager?.keepAliveWebViews[keepAliveId] { + flutterWebView = keepAliveWebView + if let view = flutterWebView?.view() { + // remove from parent + view.removeFromSuperview() + } + } + + let shouldMakeInitialLoad = flutterWebView == nil + if flutterWebView == nil { + if let keepAliveId = keepAliveId { + id = keepAliveId + } + flutterWebView = FlutterWebViewController(plugin: plugin, + withFrame: frame, + viewIdentifier: id, + params: arguments!) + } + + if let keepAliveId = keepAliveId { + plugin.inAppWebViewManager?.keepAliveWebViews[keepAliveId] = flutterWebView! + } + + flutterWebView?.webView()?.preventGestureDelay = preventGestureDelay + + if shouldMakeInitialLoad { + flutterWebView?.makeInitialLoad(params: arguments!) + } + + return flutterWebView! + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/InAppWebView.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/InAppWebView.swift new file mode 100755 index 00000000..9a78f491 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/InAppWebView.swift @@ -0,0 +1,3336 @@ +// +// InAppWebView.swift +// flutter_inappwebview +// +// Created by Lorenzo on 21/10/18. +// + +import Flutter +import Foundation +import WebKit + +public class InAppWebView: WKWebView, UIScrollViewDelegate, WKUIDelegate, + WKNavigationDelegate, WKScriptMessageHandler, UIGestureRecognizerDelegate, + WKDownloadDelegate, + PullToRefreshDelegate, + Disposable { + static let METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_" + + var id: Any? // viewId + var plugin: SwiftFlutterPlugin? + var windowId: Int64? + var windowCreated = false + var windowBeforeCreatedCallbacks: [() -> ()] = [] + var inAppBrowserDelegate: InAppBrowserDelegate? + var channelDelegate: WebViewChannelDelegate? + var settings: InAppWebViewSettings? + var pullToRefreshControl: PullToRefreshControl? + var findInteractionController: FindInteractionController? + var webMessageChannels: [String: WebMessageChannel] = [:] + var webMessageListeners: [WebMessageListener] = [] + var currentOriginalUrl: URL? + var inFullscreen = false + var preventGestureDelay = false + + private static var sslCertificatesMap: [String: SslCertificate] = [:] // [URL host name : SslCertificate] + private static var credentialsProposed: [URLCredential] = [] + + var lastScrollX: CGFloat = 0 + var lastScrollY: CGFloat = 0 + + // Used to manage pauseTimers() and resumeTimers() + var isPausedTimers = false + var isPausedTimersCompletionHandler: (() -> Void)? + + var contextMenu: [String: Any]? + var initialUserScripts: [UserScript] = [] + + // https://github.com/mozilla-mobile/firefox-ios/blob/50531a7e9e4d459fb11d4fcb7d4322e08103501f/Client/Frontend/Browser/ContextMenuHelper.swift + fileprivate var nativeHighlightLongPressRecognizer: UILongPressGestureRecognizer? + fileprivate var nativeLoupeGesture: UILongPressGestureRecognizer? + var longPressRecognizer: UILongPressGestureRecognizer! + var recognizerForDisablingContextMenuOnLinks: UILongPressGestureRecognizer! + var lastLongPressTouchPoint: CGPoint? + + var panGestureRecognizer: UIPanGestureRecognizer! + + var lastTouchPoint: CGPoint? + var lastTouchPointTimestamp = Int64(Date().timeIntervalSince1970 * 1000) + + var contextMenuIsShowing = false + // flag used for the workaround to trigger onCreateContextMenu event as the same on Android + var onCreateContextMenuEventTriggeredWhenMenuDisabled = false + + var customIMPs: [IMP] = [] + + var callAsyncJavaScriptBelowIOS14Results: [String:((Any?) -> Void)] = [:] + + var oldZoomScale = Float(1.0) + + fileprivate var interceptOnlyAsyncAjaxRequestsPluginScript: PluginScript? + + init(id: Any?, plugin: SwiftFlutterPlugin?, frame: CGRect, configuration: WKWebViewConfiguration, + contextMenu: [String: Any]?, userScripts: [UserScript] = []) { + super.init(frame: frame, configuration: configuration) + self.id = id + self.plugin = plugin + if let id = id, let registrar = plugin?.registrar { + let channel = FlutterMethodChannel(name: InAppWebView.METHOD_CHANNEL_NAME_PREFIX + String(describing: id), + binaryMessenger: registrar.messenger()) + self.channelDelegate = WebViewChannelDelegate(webView: self, channel: channel) + } + self.contextMenu = contextMenu + self.initialUserScripts = userScripts + uiDelegate = self + navigationDelegate = self + scrollView.delegate = self + longPressRecognizer = UILongPressGestureRecognizer() + longPressRecognizer.delegate = self + longPressRecognizer.addTarget(self, action: #selector(longPressGestureDetected)) + recognizerForDisablingContextMenuOnLinks = UILongPressGestureRecognizer() + recognizerForDisablingContextMenuOnLinks.delegate = self + recognizerForDisablingContextMenuOnLinks.addTarget(self, action: #selector(longPressGestureDetected)) + recognizerForDisablingContextMenuOnLinks?.minimumPressDuration = 0.45 + panGestureRecognizer = UIPanGestureRecognizer() + panGestureRecognizer.delegate = self + panGestureRecognizer.addTarget(self, action: #selector(endDraggingDetected)) + } + + override public var frame: CGRect { + get { + return super.frame + } + set { + super.frame = newValue + + self.scrollView.contentInset = .zero + if #available(iOS 11, *) { + // Above iOS 11, adjust contentInset to compensate the adjustedContentInset so the sum will + // always be 0. + if (scrollView.adjustedContentInset != UIEdgeInsets.zero) { + let insetToAdjust = self.scrollView.adjustedContentInset + scrollView.contentInset = UIEdgeInsets(top: -insetToAdjust.top, left: -insetToAdjust.left, + bottom: -insetToAdjust.bottom, right: -insetToAdjust.right) + } + } + } + } + + required public init(coder aDecoder: NSCoder) { + super.init(coder: aDecoder)! + } + + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + // BVC KVO events for all changes on the webview will call this. + // It is called frequently during a page load (particularly on progress changes and URL changes). + // As of iOS 12, WKContentView gesture setup is async, but it has been called by the time + // the webview is ready to load an URL. After this has happened, we can override the gesture. + func replaceGestureHandlerIfNeeded() { + DispatchQueue.main.async { + if self.gestureRecognizerWithDescriptionFragment("InAppWebView") == nil { + self.replaceWebViewLongPress() + } + } + } + + private func replaceWebViewLongPress() { + // WebKit installs gesture handlers async. If `replaceWebViewLongPress` is called after a wkwebview in most cases a small delay is sufficient + // See also https://bugs.webkit.org/show_bug.cgi?id=193366 + nativeHighlightLongPressRecognizer = gestureRecognizerWithDescriptionFragment("action=_highlightLongPressRecognized:") + nativeLoupeGesture = gestureRecognizerWithDescriptionFragment("action=loupeGesture:") + + if let nativeLongPressRecognizer = gestureRecognizerWithDescriptionFragment("action=_longPressRecognized:") { + nativeLongPressRecognizer.removeTarget(nil, action: nil) + nativeLongPressRecognizer.addTarget(self, action: #selector(self.longPressGestureDetected)) + } + } + + private func gestureRecognizerWithDescriptionFragment(_ descriptionFragment: String) -> UILongPressGestureRecognizer? { + let result = self.scrollView.subviews.compactMap({ $0.gestureRecognizers }).joined().first(where: { + return (($0 as? UILongPressGestureRecognizer) != nil) && $0.description.contains(descriptionFragment) + }) + return result as? UILongPressGestureRecognizer + } + + @objc func longPressGestureDetected(_ sender: UIGestureRecognizer) { + if sender.state == .cancelled { + return + } + + guard sender.state == .began else { + return + } + + if sender == recognizerForDisablingContextMenuOnLinks, + let settings = settings, !settings.disableLongPressContextMenuOnLinks { + return + } + + if sender == longPressRecognizer { + // To prevent the tapped link from proceeding with navigation, "cancel" the native WKWebView + // `_highlightLongPressRecognizer`. This preserves the original behavior as seen here: + // https://github.com/WebKit/webkit/blob/d591647baf54b4b300ca5501c21a68455429e182/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm#L1600-L1614 + if let nativeHighlightLongPressRecognizer = nativeHighlightLongPressRecognizer, + nativeHighlightLongPressRecognizer.isEnabled { + nativeHighlightLongPressRecognizer.isEnabled = false + nativeHighlightLongPressRecognizer.isEnabled = true + } + } + + //Finding actual touch location in webView + var touchLocation = sender.location(in: self) + touchLocation.x -= scrollView.contentInset.left + touchLocation.y -= scrollView.contentInset.top + touchLocation.x /= scrollView.zoomScale + touchLocation.y /= scrollView.zoomScale + + lastLongPressTouchPoint = touchLocation + + evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._findElementsAtPoint(\(touchLocation.x),\(touchLocation.y))", completionHandler: {(value, error) in + if error != nil { + print("Long press gesture recognizer error: \(error?.localizedDescription ?? "")") + } else if let value = value as? [String: Any?] { + let hitTestResult = HitTestResult.fromMap(map: value)! + self.nativeLoupeGesture = self.gestureRecognizerWithDescriptionFragment("action=loupeGesture:") + + if sender == self.recognizerForDisablingContextMenuOnLinks, + hitTestResult.type.rawValue > HitTestResultType.unknownType.rawValue, + hitTestResult.type.rawValue < HitTestResultType.editTextType.rawValue { + self.nativeLoupeGesture?.isEnabled = false + self.nativeLoupeGesture?.isEnabled = true + } else { + self.channelDelegate?.onLongPressHitTestResult(hitTestResult: hitTestResult) + } + } else if sender == self.longPressRecognizer { + self.channelDelegate?.onLongPressHitTestResult(hitTestResult: HitTestResult(type: .unknownType, extra: nil)) + } + }) + } + + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + lastTouchPoint = point + lastTouchPointTimestamp = Int64(Date().timeIntervalSince1970 * 1000) + SharedLastTouchPointTimestamp[self] = lastTouchPointTimestamp + + // re-build context menu items for the current webview + UIMenuController.shared.menuItems = [] + if let menu = self.contextMenu { + if let menuItems = menu["menuItems"] as? [[String : Any]] { + for menuItem in menuItems { + let id = menuItem["id"]! + let title = menuItem["title"] as! String + let targetMethodName = "onContextMenuActionItemClicked-" + String(self.hash) + "-" + + (id is Int64 ? String(id as! Int64) : id as! String) + if !self.responds(to: Selector(targetMethodName)) { + let customAction: () -> Void = { + self.channelDelegate?.onContextMenuActionItemClicked(id: id, title: title) + if #available(iOS 16.0, *) { + if #unavailable(iOS 16.4) { + self.onHideContextMenu() + } + } + } + let castedCustomAction: AnyObject = unsafeBitCast(customAction as @convention(block) () -> Void, to: AnyObject.self) + let swizzledImplementation = imp_implementationWithBlock(castedCustomAction) + class_addMethod(InAppWebView.self, Selector(targetMethodName), swizzledImplementation, nil) + self.customIMPs.append(swizzledImplementation) + } + let item = UIMenuItem(title: title, action: Selector(targetMethodName)) + UIMenuController.shared.menuItems!.append(item) + } + } + } + + // https://github.com/pichillilorenzo/flutter_inappwebview/pull/1665 + if preventGestureDelay, let gestures = superview?.superview?.gestureRecognizers { + for gesture in gestures { + if NSStringFromClass(type(of: gesture)) == "DelayingGestureRecognizer" { + gesture.isEnabled = false + } + } + } + + return super.hitTest(point, with: event) + } + + @available(iOS 13.0, *) + public override func buildMenu(with builder: UIMenuBuilder) { + if #available(iOS 16.0, *) { + if let menu = contextMenu { + let contextMenuSettings = ContextMenuSettings() + if let contextMenuSettingsMap = menu["settings"] as? [String: Any?] { + let _ = contextMenuSettings.parse(settings: contextMenuSettingsMap) + if contextMenuSettings.hideDefaultSystemContextMenuItems { + builder.remove(menu: .lookup) + } + } + } + + if #unavailable(iOS 16.4), settings?.disableContextMenu == false { + contextMenuIsShowing = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { + self.onCreateContextMenu() + } + } + } + super.buildMenu(with: builder) + } + + @available(iOS 16.4, *) + public func webView(_ webView: WKWebView, willPresentEditMenuWithAnimator animator: UIEditMenuInteractionAnimating) { + onCreateContextMenu() + } + + @available(iOS 16.4, *) + public func webView(_ webView: WKWebView, willDismissEditMenuWithAnimator animator: UIEditMenuInteractionAnimating) { + onHideContextMenu() + } + + public override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { + var needCheck = sender is UIMenuController + if #available(iOS 13.0, *) { + needCheck = sender is UIMenuElement || sender is UIMenuController + } + + if needCheck { + if settings?.disableContextMenu == true { + if !onCreateContextMenuEventTriggeredWhenMenuDisabled { + // workaround to trigger onCreateContextMenu event as the same on Android + onCreateContextMenu() + onCreateContextMenuEventTriggeredWhenMenuDisabled = true + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.onCreateContextMenuEventTriggeredWhenMenuDisabled = false + } + } + return false + } + + if let menu = contextMenu { + let contextMenuSettings = ContextMenuSettings() + if let contextMenuSettingsMap = menu["settings"] as? [String: Any?] { + let _ = contextMenuSettings.parse(settings: contextMenuSettingsMap) + if !action.description.starts(with: "onContextMenuActionItemClicked-") && contextMenuSettings.hideDefaultSystemContextMenuItems { + return false + } + } + } + + if contextMenuIsShowing, !action.description.starts(with: "onContextMenuActionItemClicked-") { + let id = action.description.compactMap({ $0.asciiValue?.description }).joined() + channelDelegate?.onContextMenuActionItemClicked(id: id, title: action.description) + if #available(iOS 16.0, *) { + if #unavailable(iOS 16.4) { + onHideContextMenu() + } + } + } + } + return super.canPerformAction(action, withSender: sender) + } + + // For some reasons, using the scrollViewDidEndDragging event, in some rare cases, could block + // the scroll gesture + @objc func endDraggingDetected() { + // detect end dragging + if panGestureRecognizer.state == .ended { + // fix for pull-to-refresh jittering when the touch drag event is held + if let pullToRefreshControl = pullToRefreshControl, + pullToRefreshControl.shouldCallOnRefresh { + pullToRefreshControl.onRefresh() + } + } + } + + public func prepare() { + scrollView.addGestureRecognizer(self.longPressRecognizer) + scrollView.addGestureRecognizer(self.recognizerForDisablingContextMenuOnLinks) + scrollView.addGestureRecognizer(self.panGestureRecognizer) + scrollView.addObserver(self, forKeyPath: #keyPath(UIScrollView.contentOffset), options: [.new, .old], context: nil) + scrollView.addObserver(self, forKeyPath: #keyPath(UIScrollView.zoomScale), options: [.new, .old], context: nil) + scrollView.addObserver(self, forKeyPath: #keyPath(UIScrollView.contentSize), options: [.new, .old], context: nil) + + addObserver(self, + forKeyPath: #keyPath(WKWebView.estimatedProgress), + options: .new, + context: nil) + + addObserver(self, + forKeyPath: #keyPath(WKWebView.url), + options: [.new, .old], + context: nil) + + addObserver(self, + forKeyPath: #keyPath(WKWebView.title), + options: [.new, .old], + context: nil) + + if #available(iOS 15.0, *) { + addObserver(self, + forKeyPath: #keyPath(WKWebView.cameraCaptureState), + options: [.new, .old], + context: nil) + + addObserver(self, + forKeyPath: #keyPath(WKWebView.microphoneCaptureState), + options: [.new, .old], + context: nil) + } + + if #unavailable(iOS 16.0) { + NotificationCenter.default.addObserver( + self, + selector: #selector(onCreateContextMenu), + name: UIMenuController.willShowMenuNotification, + object: nil) + + NotificationCenter.default.addObserver( + self, + selector: #selector(onHideContextMenu), + name: UIMenuController.didHideMenuNotification, + object: nil) + } + + // TODO: Still not working on iOS 16.0! +// if #available(iOS 16.0, *) { +// addObserver(self, +// forKeyPath: #keyPath(WKWebView.fullscreenState), +// options: .new, +// context: nil) +// } else { + // listen for videos playing in fullscreen + NotificationCenter.default.addObserver(self, + selector: #selector(onEnterFullscreen(_:)), + name: UIWindow.didBecomeVisibleNotification, + object: window) + + // listen for videos stopping to play in fullscreen + NotificationCenter.default.addObserver(self, + selector: #selector(onExitFullscreen(_:)), + name: UIWindow.didBecomeHiddenNotification, + object: window) +// } + + if let settings = settings { + if settings.transparentBackground { + isOpaque = false + backgroundColor = UIColor.clear + scrollView.backgroundColor = UIColor.clear + } + + // prevent webView from bouncing + if settings.disallowOverScroll { + if responds(to: #selector(getter: scrollView)) { + scrollView.bounces = false + } + else { + for subview: UIView in subviews { + if subview is UIScrollView { + (subview as! UIScrollView).bounces = false + } + } + } + } + + if #available(iOS 11.0, *) { + accessibilityIgnoresInvertColors = settings.accessibilityIgnoresInvertColors + scrollView.contentInsetAdjustmentBehavior = + UIScrollView.ContentInsetAdjustmentBehavior.init(rawValue: settings.contentInsetAdjustmentBehavior)! + } + + allowsBackForwardNavigationGestures = settings.allowsBackForwardNavigationGestures + if #available(iOS 9.0, *) { + allowsLinkPreview = settings.allowsLinkPreview + if !settings.userAgent.isEmpty { + customUserAgent = settings.userAgent + } + } + + if #available(iOS 13.0, *) { + scrollView.automaticallyAdjustsScrollIndicatorInsets = settings.automaticallyAdjustsScrollIndicatorInsets + } + + scrollView.showsVerticalScrollIndicator = !settings.disableVerticalScroll + scrollView.showsHorizontalScrollIndicator = !settings.disableHorizontalScroll + scrollView.showsVerticalScrollIndicator = settings.verticalScrollBarEnabled + scrollView.showsHorizontalScrollIndicator = settings.horizontalScrollBarEnabled + scrollView.isScrollEnabled = !(settings.disableVerticalScroll && settings.disableHorizontalScroll) + scrollView.isDirectionalLockEnabled = settings.isDirectionalLockEnabled + + scrollView.decelerationRate = Util.getDecelerationRate(type: settings.decelerationRate) + scrollView.alwaysBounceVertical = settings.alwaysBounceVertical + scrollView.alwaysBounceHorizontal = settings.alwaysBounceHorizontal + scrollView.scrollsToTop = settings.scrollsToTop + scrollView.isPagingEnabled = settings.isPagingEnabled + scrollView.maximumZoomScale = CGFloat(settings.maximumZoomScale) + scrollView.minimumZoomScale = CGFloat(settings.minimumZoomScale) + + if #available(iOS 14.0, *) { + mediaType = settings.mediaType + pageZoom = CGFloat(settings.pageZoom) + } + + if #available(iOS 15.0, *) { + if let underPageBackgroundColor = settings.underPageBackgroundColor, !underPageBackgroundColor.isEmpty { + self.underPageBackgroundColor = UIColor(hexString: underPageBackgroundColor) + } + } + + if #available(iOS 15.5, *) { + if let minViewportInset = settings.minimumViewportInset, let maxViewportInset = settings.maximumViewportInset { + setMinimumViewportInset(minViewportInset, maximumViewportInset: maxViewportInset) + } + } + + if #available(iOS 16.0, *) { + isFindInteractionEnabled = settings.isFindInteractionEnabled + } + + if #available(iOS 16.4, *) { + isInspectable = settings.isInspectable + } + + if settings.clearCache { + clearCache() + } + } + + prepareAndAddUserScripts() + + if windowId != nil { + // The new created window webview has the same WKWebViewConfiguration variable reference. + // So, we cannot set another WKWebViewConfiguration for it unfortunately! + // This is a limitation of the official WebKit API. + return + } + + configuration.preferences = WKPreferences() + if let settings = settings { + if #available(iOS 9.0, *) { + configuration.allowsAirPlayForMediaPlayback = settings.allowsAirPlayForMediaPlayback + configuration.allowsPictureInPictureMediaPlayback = settings.allowsPictureInPictureMediaPlayback + } + + configuration.preferences.javaScriptCanOpenWindowsAutomatically = settings.javaScriptCanOpenWindowsAutomatically + configuration.preferences.minimumFontSize = CGFloat(settings.minimumFontSize) + + if #available(iOS 13.0, *) { + configuration.preferences.isFraudulentWebsiteWarningEnabled = settings.isFraudulentWebsiteWarningEnabled + configuration.defaultWebpagePreferences.preferredContentMode = WKWebpagePreferences.ContentMode(rawValue: settings.preferredContentMode)! + } + + configuration.preferences.javaScriptEnabled = settings.javaScriptEnabled + if #available(iOS 14.0, *) { + configuration.defaultWebpagePreferences.allowsContentJavaScript = settings.javaScriptEnabled + } + + if #available(iOS 15.0, *) { + configuration.preferences.isTextInteractionEnabled = settings.isTextInteractionEnabled + } + if #available(iOS 15.4, *) { + configuration.preferences.isSiteSpecificQuirksModeEnabled = settings.isSiteSpecificQuirksModeEnabled + configuration.preferences.isElementFullscreenEnabled = settings.isElementFullscreenEnabled + } + if #available(iOS 16.4, *) { + configuration.preferences.shouldPrintBackgrounds = settings.shouldPrintBackgrounds + } + } + } + + public func prepareAndAddUserScripts() -> Void { + if windowId != nil { + // The new created window webview has the same WKWebViewConfiguration variable reference. + // So, we cannot set another WKWebViewConfiguration for it unfortunately! + // This is a limitation of the official WebKit API. + return + } + configuration.userContentController = WKUserContentController() + configuration.userContentController.initialize() + + if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled { + return + } + + configuration.userContentController.addPluginScript(PROMISE_POLYFILL_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(CONSOLE_LOG_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(PRINT_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(FIND_ELEMENTS_AT_POINT_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(FIND_TEXT_HIGHLIGHT_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_PLUGIN_SCRIPT) + if let settings = settings { + interceptOnlyAsyncAjaxRequestsPluginScript = createInterceptOnlyAsyncAjaxRequestsPluginScript(onlyAsync: settings.interceptOnlyAsyncAjaxRequests) + if settings.useShouldInterceptAjaxRequest { + if let interceptOnlyAsyncAjaxRequestsPluginScript = interceptOnlyAsyncAjaxRequestsPluginScript { + configuration.userContentController.addPluginScript(interceptOnlyAsyncAjaxRequestsPluginScript) + } + configuration.userContentController.addPluginScript(INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT) + } + if settings.useShouldInterceptFetchRequest { + configuration.userContentController.addPluginScript(INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT) + } + if settings.useOnLoadResource { + configuration.userContentController.addPluginScript(ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT) + } + if !settings.supportZoom { + configuration.userContentController.addPluginScript(NOT_SUPPORT_ZOOM_JS_PLUGIN_SCRIPT) + } else if settings.enableViewportScale { + configuration.userContentController.addPluginScript(ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT) + } + } + configuration.userContentController.removeScriptMessageHandler(forName: "onCallAsyncJavaScriptResultBelowIOS14Received") + configuration.userContentController.add(self, name: "onCallAsyncJavaScriptResultBelowIOS14Received") + configuration.userContentController.removeScriptMessageHandler(forName: "onWebMessagePortMessageReceived") + configuration.userContentController.add(self, name: "onWebMessagePortMessageReceived") + configuration.userContentController.removeScriptMessageHandler(forName: "onWebMessageListenerPostMessageReceived") + configuration.userContentController.add(self, name: "onWebMessageListenerPostMessageReceived") + configuration.userContentController.addUserOnlyScripts(initialUserScripts) + configuration.userContentController.sync(scriptMessageHandler: self) + } + + public static func preWKWebViewConfiguration(settings: InAppWebViewSettings?) -> WKWebViewConfiguration { + let configuration = WKWebViewConfiguration() + + configuration.processPool = WKProcessPoolManager.sharedProcessPool + + if let settings = settings { + configuration.allowsInlineMediaPlayback = settings.allowsInlineMediaPlayback + configuration.suppressesIncrementalRendering = settings.suppressesIncrementalRendering + configuration.selectionGranularity = WKSelectionGranularity.init(rawValue: settings.selectionGranularity)! + + if settings.allowUniversalAccessFromFileURLs { + configuration.setValue(settings.allowUniversalAccessFromFileURLs, forKey: "allowUniversalAccessFromFileURLs") + } + + if settings.allowFileAccessFromFileURLs { + configuration.preferences.setValue(settings.allowFileAccessFromFileURLs, forKey: "allowFileAccessFromFileURLs") + } + + if #available(iOS 9.0, *) { + if settings.incognito { + configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent() + } else if settings.cacheEnabled { + configuration.websiteDataStore = GopeedProfiles.store(settings.gopeedProfileId) ?? WKWebsiteDataStore.default() + } + if !settings.applicationNameForUserAgent.isEmpty { + if let applicationNameForUserAgent = configuration.applicationNameForUserAgent { + configuration.applicationNameForUserAgent = applicationNameForUserAgent + " " + settings.applicationNameForUserAgent + } + } + } + + if #available(iOS 10.0, *) { + configuration.ignoresViewportScaleLimits = settings.ignoresViewportScaleLimits + + var dataDetectorTypes = WKDataDetectorTypes.init(rawValue: 0) + for type in settings.dataDetectorTypes { + let dataDetectorType = Util.getDataDetectorType(type: type) + dataDetectorTypes = WKDataDetectorTypes(rawValue: dataDetectorTypes.rawValue | dataDetectorType.rawValue) + } + configuration.dataDetectorTypes = dataDetectorTypes + + configuration.mediaTypesRequiringUserActionForPlayback = settings.mediaPlaybackRequiresUserGesture ? .all : [] + } else { + // Fallback on earlier versions + configuration.mediaPlaybackRequiresUserAction = settings.mediaPlaybackRequiresUserGesture + } + + if #available(iOS 11.0, *) { + for scheme in settings.resourceCustomSchemes { + configuration.setURLSchemeHandler(CustomSchemeHandler(), forURLScheme: scheme) + } + if settings.sharedCookiesEnabled { + // More info to sending cookies with WKWebView + // https://stackoverflow.com/questions/26573137/can-i-set-the-cookies-to-be-used-by-a-wkwebview/26577303#26577303 + // Set Cookies in iOS 11 and above, initialize websiteDataStore before setting cookies + // See also https://forums.developer.apple.com/thread/97194 + // check if websiteDataStore has not been initialized before + if(!settings.incognito && !settings.cacheEnabled) { + configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent() + } + for cookie in HTTPCookieStorage.shared.cookies ?? [] { + configuration.websiteDataStore.httpCookieStore.setCookie(cookie, completionHandler: nil) + } + } + } + + if #available(iOS 14.0, *) { + configuration.limitsNavigationsToAppBoundDomains = settings.limitsNavigationsToAppBoundDomains + } + + if #available(iOS 15.0, *) { + configuration.upgradeKnownHostsToHTTPS = settings.upgradeKnownHostsToHTTPS + } + } + + return configuration + } + + @objc func onCreateContextMenu() { + let mapSorted = SharedLastTouchPointTimestamp.sorted { $0.value > $1.value } + if (mapSorted.first?.key != self) { + return + } + + contextMenuIsShowing = true + + let hitTestResult = HitTestResult(type: .unknownType, extra: nil) + + if let lastLongPressTouhLocation = lastLongPressTouchPoint { + if configuration.preferences.javaScriptEnabled { + self.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._findElementsAtPoint(\(lastLongPressTouhLocation.x),\(lastLongPressTouhLocation.y))", completionHandler: {(value, error) in + if error != nil { + print("Long press gesture recognizer error: \(error?.localizedDescription ?? "")") + } else if let value = value as? [String: Any?] { + self.channelDelegate?.onCreateContextMenu(hitTestResult: HitTestResult.fromMap(map: value) ?? hitTestResult) + } else { + self.channelDelegate?.onCreateContextMenu(hitTestResult: hitTestResult) + } + }) + } else { + channelDelegate?.onCreateContextMenu(hitTestResult: hitTestResult) + } + } else { + channelDelegate?.onCreateContextMenu(hitTestResult: hitTestResult) + } + } + + @objc func onHideContextMenu() { + if contextMenuIsShowing == false { + return + } + contextMenuIsShowing = false + channelDelegate?.onHideContextMenu() + } + + override public func observeValue(forKeyPath keyPath: String?, of object: Any?, + change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == #keyPath(WKWebView.estimatedProgress) { + initializeWindowIdJS() + let progress = Int(estimatedProgress * 100) + channelDelegate?.onProgressChanged(progress: progress) + inAppBrowserDelegate?.didChangeProgress(progress: estimatedProgress) + } else if keyPath == #keyPath(WKWebView.url) && change?[.newKey] is URL { + initializeWindowIdJS() + let newUrl = change?[NSKeyValueChangeKey.newKey] as? URL + channelDelegate?.onUpdateVisitedHistory(url: newUrl?.absoluteString, isReload: nil) + inAppBrowserDelegate?.didUpdateVisitedHistory(url: newUrl) + } else if keyPath == #keyPath(WKWebView.title) && change?[.newKey] is String { + let newTitle = change?[.newKey] as? String + channelDelegate?.onTitleChanged(title: newTitle) + inAppBrowserDelegate?.didChangeTitle(title: newTitle) + } else if keyPath == #keyPath(UIScrollView.contentOffset) { + let newContentOffset = change?[.newKey] as? CGPoint + let oldContentOffset = change?[.oldKey] as? CGPoint + let startedByUser = scrollView.isDragging || scrollView.isDecelerating + if newContentOffset != oldContentOffset { + DispatchQueue.main.async { + self.onScrollChanged(startedByUser: startedByUser, oldContentOffset: oldContentOffset) + } + } + } else if keyPath == #keyPath(UIScrollView.contentSize) { + if let newContentSize = change?[.newKey] as? CGSize, + let oldContentSize = change?[.oldKey] as? CGSize, + newContentSize != oldContentSize { + DispatchQueue.main.async { + self.onContentSizeChanged(oldContentSize: oldContentSize) + } + } + } + else if #available(iOS 15.0, *) { + if keyPath == #keyPath(WKWebView.cameraCaptureState) || keyPath == #keyPath(WKWebView.microphoneCaptureState) { + var oldState: WKMediaCaptureState? = nil + if let oldValue = change?[.oldKey] as? Int { + oldState = WKMediaCaptureState.init(rawValue: oldValue) + } + var newState: WKMediaCaptureState? = nil + if let newValue = change?[.newKey] as? Int { + newState = WKMediaCaptureState.init(rawValue: newValue) + } + if oldState != newState { + if keyPath == #keyPath(WKWebView.cameraCaptureState) { + channelDelegate?.onCameraCaptureStateChanged(oldState: oldState, newState: newState) + } else { + channelDelegate?.onMicrophoneCaptureStateChanged(oldState: oldState, newState: newState) + } + } + } + } else if #available(iOS 16.0, *) { + // TODO: Still not working on iOS 16.0! +// if keyPath == #keyPath(WKWebView.fullscreenState) { +// if fullscreenState == .enteringFullscreen { +// channelDelegate?.onEnterFullscreen() +// } else if fullscreenState == .exitingFullscreen { +// channelDelegate?.onExitFullscreen() +// } +// } + } + replaceGestureHandlerIfNeeded() + } + + public func initializeWindowIdJS() { + if let windowId = windowId { + if #available(iOS 14.0, *) { + let contentWorlds = configuration.userContentController.getContentWorlds(with: windowId) + for contentWorld in contentWorlds { + let source = WINDOW_ID_INITIALIZE_JS_SOURCE.replacingOccurrences(of: PluginScriptsUtil.VAR_PLACEHOLDER_VALUE, with: String(windowId)) + evaluateJavascript(source: source, contentWorld: contentWorld) + } + } else { + let source = WINDOW_ID_INITIALIZE_JS_SOURCE.replacingOccurrences(of: PluginScriptsUtil.VAR_PLACEHOLDER_VALUE, with: String(windowId)) + evaluateJavascript(source: source) + } + } + } + + public func goBackOrForward(steps: Int) { + if canGoBackOrForward(steps: steps) { + if (steps > 0) { + let index = steps - 1 + go(to: self.backForwardList.forwardList[index]) + } + else if (steps < 0){ + let backListLength = self.backForwardList.backList.count + let index = backListLength + steps + go(to: self.backForwardList.backList[index]) + } + } + } + + public func canGoBackOrForward(steps: Int) -> Bool { + let currentIndex = self.backForwardList.backList.count + return (steps >= 0) + ? steps <= self.backForwardList.forwardList.count + : currentIndex + steps >= 0 + } + + @available(iOS 11.0, *) + public func takeScreenshot (with: [String: Any?]?, completionHandler: @escaping (_ screenshot: Data?) -> Void) { + var snapshotConfiguration: WKSnapshotConfiguration? = nil + if let with = with { + snapshotConfiguration = WKSnapshotConfiguration() + if let rect = with["rect"] as? [String: Double] { + snapshotConfiguration!.rect = CGRect.fromMap(map: rect) + } + if let snapshotWidth = with["snapshotWidth"] as? Double { + snapshotConfiguration!.snapshotWidth = NSNumber(value: snapshotWidth) + } + if #available(iOS 13.0, *), let afterScreenUpdates = with["afterScreenUpdates"] as? Bool { + snapshotConfiguration!.afterScreenUpdates = afterScreenUpdates + } + } + takeSnapshot(with: snapshotConfiguration, completionHandler: {(image, error) -> Void in + var imageData: Data? = nil + if let screenshot = image { + if let with = with { + switch with["compressFormat"] as! String { + case "JPEG": + let quality = Float(with["quality"] as! Int) / 100 + imageData = screenshot.jpegData(compressionQuality: CGFloat(quality)) + break + case "PNG": + imageData = screenshot.pngData() + break + default: + imageData = screenshot.pngData() + } + } + else { + imageData = screenshot.pngData() + } + } + completionHandler(imageData) + }) + } + + @available(iOS 14.0, *) + public func createPdf (configuration: [String: Any?]?, completionHandler: @escaping (_ pdf: Data?) -> Void) { + let pdfConfiguration: WKPDFConfiguration = .init() + if let configuration = configuration { + if let rect = configuration["rect"] as? [String: Double] { + pdfConfiguration.rect = CGRect.fromMap(map: rect) + } + } + createPDF(configuration: pdfConfiguration) { (result) in + switch (result) { + case .success(let data): + completionHandler(data) + return + case .failure(let error): + print(error.localizedDescription) + completionHandler(nil) + return + } + } + } + + @available(iOS 14.0, *) + public func createWebArchiveData (dataCompletionHandler: @escaping (_ webArchiveData: Data?) -> Void) { + createWebArchiveData(completionHandler: { (result) in + switch (result) { + case .success(let data): + dataCompletionHandler(data) + return + case .failure(let error): + print(error.localizedDescription) + dataCompletionHandler(nil) + return + } + }) + } + + @available(iOS 14.0, *) + public func saveWebArchive (filePath: String, autoname: Bool, completionHandler: @escaping (_ path: String?) -> Void) { + createWebArchiveData(dataCompletionHandler: { (webArchiveData) in + if let webArchiveData = webArchiveData { + var localUrl = URL(fileURLWithPath: filePath) + if autoname { + if let url = self.url { + // tries to mimic Android saveWebArchive method + let invalidCharacters = CharacterSet(charactersIn: "\\/:*?\"<>|") + .union(.newlines) + .union(.illegalCharacters) + .union(.controlCharacters) + + let currentPageUrlFileName = url.path + .components(separatedBy: invalidCharacters) + .joined(separator: "") + + let fullPath = filePath + "/" + currentPageUrlFileName + ".webarchive" + localUrl = URL(fileURLWithPath: fullPath) + } else { + completionHandler(nil) + return + } + } + do { + try webArchiveData.write(to: localUrl) + completionHandler(localUrl.path) + } catch { + // Catch any errors + print(error.localizedDescription) + completionHandler(nil) + } + } else { + completionHandler(nil) + } + }) + } + + public func loadUrl(urlRequest: URLRequest, allowingReadAccessTo: URL?) { + let url = urlRequest.url! + + if #available(iOS 9.0, *), let allowingReadAccessTo = allowingReadAccessTo, url.scheme == "file", allowingReadAccessTo.scheme == "file" { + loadFileURL(url, allowingReadAccessTo: allowingReadAccessTo) + } else { + load(urlRequest) + } + } + + public func postUrl(url: URL, postData: Data) { + var request = URLRequest(url: url) + + request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + request.httpMethod = "POST" + request.httpBody = postData + load(request) + } + + public func loadData(data: String, mimeType: String, encoding: String, baseUrl: URL, allowingReadAccessTo: URL?) { + if #available(iOS 9.0, *), let allowingReadAccessTo = allowingReadAccessTo, baseUrl.scheme == "file", allowingReadAccessTo.scheme == "file" { + loadFileURL(baseUrl, allowingReadAccessTo: allowingReadAccessTo) + } + + if #available(iOS 9.0, *) { + load(data.data(using: .utf8)!, mimeType: mimeType, characterEncodingName: encoding, baseURL: baseUrl) + } else { + loadHTMLString(data, baseURL: baseUrl) + } + } + + public func loadFile(assetFilePath: String) throws { + if let plugin = plugin { + let assetURL = try Util.getUrlAsset(plugin: plugin, assetFilePath: assetFilePath) + let urlRequest = URLRequest(url: assetURL) + loadUrl(urlRequest: urlRequest, allowingReadAccessTo: nil) + } + } + + func setSettings(newSettings: InAppWebViewSettings, newSettingsMap: [String: Any]) { + + // MUST be the first! In this way, all the settings that uses evaluateJavaScript can be applied/blocked! + if #available(iOS 13.0, *) { + if newSettingsMap["applePayAPIEnabled"] != nil && settings?.applePayAPIEnabled != newSettings.applePayAPIEnabled { + if let settings = settings { + settings.applePayAPIEnabled = newSettings.applePayAPIEnabled + } + if !newSettings.applePayAPIEnabled { + // re-add WKUserScripts for the next page load + prepareAndAddUserScripts() + } else { + configuration.userContentController.removeAllUserScripts() + } + } + } + + if newSettingsMap["transparentBackground"] != nil && settings?.transparentBackground != newSettings.transparentBackground { + if newSettings.transparentBackground { + isOpaque = false + backgroundColor = UIColor.clear + scrollView.backgroundColor = UIColor.clear + } else { + isOpaque = true + backgroundColor = nil + scrollView.backgroundColor = UIColor(red: 1, green: 1, blue: 1, alpha: 1) + } + } + + if newSettingsMap["disallowOverScroll"] != nil && settings?.disallowOverScroll != newSettings.disallowOverScroll { + if responds(to: #selector(getter: scrollView)) { + scrollView.bounces = !newSettings.disallowOverScroll + } + else { + for subview: UIView in subviews { + if subview is UIScrollView { + (subview as! UIScrollView).bounces = !newSettings.disallowOverScroll + } + } + } + } + + if #available(iOS 9.0, *) { + if (newSettingsMap["incognito"] != nil && settings?.incognito != newSettings.incognito && newSettings.incognito) { + configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent() + } else if (newSettingsMap["cacheEnabled"] != nil && settings?.cacheEnabled != newSettings.cacheEnabled && newSettings.cacheEnabled) { + configuration.websiteDataStore = GopeedProfiles.store(settings?.gopeedProfileId ?? "") ?? WKWebsiteDataStore.default() + } + } + + if #available(iOS 11.0, *) { + if (newSettingsMap["sharedCookiesEnabled"] != nil && settings?.sharedCookiesEnabled != newSettings.sharedCookiesEnabled && newSettings.sharedCookiesEnabled) { + if(!newSettings.incognito && !newSettings.cacheEnabled) { + configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent() + } + for cookie in HTTPCookieStorage.shared.cookies ?? [] { + configuration.websiteDataStore.httpCookieStore.setCookie(cookie, completionHandler: nil) + } + } + if newSettingsMap["accessibilityIgnoresInvertColors"] != nil && settings?.accessibilityIgnoresInvertColors != newSettings.accessibilityIgnoresInvertColors { + accessibilityIgnoresInvertColors = newSettings.accessibilityIgnoresInvertColors + } + if newSettingsMap["contentInsetAdjustmentBehavior"] != nil && settings?.contentInsetAdjustmentBehavior != newSettings.contentInsetAdjustmentBehavior { + scrollView.contentInsetAdjustmentBehavior = + UIScrollView.ContentInsetAdjustmentBehavior.init(rawValue: newSettings.contentInsetAdjustmentBehavior)! + } + } + + if newSettingsMap["enableViewportScale"] != nil && settings?.enableViewportScale != newSettings.enableViewportScale { + if !newSettings.enableViewportScale { + if configuration.userContentController.userScripts.contains(ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT) { + configuration.userContentController.removePluginScript(ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT) + evaluateJavaScript(NOT_ENABLE_VIEWPORT_SCALE_JS_SOURCE) + } + } else { + evaluateJavaScript(ENABLE_VIEWPORT_SCALE_JS_SOURCE) + configuration.userContentController.addUserScript(ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT) + } + } + + if newSettingsMap["supportZoom"] != nil && settings?.supportZoom != newSettings.supportZoom { + if newSettings.supportZoom { + if configuration.userContentController.userScripts.contains(NOT_SUPPORT_ZOOM_JS_PLUGIN_SCRIPT) { + configuration.userContentController.removePluginScript(NOT_SUPPORT_ZOOM_JS_PLUGIN_SCRIPT) + evaluateJavaScript(SUPPORT_ZOOM_JS_SOURCE) + } + } else { + evaluateJavaScript(NOT_SUPPORT_ZOOM_JS_SOURCE) + configuration.userContentController.addUserScript(NOT_SUPPORT_ZOOM_JS_PLUGIN_SCRIPT) + } + } + + if newSettingsMap["useOnLoadResource"] != nil && settings?.useOnLoadResource != newSettings.useOnLoadResource { + if let applePayAPIEnabled = settings?.applePayAPIEnabled, !applePayAPIEnabled { + enablePluginScriptAtRuntime(flagVariable: FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE, + enable: newSettings.useOnLoadResource, + pluginScript: ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT) + } else { + newSettings.useOnLoadResource = false + } + } + + if newSettingsMap["useShouldInterceptAjaxRequest"] != nil && settings?.useShouldInterceptAjaxRequest != newSettings.useShouldInterceptAjaxRequest { + if let applePayAPIEnabled = settings?.applePayAPIEnabled, !applePayAPIEnabled { + enablePluginScriptAtRuntime(flagVariable: FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE, + enable: newSettings.useShouldInterceptAjaxRequest, + pluginScript: INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT) + } else { + newSettings.useShouldInterceptAjaxRequest = false + } + } + + if newSettingsMap["interceptOnlyAsyncAjaxRequests"] != nil && settings?.interceptOnlyAsyncAjaxRequests != newSettings.interceptOnlyAsyncAjaxRequests { + if let applePayAPIEnabled = settings?.applePayAPIEnabled, !applePayAPIEnabled, + let interceptOnlyAsyncAjaxRequestsPluginScript = interceptOnlyAsyncAjaxRequestsPluginScript { + enablePluginScriptAtRuntime(flagVariable: FLAG_VARIABLE_FOR_INTERCEPT_ONLY_ASYNC_AJAX_REQUESTS_JS_SOURCE, + enable: newSettings.interceptOnlyAsyncAjaxRequests, + pluginScript: interceptOnlyAsyncAjaxRequestsPluginScript) + } + } + + if newSettingsMap["useShouldInterceptFetchRequest"] != nil && settings?.useShouldInterceptFetchRequest != newSettings.useShouldInterceptFetchRequest { + if let applePayAPIEnabled = settings?.applePayAPIEnabled, !applePayAPIEnabled { + enablePluginScriptAtRuntime(flagVariable: FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE, + enable: newSettings.useShouldInterceptFetchRequest, + pluginScript: INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT) + } else { + newSettings.useShouldInterceptFetchRequest = false + } + } + + if newSettingsMap["mediaPlaybackRequiresUserGesture"] != nil && settings?.mediaPlaybackRequiresUserGesture != newSettings.mediaPlaybackRequiresUserGesture { + if #available(iOS 10.0, *) { + configuration.mediaTypesRequiringUserActionForPlayback = (newSettings.mediaPlaybackRequiresUserGesture) ? .all : [] + } else { + // Fallback on earlier versions + configuration.mediaPlaybackRequiresUserAction = newSettings.mediaPlaybackRequiresUserGesture + } + } + + if newSettingsMap["allowsInlineMediaPlayback"] != nil && settings?.allowsInlineMediaPlayback != newSettings.allowsInlineMediaPlayback { + configuration.allowsInlineMediaPlayback = newSettings.allowsInlineMediaPlayback + } + + if newSettingsMap["suppressesIncrementalRendering"] != nil && settings?.suppressesIncrementalRendering != newSettings.suppressesIncrementalRendering { + configuration.suppressesIncrementalRendering = newSettings.suppressesIncrementalRendering + } + + if newSettingsMap["allowsBackForwardNavigationGestures"] != nil && settings?.allowsBackForwardNavigationGestures != newSettings.allowsBackForwardNavigationGestures { + allowsBackForwardNavigationGestures = newSettings.allowsBackForwardNavigationGestures + } + + if newSettingsMap["javaScriptCanOpenWindowsAutomatically"] != nil && settings?.javaScriptCanOpenWindowsAutomatically != newSettings.javaScriptCanOpenWindowsAutomatically { + configuration.preferences.javaScriptCanOpenWindowsAutomatically = newSettings.javaScriptCanOpenWindowsAutomatically + } + + if newSettingsMap["minimumFontSize"] != nil && settings?.minimumFontSize != newSettings.minimumFontSize { + configuration.preferences.minimumFontSize = CGFloat(newSettings.minimumFontSize) + } + + if newSettingsMap["selectionGranularity"] != nil && settings?.selectionGranularity != newSettings.selectionGranularity { + configuration.selectionGranularity = WKSelectionGranularity.init(rawValue: newSettings.selectionGranularity)! + } + + if #available(iOS 10.0, *) { + if newSettingsMap["ignoresViewportScaleLimits"] != nil && settings?.ignoresViewportScaleLimits != newSettings.ignoresViewportScaleLimits { + configuration.ignoresViewportScaleLimits = newSettings.ignoresViewportScaleLimits + } + + if newSettingsMap["dataDetectorTypes"] != nil && settings?.dataDetectorTypes != newSettings.dataDetectorTypes { + var dataDetectorTypes = WKDataDetectorTypes.init(rawValue: 0) + for type in newSettings.dataDetectorTypes { + let dataDetectorType = Util.getDataDetectorType(type: type) + dataDetectorTypes = WKDataDetectorTypes(rawValue: dataDetectorTypes.rawValue | dataDetectorType.rawValue) + } + configuration.dataDetectorTypes = dataDetectorTypes + } + } + + if #available(iOS 13.0, *) { + if newSettingsMap["isFraudulentWebsiteWarningEnabled"] != nil && settings?.isFraudulentWebsiteWarningEnabled != newSettings.isFraudulentWebsiteWarningEnabled { + configuration.preferences.isFraudulentWebsiteWarningEnabled = newSettings.isFraudulentWebsiteWarningEnabled + } + if newSettingsMap["preferredContentMode"] != nil && settings?.preferredContentMode != newSettings.preferredContentMode { + configuration.defaultWebpagePreferences.preferredContentMode = WKWebpagePreferences.ContentMode(rawValue: newSettings.preferredContentMode)! + } + if newSettingsMap["automaticallyAdjustsScrollIndicatorInsets"] != nil && settings?.automaticallyAdjustsScrollIndicatorInsets != newSettings.automaticallyAdjustsScrollIndicatorInsets { + scrollView.automaticallyAdjustsScrollIndicatorInsets = newSettings.automaticallyAdjustsScrollIndicatorInsets + } + } + + if newSettingsMap["disableVerticalScroll"] != nil && settings?.disableVerticalScroll != newSettings.disableVerticalScroll { + scrollView.showsVerticalScrollIndicator = !newSettings.disableVerticalScroll + } + if newSettingsMap["disableHorizontalScroll"] != nil && settings?.disableHorizontalScroll != newSettings.disableHorizontalScroll { + scrollView.showsHorizontalScrollIndicator = !newSettings.disableHorizontalScroll + } + + if newSettingsMap["verticalScrollBarEnabled"] != nil && settings?.verticalScrollBarEnabled != newSettings.verticalScrollBarEnabled { + scrollView.showsVerticalScrollIndicator = newSettings.verticalScrollBarEnabled + } + if newSettingsMap["horizontalScrollBarEnabled"] != nil && settings?.horizontalScrollBarEnabled != newSettings.horizontalScrollBarEnabled { + scrollView.showsHorizontalScrollIndicator = newSettings.horizontalScrollBarEnabled + } + + if newSettingsMap["isDirectionalLockEnabled"] != nil && settings?.isDirectionalLockEnabled != newSettings.isDirectionalLockEnabled { + scrollView.isDirectionalLockEnabled = newSettings.isDirectionalLockEnabled + } + + if newSettingsMap["decelerationRate"] != nil && settings?.decelerationRate != newSettings.decelerationRate { + scrollView.decelerationRate = Util.getDecelerationRate(type: newSettings.decelerationRate) + } + if newSettingsMap["alwaysBounceVertical"] != nil && settings?.alwaysBounceVertical != newSettings.alwaysBounceVertical { + scrollView.alwaysBounceVertical = newSettings.alwaysBounceVertical + } + if newSettingsMap["alwaysBounceHorizontal"] != nil && settings?.alwaysBounceHorizontal != newSettings.alwaysBounceHorizontal { + scrollView.alwaysBounceHorizontal = newSettings.alwaysBounceHorizontal + } + if newSettingsMap["scrollsToTop"] != nil && settings?.scrollsToTop != newSettings.scrollsToTop { + scrollView.scrollsToTop = newSettings.scrollsToTop + } + if newSettingsMap["isPagingEnabled"] != nil && settings?.isPagingEnabled != newSettings.isPagingEnabled { + scrollView.scrollsToTop = newSettings.isPagingEnabled + } + if newSettingsMap["maximumZoomScale"] != nil && settings?.maximumZoomScale != newSettings.maximumZoomScale { + scrollView.maximumZoomScale = CGFloat(newSettings.maximumZoomScale) + } + if newSettingsMap["minimumZoomScale"] != nil && settings?.minimumZoomScale != newSettings.minimumZoomScale { + scrollView.minimumZoomScale = CGFloat(newSettings.minimumZoomScale) + } + + if #available(iOS 9.0, *) { + if newSettingsMap["allowsLinkPreview"] != nil && settings?.allowsLinkPreview != newSettings.allowsLinkPreview { + allowsLinkPreview = newSettings.allowsLinkPreview + } + if newSettingsMap["allowsAirPlayForMediaPlayback"] != nil && settings?.allowsAirPlayForMediaPlayback != newSettings.allowsAirPlayForMediaPlayback { + configuration.allowsAirPlayForMediaPlayback = newSettings.allowsAirPlayForMediaPlayback + } + if newSettingsMap["allowsPictureInPictureMediaPlayback"] != nil && settings?.allowsPictureInPictureMediaPlayback != newSettings.allowsPictureInPictureMediaPlayback { + configuration.allowsPictureInPictureMediaPlayback = newSettings.allowsPictureInPictureMediaPlayback + } + if newSettingsMap["applicationNameForUserAgent"] != nil && settings?.applicationNameForUserAgent != newSettings.applicationNameForUserAgent && newSettings.applicationNameForUserAgent != "" { + configuration.applicationNameForUserAgent = newSettings.applicationNameForUserAgent + } + if newSettingsMap["userAgent"] != nil && settings?.userAgent != newSettings.userAgent && newSettings.userAgent != "" { + customUserAgent = newSettings.userAgent + } + } + + if newSettingsMap["allowUniversalAccessFromFileURLs"] != nil && settings?.allowUniversalAccessFromFileURLs != newSettings.allowUniversalAccessFromFileURLs { + configuration.setValue(newSettings.allowUniversalAccessFromFileURLs, forKey: "allowUniversalAccessFromFileURLs") + } + + if newSettingsMap["allowFileAccessFromFileURLs"] != nil && settings?.allowFileAccessFromFileURLs != newSettings.allowFileAccessFromFileURLs { + configuration.preferences.setValue(newSettings.allowFileAccessFromFileURLs, forKey: "allowFileAccessFromFileURLs") + } + + if newSettingsMap["clearCache"] != nil && newSettings.clearCache { + clearCache() + } + + if newSettingsMap["javaScriptEnabled"] != nil && settings?.javaScriptEnabled != newSettings.javaScriptEnabled { + configuration.preferences.javaScriptEnabled = newSettings.javaScriptEnabled + } + + if #available(iOS 14.0, *) { + if settings?.mediaType != newSettings.mediaType { + mediaType = newSettings.mediaType + } + + if newSettingsMap["pageZoom"] != nil && settings?.pageZoom != newSettings.pageZoom { + pageZoom = CGFloat(newSettings.pageZoom) + } + + if newSettingsMap["limitsNavigationsToAppBoundDomains"] != nil && settings?.limitsNavigationsToAppBoundDomains != newSettings.limitsNavigationsToAppBoundDomains { + configuration.limitsNavigationsToAppBoundDomains = newSettings.limitsNavigationsToAppBoundDomains + } + + if newSettingsMap["javaScriptEnabled"] != nil && settings?.javaScriptEnabled != newSettings.javaScriptEnabled { + configuration.defaultWebpagePreferences.allowsContentJavaScript = newSettings.javaScriptEnabled + } + } + + if #available(iOS 11.0, *), newSettingsMap["contentBlockers"] != nil { + configuration.userContentController.removeAllContentRuleLists() + let contentBlockers = newSettings.contentBlockers + if contentBlockers.count > 0 { + do { + let jsonData = try JSONSerialization.data(withJSONObject: contentBlockers, options: []) + let blockRules = String(data: jsonData, encoding: .utf8) + WKContentRuleListStore.default().compileContentRuleList( + forIdentifier: "ContentBlockingRules", + encodedContentRuleList: blockRules) { (contentRuleList, error) in + if let error = error { + print(error.localizedDescription) + return + } + self.configuration.userContentController.add(contentRuleList!) + } + } catch { + print(error.localizedDescription) + } + } + } + + if #available(iOS 15.0, *) { + if newSettingsMap["upgradeKnownHostsToHTTPS"] != nil && settings?.upgradeKnownHostsToHTTPS != newSettings.upgradeKnownHostsToHTTPS { + configuration.upgradeKnownHostsToHTTPS = newSettings.upgradeKnownHostsToHTTPS + } + if newSettingsMap["isTextInteractionEnabled"] != nil && settings?.isTextInteractionEnabled != newSettings.isTextInteractionEnabled { + configuration.preferences.isTextInteractionEnabled = newSettings.isTextInteractionEnabled + } + if newSettingsMap["underPageBackgroundColor"] != nil, settings?.underPageBackgroundColor != newSettings.underPageBackgroundColor, + let underPageBackgroundColor = newSettings.underPageBackgroundColor, !underPageBackgroundColor.isEmpty { + self.underPageBackgroundColor = UIColor(hexString: underPageBackgroundColor) + } + } + if #available(iOS 15.4, *) { + if newSettingsMap["isSiteSpecificQuirksModeEnabled"] != nil, settings?.isSiteSpecificQuirksModeEnabled != newSettings.isSiteSpecificQuirksModeEnabled { + configuration.preferences.isSiteSpecificQuirksModeEnabled = newSettings.isSiteSpecificQuirksModeEnabled + } + } + if #available(iOS 15.5, *) { + if ((newSettingsMap["minimumViewportInset"] != nil && settings?.minimumViewportInset != newSettings.minimumViewportInset) || + (newSettingsMap["maximumViewportInset"] != nil && settings?.maximumViewportInset != newSettings.maximumViewportInset)), + let minViewportInset = newSettings.minimumViewportInset, let maxViewportInset = newSettings.maximumViewportInset { + setMinimumViewportInset(minViewportInset, maximumViewportInset: maxViewportInset) + } + } + if #available(iOS 16.0, *) { + if newSettingsMap["isFindInteractionEnabled"] != nil, settings?.isFindInteractionEnabled != newSettings.isFindInteractionEnabled { + isFindInteractionEnabled = newSettings.isFindInteractionEnabled + } + } + if #available(iOS 16.4, *) { + if newSettingsMap["isInspectable"] != nil, settings?.isInspectable != newSettings.isInspectable { + isInspectable = newSettings.isInspectable + } + if newSettingsMap["shouldPrintBackgrounds"] != nil, settings?.shouldPrintBackgrounds != newSettings.shouldPrintBackgrounds { + configuration.preferences.shouldPrintBackgrounds = newSettings.shouldPrintBackgrounds + } + } + + scrollView.isScrollEnabled = !(newSettings.disableVerticalScroll && newSettings.disableHorizontalScroll) + + self.settings = newSettings + } + + func getSettings() -> [String: Any?]? { + if (self.settings == nil) { + return nil + } + return self.settings!.getRealSettings(obj: self) + } + + public func enablePluginScriptAtRuntime(flagVariable: String, enable: Bool, pluginScript: PluginScript) { + evaluateJavascript(source: flagVariable) { (alreadyLoaded) in + if let alreadyLoaded = alreadyLoaded as? Bool, alreadyLoaded { + let enableSource = "\(flagVariable) = \(enable);" + if #available(iOS 14.0, *), pluginScript.requiredInAllContentWorlds { + for contentWorld in self.configuration.userContentController.contentWorlds { + self.evaluateJavaScript(enableSource, frame: nil, contentWorld: contentWorld, completionHandler: nil) + } + } else { + self.evaluateJavaScript(enableSource, completionHandler: nil) + } + if !enable { + self.configuration.userContentController.removePluginScripts(with: pluginScript.groupName!) + } + } + else if enable { + if #available(iOS 14.0, *), pluginScript.requiredInAllContentWorlds { + for contentWorld in self.configuration.userContentController.contentWorlds { + self.evaluateJavaScript(pluginScript.source, frame: nil, contentWorld: contentWorld, completionHandler: nil) + self.configuration.userContentController.addPluginScript(pluginScript) + } + } else { + self.evaluateJavaScript(pluginScript.source, completionHandler: nil) + self.configuration.userContentController.addPluginScript(pluginScript) + } + self.configuration.userContentController.sync(scriptMessageHandler: self) + } + } + } + + @available(*, deprecated, message: "Use InAppWebViewManager.clearAllCache instead.") + public func clearCache() { + if #available(iOS 9.0, *) { + let date = NSDate(timeIntervalSince1970: 0) + WKWebsiteDataStore.default().removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), modifiedSince: date as Date, completionHandler:{ }) + } else { + var libraryPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.libraryDirectory, FileManager.SearchPathDomainMask.userDomainMask, false).first! + libraryPath += "/Cookies" + + do { + try FileManager.default.removeItem(atPath: libraryPath) + } catch { + print("can't clear cache") + } + URLCache.shared.removeAllCachedResponses() + } + } + + public func injectDeferredObject(source: String, withWrapper jsWrapper: String?, completionHandler: ((Any?) -> Void)? = nil) { + var jsToInject = source + if let wrapper = jsWrapper { + let jsonData: Data? = try? JSONSerialization.data(withJSONObject: [source], options: []) + let sourceArrayString = String(data: jsonData!, encoding: .utf8) + let sourceString: String? = (sourceArrayString! as NSString).substring(with: NSRange(location: 1, length: (sourceArrayString?.count ?? 0) - 2)) + jsToInject = String(format: wrapper, sourceString!) + } + + evaluateJavaScript(jsToInject) { (value, error) in + guard let completionHandler = completionHandler else { + return + } + + if let error = error { + let userInfo = (error as NSError).userInfo + let errorMessage = userInfo["WKJavaScriptExceptionMessage"] ?? + userInfo["NSLocalizedDescription"] as? String ?? + error.localizedDescription + self.channelDelegate?.onConsoleMessage(message: String(describing: errorMessage), messageLevel: 3) + } + + if value == nil { + completionHandler(nil) + return + } + + completionHandler(value) + } + } + + @available(iOS 14.0, *) + public func injectDeferredObject(source: String, contentWorld: WKContentWorld, withWrapper jsWrapper: String?, completionHandler: ((Any?) -> Void)? = nil) { + var jsToInject = source + if let wrapper = jsWrapper { + let jsonData: Data? = try? JSONSerialization.data(withJSONObject: [source], options: []) + let sourceArrayString = String(data: jsonData!, encoding: .utf8) + let sourceString: String? = (sourceArrayString! as NSString).substring(with: NSRange(location: 1, length: (sourceArrayString?.count ?? 0) - 2)) + jsToInject = String(format: wrapper, sourceString!) + } + + jsToInject = configuration.userContentController.generateCodeForScriptEvaluation(scriptMessageHandler: self, source: jsToInject, contentWorld: contentWorld) + + evaluateJavaScript(jsToInject, frame: nil, contentWorld: contentWorld) { (evalResult) in + guard let completionHandler = completionHandler else { + return + } + + switch (evalResult) { + case .success(let value): + completionHandler(value) + return + case .failure(let error): + let userInfo = (error as NSError).userInfo + let errorMessage = userInfo["WKJavaScriptExceptionMessage"] ?? + userInfo["NSLocalizedDescription"] as? String ?? + error.localizedDescription + self.channelDelegate?.onConsoleMessage(message: String(describing: errorMessage), messageLevel: 3) + break + } + + completionHandler(nil) + } + } + +#if compiler(>=6.0) + public override func evaluateJavaScript(_ javaScriptString: String, completionHandler: (@MainActor @Sendable (Any?, (any Error)?) -> Void)? = nil) { + if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled { + if let completionHandler = completionHandler { + completionHandler(nil, nil) + } + return + } + super.evaluateJavaScript(javaScriptString, completionHandler: completionHandler) + } +#else + public override func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil) { + if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled { + if let completionHandler = completionHandler { + completionHandler(nil, nil) + } + return + } + super.evaluateJavaScript(javaScriptString, completionHandler: completionHandler) + } +#endif + + @available(iOS 14.0, *) + public func evaluateJavaScript(_ javaScript: String, frame: WKFrameInfo? = nil, contentWorld: WKContentWorld, completionHandler: ((Result) -> Void)? = nil) { + if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled { + return + } + super.evaluateJavaScript(javaScript, in: frame, in: contentWorld, completionHandler: completionHandler) + } + + public func evaluateJavascript(source: String, completionHandler: ((Any?) -> Void)? = nil) { + injectDeferredObject(source: source, withWrapper: nil, completionHandler: completionHandler) + } + + @available(iOS 14.0, *) + public func evaluateJavascript(source: String, contentWorld: WKContentWorld, completionHandler: ((Any?) -> Void)? = nil) { + injectDeferredObject(source: source, contentWorld: contentWorld, withWrapper: nil, completionHandler: completionHandler) + } + + @available(iOS 14.0, *) + public func callAsyncJavaScript(_ functionBody: String, arguments: [String : Any] = [:], frame: WKFrameInfo? = nil, contentWorld: WKContentWorld, completionHandler: ((Result) -> Void)? = nil) { + if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled { + return + } + super.callAsyncJavaScript(functionBody, arguments: arguments, in: frame, in: contentWorld, completionHandler: completionHandler) + } + + @available(iOS 14.0, *) + public func callAsyncJavaScript(functionBody: String, arguments: [String:Any], contentWorld: WKContentWorld, completionHandler: ((Any?) -> Void)? = nil) { + let jsToInject = configuration.userContentController.generateCodeForScriptEvaluation(scriptMessageHandler: self, source: functionBody, contentWorld: contentWorld) + + callAsyncJavaScript(jsToInject, arguments: arguments, frame: nil, contentWorld: contentWorld) { (evalResult) in + guard let completionHandler = completionHandler else { + return + } + + var body: [String: Any?] = [ + "value": nil, + "error": nil + ] + + switch (evalResult) { + case .success(let value): + body["value"] = value + break + case .failure(let error): + let userInfo = (error as NSError).userInfo + body["error"] = userInfo["WKJavaScriptExceptionMessage"] ?? + userInfo["NSLocalizedDescription"] as? String ?? + error.localizedDescription + self.channelDelegate?.onConsoleMessage(message: String(describing: body["error"]), messageLevel: 3) + break + } + + completionHandler(body) + } + } + + @available(iOS 10.3, *) + public func callAsyncJavaScript(functionBody: String, arguments: [String:Any], completionHandler: ((Any?) -> Void)? = nil) { + if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled { + completionHandler?(nil) + } + + var jsToInject = functionBody + + let resultUuid = NSUUID().uuidString + if let completionHandler = completionHandler { + callAsyncJavaScriptBelowIOS14Results[resultUuid] = completionHandler + } + + var functionArgumentNamesList: [String] = [] + var functionArgumentValuesList: [String] = [] + let keys = arguments.keys + keys.forEach { (key) in + functionArgumentNamesList.append(key) + functionArgumentValuesList.append("obj.\(key)") + } + + let functionArgumentNames = functionArgumentNamesList.joined(separator: ", ") + let functionArgumentValues = functionArgumentValuesList.joined(separator: ", ") + + jsToInject = CALL_ASYNC_JAVASCRIPT_BELOW_IOS_14_WRAPPER_JS + .replacingOccurrences(of: PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_NAMES, with: functionArgumentNames) + .replacingOccurrences(of: PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_VALUES, with: functionArgumentValues) + .replacingOccurrences(of: PluginScriptsUtil.VAR_FUNCTION_ARGUMENTS_OBJ, with: Util.JSONStringify(value: arguments)) + .replacingOccurrences(of: PluginScriptsUtil.VAR_FUNCTION_BODY, with: jsToInject) + .replacingOccurrences(of: PluginScriptsUtil.VAR_RESULT_UUID, with: resultUuid) + + evaluateJavaScript(jsToInject) { (value, error) in + if let error = error { + let userInfo = (error as NSError).userInfo + let errorMessage = userInfo["WKJavaScriptExceptionMessage"] ?? + userInfo["NSLocalizedDescription"] as? String ?? + error.localizedDescription + self.channelDelegate?.onConsoleMessage(message: String(describing: errorMessage), messageLevel: 3) + completionHandler?(nil) + self.callAsyncJavaScriptBelowIOS14Results.removeValue(forKey: resultUuid) + } + } + } + + public func injectJavascriptFileFromUrl(urlFile: String, scriptHtmlTagAttributes: [String:Any?]?) { + var scriptAttributes = "" + if let scriptHtmlTagAttributes = scriptHtmlTagAttributes { + if let typeAttr = scriptHtmlTagAttributes["type"] as? String { + scriptAttributes += " script.type = '\(typeAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let idAttr = scriptHtmlTagAttributes["id"] as? String { + let scriptIdEscaped = idAttr.replacingOccurrences(of: "\'", with: "\\'") + scriptAttributes += " script.id = '\(scriptIdEscaped)'; " + scriptAttributes += """ + script.onload = function() { + if (window.\(JAVASCRIPT_BRIDGE_NAME) != null) { + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onInjectedScriptLoaded', '\(scriptIdEscaped)'); + } + }; + """ + scriptAttributes += """ + script.onerror = function() { + if (window.\(JAVASCRIPT_BRIDGE_NAME) != null) { + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onInjectedScriptError', '\(scriptIdEscaped)'); + } + }; + """ + } + if let asyncAttr = scriptHtmlTagAttributes["async"] as? Bool, asyncAttr { + scriptAttributes += " script.async = true; " + } + if let deferAttr = scriptHtmlTagAttributes["defer"] as? Bool, deferAttr { + scriptAttributes += " script.defer = true; " + } + if let crossOriginAttr = scriptHtmlTagAttributes["crossOrigin"] as? String { + scriptAttributes += " script.crossOrigin = '\(crossOriginAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let integrityAttr = scriptHtmlTagAttributes["integrity"] as? String { + scriptAttributes += " script.integrity = '\(integrityAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let noModuleAttr = scriptHtmlTagAttributes["noModule"] as? Bool, noModuleAttr { + scriptAttributes += " script.noModule = true; " + } + if let nonceAttr = scriptHtmlTagAttributes["nonce"] as? String { + scriptAttributes += " script.nonce = '\(nonceAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let referrerPolicyAttr = scriptHtmlTagAttributes["referrerPolicy"] as? String { + scriptAttributes += " script.referrerPolicy = '\(referrerPolicyAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + } + let jsWrapper = "(function(d) { var script = d.createElement('script'); \(scriptAttributes) script.src = %@; d.body.appendChild(script); })(document);" + injectDeferredObject(source: urlFile, withWrapper: jsWrapper, completionHandler: nil) + } + + public func injectCSSCode(source: String) { + let jsWrapper = "(function(d) { var style = d.createElement('style'); style.innerHTML = %@; d.head.appendChild(style); })(document);" + injectDeferredObject(source: source, withWrapper: jsWrapper, completionHandler: nil) + } + + public func injectCSSFileFromUrl(urlFile: String, cssLinkHtmlTagAttributes: [String:Any?]?) { + var cssLinkAttributes = "" + var alternateStylesheet = "" + if let cssLinkHtmlTagAttributes = cssLinkHtmlTagAttributes { + if let idAttr = cssLinkHtmlTagAttributes["id"] as? String { + cssLinkAttributes += " link.id = '\(idAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let mediaAttr = cssLinkHtmlTagAttributes["media"] as? String { + cssLinkAttributes += " link.media = '\(mediaAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let crossOriginAttr = cssLinkHtmlTagAttributes["crossOrigin"] as? String { + cssLinkAttributes += " link.crossOrigin = '\(crossOriginAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let integrityAttr = cssLinkHtmlTagAttributes["integrity"] as? String { + cssLinkAttributes += " link.integrity = '\(integrityAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let referrerPolicyAttr = cssLinkHtmlTagAttributes["referrerPolicy"] as? String { + cssLinkAttributes += " link.referrerPolicy = '\(referrerPolicyAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let disabledAttr = cssLinkHtmlTagAttributes["disabled"] as? Bool, disabledAttr { + cssLinkAttributes += " link.disabled = true; " + } + if let alternateAttr = cssLinkHtmlTagAttributes["alternate"] as? Bool, alternateAttr { + alternateStylesheet = "alternate " + } + if let titleAttr = cssLinkHtmlTagAttributes["title"] as? String { + cssLinkAttributes += " link.title = '\(titleAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + } + let jsWrapper = "(function(d) { var link = d.createElement('link'); link.rel='\(alternateStylesheet)stylesheet', link.type='text/css'; \(cssLinkAttributes) link.href = %@; d.head.appendChild(link); })(document);" + injectDeferredObject(source: urlFile, withWrapper: jsWrapper, completionHandler: nil) + } + + public func getCopyBackForwardList() -> [String: Any] { + let currentList = backForwardList + let currentIndex = currentList.backList.count + var completeList = currentList.backList + if currentList.currentItem != nil { + completeList.append(currentList.currentItem!) + } + completeList.append(contentsOf: currentList.forwardList) + + var history: [[String: String]] = [] + + for historyItem in completeList { + var historyItemMap: [String: String] = [:] + historyItemMap["originalUrl"] = historyItem.initialURL.absoluteString + historyItemMap["title"] = historyItem.title + historyItemMap["url"] = historyItem.url.absoluteString + history.append(historyItemMap) + } + + var result: [String: Any] = [:] + result["list"] = history + result["currentIndex"] = currentIndex + + return result; + } + + @available(iOS 15.0, *) + public func webView(_ webView: WKWebView, + requestMediaCapturePermissionFor origin: WKSecurityOrigin, + initiatedByFrame frame: WKFrameInfo, + type: WKMediaCaptureType, + decisionHandler: @escaping (WKPermissionDecision) -> Void) { + let origin = "\(origin.protocol)://\(origin.host)\(origin.port != 0 ? ":" + String(origin.port) : "")" + let permissionRequest = PermissionRequest(origin: origin, resources: [type.rawValue], frame: frame) + + var decisionHandlerCalled = false + let callback = WebViewChannelDelegate.PermissionRequestCallback() + callback.nonNullSuccess = { (response: PermissionResponse) in + if let action = response.action { + decisionHandlerCalled = true + switch action { + case 1: + decisionHandler(.grant) + break + case 2: + decisionHandler(.prompt) + break + default: + decisionHandler(.deny) + } + return false + } + return true + } + callback.defaultBehaviour = { (response: PermissionResponse?) in + if !decisionHandlerCalled { + decisionHandlerCalled = true + decisionHandler(.deny) + } + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + if let channelDelegate = channelDelegate { + channelDelegate.onPermissionRequest(request: permissionRequest, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + @available(iOS 15.0, *) + public func webView(_ webView: WKWebView, + requestDeviceOrientationAndMotionPermissionFor origin: WKSecurityOrigin, + initiatedByFrame frame: WKFrameInfo, + decisionHandler: @escaping (WKPermissionDecision) -> Void) { + let origin = "\(origin.protocol)://\(origin.host)\(origin.port != 0 ? ":" + String(origin.port) : "")" + let permissionRequest = PermissionRequest(origin: origin, resources: ["deviceOrientationAndMotion"], frame: frame) + + var decisionHandlerCalled = false + let callback = WebViewChannelDelegate.PermissionRequestCallback() + callback.nonNullSuccess = { (response: PermissionResponse) in + if let action = response.action { + decisionHandlerCalled = true + switch action { + case 1: + decisionHandler(.grant) + break + case 2: + decisionHandler(.prompt) + break + default: + decisionHandler(.deny) + } + return false + } + return true + } + callback.defaultBehaviour = { (response: PermissionResponse?) in + if !decisionHandlerCalled { + decisionHandlerCalled = true + decisionHandler(.deny) + } + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + if let channelDelegate = channelDelegate { + channelDelegate.onPermissionRequest(request: permissionRequest, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + @available(iOS 13.0, *) + public func webView(_ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + preferences: WKWebpagePreferences, + decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) { + self.webView(webView, decidePolicyFor: navigationAction, decisionHandler: {(navigationActionPolicy) -> Void in + decisionHandler(navigationActionPolicy, preferences) + }) + } + + @available(iOS 14.5, *) + public func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String, completionHandler: @escaping (URL?) -> Void) { + if let url = response.url, let useOnDownloadStart = settings?.useOnDownloadStart, useOnDownloadStart { + let downloadStartRequest = DownloadStartRequest(url: url.absoluteString, + userAgent: nil, + contentDisposition: nil, + mimeType: response.mimeType, + contentLength: response.expectedContentLength, + suggestedFilename: suggestedFilename, + textEncodingName: response.textEncodingName) + channelDelegate?.onDownloadStartRequest(request: downloadStartRequest) + } + download.delegate = nil + // cancel the download + completionHandler(nil) + } + + @available(iOS 14.5, *) + public func webView(_ webView: WKWebView, navigationResponse: WKNavigationResponse, didBecome download: WKDownload) { + let response = navigationResponse.response + if let url = response.url, let useOnDownloadStart = settings?.useOnDownloadStart, useOnDownloadStart { + let downloadStartRequest = DownloadStartRequest(url: url.absoluteString, + userAgent: nil, + contentDisposition: nil, + mimeType: response.mimeType, + contentLength: response.expectedContentLength, + suggestedFilename: response.suggestedFilename, + textEncodingName: response.textEncodingName) + channelDelegate?.onDownloadStartRequest(request: downloadStartRequest) + } + download.delegate = nil + } + + public func webView(_ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + var decisionHandlerCalled = false + let callback = WebViewChannelDelegate.ShouldOverrideUrlLoadingCallback() + callback.nonNullSuccess = { (response: WKNavigationActionPolicy) in + decisionHandlerCalled = true + decisionHandler(response) + return false + } + callback.defaultBehaviour = { (response: WKNavigationActionPolicy?) in + if !decisionHandlerCalled { + decisionHandlerCalled = true + decisionHandler(.allow) + } + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + let runCallback = { + if let useShouldOverrideUrlLoading = self.settings?.useShouldOverrideUrlLoading, useShouldOverrideUrlLoading, let channelDelegate = self.channelDelegate { + channelDelegate.shouldOverrideUrlLoading(navigationAction: navigationAction, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + if windowId != nil, !windowCreated { + windowBeforeCreatedCallbacks.append(runCallback) + } else { + runCallback() + } + } + + public func webView(_ webView: WKWebView, + decidePolicyFor navigationResponse: WKNavigationResponse, + decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { + if let response = navigationResponse.response as? HTTPURLResponse, response.statusCode >= 400 { + let request = WebResourceRequest.init(fromWKNavigationResponse: navigationResponse) + let errorResponse = WebResourceResponse.init(fromWKNavigationResponse: navigationResponse) + channelDelegate?.onReceivedHttpError(request: request, errorResponse: errorResponse) + } + + let useOnNavigationResponse = settings?.useOnNavigationResponse + + if useOnNavigationResponse != nil, useOnNavigationResponse! { + var decisionHandlerCalled = false + let callback = WebViewChannelDelegate.NavigationResponseCallback() + callback.nonNullSuccess = { (response: WKNavigationResponsePolicy) in + decisionHandlerCalled = true + decisionHandler(response) + return false + } + callback.defaultBehaviour = { (response: WKNavigationResponsePolicy?) in + if !decisionHandlerCalled { + decisionHandlerCalled = true + decisionHandler(.allow) + } + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + if let channelDelegate = channelDelegate { + channelDelegate.onNavigationResponse(navigationResponse: navigationResponse, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + if let useOnDownloadStart = settings?.useOnDownloadStart, useOnDownloadStart { + if #available(iOS 14.5, *), !navigationResponse.canShowMIMEType, useOnNavigationResponse == nil || !useOnNavigationResponse! { + decisionHandler(.download) + return + } else { + let mimeType = navigationResponse.response.mimeType + if let url = navigationResponse.response.url, navigationResponse.isForMainFrame { + if url.scheme != "file", mimeType != nil, !mimeType!.starts(with: "text/") { + let downloadStartRequest = DownloadStartRequest(url: url.absoluteString, + userAgent: nil, + contentDisposition: nil, + mimeType: mimeType, + contentLength: navigationResponse.response.expectedContentLength, + suggestedFilename: navigationResponse.response.suggestedFilename, + textEncodingName: navigationResponse.response.textEncodingName) + channelDelegate?.onDownloadStartRequest(request: downloadStartRequest) + if useOnNavigationResponse == nil || !useOnNavigationResponse! { + decisionHandler(.cancel) + } + return + } + } + } + } + + if useOnNavigationResponse == nil || !useOnNavigationResponse! { + decisionHandler(.allow) + } + } + + public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + currentOriginalUrl = url + lastTouchPoint = nil + + disposeWebMessageChannels() + initializeWindowIdJS() + + if #available(iOS 14.0, *) { + configuration.userContentController.resetContentWorlds(windowId: windowId) + } + + channelDelegate?.onLoadStart(url: url?.absoluteString) + + inAppBrowserDelegate?.didStartNavigation(url: url) + } + + public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + initializeWindowIdJS() + + InAppWebView.credentialsProposed = [] + evaluateJavaScript(PLATFORM_READY_JS_SOURCE, completionHandler: nil) + + // sometimes scrollView.contentSize doesn't fit all the frame.size available + // so, we call setNeedsLayout to redraw the layout + let webViewFrameSize = frame.size + let scrollViewSize = scrollView.contentSize + if (scrollViewSize.width < webViewFrameSize.width || scrollViewSize.height < webViewFrameSize.height) { + setNeedsLayout() + } + + channelDelegate?.onLoadStop(url: url?.absoluteString) + + inAppBrowserDelegate?.didFinishNavigation(url: url) + } + + public func webView(_ view: WKWebView, + didFailProvisionalNavigation navigation: WKNavigation!, + withError error: Error) { + webView(view, didFail: navigation, withError: error) + } + + public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + InAppWebView.credentialsProposed = [] + + var urlError: URL = url ?? URL(string: "about:blank")! + var errorCode = -1 + var errorDescription = "domain=\(error._domain), code=\(error._code), \(error.localizedDescription)" + + if let info = error as? URLError { + if let failingURL = info.failingURL { + urlError = failingURL + } + errorCode = info.code.rawValue + errorDescription = info.localizedDescription + } + else if let info = error._userInfo as? [String: Any] { + if let failingUrl = info[NSURLErrorFailingURLErrorKey] as? URL { + urlError = failingUrl + } + if let failingUrlString = info[NSURLErrorFailingURLStringErrorKey] as? String, + let failingUrl = URL(string: failingUrlString) { + urlError = failingUrl + } + } + + let webResourceRequest = WebResourceRequest(url: urlError, headers: nil) + let webResourceError = WebResourceError(type: errorCode, errorDescription: errorDescription) + + channelDelegate?.onReceivedError(request: webResourceRequest, error: webResourceError) + + inAppBrowserDelegate?.didFailNavigation(url: url, error: error) + } + + public func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + var completionHandlerCalled = false + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic || + challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodDefault || + challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPDigest || + challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodNegotiate || + challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodNTLM { + let host = challenge.protectionSpace.host + let prot = challenge.protectionSpace.protocol + let realm = challenge.protectionSpace.realm + let port = challenge.protectionSpace.port + + let callback = WebViewChannelDelegate.ReceivedHttpAuthRequestCallback() + callback.nonNullSuccess = { (response: HttpAuthResponse) in + if let action = response.action { + completionHandlerCalled = true + switch action { + case 0: + InAppWebView.credentialsProposed = [] + // used .performDefaultHandling to maintain consistency with Android + // because .cancelAuthenticationChallenge will call webView(_:didFail:withError:) + completionHandler(.performDefaultHandling, nil) + //completionHandler(.cancelAuthenticationChallenge, nil) + break + case 1: + let username = response.username + let password = response.password + let permanentPersistence = response.permanentPersistence + let persistence = (permanentPersistence) ? URLCredential.Persistence.permanent : URLCredential.Persistence.forSession + let credential = URLCredential(user: username, password: password, persistence: persistence) + completionHandler(.useCredential, credential) + break + case 2: + if InAppWebView.credentialsProposed.count == 0 { + for (protectionSpace, credentials) in CredentialDatabase.credentialStore.allCredentials { + if protectionSpace.host == host && protectionSpace.realm == realm && + protectionSpace.protocol == prot && protectionSpace.port == port { + for credential in credentials { + InAppWebView.credentialsProposed.append(credential.value) + } + break + } + } + } + if InAppWebView.credentialsProposed.count == 0, let credential = challenge.proposedCredential { + InAppWebView.credentialsProposed.append(credential) + } + + if let credential = InAppWebView.credentialsProposed.popLast() { + completionHandler(.useCredential, credential) + } + else { + completionHandler(.performDefaultHandling, nil) + } + break + default: + InAppWebView.credentialsProposed = [] + completionHandler(.performDefaultHandling, nil) + } + return false + } + return true + } + callback.defaultBehaviour = { (response: HttpAuthResponse?) in + if !completionHandlerCalled { + completionHandlerCalled = true + completionHandler(.performDefaultHandling, nil) + } + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + let runCallback = { + if let channelDelegate = self.channelDelegate { + channelDelegate.onReceivedHttpAuthRequest(challenge: HttpAuthenticationChallenge(fromChallenge: challenge), callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + if windowId != nil, !windowCreated { + windowBeforeCreatedCallbacks.append(runCallback) + } else { + runCallback() + } + } + else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { + guard let serverTrust = challenge.protectionSpace.serverTrust else { + completionHandler(.performDefaultHandling, nil) + return + } + + if let scheme = challenge.protectionSpace.protocol, scheme == "https" { + // workaround for ProtectionSpace SSL Certificate + // https://github.com/pichillilorenzo/flutter_inappwebview/issues/1678 + DispatchQueue.global(qos: .background).async { + if let sslCertificate = challenge.protectionSpace.sslCertificate { + DispatchQueue.main.async { + InAppWebView.sslCertificatesMap[challenge.protectionSpace.host] = sslCertificate + } + } + } + } + + let callback = WebViewChannelDelegate.ReceivedServerTrustAuthRequestCallback() + callback.nonNullSuccess = { (response: ServerTrustAuthResponse) in + if let action = response.action { + completionHandlerCalled = true + switch action { + case 0: + InAppWebView.credentialsProposed = [] + completionHandler(.cancelAuthenticationChallenge, nil) + break + case 1: + // workaround for https://github.com/pichillilorenzo/flutter_inappwebview/issues/1924 + DispatchQueue.global(qos: .background).async { + let exceptions = SecTrustCopyExceptions(serverTrust) + SecTrustSetExceptions(serverTrust, exceptions) + let credential = URLCredential(trust: serverTrust) + completionHandler(.useCredential, credential) + } + break + default: + InAppWebView.credentialsProposed = [] + completionHandler(.performDefaultHandling, nil) + } + return false + } + return true + } + callback.defaultBehaviour = { (response: ServerTrustAuthResponse?) in + if !completionHandlerCalled { + completionHandlerCalled = true + completionHandler(.performDefaultHandling, nil) + } + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + let runCallback = { + if let channelDelegate = self.channelDelegate { + channelDelegate.onReceivedServerTrustAuthRequest(challenge: ServerTrustChallenge(fromChallenge: challenge), callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + if windowId != nil, !windowCreated { + windowBeforeCreatedCallbacks.append(runCallback) + } else { + runCallback() + } + } + else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate { + let callback = WebViewChannelDelegate.ReceivedClientCertRequestCallback() + callback.nonNullSuccess = { (response: ClientCertResponse) in + if let action = response.action { + completionHandlerCalled = true + switch action { + case 0: + completionHandler(.cancelAuthenticationChallenge, nil) + break + case 1: + let certificatePath = response.certificatePath + let certificatePassword = response.certificatePassword ?? ""; + + var path: String = certificatePath + do { + if let plugin = self.plugin { + path = try Util.getAbsPathAsset(plugin: plugin, assetFilePath: certificatePath) + } + } catch {} + + if let PKCS12Data = NSData(contentsOfFile: path), + let identityAndTrust: IdentityAndTrust = self.extractIdentity(PKCS12Data: PKCS12Data, password: certificatePassword) { + let urlCredential: URLCredential = URLCredential( + identity: identityAndTrust.identityRef, + certificates: identityAndTrust.certArray as? [AnyObject], + persistence: URLCredential.Persistence.forSession); + completionHandler(.useCredential, urlCredential) + } else { + completionHandler(.performDefaultHandling, nil) + } + + break + case 2: + completionHandler(.cancelAuthenticationChallenge, nil) + break + default: + completionHandler(.performDefaultHandling, nil) + } + return false + } + return true + } + callback.defaultBehaviour = { (response: ClientCertResponse?) in + if !completionHandlerCalled { + completionHandlerCalled = true + completionHandler(.performDefaultHandling, nil) + } + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + let runCallback = { + if let channelDelegate = self.channelDelegate { + channelDelegate.onReceivedClientCertRequest(challenge: ClientCertChallenge(fromChallenge: challenge), callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + if windowId != nil, !windowCreated { + windowBeforeCreatedCallbacks.append(runCallback) + } else { + runCallback() + } + } + else { + completionHandler(.performDefaultHandling, nil) + } + } + + struct IdentityAndTrust { + var identityRef: SecIdentity + var trust: SecTrust + var certArray: AnyObject + } + + func extractIdentity(PKCS12Data: NSData, password: String) -> IdentityAndTrust? { + var identityAndTrust: IdentityAndTrust? + var securityError: OSStatus = errSecSuccess + + var importResult: CFArray? + securityError = SecPKCS12Import( + PKCS12Data as NSData, + [kSecImportExportPassphrase as String: password] as NSDictionary, + &importResult + ) + + if securityError == errSecSuccess { + let certItems: CFArray = importResult! as CFArray + let certItemsArray: Array = certItems as Array + let dict: AnyObject? = certItemsArray.first + if let certEntry: Dictionary = dict as? Dictionary { + // grab the identity + let identityPointer: AnyObject? = certEntry["identity"] + let secIdentityRef:SecIdentity = (identityPointer as! SecIdentity?)! + // grab the trust + let trustPointer: AnyObject? = certEntry["trust"] + let trustRef:SecTrust = trustPointer as! SecTrust + // grab the cert + let chainPointer: AnyObject? = certEntry["chain"] + identityAndTrust = IdentityAndTrust(identityRef: secIdentityRef, trust: trustRef, certArray: chainPointer!) + } + } else { + print("Security Error: " + securityError.description) + if #available(iOS 11.3, *) { + print(SecCopyErrorMessageString(securityError, nil) ?? "") + } + } + return identityAndTrust + } + + func createAlertDialog(message: String?, responseMessage: String?, confirmButtonTitle: String?, completionHandler: @escaping () -> Void) { + let title = responseMessage != nil && !responseMessage!.isEmpty ? responseMessage : message + let okButton = confirmButtonTitle != nil && !confirmButtonTitle!.isEmpty ? confirmButtonTitle : NSLocalizedString("Ok", comment: "") + let alertController = UIAlertController(title: title, message: nil, + preferredStyle: UIAlertController.Style.alert); + + alertController.addAction(UIAlertAction(title: okButton, style: UIAlertAction.Style.default) { + _ in completionHandler()} + ) + + guard let presentingViewController = inAppBrowserDelegate != nil ? inAppBrowserDelegate as? InAppBrowserWebViewController : window?.rootViewController else { + completionHandler() + return + } + presentingViewController.present(alertController, animated: true, completion: {}) + } + + public func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, + initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) { + + if (isPausedTimers) { + isPausedTimersCompletionHandler = completionHandler + return + } + + var completionHandlerCalled = false + + let callback = WebViewChannelDelegate.JsAlertCallback() + callback.nonNullSuccess = { (response: JsAlertResponse) in + if response.handledByClient { + completionHandlerCalled = true + let action = response.action ?? 1 + switch action { + case 0: + completionHandler() + break + default: + completionHandler() + } + return false + } + return true + } + callback.defaultBehaviour = { [weak self] (response: JsAlertResponse?) in + if !completionHandlerCalled { + completionHandlerCalled = true + let responseMessage = response?.message + let confirmButtonTitle = response?.confirmButtonTitle + self?.createAlertDialog(message: message, responseMessage: responseMessage, + confirmButtonTitle: confirmButtonTitle, completionHandler: completionHandler) + } + } + callback.error = { (code: String, message: String?, details: Any?) in + if !completionHandlerCalled { + completionHandlerCalled = true + print(code + ", " + (message ?? "")) + completionHandler() + } + } + + if let channelDelegate = channelDelegate { + channelDelegate.onJsAlert(url: frame.request.url, message: message, isMainFrame: frame.isMainFrame, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + func createConfirmDialog(message: String?, responseMessage: String?, confirmButtonTitle: String?, cancelButtonTitle: String?, completionHandler: @escaping (Bool) -> Void) { + let dialogMessage = responseMessage != nil && !responseMessage!.isEmpty ? responseMessage : message + let okButton = confirmButtonTitle != nil && !confirmButtonTitle!.isEmpty ? confirmButtonTitle : NSLocalizedString("Ok", comment: "") + let cancelButton = cancelButtonTitle != nil && !cancelButtonTitle!.isEmpty ? cancelButtonTitle : NSLocalizedString("Cancel", comment: "") + + let confirmController = UIAlertController(title: nil, message: dialogMessage, preferredStyle: .alert) + + confirmController.addAction(UIAlertAction(title: okButton, style: .default, handler: { (action) in + completionHandler(true) + })) + + confirmController.addAction(UIAlertAction(title: cancelButton, style: .cancel, handler: { (action) in + completionHandler(false) + })) + + guard let presentingViewController = inAppBrowserDelegate != nil ? inAppBrowserDelegate as? InAppBrowserWebViewController : window?.rootViewController else { + completionHandler(false) + return + } + presentingViewController.present(confirmController, animated: true, completion: nil) + } + + public func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, + completionHandler: @escaping (Bool) -> Void) { + var completionHandlerCalled = false + + let callback = WebViewChannelDelegate.JsConfirmCallback() + callback.nonNullSuccess = { (response: JsConfirmResponse) in + if response.handledByClient { + completionHandlerCalled = true + let action = response.action ?? 1 + switch action { + case 0: + completionHandler(true) + break + case 1: + completionHandler(false) + break + default: + completionHandler(false) + } + return false + } + return true + } + callback.defaultBehaviour = { [weak self] (response: JsConfirmResponse?) in + if !completionHandlerCalled { + completionHandlerCalled = true + let responseMessage = response?.message + let confirmButtonTitle = response?.confirmButtonTitle + let cancelButtonTitle = response?.cancelButtonTitle + self?.createConfirmDialog(message: message, responseMessage: responseMessage, confirmButtonTitle: confirmButtonTitle, cancelButtonTitle: cancelButtonTitle, completionHandler: completionHandler) + } + } + callback.error = { (code: String, message: String?, details: Any?) in + if !completionHandlerCalled { + completionHandlerCalled = true + print(code + ", " + (message ?? "")) + completionHandler(false) + } + } + + if let channelDelegate = channelDelegate { + channelDelegate.onJsConfirm(url: frame.request.url, message: message, isMainFrame: frame.isMainFrame, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + func createPromptDialog(message: String, defaultValue: String?, responseMessage: String?, confirmButtonTitle: String?, cancelButtonTitle: String?, value: String?, completionHandler: @escaping (String?) -> Void) { + let dialogMessage = responseMessage != nil && !responseMessage!.isEmpty ? responseMessage : message + let okButton = confirmButtonTitle != nil && !confirmButtonTitle!.isEmpty ? confirmButtonTitle : NSLocalizedString("Ok", comment: "") + let cancelButton = cancelButtonTitle != nil && !cancelButtonTitle!.isEmpty ? cancelButtonTitle : NSLocalizedString("Cancel", comment: "") + + let promptController = UIAlertController(title: nil, message: dialogMessage, preferredStyle: .alert) + + promptController.addTextField { (textField) in + textField.text = defaultValue + } + + promptController.addAction(UIAlertAction(title: okButton, style: .default, handler: { (action) in + if let v = value { + completionHandler(v) + } + else if let text = promptController.textFields?.first?.text { + completionHandler(text) + } else { + completionHandler("") + } + })) + + promptController.addAction(UIAlertAction(title: cancelButton, style: .cancel, handler: { (action) in + completionHandler(nil) + })) + + guard let presentingViewController = inAppBrowserDelegate != nil ? inAppBrowserDelegate as? InAppBrowserWebViewController : window?.rootViewController else { + completionHandler(nil) + return + } + presentingViewController.present(promptController, animated: true, completion: nil) + } + + public func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt message: String, defaultText defaultValue: String?, initiatedByFrame frame: WKFrameInfo, + completionHandler: @escaping (String?) -> Void) { + + var completionHandlerCalled = false + + let callback = WebViewChannelDelegate.JsPromptCallback() + callback.nonNullSuccess = { (response: JsPromptResponse) in + if response.handledByClient { + completionHandlerCalled = true + let action = response.action ?? 1 + switch action { + case 0: + completionHandler(response.value) + break + case 1: + completionHandler(nil) + break + default: + completionHandler(nil) + } + return false + } + return true + } + callback.defaultBehaviour = { [weak self] (response: JsPromptResponse?) in + if !completionHandlerCalled { + completionHandlerCalled = true + let responseMessage = response?.message + let confirmButtonTitle = response?.confirmButtonTitle + let cancelButtonTitle = response?.cancelButtonTitle + let value = response?.value + self?.createPromptDialog(message: message, defaultValue: defaultValue, responseMessage: responseMessage, confirmButtonTitle: confirmButtonTitle, + cancelButtonTitle: cancelButtonTitle, value: value, completionHandler: completionHandler) + } + } + callback.error = { (code: String, message: String?, details: Any?) in + if !completionHandlerCalled { + completionHandlerCalled = true + print(code + ", " + (message ?? "")) + completionHandler(nil) + } + } + + if let channelDelegate = channelDelegate { + channelDelegate.onJsPrompt(url: frame.request.url, message: message, defaultValue: defaultValue, isMainFrame: frame.isMainFrame, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + /// UIScrollViewDelegate is somehow bugged: + /// if InAppWebView implements the UIScrollViewDelegate protocol and implement the scrollViewDidScroll event, + /// then, when the user scrolls the content, the webview content is not rendered (just white space). + /// Calling setNeedsLayout() resolves this problem, but, for some reason, the bounce effect is canceled. + /// + /// So, to track the same event, without implementing the scrollViewDidScroll event, we create + /// an observer that observes the scrollView.contentOffset property. + /// This way, we don't need to call setNeedsLayout() and all works fine. + public func onScrollChanged(startedByUser: Bool, oldContentOffset: CGPoint?) { + let disableVerticalScroll = settings?.disableVerticalScroll ?? false + let disableHorizontalScroll = settings?.disableHorizontalScroll ?? false + if startedByUser { + if disableVerticalScroll && disableHorizontalScroll { + scrollView.contentOffset = CGPoint(x: lastScrollX, y: lastScrollY) + } + else if disableVerticalScroll { + if scrollView.contentOffset.y >= 0 || scrollView.contentOffset.y < 0 { + scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: lastScrollY) + } + } + else if disableHorizontalScroll { + if scrollView.contentOffset.x >= 0 || scrollView.contentOffset.x < 0 { + scrollView.contentOffset = CGPoint(x: lastScrollX, y: scrollView.contentOffset.y) + } + } + } + if (!disableVerticalScroll && !disableHorizontalScroll) || + (disableVerticalScroll && scrollView.contentOffset.x != oldContentOffset?.x) || + (disableHorizontalScroll && scrollView.contentOffset.y != oldContentOffset?.y) { + let x = Int(scrollView.contentOffset.x / scrollView.contentScaleFactor) + let y = Int(scrollView.contentOffset.y / scrollView.contentScaleFactor) + channelDelegate?.onScrollChanged(x: x, y: y) + } + lastScrollX = scrollView.contentOffset.x + lastScrollY = scrollView.contentOffset.y + + let overScrolledHorizontally = lastScrollX < 0 || lastScrollX > (scrollView.contentSize.width - scrollView.frame.size.width) + let overScrolledVertically = lastScrollY < 0 || lastScrollY > (scrollView.contentSize.height - scrollView.frame.size.height) + if overScrolledHorizontally || overScrolledVertically { + let x = Int(lastScrollX / scrollView.contentScaleFactor) + let y = Int(lastScrollY / scrollView.contentScaleFactor) + channelDelegate?.onOverScrolled(x: x, y: y, + clampedX: overScrolledHorizontally, + clampedY: overScrolledVertically) + } + } + + public func onContentSizeChanged(oldContentSize: CGSize) { + channelDelegate?.onContentSizeChanged(oldContentSize: oldContentSize, + newContentSize: scrollView.contentSize) + } + + public func scrollViewDidZoom(_ scrollView: UIScrollView) { + let newScale = Float(scrollView.zoomScale) + if newScale != oldZoomScale { + channelDelegate?.onZoomScaleChanged(newScale: newScale, oldScale: oldZoomScale) + oldZoomScale = newScale + } + } + + public func webView(_ webView: WKWebView, + createWebViewWith configuration: WKWebViewConfiguration, + for navigationAction: WKNavigationAction, + windowFeatures: WKWindowFeatures) -> WKWebView? { + + var windowId: Int64 = 0 + let inAppWebViewManager = plugin?.inAppWebViewManager + if let inAppWebViewManager = inAppWebViewManager { + inAppWebViewManager.windowAutoincrementId += 1 + windowId = inAppWebViewManager.windowAutoincrementId + } + + let windowWebView = InAppWebView(id: nil, plugin: nil, frame: self.bounds, configuration: configuration, contextMenu: nil) + windowWebView.windowId = windowId + + let webViewTransport = WebViewTransport( + webView: windowWebView, + request: navigationAction.request + ) + + inAppWebViewManager?.windowWebViews[windowId] = webViewTransport + + let createWindowAction = CreateWindowAction(navigationAction: navigationAction, windowId: windowId, windowFeatures: windowFeatures, isDialog: nil) + + let callback = WebViewChannelDelegate.CreateWindowCallback() + callback.nonNullSuccess = { (handledByClient: Bool) in + return !handledByClient + } + callback.defaultBehaviour = { [weak self] (handledByClient: Bool?) in + if inAppWebViewManager?.windowWebViews[windowId] != nil { + inAppWebViewManager?.windowWebViews.removeValue(forKey: windowId) + } + self?.loadUrl(urlRequest: navigationAction.request, allowingReadAccessTo: nil) + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + if let channelDelegate = channelDelegate { + channelDelegate.onCreateWindow(createWindowAction: createWindowAction, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + + return windowWebView + } + + public func webView(_ webView: WKWebView, + authenticationChallenge challenge: URLAuthenticationChallenge, + shouldAllowDeprecatedTLS decisionHandler: @escaping (Bool) -> Void) { + var decisionHandlerCalled = false + let callback = WebViewChannelDelegate.ShouldAllowDeprecatedTLSCallback() + callback.nonNullSuccess = { (action: Bool) in + decisionHandlerCalled = true + decisionHandler(action) + return false + } + callback.defaultBehaviour = { (action: Bool?) in + if !decisionHandlerCalled { + decisionHandlerCalled = true + decisionHandler(false) + } + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + let runCallback = { + if let channelDelegate = self.channelDelegate { + channelDelegate.shouldAllowDeprecatedTLS(challenge: challenge, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + if windowId != nil, !windowCreated { + windowBeforeCreatedCallbacks.append(runCallback) + } else { + runCallback() + } + } + + public func webViewDidClose(_ webView: WKWebView) { + channelDelegate?.onCloseWindow() + } + + public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { + channelDelegate?.onWebContentProcessDidTerminate() + } + + public func webView(_ webView: WKWebView, + didCommit navigation: WKNavigation!) { + channelDelegate?.onPageCommitVisible(url: url?.absoluteString) + } + + public func webView(_ webView: WKWebView, + didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!) { + channelDelegate?.onDidReceiveServerRedirectForProvisionalNavigation() + } + +// @available(iOS 13.0, *) +// public func webView(_ webView: WKWebView, +// contextMenuConfigurationForElement elementInfo: WKContextMenuElementInfo, +// completionHandler: @escaping (UIContextMenuConfiguration?) -> Void) { +// print("contextMenuConfigurationForElement") +// let actionProvider: UIContextMenuActionProvider = { _ in +// let editMenu = UIMenu(title: "Edit...", children: [ +// UIAction(title: "Copy") { action in +// +// }, +// UIAction(title: "Duplicate") { action in +// +// } +// ]) +// return UIMenu(title: "Title", children: [ +// UIAction(title: "Share") { action in +// +// }, +// editMenu +// ]) +// } +// let contextMenuConfiguration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: actionProvider) +// //completionHandler(contextMenuConfiguration) +// completionHandler(nil) +//// onContextMenuConfigurationForElement(linkURL: elementInfo.linkURL?.absoluteString, result: nil/*{(result) -> Void in +//// if result is FlutterError { +//// print((result as! FlutterError).message ?? "") +//// } +//// else if (result as? NSObject) == FlutterMethodNotImplemented { +//// completionHandler(nil) +//// } +//// else { +//// var response: [String: Any] +//// if let r = result { +//// response = r as! [String: Any] +//// var action = response["action"] as? Int +//// action = action != nil ? action : 0; +//// switch action { +//// case 0: +//// break +//// case 1: +//// break +//// default: +//// completionHandler(nil) +//// } +//// return; +//// } +//// completionHandler(nil) +//// } +//// }*/) +// } +//// +// @available(iOS 13.0, *) +// public func webView(_ webView: WKWebView, +// contextMenuDidEndForElement elementInfo: WKContextMenuElementInfo) { +// print("contextMenuDidEndForElement") +// print(elementInfo) +// //onContextMenuDidEndForElement(linkURL: elementInfo.linkURL?.absoluteString) +// } +// +// @available(iOS 13.0, *) +// public func webView(_ webView: WKWebView, +// contextMenuForElement elementInfo: WKContextMenuElementInfo, +// willCommitWithAnimator animator: UIContextMenuInteractionCommitAnimating) { +// print("willCommitWithAnimator") +// print(elementInfo) +//// onWillCommitWithAnimator(linkURL: elementInfo.linkURL?.absoluteString, result: nil/*{(result) -> Void in +//// if result is FlutterError { +//// print((result as! FlutterError).message ?? "") +//// } +//// else if (result as? NSObject) == FlutterMethodNotImplemented { +//// +//// } +//// else { +//// var response: [String: Any] +//// if let r = result { +//// response = r as! [String: Any] +//// var action = response["action"] as? Int +//// action = action != nil ? action : 0; +////// switch action { +////// case 0: +////// break +////// case 1: +////// break +////// default: +////// +////// } +//// return; +//// } +//// +//// } +//// }*/) +// } +// +// @available(iOS 13.0, *) +// public func webView(_ webView: WKWebView, +// contextMenuWillPresentForElement elementInfo: WKContextMenuElementInfo) { +// print("contextMenuWillPresentForElement") +// print(elementInfo.linkURL) +// //onContextMenuWillPresentForElement(linkURL: elementInfo.linkURL?.absoluteString) +// } + + + // https://stackoverflow.com/a/42840541/4637638 + public func isVideoPlayerWindow(_ notificationObject: AnyObject?) -> Bool { + let nonVideoClasses = ["_UIAlertControllerShimPresenterWindow", + "UITextEffectsWindow", + "UIRemoteKeyboardWindow", + "PGHostedWindow"] + + var isVideo = true + if let obj = notificationObject { + for nonVideoClass in nonVideoClasses { + if let clazz = NSClassFromString(nonVideoClass) { + isVideo = isVideo && !(obj.isKind(of: clazz)) + } + } + } + return isVideo + } + + @objc func onEnterFullscreen(_ notification: Notification) { + // TODO: Still not working on iOS 16.0! +// if #available(iOS 16.0, *) { +// channelDelegate?.onEnterFullscreen() +// inFullscreen = true +// } +// else + if (isVideoPlayerWindow(notification.object as AnyObject?)) { + channelDelegate?.onEnterFullscreen() + inFullscreen = true + } + } + + @objc func onExitFullscreen(_ notification: Notification) { + // TODO: Still not working on iOS 16.0! +// if #available(iOS 16.0, *) { +// channelDelegate?.onExitFullscreen() +// inFullscreen = false +// } +// else + if (isVideoPlayerWindow(notification.object as AnyObject?)) { + channelDelegate?.onExitFullscreen() + inFullscreen = false + } + } + +// public func onContextMenuConfigurationForElement(linkURL: String?, result: FlutterResult?) { +// let arguments: [String: Any?] = ["linkURL": linkURL] +// channel?.invokeMethod("onContextMenuConfigurationForElement", arguments: arguments, result: result) +// } +// +// public func onContextMenuDidEndForElement(linkURL: String?) { +// let arguments: [String: Any?] = ["linkURL": linkURL] +// channel?.invokeMethod("onContextMenuDidEndForElement", arguments: arguments) +// } +// +// public func onWillCommitWithAnimator(linkURL: String?, result: FlutterResult?) { +// let arguments: [String: Any?] = ["linkURL": linkURL] +// channel?.invokeMethod("onWillCommitWithAnimator", arguments: arguments, result: result) +// } +// +// public func onContextMenuWillPresentForElement(linkURL: String?) { +// let arguments: [String: Any?] = ["linkURL": linkURL] +// channel?.invokeMethod("onContextMenuWillPresentForElement", arguments: arguments) +// } + + public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + guard let body = message.body as? [String: Any?] else { + return + } + + if ["consoleLog", "consoleDebug", "consoleError", "consoleInfo", "consoleWarn"].contains(message.name) { + var messageLevel = 1 + switch (message.name) { + case "consoleLog": + messageLevel = 1 + break + case "consoleDebug": + // on Android, console.debug is TIP + messageLevel = 0 + break + case "consoleError": + messageLevel = 3 + break + case "consoleInfo": + // on Android, console.info is LOG + messageLevel = 1 + break + case "consoleWarn": + messageLevel = 2 + break + default: + messageLevel = 1 + break + } + let consoleMessage = body["message"] as? String ?? "" + + let _windowId = body["_windowId"] as? Int64 + var webView = self + if let wId = _windowId, let webViewTransport = plugin?.inAppWebViewManager?.windowWebViews[wId] { + webView = webViewTransport.webView + } + webView.channelDelegate?.onConsoleMessage(message: consoleMessage, messageLevel: messageLevel) + } else if message.name == "callHandler", let handlerName = body["handlerName"] as? String { + if handlerName == "onPrintRequest" { + let settings = PrintJobSettings() + settings.handledByClient = true + if let printJobId = printCurrentPage(settings: settings) { + let callback = WebViewChannelDelegate.PrintRequestCallback() + callback.nonNullSuccess = { (handledByClient: Bool) in + return !handledByClient + } + callback.defaultBehaviour = { [weak self] (handledByClient: Bool?) in + if let printJob = self?.plugin?.printJobManager?.jobs[printJobId] { + printJob?.disposeNoDismiss() + } + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + channelDelegate?.onPrintRequest(url: url, printJobId: printJobId, callback: callback) + } + return + } + + let _callHandlerID = body["_callHandlerID"] as? Int64 ?? 0 + let args = body["args"] as? String ?? "" + + let _windowId = body["_windowId"] as? Int64 + var webView = self + if let wId = _windowId, let webViewTransport = plugin?.inAppWebViewManager?.windowWebViews[wId] { + webView = webViewTransport.webView + } + + let callback = WebViewChannelDelegate.CallJsHandlerCallback() + callback.defaultBehaviour = { [weak self] (response: Any?) in + var json = "null" + if let r = response as? String { + json = r + } + + self?.evaluateJavaScript(""" +if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { + window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)].resolve(\(json)); + delete window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)]; +} +""", completionHandler: nil) + } + callback.error = { [weak self] (code: String, message: String?, details: Any?) in + let errorMessage = code + (message != nil ? ", " + (message ?? "") : "") + print(errorMessage) + + self?.evaluateJavaScript(""" +if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { + window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)].reject(new Error('\(errorMessage.replacingOccurrences(of: "\'", with: "\\'"))')); + delete window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)]; +} +""", completionHandler: nil) + } + + if let channelDelegate = webView.channelDelegate { + channelDelegate.onCallJsHandler(handlerName: handlerName, args: args, callback: callback) + } + } else if message.name == "onFindResultReceived", + let findResult = body["findResult"] as? [String: Any], + let activeMatchOrdinal = findResult["activeMatchOrdinal"] as? Int, + let numberOfMatches = findResult["numberOfMatches"] as? Int, + let isDoneCounting = findResult["isDoneCounting"] as? Bool { + + let _windowId = body["_windowId"] as? Int64 + var webView = self + if let wId = _windowId, let webViewTransport = plugin?.inAppWebViewManager?.windowWebViews[wId] { + webView = webViewTransport.webView + } + webView.findInteractionController?.channelDelegate?.onFindResultReceived(activeMatchOrdinal: activeMatchOrdinal, numberOfMatches: numberOfMatches, isDoneCounting: isDoneCounting) + webView.channelDelegate?.onFindResultReceived(activeMatchOrdinal: activeMatchOrdinal, numberOfMatches: numberOfMatches, isDoneCounting: isDoneCounting) + } else if message.name == "onCallAsyncJavaScriptResultBelowIOS14Received", + let resultUuid = body["resultUuid"] as? String, + let result = callAsyncJavaScriptBelowIOS14Results[resultUuid] { + result([ + "value": body["value"], + "error": body["error"] + ]) + callAsyncJavaScriptBelowIOS14Results.removeValue(forKey: resultUuid) + } else if message.name == "onWebMessagePortMessageReceived", + let webMessageChannelId = body["webMessageChannelId"] as? String, + let index = body["index"] as? Int64 { + var webMessage: WebMessage? = nil + if let webMessageMap = body["message"] as? [String : Any?] { + webMessage = WebMessage.fromMap(map: webMessageMap) + } + + if let webMessageChannel = webMessageChannels[webMessageChannelId] { + webMessageChannel.channelDelegate?.onMessage(index: index, message: webMessage) + } + } else if message.name == "onWebMessageListenerPostMessageReceived", let jsObjectName = body["jsObjectName"] as? String { + var webMessage: WebMessage? = nil + if let webMessageMap = body["message"] as? [String : Any?] { + webMessage = WebMessage.fromMap(map: webMessageMap) + } + + if let webMessageListener = webMessageListeners.first(where: ({($0.jsObjectName == jsObjectName)})) { + let isMainFrame = message.frameInfo.isMainFrame + + var scheme: String? = nil + var host: String? = nil + var port: Int? = nil + if #available(iOS 9.0, *) { + let sourceOrigin = message.frameInfo.securityOrigin + scheme = sourceOrigin.protocol + host = sourceOrigin.host + port = sourceOrigin.port + } else if let url = message.frameInfo.request.url { + scheme = url.scheme + host = url.host + port = url.port + } + + if !webMessageListener.isOriginAllowed(scheme: scheme, host: host, port: port) { + return + } + + var sourceOrigin: URL? = nil + if let scheme = scheme, !scheme.isEmpty, let host = host, !host.isEmpty { + sourceOrigin = URL(string: "\(scheme)://\(host)\(port != nil && port != 0 ? ":" + String(port!) : "")") + } + webMessageListener.channelDelegate?.onPostMessage(message: webMessage, sourceOrigin: sourceOrigin, isMainFrame: isMainFrame) + } + } + } + + public func scrollTo(x: Int, y: Int, animated: Bool) { + scrollView.setContentOffset(CGPoint(x: x, y: y), animated: animated) + } + + public func scrollBy(x: Int, y: Int, animated: Bool) { + let newX = CGFloat(x) + scrollView.contentOffset.x + let newY = CGFloat(y) + scrollView.contentOffset.y + scrollView.setContentOffset(CGPoint(x: newX, y: newY), animated: animated) + } + + + public func pauseTimers() { + if !isPausedTimers { + isPausedTimers = true + let script = "alert();"; + self.evaluateJavaScript(script, completionHandler: nil) + } + } + + public func resumeTimers() { + if isPausedTimers { + if let completionHandler = isPausedTimersCompletionHandler { + self.isPausedTimersCompletionHandler = nil + completionHandler() + } + isPausedTimers = false + } + } + + public func printCurrentPage(settings: PrintJobSettings? = nil, + completionHandler: UIPrintInteractionController.CompletionHandler? = nil) -> String? { + var printJobId: String? = nil + if let settings = settings, settings.handledByClient { + printJobId = NSUUID().uuidString + } + + let printController = UIPrintInteractionController.shared + let printFormatter = self.viewPrintFormatter() + if let settings = settings { + if let margins = settings.margins { + printFormatter.perPageContentInsets = margins + } + if let maximumContentHeight = settings.maximumContentHeight { + printFormatter.maximumContentHeight = maximumContentHeight + } + if let maximumContentWidth = settings.maximumContentWidth { + printFormatter.maximumContentWidth = maximumContentWidth + } + } + printController.printFormatter = printFormatter + + printController.printInfo = UIPrintInfo(dictionary: nil) + if let printInfo = printController.printInfo { + printInfo.jobName = settings?.jobName ?? (title ?? url?.absoluteString ?? "") + " Document" + if let settings = settings { + if let orientationValue = settings.orientation, + let orientation = UIPrintInfo.Orientation.init(rawValue: orientationValue) { + printInfo.orientation = orientation + } + if let duplexModeValue = settings.duplexMode, + let duplexMode = UIPrintInfo.Duplex.init(rawValue: duplexModeValue) { + printInfo.duplex = duplexMode + } + if let outputTypeValue = settings.outputType, + let outputType = UIPrintInfo.OutputType.init(rawValue: outputTypeValue) { + printInfo.outputType = outputType + } + } + } + + // initialize print renderer and set its formatter + let printRenderer = CustomUIPrintPageRenderer(numberOfPage: settings?.numberOfPages, + forceRenderingQuality: settings?.forceRenderingQuality) + printRenderer.addPrintFormatter(printFormatter, startingAtPageAt: 0) + if let settings = settings { + if let footerHeight = settings.footerHeight { + printRenderer.footerHeight = footerHeight + } + if let headerHeight = settings.headerHeight { + printRenderer.headerHeight = headerHeight + } + } + printController.printPageRenderer = printRenderer + + if let settings = settings { + printController.showsNumberOfCopies = settings.showsNumberOfCopies + printController.showsPaperSelectionForLoadedPapers = settings.showsPaperSelectionForLoadedPapers + if #available(iOS 15.0, *) { + printController.showsPaperOrientation = settings.showsPaperOrientation + } + } + + let animated = settings?.animated ?? true + if let id = printJobId, let plugin = plugin { + let printJob = PrintJobController(plugin: plugin, id: id, job: printController, settings: settings) + plugin.printJobManager?.jobs[id] = printJob + printJob.present(animated: animated, completionHandler: completionHandler) + } else { + printController.present(animated: animated, completionHandler: completionHandler) + } + + return printJobId + } + + public func getContentHeight() -> Int64 { + return Int64(scrollView.contentSize.height) + } + + public func getContentWidth() -> Int64 { + return Int64(scrollView.contentSize.width) + } + + public func zoomBy(zoomFactor: Float, animated: Bool) { + let currentZoomScale = scrollView.zoomScale + scrollView.setZoomScale(currentZoomScale * CGFloat(zoomFactor), animated: animated) + } + + public func getOriginalUrl() -> URL? { + return currentOriginalUrl + } + + public func getZoomScale() -> Float { + return Float(scrollView.zoomScale) + } + + public func getSelectedText(completionHandler: @escaping (Any?, Error?) -> Void) { + if configuration.preferences.javaScriptEnabled { + evaluateJavaScript(PluginScriptsUtil.GET_SELECTED_TEXT_JS_SOURCE, completionHandler: completionHandler) + } else { + completionHandler(nil, nil) + } + } + + public func getHitTestResult(completionHandler: @escaping (HitTestResult) -> Void) { + if configuration.preferences.javaScriptEnabled, let lastTouchLocation = lastTouchPoint { + self.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._findElementsAtPoint(\(lastTouchLocation.x),\(lastTouchLocation.y))", completionHandler: {(value, error) in + if error != nil { + print("getHitTestResult error: \(error?.localizedDescription ?? "")") + completionHandler(HitTestResult(type: .unknownType, extra: nil)) + } else if let value = value as? [String: Any?] { + let hitTestResult = HitTestResult.fromMap(map: value)! + completionHandler(hitTestResult) + } else { + completionHandler(HitTestResult(type: .unknownType, extra: nil)) + } + }) + } else { + completionHandler(HitTestResult(type: .unknownType, extra: nil)) + } + } + + public func requestFocusNodeHref(completionHandler: @escaping ([String: Any?]?, Error?) -> Void) { + if configuration.preferences.javaScriptEnabled { + // add some delay to make it sure _lastAnchorOrImageTouched is updated + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { + self.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._lastAnchorOrImageTouched", completionHandler: {(value, error) in + let lastAnchorOrImageTouched = value as? [String: Any?] + completionHandler(lastAnchorOrImageTouched, error) + }) + } + } else { + completionHandler(nil, nil) + } + } + + public func requestImageRef(completionHandler: @escaping ([String: Any?]?, Error?) -> Void) { + if configuration.preferences.javaScriptEnabled { + // add some delay to make it sure _lastImageTouched is updated + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { + self.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._lastImageTouched", completionHandler: {(value, error) in + let lastImageTouched = value as? [String: Any?] + completionHandler(lastImageTouched, error) + }) + } + } else { + completionHandler(nil, nil) + } + } + + public func clearFocus() { + self.scrollView.subviews.first?.resignFirstResponder() + } + + public func getCertificate() -> SslCertificate? { + guard let scheme = url?.scheme, + scheme == "https", + let host = url?.host, + let sslCertificate = InAppWebView.sslCertificatesMap[host] else { + return nil + } + return sslCertificate + } + + public func isSecureContext(completionHandler: @escaping (_ isSecureContext: Bool) -> Void) { + evaluateJavascript(source: "window.isSecureContext") { (isSecureContext) in + if let isSecureContext = isSecureContext { + completionHandler(isSecureContext as? Bool ?? false) + return + } + completionHandler(false) + } + } + + public func canScrollVertically() -> Bool { + return scrollView.contentSize.height > self.frame.height + } + + public func canScrollHorizontally() -> Bool { + return scrollView.contentSize.width > self.frame.width + } + + public func enablePullToRefresh() { + if let pullToRefreshControl = pullToRefreshControl { + if #available(iOS 10.0, *) { + scrollView.refreshControl = pullToRefreshControl + } else { + scrollView.addSubview(pullToRefreshControl) + } + } + } + + public func disablePullToRefresh() { + pullToRefreshControl?.removeFromSuperview() + if #available(iOS 10.0, *) { + scrollView.refreshControl = nil + } + } + + public func isPullToRefreshEnabled() -> Bool { + if #available(iOS 10.0, *) { + return scrollView.refreshControl != nil + } else { + return pullToRefreshControl?.superview != nil + } + } + + public func createWebMessageChannel(completionHandler: ((WebMessageChannel?) -> Void)? = nil) -> WebMessageChannel? { + guard let plugin = plugin else { + completionHandler?(nil) + return nil + } + let id = NSUUID().uuidString + let webMessageChannel = WebMessageChannel(plugin: plugin, id: id) + webMessageChannel.initJsInstance(webView: self, completionHandler: completionHandler) + webMessageChannels[id] = webMessageChannel + + return webMessageChannel + } + + public func postWebMessage(message: WebMessage, targetOrigin: String, completionHandler: ((Any?) -> Void)? = nil) throws { + var portsString = "null" + if let ports = message.ports { + var portArrayString: [String] = [] + for port in ports { + if port.isStarted { + throw NSError(domain: "Port is already started", code: 0) + } + if port.isClosed || port.isTransferred { + throw NSError(domain: "Port is already closed or transferred", code: 0) + } + port.isTransferred = true + portArrayString.append("\(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)['\(port.webMessageChannel!.id)'].\(port.name)") + } + portsString = "[" + portArrayString.joined(separator: ", ") + "]" + } + + let url = URL(string: targetOrigin)?.absoluteString ?? "*" + let source = """ + (function() { + window.postMessage(\(message.jsData), '\(url)', \(portsString)); + })(); + """ + evaluateJavascript(source: source, completionHandler: completionHandler) + message.dispose() + } + + public func addWebMessageListener(webMessageListener: WebMessageListener) throws { + if webMessageListeners.map({ ($0.jsObjectName) }).contains(webMessageListener.jsObjectName) { + throw NSError(domain: "jsObjectName \(webMessageListener.jsObjectName) was already added.", code: 0) + } + try webMessageListener.assertOriginRulesValid() + webMessageListener.initJsInstance(webView: self) + webMessageListeners.append(webMessageListener) + } + + public func disposeWebMessageChannels() { + for webMessageChannel in webMessageChannels.values { + webMessageChannel.dispose() + } + webMessageChannels.removeAll() + } + + // https://stackoverflow.com/a/58001395/4637638 + public override var inputAccessoryView: UIView? { + return settings?.disableInputAccessoryView ?? false ? nil : super.inputAccessoryView + } + + public func runWindowBeforeCreatedCallbacks() { + let callbacks = windowBeforeCreatedCallbacks + callbacks.forEach { (callback) in + callback() + } + windowBeforeCreatedCallbacks.removeAll() + } + + public func dispose() { + channelDelegate?.dispose() + channelDelegate = nil + runWindowBeforeCreatedCallbacks() + removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress)) + removeObserver(self, forKeyPath: #keyPath(WKWebView.url)) + removeObserver(self, forKeyPath: #keyPath(WKWebView.title)) + if #available(iOS 15.0, *) { + removeObserver(self, forKeyPath: #keyPath(WKWebView.cameraCaptureState)) + removeObserver(self, forKeyPath: #keyPath(WKWebView.microphoneCaptureState)) + } + // TODO: Still not working on iOS 16.0! +// if #available(iOS 16.0, *) { +// removeObserver(self, forKeyPath: #keyPath(WKWebView.fullscreenState)) +// } + scrollView.removeObserver(self, forKeyPath: #keyPath(UIScrollView.contentOffset)) + scrollView.removeObserver(self, forKeyPath: #keyPath(UIScrollView.zoomScale)) + scrollView.removeObserver(self, forKeyPath: #keyPath(UIScrollView.contentSize)) + resumeTimers() + stopLoading() + disposeWebMessageChannels() + for webMessageListener in webMessageListeners { + webMessageListener.dispose() + } + webMessageListeners.removeAll() + interceptOnlyAsyncAjaxRequestsPluginScript = nil + if windowId == nil { + configuration.userContentController.removeAllPluginScriptMessageHandlers() + configuration.userContentController.removeScriptMessageHandler(forName: "onCallAsyncJavaScriptResultBelowIOS14Received") + configuration.userContentController.removeScriptMessageHandler(forName: "onWebMessagePortMessageReceived") + configuration.userContentController.removeScriptMessageHandler(forName: "onWebMessageListenerPostMessageReceived") + configuration.userContentController.removeAllUserScripts() + if #available(iOS 11.0, *) { + configuration.userContentController.removeAllContentRuleLists() + } + } else if let wId = windowId, plugin?.inAppWebViewManager?.windowWebViews[wId] != nil { + plugin?.inAppWebViewManager?.windowWebViews.removeValue(forKey: wId) + } + configuration.userContentController.dispose(windowId: windowId) + NotificationCenter.default.removeObserver(self) + for imp in customIMPs { + imp_removeBlock(imp) + } + longPressRecognizer.removeTarget(self, action: #selector(longPressGestureDetected)) + longPressRecognizer.delegate = nil + scrollView.removeGestureRecognizer(longPressRecognizer) + recognizerForDisablingContextMenuOnLinks.removeTarget(self, action: #selector(longPressGestureDetected)) + recognizerForDisablingContextMenuOnLinks.delegate = nil + scrollView.removeGestureRecognizer(recognizerForDisablingContextMenuOnLinks) + panGestureRecognizer.removeTarget(self, action: #selector(endDraggingDetected)) + panGestureRecognizer.delegate = nil + scrollView.removeGestureRecognizer(panGestureRecognizer) + disablePullToRefresh() + pullToRefreshControl?.dispose() + pullToRefreshControl = nil + findInteractionController?.dispose() + findInteractionController = nil + uiDelegate = nil + navigationDelegate = nil + scrollView.delegate = nil + isPausedTimersCompletionHandler = nil + SharedLastTouchPointTimestamp.removeValue(forKey: self) + callAsyncJavaScriptBelowIOS14Results.removeAll() + plugin = nil + } + + deinit { + debugPrint("InAppWebView - dealloc") + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/InAppWebViewManager.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/InAppWebViewManager.swift new file mode 100755 index 00000000..df5c3b59 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/InAppWebViewManager.swift @@ -0,0 +1,128 @@ +// +// InAppWebViewManager.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/12/2019. +// + +import Foundation +import WebKit + +public class InAppWebViewManager: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_manager" + var plugin: SwiftFlutterPlugin? + var webViewForUserAgent: WKWebView? + var defaultUserAgent: String? + + var keepAliveWebViews: [String:FlutterWebViewController?] = [:] + var windowWebViews: [Int64:WebViewTransport] = [:] + var windowAutoincrementId: Int64 = 0 + + init(plugin: SwiftFlutterPlugin) { + super.init(channel: FlutterMethodChannel(name: InAppWebViewManager.METHOD_CHANNEL_NAME, binaryMessenger: plugin.registrar!.messenger())) + self.plugin = plugin + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "getDefaultUserAgent": + getDefaultUserAgent(completionHandler: { (value) in + result(value) + }) + break + case "handlesURLScheme": + let urlScheme = arguments!["urlScheme"] as! String + if #available(iOS 11.0, *) { + result(WKWebView.handlesURLScheme(urlScheme)) + } else { + result(false) + } + break + case "disposeKeepAlive": + let keepAliveId = arguments!["keepAliveId"] as! String + disposeKeepAlive(keepAliveId: keepAliveId) + result(true) + break + case "clearAllCache": + let includeDiskFiles = arguments!["includeDiskFiles"] as! Bool + clearAllCache(includeDiskFiles: includeDiskFiles, completionHandler: { + result(true) + }) + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func getDefaultUserAgent(completionHandler: @escaping (_ value: String?) -> Void) { + if defaultUserAgent == nil { + webViewForUserAgent = WKWebView() + webViewForUserAgent?.evaluateJavaScript("navigator.userAgent") { (value, error) in + + if error != nil { + print("Error occurred to get userAgent") + self.webViewForUserAgent = nil + completionHandler(nil) + return + } + + if let unwrappedUserAgent = value as? String { + self.defaultUserAgent = unwrappedUserAgent + completionHandler(self.defaultUserAgent) + } else { + print("Failed to get userAgent") + } + self.webViewForUserAgent = nil + } + } else { + completionHandler(defaultUserAgent) + } + } + + public func disposeKeepAlive(keepAliveId: String) { + if let flutterWebView = keepAliveWebViews[keepAliveId] as? FlutterWebViewController { + flutterWebView.keepAliveId = nil + flutterWebView.dispose(removeFromSuperview: true) + keepAliveWebViews[keepAliveId] = nil + } + } + + public func clearAllCache(includeDiskFiles: Bool, completionHandler: @escaping () -> Void) { + if #available(iOS 9.0, *) { + var websiteDataTypes = Set([WKWebsiteDataTypeMemoryCache]) + if includeDiskFiles { + websiteDataTypes.insert(WKWebsiteDataTypeDiskCache) + if #available(iOS 11.3, *) { + websiteDataTypes.insert(WKWebsiteDataTypeFetchCache) + } + websiteDataTypes.insert(WKWebsiteDataTypeOfflineWebApplicationCache) + } + let date = NSDate(timeIntervalSince1970: 0) + WKWebsiteDataStore.default().removeData(ofTypes: websiteDataTypes, modifiedSince: date as Date, completionHandler: completionHandler) + } else { + URLCache.shared.removeAllCachedResponses() + completionHandler() + } + } + + public override func dispose() { + super.dispose() + let keepAliveWebViewValues = keepAliveWebViews.values + keepAliveWebViewValues.forEach {(keepAliveWebView: FlutterWebViewController?) in + if let keepAliveId = keepAliveWebView?.keepAliveId { + disposeKeepAlive(keepAliveId: keepAliveId) + } + } + keepAliveWebViews.removeAll() + windowWebViews.removeAll() + webViewForUserAgent = nil + defaultUserAgent = nil + plugin = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/InAppWebViewSettings.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/InAppWebViewSettings.swift new file mode 100755 index 00000000..faa947ce --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/InAppWebViewSettings.swift @@ -0,0 +1,181 @@ +// +// InAppWebViewSettings.swift +// flutter_inappwebview +// +// Created by Lorenzo on 21/10/18. +// + +import Foundation +import WebKit + +@objcMembers +public class InAppWebViewSettings: ISettings { + + var gopeedProfileId = "" + var useShouldOverrideUrlLoading = false + var useOnLoadResource = false + var useOnDownloadStart = false + @available(*, deprecated, message: "Use InAppWebViewManager.clearAllCache instead.") + var clearCache = false + var userAgent = "" + var applicationNameForUserAgent = "" + var javaScriptEnabled = true + var javaScriptCanOpenWindowsAutomatically = false + var mediaPlaybackRequiresUserGesture = true + var verticalScrollBarEnabled = true + var horizontalScrollBarEnabled = true + var resourceCustomSchemes: [String] = [] + var contentBlockers: [[String: [String : Any]]] = [] + var minimumFontSize = 0 + var useShouldInterceptAjaxRequest = false + var interceptOnlyAsyncAjaxRequests = true + var useShouldInterceptFetchRequest = false + var incognito = false + var cacheEnabled = true + var transparentBackground = false + var disableVerticalScroll = false + var disableHorizontalScroll = false + var disableContextMenu = false + var supportZoom = true + var allowUniversalAccessFromFileURLs = false + var allowFileAccessFromFileURLs = false + + var disallowOverScroll = false + var enableViewportScale = false + var suppressesIncrementalRendering = false + var allowsAirPlayForMediaPlayback = true + var allowsBackForwardNavigationGestures = true + var allowsLinkPreview = true + var ignoresViewportScaleLimits = false + var allowsInlineMediaPlayback = false + var allowsPictureInPictureMediaPlayback = true + var isFraudulentWebsiteWarningEnabled = true + var selectionGranularity = 0 + var dataDetectorTypes: [String] = ["NONE"] // WKDataDetectorTypeNone + var preferredContentMode = 0 + var sharedCookiesEnabled = false + var automaticallyAdjustsScrollIndicatorInsets = false + var accessibilityIgnoresInvertColors = false + var decelerationRate = "NORMAL" // UIScrollView.DecelerationRate.normal + var alwaysBounceVertical = false + var alwaysBounceHorizontal = false + var scrollsToTop = true + var isPagingEnabled = false + var maximumZoomScale = 1.0 + var minimumZoomScale = 1.0 + var contentInsetAdjustmentBehavior = 2 // UIScrollView.ContentInsetAdjustmentBehavior.never + var isDirectionalLockEnabled = false + var mediaType: String? = nil + var pageZoom = 1.0 + var limitsNavigationsToAppBoundDomains = false + var useOnNavigationResponse = false + var applePayAPIEnabled = false + var allowingReadAccessTo: String? = nil + var disableLongPressContextMenuOnLinks = false + var disableInputAccessoryView = false + var underPageBackgroundColor: String? + var isTextInteractionEnabled = true + var isSiteSpecificQuirksModeEnabled = true + var upgradeKnownHostsToHTTPS = true + var isElementFullscreenEnabled = true + var isFindInteractionEnabled = false + var minimumViewportInset: UIEdgeInsets? = nil + var maximumViewportInset: UIEdgeInsets? = nil + var isInspectable = false + var shouldPrintBackgrounds = false + + override init(){ + super.init() + } + + override func parse(settings: [String: Any?]) -> InAppWebViewSettings { + var settings = settings // re-assing to be able to use removeValue + if let minimumViewportInsetMap = settings["minimumViewportInset"] as? [String : Double] { + minimumViewportInset = UIEdgeInsets.fromMap(map: minimumViewportInsetMap) + settings.removeValue(forKey: "minimumViewportInset") + } + if let maximumViewportInsetMap = settings["maximumViewportInset"] as? [String : Double] { + maximumViewportInset = UIEdgeInsets.fromMap(map: maximumViewportInsetMap) + settings.removeValue(forKey: "maximumViewportInset") + } + let _ = super.parse(settings: settings) + if #available(iOS 13.0, *) {} else { + applePayAPIEnabled = false + } + return self + } + + override func getRealSettings(obj: InAppWebView?) -> [String: Any?] { + var realSettings: [String: Any?] = toMap() + if let webView = obj { + let configuration = webView.configuration + if #available(iOS 9.0, *) { + realSettings["userAgent"] = webView.customUserAgent + realSettings["applicationNameForUserAgent"] = configuration.applicationNameForUserAgent + realSettings["allowsAirPlayForMediaPlayback"] = configuration.allowsAirPlayForMediaPlayback + realSettings["allowsLinkPreview"] = webView.allowsLinkPreview + realSettings["allowsPictureInPictureMediaPlayback"] = configuration.allowsPictureInPictureMediaPlayback + } + realSettings["javaScriptCanOpenWindowsAutomatically"] = configuration.preferences.javaScriptCanOpenWindowsAutomatically + if #available(iOS 10.0, *) { + realSettings["mediaPlaybackRequiresUserGesture"] = configuration.mediaTypesRequiringUserActionForPlayback == .all + realSettings["ignoresViewportScaleLimits"] = configuration.ignoresViewportScaleLimits + realSettings["dataDetectorTypes"] = Util.getDataDetectorTypeString(type: configuration.dataDetectorTypes) + } else { + realSettings["mediaPlaybackRequiresUserGesture"] = configuration.mediaPlaybackRequiresUserAction + } + realSettings["minimumFontSize"] = Int(configuration.preferences.minimumFontSize) + realSettings["suppressesIncrementalRendering"] = configuration.suppressesIncrementalRendering + realSettings["allowsBackForwardNavigationGestures"] = webView.allowsBackForwardNavigationGestures + realSettings["allowsInlineMediaPlayback"] = configuration.allowsInlineMediaPlayback + if #available(iOS 13.0, *) { + realSettings["isFraudulentWebsiteWarningEnabled"] = configuration.preferences.isFraudulentWebsiteWarningEnabled + realSettings["preferredContentMode"] = configuration.defaultWebpagePreferences.preferredContentMode.rawValue + realSettings["automaticallyAdjustsScrollIndicatorInsets"] = webView.scrollView.automaticallyAdjustsScrollIndicatorInsets + } + realSettings["selectionGranularity"] = configuration.selectionGranularity.rawValue + if #available(iOS 11.0, *) { + realSettings["accessibilityIgnoresInvertColors"] = webView.accessibilityIgnoresInvertColors + realSettings["contentInsetAdjustmentBehavior"] = webView.scrollView.contentInsetAdjustmentBehavior.rawValue + } + realSettings["decelerationRate"] = Util.getDecelerationRateString(type: webView.scrollView.decelerationRate) + realSettings["alwaysBounceVertical"] = webView.scrollView.alwaysBounceVertical + realSettings["alwaysBounceHorizontal"] = webView.scrollView.alwaysBounceHorizontal + realSettings["scrollsToTop"] = webView.scrollView.scrollsToTop + realSettings["isPagingEnabled"] = webView.scrollView.isPagingEnabled + realSettings["maximumZoomScale"] = webView.scrollView.maximumZoomScale + realSettings["minimumZoomScale"] = webView.scrollView.minimumZoomScale + realSettings["allowUniversalAccessFromFileURLs"] = configuration.value(forKey: "allowUniversalAccessFromFileURLs") + realSettings["allowFileAccessFromFileURLs"] = configuration.preferences.value(forKey: "allowFileAccessFromFileURLs") + realSettings["isDirectionalLockEnabled"] = webView.scrollView.isDirectionalLockEnabled + realSettings["javaScriptEnabled"] = configuration.preferences.javaScriptEnabled + if #available(iOS 14.0, *) { + realSettings["mediaType"] = webView.mediaType + realSettings["pageZoom"] = Float(webView.pageZoom) + realSettings["limitsNavigationsToAppBoundDomains"] = configuration.limitsNavigationsToAppBoundDomains + realSettings["javaScriptEnabled"] = configuration.defaultWebpagePreferences.allowsContentJavaScript + } + if #available(iOS 15.0, *) { + realSettings["isTextInteractionEnabled"] = configuration.preferences.isTextInteractionEnabled + realSettings["upgradeKnownHostsToHTTPS"] = configuration.upgradeKnownHostsToHTTPS + realSettings["underPageBackgroundColor"] = webView.underPageBackgroundColor.hexString + } + if #available(iOS 15.4, *) { + realSettings["isSiteSpecificQuirksModeEnabled"] = configuration.preferences.isSiteSpecificQuirksModeEnabled + realSettings["isElementFullscreenEnabled"] = configuration.preferences.isElementFullscreenEnabled + } + if #available(iOS 15.5, *) { + realSettings["minimumViewportInset"] = webView.minimumViewportInset.toMap() + realSettings["maximumViewportInset"] = webView.maximumViewportInset.toMap() + } + if #available(iOS 16.0, *) { + realSettings["isFindInteractionEnabled"] = webView.isFindInteractionEnabled + } + if #available(iOS 16.4, *) { + realSettings["isInspectable"] = webView.isInspectable + realSettings["shouldPrintBackgrounds"] = configuration.preferences.shouldPrintBackgrounds + } + } + return realSettings + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/WebMessage/WebMessageChannel.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/WebMessage/WebMessageChannel.swift new file mode 100644 index 00000000..8e7b3784 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/WebMessage/WebMessageChannel.swift @@ -0,0 +1,79 @@ +// +// WebMessageChannel.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/03/21. +// + +import Foundation + +public class WebMessageChannel: FlutterMethodCallDelegate { + static var METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_web_message_channel_" + var id: String + var plugin: SwiftFlutterPlugin? + var channelDelegate: WebMessageChannelChannelDelegate? + weak var webView: InAppWebView? + var ports: [WebMessagePort] = [] + + public init(plugin: SwiftFlutterPlugin, id: String) { + self.id = id + self.plugin = plugin + super.init() + if let registrar = plugin.registrar { + let channel = FlutterMethodChannel(name: WebMessageChannel.METHOD_CHANNEL_NAME_PREFIX + id, + binaryMessenger: registrar.messenger()) + self.channelDelegate = WebMessageChannelChannelDelegate(webMessageChannel: self, channel: channel) + } + self.ports = [ + WebMessagePort(name: "port1", index: 0, webMessageChannelId: self.id, webMessageChannel: self), + WebMessagePort(name: "port2", index: 1, webMessageChannelId: self.id, webMessageChannel: self) + ] + } + + public func initJsInstance(webView: InAppWebView, completionHandler: ((WebMessageChannel?) -> Void)? = nil) { + self.webView = webView + if let webView = self.webView { + webView.evaluateJavascript(source: """ + (function() { + \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(id)"] = new MessageChannel(); + })(); + """) { (_) in + completionHandler?(self) + } + } else { + completionHandler?(nil) + } + } + + public func toMap() -> [String:Any?] { + return [ + "id": id + ] + } + + public func dispose() { + channelDelegate?.dispose() + channelDelegate = nil + for port in ports { + port.dispose() + } + ports.removeAll() + webView?.evaluateJavascript(source: """ + (function() { + var webMessageChannel = \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(id)"]; + if (webMessageChannel != null) { + webMessageChannel.port1.close(); + webMessageChannel.port2.close(); + delete \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(id)"]; + } + })(); + """) + webView = nil + plugin = nil + } + + deinit { + debugPrint("WebMessageChannel - dealloc") + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/WebMessage/WebMessageChannelChannelDelegate.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/WebMessage/WebMessageChannelChannelDelegate.swift new file mode 100644 index 00000000..2109394a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/WebMessage/WebMessageChannelChannelDelegate.swift @@ -0,0 +1,102 @@ +// +// WebMessageChannelChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class WebMessageChannelChannelDelegate: ChannelDelegate { + private weak var webMessageChannel: WebMessageChannel? + + public init(webMessageChannel: WebMessageChannel, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.webMessageChannel = webMessageChannel + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "setWebMessageCallback": + if let _ = webMessageChannel?.webView, let ports = webMessageChannel?.ports, ports.count > 0 { + let index = arguments!["index"] as! Int + let port = ports[index] + do { + try port.setWebMessageCallback { (_) in + result(true) + } + } catch let error as NSError { + result(FlutterError(code: "WebMessageChannel", message: error.domain, details: nil)) + } + + } else { + result(true) + } + break + case "postMessage": + if let webView = webMessageChannel?.webView, let ports = webMessageChannel?.ports, ports.count > 0 { + let index = arguments!["index"] as! Int + let port = ports[index] + let message = WebMessage.fromMap(map: arguments!["message"] as! [String: Any?]) + + var ports: [WebMessagePort] = [] + if let notConnectedPorts = message.ports { + for notConnectedPort in notConnectedPorts { + if let webMessageChannel = webView.webMessageChannels[notConnectedPort.webMessageChannelId] { + ports.append(webMessageChannel.ports[Int(notConnectedPort.index)]) + } + } + } + message.ports = ports + + do { + try port.postMessage(message: message) { (_) in + result(true) + } + } catch let error as NSError { + result(FlutterError(code: "WebMessageChannel", message: error.domain, details: nil)) + } + } else { + result(true) + } + break + case "close": + if let _ = webMessageChannel?.webView, let ports = webMessageChannel?.ports, ports.count > 0 { + let index = arguments!["index"] as! Int + let port = ports[index] + do { + try port.close { (_) in + result(true) + } + } catch let error as NSError { + result(FlutterError(code: "WebMessageChannel", message: error.domain, details: nil)) + } + } else { + result(true) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onMessage(index: Int64, message: WebMessage?) { + let arguments: [String:Any?] = [ + "index": index, + "message": message?.toMap() + ] + channel?.invokeMethod("onMessage", arguments: arguments) + } + + public override func dispose() { + super.dispose() + webMessageChannel = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/WebMessage/WebMessageListener.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/WebMessage/WebMessageListener.swift new file mode 100644 index 00000000..e902aedb --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/WebMessage/WebMessageListener.swift @@ -0,0 +1,192 @@ +// +// WebMessageListener.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/03/21. +// + +import Foundation +import WebKit + +public class WebMessageListener: FlutterMethodCallDelegate { + static var METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_web_message_listener_" + var id: String + var jsObjectName: String + var allowedOriginRules: Set + var channelDelegate: WebMessageListenerChannelDelegate? + weak var webView: InAppWebView? + var plugin: SwiftFlutterPlugin? + + public init(plugin: SwiftFlutterPlugin, id: String, jsObjectName: String, allowedOriginRules: Set) { + self.id = id + self.plugin = plugin + self.jsObjectName = jsObjectName + self.allowedOriginRules = allowedOriginRules + super.init() + if let registrar = plugin.registrar { + let channel = FlutterMethodChannel(name: WebMessageListener.METHOD_CHANNEL_NAME_PREFIX + self.id + "_" + self.jsObjectName, + binaryMessenger: registrar.messenger()) + self.channelDelegate = WebMessageListenerChannelDelegate(webMessageListener: self, channel: channel) + } + } + + public func assertOriginRulesValid() throws { + for (index, originRule) in allowedOriginRules.enumerated() { + if originRule.isEmpty { + throw NSError(domain: "allowedOriginRules[\(index)] is empty", code: 0) + } + if originRule == "*" { + continue + } + if let url = URL(string: originRule) { + guard let scheme = url.scheme else { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + if scheme == "http" || scheme == "https", url.host == nil || url.host!.isEmpty { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + if scheme != "http", scheme != "https", url.host != nil || url.port != nil { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + if url.host == nil || url.host!.isEmpty, url.port != nil { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + if !url.path.isEmpty { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + if let hostname = url.host { + if let firstIndex = hostname.firstIndex(of: "*") { + let distance = hostname.distance(from: hostname.startIndex, to: firstIndex) + if distance != 0 || (distance == 0 && hostname.prefix(2) != "*.") { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + } + if hostname.hasPrefix("[") { + if !hostname.hasSuffix("]") { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + let fromIndex = hostname.index(hostname.startIndex, offsetBy: 1) + let toIndex = hostname.index(hostname.startIndex, offsetBy: hostname.count - 1) + let indexRange = Range(uncheckedBounds: (lower: fromIndex, upper: toIndex)) + let ipv6 = String(hostname[indexRange]) + if !Util.isIPv6(address: ipv6) { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + } + } + } else { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + } + } + + public func initJsInstance(webView: InAppWebView) { + self.webView = webView + if let webView = self.webView { + let jsObjectNameEscaped = jsObjectName.replacingOccurrences(of: "\'", with: "\\'") + let allowedOriginRulesString = allowedOriginRules.map { (allowedOriginRule) -> String in + if allowedOriginRule == "*" { + return "'*'" + } + let rule = URL(string: allowedOriginRule)! + let host = rule.host != nil ? "'" + rule.host!.replacingOccurrences(of: "\'", with: "\\'") + "'" : "null" + return """ + {scheme: '\(rule.scheme!)', host: \(host), port: \(rule.port != nil ? String(rule.port!) : "null")} + """ + }.joined(separator: ", ") + let source = """ + (function() { + var allowedOriginRules = [\(allowedOriginRulesString)]; + var isPageBlank = window.location.href === "about:blank"; + var scheme = !isPageBlank ? window.location.protocol.replace(":", "") : null; + var host = !isPageBlank ? window.location.hostname : null; + var port = !isPageBlank ? window.location.port : null; + if (window.\(JAVASCRIPT_BRIDGE_NAME)._isOriginAllowed(allowedOriginRules, scheme, host, port)) { + window['\(jsObjectNameEscaped)'] = new FlutterInAppWebViewWebMessageListener('\(jsObjectNameEscaped)'); + } + })(); + """ + webView.configuration.userContentController.addPluginScript(PluginScript( + groupName: "WebMessageListener-" + id + "-" + jsObjectName, + source: source, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: false, + messageHandlerNames: ["onWebMessageListenerPostMessageReceived"] + )) + webView.configuration.userContentController.sync(scriptMessageHandler: webView) + } + } + + public static func fromMap(plugin: SwiftFlutterPlugin, map: [String:Any?]?) -> WebMessageListener? { + guard let map = map else { + return nil + } + return WebMessageListener( + plugin: plugin, + id: map["id"] as! String, + jsObjectName: map["jsObjectName"] as! String, + allowedOriginRules: Set(map["allowedOriginRules"] as! [String]) + ) + } + + public func isOriginAllowed(scheme: String?, host: String?, port: Int?) -> Bool { + for allowedOriginRule in allowedOriginRules { + if allowedOriginRule == "*" { + return true + } + if scheme == nil || scheme!.isEmpty { + continue + } + if scheme == nil || scheme!.isEmpty, host == nil || host!.isEmpty, port == nil || port == 0 { + continue + } + if let rule = URL(string: allowedOriginRule) { + let rulePort = rule.port == nil || rule.port == 0 ? (rule.scheme == "https" ? 443 : 80) : rule.port! + let currentPort = port == nil || port == 0 ? (scheme == "https" ? 443 : 80) : port! + var IPv6: String? = nil + if let hostname = rule.host, hostname.hasPrefix("[") { + let fromIndex = hostname.index(hostname.startIndex, offsetBy: 1) + let toIndex = hostname.index(hostname.startIndex, offsetBy: hostname.count - 1) + let indexRange = Range(uncheckedBounds: (lower: fromIndex, upper: toIndex)) + do { + IPv6 = try Util.normalizeIPv6(address: String(hostname[indexRange])) + } catch {} + } + var hostIPv6: String? = nil + if let host = host, Util.isIPv6(address: host) { + do { + hostIPv6 = try Util.normalizeIPv6(address: host) + } catch {} + } + + let schemeAllowed = scheme != nil && !scheme!.isEmpty && scheme == rule.scheme + + let hostAllowed = rule.host == nil || + rule.host!.isEmpty || + host == rule.host || + (rule.host!.hasPrefix("*") && host != nil && host!.hasSuffix(rule.host!.split(separator: "*", omittingEmptySubsequences: false)[1])) || + (hostIPv6 != nil && IPv6 != nil && hostIPv6 == IPv6) + + let portAllowed = rulePort == currentPort + + if schemeAllowed, hostAllowed, portAllowed { + return true + } + } + } + return false + } + + public func dispose() { + channelDelegate?.dispose() + channelDelegate = nil + webView = nil + plugin = nil + } + + deinit { + debugPrint("WebMessageListener - dealloc") + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/WebMessage/WebMessageListenerChannelDelegate.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/WebMessage/WebMessageListenerChannelDelegate.swift new file mode 100644 index 00000000..365a3d1a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/WebMessage/WebMessageListenerChannelDelegate.swift @@ -0,0 +1,72 @@ +// +// WebMessageListenerChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class WebMessageListenerChannelDelegate: ChannelDelegate { + private weak var webMessageListener: WebMessageListener? + + public init(webMessageListener: WebMessageListener, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.webMessageListener = webMessageListener + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "postMessage": + if let webView = webMessageListener?.webView, let jsObjectName = webMessageListener?.jsObjectName { + let jsObjectNameEscaped = jsObjectName.replacingOccurrences(of: "\'", with: "\\'") + let message = WebMessage.fromMap(map: arguments!["message"] as! [String: Any?]) + + let source = """ + (function() { + var webMessageListener = window['\(jsObjectNameEscaped)']; + if (webMessageListener != null) { + var event = {data: \(message.jsData)}; + if (webMessageListener.onmessage != null) { + webMessageListener.onmessage(event); + } + for (var listener of webMessageListener.listeners) { + listener(event); + } + } + })(); + """ + webView.evaluateJavascript(source: source) { (_) in + result(true) + } + } else { + result(true) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onPostMessage(message: WebMessage?, sourceOrigin: URL?, isMainFrame: Bool) { + let arguments: [String:Any?] = [ + "message": message?.toMap(), + "sourceOrigin": sourceOrigin?.absoluteString, + "isMainFrame": isMainFrame + ] + channel?.invokeMethod("onPostMessage", arguments: arguments) + } + + public override func dispose() { + super.dispose() + webMessageListener = nil + } + + deinit { + dispose() + } +} + diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/WebViewChannelDelegate.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/WebViewChannelDelegate.swift new file mode 100644 index 00000000..46bab6d0 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/WebViewChannelDelegate.swift @@ -0,0 +1,1215 @@ +// +// WebViewChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 06/05/22. +// + +import Foundation +import WebKit + +public class WebViewChannelDelegate: ChannelDelegate { + private weak var webView: InAppWebView? + + public init(webView: InAppWebView, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.webView = webView + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + guard let method = WebViewChannelDelegateMethods.init(rawValue: call.method) else { + result(FlutterMethodNotImplemented) + return + } + + switch method { + case .getUrl: + result(webView?.url?.absoluteString) + break + case .getTitle: + result(webView?.title) + break + case .getProgress: + result( (webView != nil) ? Int(webView!.estimatedProgress * 100) : nil ) + break + case .loadUrl: + let urlRequest = arguments!["urlRequest"] as! [String:Any?] + let allowingReadAccessTo = arguments!["allowingReadAccessTo"] as? String + var allowingReadAccessToURL: URL? = nil + if let allowingReadAccessTo = allowingReadAccessTo { + allowingReadAccessToURL = URL(string: allowingReadAccessTo) + } + webView?.loadUrl(urlRequest: URLRequest.init(fromPluginMap: urlRequest), allowingReadAccessTo: allowingReadAccessToURL) + result(true) + break + case .postUrl: + if let webView = webView { + let url = arguments!["url"] as! String + let postData = arguments!["postData"] as! FlutterStandardTypedData + webView.postUrl(url: URL(string: url)!, postData: postData.data) + } + result(true) + break + case .loadData: + let data = arguments!["data"] as! String + let mimeType = arguments!["mimeType"] as! String + let encoding = arguments!["encoding"] as! String + let baseUrl = URL(string: arguments!["baseUrl"] as! String)! + let allowingReadAccessTo = arguments!["allowingReadAccessTo"] as? String + var allowingReadAccessToURL: URL? = nil + if let allowingReadAccessTo = allowingReadAccessTo { + allowingReadAccessToURL = URL(string: allowingReadAccessTo) + } + webView?.loadData(data: data, mimeType: mimeType, encoding: encoding, baseUrl: baseUrl, allowingReadAccessTo: allowingReadAccessToURL) + result(true) + break + case .loadFile: + let assetFilePath = arguments!["assetFilePath"] as! String + + do { + try webView?.loadFile(assetFilePath: assetFilePath) + } + catch let error as NSError { + result(FlutterError(code: "WebViewChannelDelegate", message: error.domain, details: nil)) + return + } + result(true) + break + case .evaluateJavascript: + if let webView = webView { + let source = arguments!["source"] as! String + let contentWorldMap = arguments!["contentWorld"] as? [String:Any?] + if #available(iOS 14.0, *), let contentWorldMap = contentWorldMap { + let contentWorld = WKContentWorld.fromMap(map: contentWorldMap, windowId: webView.windowId)! + webView.evaluateJavascript(source: source, contentWorld: contentWorld) { (value) in + result(value) + } + } else { + webView.evaluateJavascript(source: source) { (value) in + result(value) + } + } + } + else { + result(nil) + } + break + case .injectJavascriptFileFromUrl: + let urlFile = arguments!["urlFile"] as! String + let scriptHtmlTagAttributes = arguments!["scriptHtmlTagAttributes"] as? [String:Any?] + webView?.injectJavascriptFileFromUrl(urlFile: urlFile, scriptHtmlTagAttributes: scriptHtmlTagAttributes) + result(true) + break + case .injectCSSCode: + let source = arguments!["source"] as! String + webView?.injectCSSCode(source: source) + result(true) + break + case .injectCSSFileFromUrl: + let urlFile = arguments!["urlFile"] as! String + let cssLinkHtmlTagAttributes = arguments!["cssLinkHtmlTagAttributes"] as? [String:Any?] + webView?.injectCSSFileFromUrl(urlFile: urlFile, cssLinkHtmlTagAttributes: cssLinkHtmlTagAttributes) + result(true) + break + case .reload: + webView?.reload() + result(true) + break + case .goBack: + webView?.goBack() + result(true) + break + case .canGoBack: + result(webView?.canGoBack ?? false) + break + case .goForward: + webView?.goForward() + result(true) + break + case .canGoForward: + result(webView?.canGoForward ?? false) + break + case .goBackOrForward: + let steps = arguments!["steps"] as! Int + webView?.goBackOrForward(steps: steps) + result(true) + break + case .canGoBackOrForward: + let steps = arguments!["steps"] as! Int + result(webView?.canGoBackOrForward(steps: steps) ?? false) + break + case .stopLoading: + webView?.stopLoading() + result(true) + break + case .isLoading: + result(webView?.isLoading ?? false) + break + case .takeScreenshot: + if let webView = webView, #available(iOS 11.0, *) { + let screenshotConfiguration = arguments!["screenshotConfiguration"] as? [String: Any?] + webView.takeScreenshot(with: screenshotConfiguration, completionHandler: { (screenshot) -> Void in + result(screenshot) + }) + } + else { + result(nil) + } + break + case .setSettings: + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { + let inAppBrowserSettings = InAppBrowserSettings() + let inAppBrowserSettingsMap = arguments!["settings"] as! [String: Any] + let _ = inAppBrowserSettings.parse(settings: inAppBrowserSettingsMap) + iabController.setSettings(newSettings: inAppBrowserSettings, newSettingsMap: inAppBrowserSettingsMap) + } else { + let inAppWebViewSettings = InAppWebViewSettings() + let inAppWebViewSettingsMap = arguments!["settings"] as! [String: Any] + let _ = inAppWebViewSettings.parse(settings: inAppWebViewSettingsMap) + webView?.setSettings(newSettings: inAppWebViewSettings, newSettingsMap: inAppWebViewSettingsMap) + } + result(true) + break + case .getSettings: + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { + result(iabController.getSettings()) + } else { + result(webView?.getSettings()) + } + break + case .close: + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { + iabController.close { + result(true) + } + } else { + result(FlutterMethodNotImplemented) + } + break + case .show: + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { + iabController.show { + result(true) + } + } else { + result(FlutterMethodNotImplemented) + } + break + case .hide: + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { + iabController.hide { + result(true) + } + } else { + result(FlutterMethodNotImplemented) + } + break + case .isHidden: + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { + result(iabController.isHidden) + } else { + result(FlutterMethodNotImplemented) + } + break + case .getCopyBackForwardList: + result(webView?.getCopyBackForwardList()) + break + case .findAll: + if let webView = webView, let findInteractionController = webView.findInteractionController { + let find = arguments!["find"] as! String + findInteractionController.findAll(find: find, completionHandler: {(value, error) in + if error != nil { + result(FlutterError(code: "WebViewChannelDelegate", message: error?.localizedDescription, details: nil)) + return + } + result(true) + }) + } else { + result(false) + } + break + case .findNext: + if let webView = webView, let findInteractionController = webView.findInteractionController { + let forward = arguments!["forward"] as! Bool + findInteractionController.findNext(forward: forward, completionHandler: {(value, error) in + if error != nil { + result(FlutterError(code: "WebViewChannelDelegate", message: error?.localizedDescription, details: nil)) + return + } + result(true) + }) + } else { + result(false) + } + break + case .clearMatches: + if let webView = webView, let findInteractionController = webView.findInteractionController { + findInteractionController.clearMatches(completionHandler: {(value, error) in + if error != nil { + result(FlutterError(code: "WebViewChannelDelegate", message: error?.localizedDescription, details: nil)) + return + } + result(true) + }) + } else { + result(false) + } + break + case .clearCache: + webView?.clearCache() + result(true) + break + case .scrollTo: + let x = arguments!["x"] as! Int + let y = arguments!["y"] as! Int + let animated = arguments!["animated"] as! Bool + webView?.scrollTo(x: x, y: y, animated: animated) + result(true) + break + case .scrollBy: + let x = arguments!["x"] as! Int + let y = arguments!["y"] as! Int + let animated = arguments!["animated"] as! Bool + webView?.scrollBy(x: x, y: y, animated: animated) + result(true) + break + case .pauseTimers: + webView?.pauseTimers() + result(true) + break + case .resumeTimers: + webView?.resumeTimers() + result(true) + break + case .printCurrentPage: + if let webView = webView { + let settings = PrintJobSettings() + if let settingsMap = arguments!["settings"] as? [String: Any?] { + let _ = settings.parse(settings: settingsMap) + } + result(webView.printCurrentPage(settings: settings)) + } else { + result(nil) + } + break + case .getContentHeight: + result(webView?.getContentHeight()) + break + case .getContentWidth: + result(webView?.getContentWidth()) + break + case .zoomBy: + let zoomFactor = (arguments!["zoomFactor"] as! NSNumber).floatValue + let animated = arguments!["animated"] as! Bool + webView?.zoomBy(zoomFactor: zoomFactor, animated: animated) + result(true) + break + case .reloadFromOrigin: + webView?.reloadFromOrigin() + result(true) + break + case .getOriginalUrl: + result(webView?.getOriginalUrl()?.absoluteString) + break + case .getZoomScale: + result(webView?.getZoomScale()) + break + case .hasOnlySecureContent: + result(webView?.hasOnlySecureContent ?? false) + break + case .getSelectedText: + if let webView = webView { + webView.getSelectedText { (value, error) in + if let err = error { + print(err.localizedDescription) + result("") + return + } + result(value) + } + } + else { + result(nil) + } + break + case .getHitTestResult: + if let webView = webView { + webView.getHitTestResult { (hitTestResult) in + result(hitTestResult.toMap()) + } + } + else { + result(nil) + } + break + case .clearFocus: + webView?.clearFocus() + result(true) + break + case .setContextMenu: + if let webView = webView { + let contextMenu = arguments!["contextMenu"] as? [String: Any] + webView.contextMenu = contextMenu + result(true) + } else { + result(false) + } + break + case .requestFocusNodeHref: + if let webView = webView { + webView.requestFocusNodeHref { (value, error) in + if let err = error { + print(err.localizedDescription) + result(nil) + return + } + result(value) + } + } else { + result(nil) + } + break + case .requestImageRef: + if let webView = webView { + webView.requestImageRef { (value, error) in + if let err = error { + print(err.localizedDescription) + result(nil) + return + } + result(value) + } + } else { + result(nil) + } + break + case .getScrollX: + if let webView = webView { + result(Int(webView.scrollView.contentOffset.x)) + } else { + result(nil) + } + break + case .getScrollY: + if let webView = webView { + result(Int(webView.scrollView.contentOffset.y)) + } else { + result(nil) + } + break + case .getCertificate: + result(webView?.getCertificate()?.toMap()) + break + case .addUserScript: + if let webView = webView { + let userScriptMap = arguments!["userScript"] as! [String: Any?] + let userScript = UserScript.fromMap(map: userScriptMap, windowId: webView.windowId)! + webView.configuration.userContentController.addUserOnlyScript(userScript) + webView.configuration.userContentController.sync(scriptMessageHandler: webView) + } + result(true) + break + case .removeUserScript: + let index = arguments!["index"] as! Int + let userScriptMap = arguments!["userScript"] as! [String: Any?] + let userScript = UserScript.fromMap(map: userScriptMap, windowId: webView?.windowId)! + webView?.configuration.userContentController.removeUserOnlyScript(at: index, injectionTime: userScript.injectionTime) + result(true) + break + case .removeUserScriptsByGroupName: + let groupName = arguments!["groupName"] as! String + webView?.configuration.userContentController.removeUserOnlyScripts(with: groupName) + result(true) + break + case .removeAllUserScripts: + webView?.configuration.userContentController.removeAllUserOnlyScripts() + result(true) + break + case .callAsyncJavaScript: + if let webView = webView, #available(iOS 10.3, *) { + if #available(iOS 14.3, *) { // on iOS 14.0, for some reason, it crashes + let functionBody = arguments!["functionBody"] as! String + let functionArguments = arguments!["arguments"] as! [String:Any] + var contentWorld = WKContentWorld.page + if let contentWorldMap = arguments!["contentWorld"] as? [String:Any?] { + contentWorld = WKContentWorld.fromMap(map: contentWorldMap, windowId: webView.windowId)! + } + webView.callAsyncJavaScript(functionBody: functionBody, arguments: functionArguments, contentWorld: contentWorld) { (value) in + result(value) + } + } else { + let functionBody = arguments!["functionBody"] as! String + let functionArguments = arguments!["arguments"] as! [String:Any] + webView.callAsyncJavaScript(functionBody: functionBody, arguments: functionArguments) { (value) in + result(value) + } + } + } + else { + result(nil) + } + break + case .createPdf: + if let webView = webView, #available(iOS 14.0, *) { + let configuration = arguments!["pdfConfiguration"] as? [String: Any?] + webView.createPdf(configuration: configuration, completionHandler: { (pdf) -> Void in + result(pdf) + }) + } + else { + result(nil) + } + break + case .createWebArchiveData: + if let webView = webView, #available(iOS 14.0, *) { + webView.createWebArchiveData(dataCompletionHandler: { (webArchiveData) -> Void in + result(webArchiveData) + }) + } + else { + result(nil) + } + break + case .saveWebArchive: + if let webView = webView, #available(iOS 14.0, *) { + let filePath = arguments!["filePath"] as! String + let autoname = arguments!["autoname"] as! Bool + webView.saveWebArchive(filePath: filePath, autoname: autoname, completionHandler: { (path) -> Void in + result(path) + }) + } + else { + result(nil) + } + break + case .isSecureContext: + if let webView = webView { + webView.isSecureContext(completionHandler: { (isSecureContext) in + result(isSecureContext) + }) + } + else { + result(false) + } + break + case .createWebMessageChannel: + if let webView = webView { + let _ = webView.createWebMessageChannel { (webMessageChannel) in + guard let webMessageChannel = webMessageChannel else { + result(nil) + return + } + result(webMessageChannel.toMap()) + } + } else { + result(nil) + } + break + case .postWebMessage: + if let webView = webView { + let message = WebMessage.fromMap(map: arguments!["message"] as! [String: Any?]) + let targetOrigin = arguments!["targetOrigin"] as! String + + var ports: [WebMessagePort] = [] + if let notConnectedPorts = message.ports { + for notConnectedPort in notConnectedPorts { + if let webMessageChannel = webView.webMessageChannels[notConnectedPort.webMessageChannelId] { + ports.append(webMessageChannel.ports[Int(notConnectedPort.index)]) + } + } + } + message.ports = ports + + do { + try webView.postWebMessage(message: message, targetOrigin: targetOrigin) { (_) in + result(true) + } + } catch let error as NSError { + result(FlutterError(code: "WebViewChannelDelegate", message: error.domain, details: nil)) + } + } else { + result(false) + } + break + case .addWebMessageListener: + if let webView = webView, let plugin = webView.plugin { + let webMessageListenerMap = arguments!["webMessageListener"] as! [String: Any?] + let webMessageListener = WebMessageListener.fromMap(plugin: plugin, map: webMessageListenerMap)! + do { + try webView.addWebMessageListener(webMessageListener: webMessageListener) + result(false) + } catch let error as NSError { + result(FlutterError(code: "WebViewChannelDelegate", message: error.domain, details: nil)) + } + } else { + result(false) + } + break + case .canScrollVertically: + if let webView = webView { + result(webView.canScrollVertically()) + } else { + result(false) + } + break + case .canScrollHorizontally: + if let webView = webView { + result(webView.canScrollHorizontally()) + } else { + result(false) + } + break + case .pauseAllMediaPlayback: + if let webView = webView, #available(iOS 15.0, *) { + webView.pauseAllMediaPlayback(completionHandler: { () -> Void in + result(true) + }) + } else { + result(false) + } + break + case .setAllMediaPlaybackSuspended: + if let webView = webView, #available(iOS 15.0, *) { + let suspended = arguments!["suspended"] as! Bool + webView.setAllMediaPlaybackSuspended(suspended, completionHandler: { () -> Void in + result(true) + }) + } else { + result(false) + } + break + case .closeAllMediaPresentations: + if let webView = self.webView, #available(iOS 14.5, *) { + // closeAllMediaPresentations with completionHandler v15.0 makes the app crash + // with error EXC_BAD_ACCESS, so use closeAllMediaPresentations v14.5 + if #available(iOS 16.0, *) { + webView.closeAllMediaPresentations { + result(true) + } + } else { + webView.closeAllMediaPresentations() + result(true) + } + } else { + result(false) + } + break + case .requestMediaPlaybackState: + if let webView = webView, #available(iOS 15.0, *) { + webView.requestMediaPlaybackState(completionHandler: { (state) -> Void in + result(state.rawValue) + }) + } else { + result(nil) + } + break + case .getMetaThemeColor: + if let webView = webView, #available(iOS 15.0, *) { + result(webView.themeColor?.hexString) + } else { + result(nil) + } + break + case .isInFullscreen: + if let webView = webView { + if #available(iOS 16.0, *) { + result(webView.fullscreenState == .inFullscreen) + } else { + result(webView.inFullscreen) + } + } + else { + result(false) + } + break + case .getCameraCaptureState: + if let webView = webView, #available(iOS 15.0, *) { + result(webView.cameraCaptureState.rawValue) + } else { + result(nil) + } + break + case .setCameraCaptureState: + if let webView = webView, #available(iOS 15.0, *) { + let state = WKMediaCaptureState.init(rawValue: arguments!["state"] as! Int) ?? WKMediaCaptureState.none + webView.setCameraCaptureState(state) { + result(true) + } + } else { + result(false) + } + break + case .getMicrophoneCaptureState: + if let webView = webView, #available(iOS 15.0, *) { + result(webView.microphoneCaptureState.rawValue) + } else { + result(nil) + } + break + case .setMicrophoneCaptureState: + if let webView = webView, #available(iOS 15.0, *) { + let state = WKMediaCaptureState.init(rawValue: arguments!["state"] as! Int) ?? WKMediaCaptureState.none + webView.setMicrophoneCaptureState(state) { + result(true) + } + } else { + result(false) + } + break + case .loadSimulatedRequest: + if let webView = webView, #available(iOS 15.0, *) { + let request = URLRequest.init(fromPluginMap: arguments!["urlRequest"] as! [String:Any?]) + let data = arguments!["data"] as! FlutterStandardTypedData + var response: URLResponse? = nil + if let urlResponse = arguments!["urlResponse"] as? [String:Any?] { + response = URLResponse.init(fromPluginMap: urlResponse) + } + if let response = response { + webView.loadSimulatedRequest(request, response: response, responseData: data.data) + } else { + webView.loadSimulatedRequest(request, responseHTML: String(decoding: data.data, as: UTF8.self)) + } + result(true) + } else { + result(false) + } + } + } + + @available(*, deprecated, message: "Use FindInteractionChannelDelegate.onFindResultReceived instead.") + public func onFindResultReceived(activeMatchOrdinal: Int, numberOfMatches: Int, isDoneCounting: Bool) { + let arguments: [String : Any?] = [ + "activeMatchOrdinal": activeMatchOrdinal, + "numberOfMatches": numberOfMatches, + "isDoneCounting": isDoneCounting + ] + channel?.invokeMethod("onFindResultReceived", arguments: arguments) + } + + public func onLongPressHitTestResult(hitTestResult: HitTestResult) { + channel?.invokeMethod("onLongPressHitTestResult", arguments: hitTestResult.toMap()) + } + + public func onScrollChanged(x: Int, y: Int) { + let arguments: [String: Any?] = ["x": x, "y": y] + channel?.invokeMethod("onScrollChanged", arguments: arguments) + } + + public func onContentSizeChanged(oldContentSize: CGSize, newContentSize: CGSize) { + let arguments: [String: Any?] = [ + "oldContentSize": oldContentSize.toMap(), + "newContentSize": newContentSize.toMap() + ] + channel?.invokeMethod("onContentSizeChanged", arguments: arguments) + } + + public func onDownloadStartRequest(request: DownloadStartRequest) { + channel?.invokeMethod("onDownloadStartRequest", arguments: request.toMap()) + } + + public func onCreateContextMenu(hitTestResult: HitTestResult) { + channel?.invokeMethod("onCreateContextMenu", arguments: hitTestResult.toMap()) + } + + public func onOverScrolled(x: Int, y: Int, clampedX: Bool, clampedY: Bool) { + let arguments: [String: Any?] = ["x": x, "y": y, "clampedX": clampedX, "clampedY": clampedY] + channel?.invokeMethod("onOverScrolled", arguments: arguments) + } + + public func onContextMenuActionItemClicked(id: Any, title: String) { + let arguments: [String: Any?] = [ + "id": id, + "iosId": id is Int64 ? String(id as! Int64) : id as! String, + "androidId": nil, + "title": title + ] + channel?.invokeMethod("onContextMenuActionItemClicked", arguments: arguments) + } + + public func onHideContextMenu() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onHideContextMenu", arguments: arguments) + } + + public func onEnterFullscreen() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onEnterFullscreen", arguments: arguments) + } + + public func onExitFullscreen() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onExitFullscreen", arguments: arguments) + } + + public class JsAlertCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return JsAlertResponse.fromMap(map: obj as? [String:Any?]) + } + } + + deinit { + self.defaultBehaviour(nil) + } + } + + public func onJsAlert(url: URL?, message: String, isMainFrame: Bool, callback: JsAlertCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + let arguments: [String: Any?] = [ + "url": url?.absoluteString, + "message": message, + "isMainFrame": isMainFrame + ] + channel?.invokeMethod("onJsAlert", arguments: arguments, callback: callback) + } + + public class JsConfirmCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return JsConfirmResponse.fromMap(map: obj as? [String:Any?]) + } + } + + deinit { + self.defaultBehaviour(nil) + } + } + + public func onJsConfirm(url: URL?, message: String, isMainFrame: Bool, callback: JsConfirmCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + let arguments: [String: Any?] = [ + "url": url?.absoluteString, + "message": message, + "isMainFrame": isMainFrame + ] + channel?.invokeMethod("onJsConfirm", arguments: arguments, callback: callback) + } + + public class JsPromptCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return JsPromptResponse.fromMap(map: obj as? [String:Any?]) + } + } + + deinit { + self.defaultBehaviour(nil) + } + } + + public func onJsPrompt(url: URL?, message: String, defaultValue: String?, isMainFrame: Bool, callback: JsPromptCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + let arguments: [String: Any?] = [ + "url": url?.absoluteString, + "message": message, + "defaultValue": defaultValue, + "isMainFrame": isMainFrame + ] + channel?.invokeMethod("onJsPrompt", arguments: arguments, callback: callback) + } + + public class CreateWindowCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return obj is Bool && obj as! Bool + } + } + } + + public func onCreateWindow(createWindowAction: CreateWindowAction, callback: CreateWindowCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + channel?.invokeMethod("onCreateWindow", arguments: createWindowAction.toMap(), callback: callback) + } + + public func onCloseWindow() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onCloseWindow", arguments: arguments) + } + + public func onConsoleMessage(message: String, messageLevel: Int) { + let arguments: [String: Any?] = [ + "message": message, + "messageLevel": messageLevel + ] + channel?.invokeMethod("onConsoleMessage", arguments: arguments) + } + + public func onProgressChanged(progress: Int) { + let arguments: [String: Any?] = [ + "progress": progress + ] + channel?.invokeMethod("onProgressChanged", arguments: arguments) + } + + public func onTitleChanged(title: String?) { + let arguments: [String: Any?] = [ + "title": title + ] + channel?.invokeMethod("onTitleChanged", arguments: arguments) + } + + public class PermissionRequestCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return PermissionResponse.fromMap(map: obj as? [String:Any?]) + } + } + + deinit { + self.defaultBehaviour(nil) + } + } + + public func onPermissionRequest(request: PermissionRequest, callback: PermissionRequestCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + channel?.invokeMethod("onPermissionRequest", arguments: request.toMap(), callback: callback) + } + + public class ShouldOverrideUrlLoadingCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + if let action = obj as? Int { + return WKNavigationActionPolicy.init(rawValue: action) ?? WKNavigationActionPolicy.cancel + } + return WKNavigationActionPolicy.cancel + } + } + + deinit { + self.defaultBehaviour(nil) + } + } + + public func shouldOverrideUrlLoading(navigationAction: WKNavigationAction, callback: ShouldOverrideUrlLoadingCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + channel?.invokeMethod("shouldOverrideUrlLoading", arguments: navigationAction.toMap(), callback: callback) + } + + public func onLoadStart(url: String?) { + let arguments: [String: Any?] = ["url": url] + channel?.invokeMethod("onLoadStart", arguments: arguments) + } + + public func onLoadStop(url: String?) { + let arguments: [String: Any?] = ["url": url] + channel?.invokeMethod("onLoadStop", arguments: arguments) + } + + public func onUpdateVisitedHistory(url: String?, isReload: Bool?) { + let arguments: [String: Any?] = [ + "url": url, + "isReload": nil + ] + channel?.invokeMethod("onUpdateVisitedHistory", arguments: arguments) + } + + public func onReceivedError(request: WebResourceRequest, error: WebResourceError) { + let arguments: [String: Any?] = [ + "request": request.toMap(), + "error": error.toMap() + ] + channel?.invokeMethod("onReceivedError", arguments: arguments) + } + + public func onReceivedHttpError(request: WebResourceRequest, errorResponse: WebResourceResponse) { + let arguments: [String: Any?] = [ + "request": request.toMap(), + "errorResponse": errorResponse.toMap() + ] + channel?.invokeMethod("onReceivedHttpError", arguments: arguments) + } + + public class ReceivedHttpAuthRequestCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return HttpAuthResponse.fromMap(map: obj as? [String:Any?]) + } + } + + deinit { + self.defaultBehaviour(nil) + } + } + + public func onReceivedHttpAuthRequest(challenge: HttpAuthenticationChallenge, callback: ReceivedHttpAuthRequestCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + // workaround for ProtectionSpace.toMap() SSL Certificate + // https://github.com/pichillilorenzo/flutter_inappwebview/issues/1678 + DispatchQueue.global(qos: .background).async { + let arguments = challenge.toMap() + DispatchQueue.main.async { [weak self] in + if self?.channel == nil { + callback.defaultBehaviour(nil) + return + } + self?.channel?.invokeMethod("onReceivedHttpAuthRequest", arguments: arguments, callback: callback) + } + } + } + + public class ReceivedServerTrustAuthRequestCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return ServerTrustAuthResponse.fromMap(map: obj as? [String:Any?]) + } + } + + deinit { + self.defaultBehaviour(nil) + } + } + + public func onReceivedServerTrustAuthRequest(challenge: ServerTrustChallenge, callback: ReceivedServerTrustAuthRequestCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + // workaround for ProtectionSpace.toMap() SSL Certificate + // https://github.com/pichillilorenzo/flutter_inappwebview/issues/1678 + DispatchQueue.global(qos: .background).async { + let arguments = challenge.toMap() + DispatchQueue.main.async { [weak self] in + if self?.channel == nil { + callback.defaultBehaviour(nil) + return + } + self?.channel?.invokeMethod("onReceivedServerTrustAuthRequest", arguments: arguments, callback: callback) + } + } + } + + public class ReceivedClientCertRequestCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return ClientCertResponse.fromMap(map: obj as? [String:Any?]) + } + } + + deinit { + self.defaultBehaviour(nil) + } + } + + public func onReceivedClientCertRequest(challenge: ClientCertChallenge, callback: ReceivedClientCertRequestCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + // workaround for ProtectionSpace.toMap() SSL Certificate + // https://github.com/pichillilorenzo/flutter_inappwebview/issues/1678 + DispatchQueue.global(qos: .background).async { + let arguments = challenge.toMap() + DispatchQueue.main.async { [weak self] in + if self?.channel == nil { + callback.defaultBehaviour(nil) + return + } + self?.channel?.invokeMethod("onReceivedClientCertRequest", arguments: arguments, callback: callback) + } + } + } + + public func onZoomScaleChanged(newScale: Float, oldScale: Float) { + let arguments: [String: Any?] = [ + "newScale": newScale, + "oldScale": oldScale + ] + channel?.invokeMethod("onZoomScaleChanged", arguments: arguments) + } + + public func onPageCommitVisible(url: String?) { + let arguments: [String: Any?] = [ + "url": url + ] + channel?.invokeMethod("onPageCommitVisible", arguments: arguments) + } + + public class LoadResourceWithCustomSchemeCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return CustomSchemeResponse.fromMap(map: obj as? [String:Any?]) + } + } + } + + public func onLoadResourceWithCustomScheme(request: WebResourceRequest, callback: LoadResourceWithCustomSchemeCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + let arguments: [String: Any?] = ["request": request.toMap()] + channel?.invokeMethod("onLoadResourceWithCustomScheme", arguments: arguments, callback: callback) + } + + public class CallJsHandlerCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return obj + } + } + } + + public func onCallJsHandler(handlerName: String, args: String, callback: CallJsHandlerCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + let arguments: [String: Any?] = [ + "handlerName": handlerName, + "args": args + ] + channel?.invokeMethod("onCallJsHandler", arguments: arguments, callback: callback) + } + + public class NavigationResponseCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + if let action = obj as? Int { + return WKNavigationResponsePolicy.init(rawValue: action) ?? WKNavigationResponsePolicy.cancel + } + return WKNavigationResponsePolicy.cancel + } + } + + deinit { + self.defaultBehaviour(nil) + } + } + + public func onNavigationResponse(navigationResponse: WKNavigationResponse, callback: NavigationResponseCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + channel?.invokeMethod("onNavigationResponse", arguments: navigationResponse.toMap(), callback: callback) + } + + public class ShouldAllowDeprecatedTLSCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + if let action = obj as? Int { + return action == 1 + } + return false + } + } + + deinit { + self.defaultBehaviour(nil) + } + } + + public func shouldAllowDeprecatedTLS(challenge: URLAuthenticationChallenge, callback: ShouldAllowDeprecatedTLSCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + // workaround for ProtectionSpace.toMap() SSL Certificate + // https://github.com/pichillilorenzo/flutter_inappwebview/issues/1678 + DispatchQueue.global(qos: .background).async { + let arguments = challenge.toMap() + DispatchQueue.main.async { [weak self] in + if self?.channel == nil { + callback.defaultBehaviour(nil) + return + } + self?.channel?.invokeMethod("shouldAllowDeprecatedTLS", arguments: arguments, callback: callback) + } + } + } + + public func onWebContentProcessDidTerminate() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onWebContentProcessDidTerminate", arguments: arguments) + } + + public func onDidReceiveServerRedirectForProvisionalNavigation() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onDidReceiveServerRedirectForProvisionalNavigation", arguments: arguments) + } + + @available(iOS 15.0, *) + public func onCameraCaptureStateChanged(oldState: WKMediaCaptureState?, newState: WKMediaCaptureState?) { + let arguments = [ + "oldState": oldState?.rawValue, + "newState": newState?.rawValue + ] + channel?.invokeMethod("onCameraCaptureStateChanged", arguments: arguments) + } + + @available(iOS 15.0, *) + public func onMicrophoneCaptureStateChanged(oldState: WKMediaCaptureState?, newState: WKMediaCaptureState?) { + let arguments = [ + "oldState": oldState?.rawValue, + "newState": newState?.rawValue + ] + channel?.invokeMethod("onMicrophoneCaptureStateChanged", arguments: arguments) + } + + public class PrintRequestCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return obj is Bool && obj as! Bool + } + } + } + + public func onPrintRequest(url: URL?, printJobId: String?, callback: PrintRequestCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + let arguments = [ + "url": url?.absoluteString, + "printJobId": printJobId, + ] + channel?.invokeMethod("onPrintRequest", arguments: arguments, callback: callback) + } + + public override func dispose() { + super.dispose() + webView = nil + } + + deinit { + debugPrint("WebViewChannelDelegate - dealloc") + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/WebViewChannelDelegateMethods.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/WebViewChannelDelegateMethods.swift new file mode 100644 index 00000000..899cd769 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebView/WebViewChannelDelegateMethods.swift @@ -0,0 +1,93 @@ +// +// WebViewChannelDelegateMethods.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/10/22. +// + +import Foundation + +public enum WebViewChannelDelegateMethods: String { + case getUrl = "getUrl" + case getTitle = "getTitle" + case getProgress = "getProgress" + case loadUrl = "loadUrl" + case postUrl = "postUrl" + case loadData = "loadData" + case loadFile = "loadFile" + case evaluateJavascript = "evaluateJavascript" + case injectJavascriptFileFromUrl = "injectJavascriptFileFromUrl" + case injectCSSCode = "injectCSSCode" + case injectCSSFileFromUrl = "injectCSSFileFromUrl" + case reload = "reload" + case goBack = "goBack" + case canGoBack = "canGoBack" + case goForward = "goForward" + case canGoForward = "canGoForward" + case goBackOrForward = "goBackOrForward" + case canGoBackOrForward = "canGoBackOrForward" + case stopLoading = "stopLoading" + case isLoading = "isLoading" + case takeScreenshot = "takeScreenshot" + case setSettings = "setSettings" + case getSettings = "getSettings" + case close = "close" + case show = "show" + case hide = "hide" + case isHidden = "isHidden" + case getCopyBackForwardList = "getCopyBackForwardList" + @available(*, deprecated, message: "Use FindInteractionController.findAll instead.") + case findAll = "findAll" + @available(*, deprecated, message: "Use FindInteractionController.findNext instead.") + case findNext = "findNext" + @available(*, deprecated, message: "Use FindInteractionController.clearMatches instead.") + case clearMatches = "clearMatches" + @available(*, deprecated, message: "Use InAppWebViewManager.clearAllCache instead.") + case clearCache = "clearCache" + case scrollTo = "scrollTo" + case scrollBy = "scrollBy" + case pauseTimers = "pauseTimers" + case resumeTimers = "resumeTimers" + case printCurrentPage = "printCurrentPage" + case getContentHeight = "getContentHeight" + case getContentWidth = "getContentWidth" + case zoomBy = "zoomBy" + case reloadFromOrigin = "reloadFromOrigin" + case getOriginalUrl = "getOriginalUrl" + case getZoomScale = "getZoomScale" + case hasOnlySecureContent = "hasOnlySecureContent" + case getSelectedText = "getSelectedText" + case getHitTestResult = "getHitTestResult" + case clearFocus = "clearFocus" + case setContextMenu = "setContextMenu" + case requestFocusNodeHref = "requestFocusNodeHref" + case requestImageRef = "requestImageRef" + case getScrollX = "getScrollX" + case getScrollY = "getScrollY" + case getCertificate = "getCertificate" + case addUserScript = "addUserScript" + case removeUserScript = "removeUserScript" + case removeUserScriptsByGroupName = "removeUserScriptsByGroupName" + case removeAllUserScripts = "removeAllUserScripts" + case callAsyncJavaScript = "callAsyncJavaScript" + case createPdf = "createPdf" + case createWebArchiveData = "createWebArchiveData" + case saveWebArchive = "saveWebArchive" + case isSecureContext = "isSecureContext" + case createWebMessageChannel = "createWebMessageChannel" + case postWebMessage = "postWebMessage" + case addWebMessageListener = "addWebMessageListener" + case canScrollVertically = "canScrollVertically" + case canScrollHorizontally = "canScrollHorizontally" + case pauseAllMediaPlayback = "pauseAllMediaPlayback" + case setAllMediaPlaybackSuspended = "setAllMediaPlaybackSuspended" + case closeAllMediaPresentations = "closeAllMediaPresentations" + case requestMediaPlaybackState = "requestMediaPlaybackState" + case getMetaThemeColor = "getMetaThemeColor" + case isInFullscreen = "isInFullscreen" + case getCameraCaptureState = "getCameraCaptureState" + case setCameraCaptureState = "setCameraCaptureState" + case getMicrophoneCaptureState = "getMicrophoneCaptureState" + case setMicrophoneCaptureState = "setMicrophoneCaptureState" + case loadSimulatedRequest = "loadSimulatedRequest" +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebViewFlutterPlugin.h b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebViewFlutterPlugin.h new file mode 100755 index 00000000..cb448aa0 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebViewFlutterPlugin.h @@ -0,0 +1,23 @@ +/* + 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. + */ + +#import + +@interface InAppWebViewFlutterPlugin : NSObject ++ (void)registerWithRegistrar:(nonnull NSObject *)registrar; + +@end diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebViewFlutterPlugin.m b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebViewFlutterPlugin.m new file mode 100755 index 00000000..adcf1123 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/InAppWebViewFlutterPlugin.m @@ -0,0 +1,32 @@ +/* + 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. + */ + +#import "InAppWebViewFlutterPlugin.h" +#if __has_include() +#import +#else +// Support project import fallback if the generated compatibility header +// is not copied when this plugin is created as a library. +// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 +#import "flutter_inappwebview_ios-Swift.h" +#endif + +@implementation InAppWebViewFlutterPlugin : NSObject ++ (void)registerWithRegistrar:(NSObject*)registrar { + [SwiftFlutterPlugin registerWithRegistrar:registrar]; +} +@end diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/LeakAvoider.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/LeakAvoider.swift new file mode 100755 index 00000000..6314ff0f --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/LeakAvoider.swift @@ -0,0 +1,25 @@ +// +// LeakAvoider.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/12/2019. +// + +import Foundation + +public class LeakAvoider: NSObject { + weak var delegate : FlutterMethodCallDelegate? + + init(delegate: FlutterMethodCallDelegate) { + super.init() + self.delegate = delegate + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + self.delegate?.handle(call, result: result) + } + + deinit { + debugPrint("LeakAvoider - dealloc") + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/MyCookieManager.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/MyCookieManager.swift new file mode 100755 index 00000000..acc147bb --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/MyCookieManager.swift @@ -0,0 +1,314 @@ +// +// MyCookieManager.swift +// flutter_inappwebview +// +// Created by Lorenzo on 26/10/18. +// + +import Foundation +import WebKit + +@available(iOS 11.0, *) +public class MyCookieManager: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_cookiemanager" + static let httpCookieStore = WKWebsiteDataStore.default().httpCookieStore + + private var plugin: SwiftFlutterPlugin? + + init(plugin: SwiftFlutterPlugin) { + super.init(channel: FlutterMethodChannel(name: MyCookieManager.METHOD_CHANNEL_NAME, binaryMessenger: plugin.registrar!.messenger())) + self.plugin = plugin + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + if call.method == "gopeed.prepareProfile" { + GopeedProfiles.prepare(arguments: arguments, result: result) + return + } + let profileID = arguments?["gopeedProfileId"] as? String ?? "" + let selectedStore = GopeedProfiles.store(profileID) + if !profileID.isEmpty && selectedStore == nil { + result(FlutterError(code: "UNAVAILABLE", message: "WebView profile is not initialized", details: nil)) + return + } + let cookieStore = (selectedStore ?? WKWebsiteDataStore.default()).httpCookieStore + + switch call.method { + case "setCookie": + let url = arguments!["url"] as! String + let name = arguments!["name"] as! String + let value = arguments!["value"] as! String + let path = arguments!["path"] as! String + + var expiresDate: Int64? + if let expiresDateString = arguments!["expiresDate"] as? String { + expiresDate = Int64(expiresDateString) + } + + let maxAge = arguments!["maxAge"] as? Int64 + let isSecure = arguments!["isSecure"] as? Bool + let isHttpOnly = arguments!["isHttpOnly"] as? Bool + let sameSite = arguments!["sameSite"] as? String + let domain = arguments!["domain"] as? String + + MyCookieManager.setCookie(url: url, + name: name, + value: value, + path: path, + domain: domain, + expiresDate: expiresDate, + maxAge: maxAge, + isSecure: isSecure, + isHttpOnly: isHttpOnly, + sameSite: sameSite, + result: result, cookieStore: cookieStore) + break + case "getCookies": + let url = arguments!["url"] as! String + MyCookieManager.getCookies(url: url, result: result, cookieStore: cookieStore) + break + case "getAllCookies": + MyCookieManager.getAllCookies(result: result, cookieStore: cookieStore) + break + case "deleteCookie": + let url = arguments!["url"] as! String + let name = arguments!["name"] as! String + let path = arguments!["path"] as! String + let domain = arguments!["domain"] as? String + MyCookieManager.deleteCookie(url: url, name: name, path: path, domain: domain, result: result, cookieStore: cookieStore) + break + case "deleteCookies": + let url = arguments!["url"] as! String + let path = arguments!["path"] as! String + let domain = arguments!["domain"] as? String + MyCookieManager.deleteCookies(url: url, path: path, domain: domain, result: result, cookieStore: cookieStore) + break + case "deleteAllCookies": + MyCookieManager.deleteAllCookies(result: result, cookieStore: cookieStore) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public static func setCookie(url: String, + name: String, + value: String, + path: String, + domain: String?, + expiresDate: Int64?, + maxAge: Int64?, + isSecure: Bool?, + isHttpOnly: Bool?, + sameSite: String?, + result: @escaping FlutterResult, cookieStore: WKHTTPCookieStore = WKWebsiteDataStore.default().httpCookieStore) { + var properties: [HTTPCookiePropertyKey: Any] = [:] + properties[.originURL] = url + properties[.name] = name + properties[.value] = value + properties[.path] = path + + if domain != nil { + properties[.domain] = domain + } + + if expiresDate != nil { + // convert from milliseconds + properties[.expires] = Date(timeIntervalSince1970: TimeInterval(Double(expiresDate!)/1000)) + } + if maxAge != nil { + properties[.maximumAge] = String(maxAge!) + } + if isSecure != nil && isSecure! { + properties[.secure] = "TRUE" + } + if isHttpOnly != nil && isHttpOnly! { + properties[.init("HttpOnly")] = "YES" + } + if sameSite != nil { + if #available(iOS 13.0, *) { + var sameSiteValue = HTTPCookieStringPolicy(rawValue: "None") + switch sameSite { + case "Lax": + sameSiteValue = HTTPCookieStringPolicy.sameSiteLax + case "Strict": + sameSiteValue = HTTPCookieStringPolicy.sameSiteStrict + default: + break + } + properties[.sameSitePolicy] = sameSiteValue + } else { + properties[.init("SameSite")] = sameSite + } + } + + + if let cookie = HTTPCookie(properties: properties) { + cookieStore.setCookie(cookie, completionHandler: {() in + result(true) + }) + } else { + result(false) + } + } + + public static func getCookies(url: String, result: @escaping FlutterResult, cookieStore: WKHTTPCookieStore = WKWebsiteDataStore.default().httpCookieStore) { + var cookieList: [[String: Any?]] = [] + + if let urlHost = URL(string: url)?.host { + cookieStore.getAllCookies { (cookies) in + for cookie in cookies { + if urlHost.hasSuffix(cookie.domain) || ".\(urlHost)".hasSuffix(cookie.domain) { + var sameSite: String? = nil + if #available(iOS 13.0, *) { + if let sameSiteValue = cookie.sameSitePolicy?.rawValue { + sameSite = sameSiteValue.prefix(1).capitalized + sameSiteValue.dropFirst() + } + } + + var expiresDateTimestamp: Int64 = -1 + if let expiresDate = cookie.expiresDate?.timeIntervalSince1970 { + // convert to milliseconds + expiresDateTimestamp = Int64(expiresDate * 1000) + } + + cookieList.append([ + "name": cookie.name, + "value": cookie.value, + "expiresDate": expiresDateTimestamp != -1 ? expiresDateTimestamp : nil, + "isSessionOnly": cookie.isSessionOnly, + "domain": cookie.domain, + "sameSite": sameSite, + "isSecure": cookie.isSecure, + "isHttpOnly": cookie.isHTTPOnly, + "path": cookie.path, + ]) + } + } + result(cookieList) + } + return + } else { + print("Cannot get WebView cookies. No HOST found for URL: \(url)") + } + + result(cookieList) + } + + public static func getAllCookies(result: @escaping FlutterResult, cookieStore: WKHTTPCookieStore = WKWebsiteDataStore.default().httpCookieStore) { + var cookieList: [[String: Any?]] = [] + + cookieStore.getAllCookies { (cookies) in + for cookie in cookies { + var sameSite: String? = nil + if #available(iOS 13.0, *) { + if let sameSiteValue = cookie.sameSitePolicy?.rawValue { + sameSite = sameSiteValue.prefix(1).capitalized + sameSiteValue.dropFirst() + } + } + + var expiresDateTimestamp: Int64 = -1 + if let expiresDate = cookie.expiresDate?.timeIntervalSince1970 { + // convert to milliseconds + expiresDateTimestamp = Int64(expiresDate * 1000) + } + + cookieList.append([ + "name": cookie.name, + "value": cookie.value, + "expiresDate": expiresDateTimestamp != -1 ? expiresDateTimestamp : nil, + "isSessionOnly": cookie.isSessionOnly, + "domain": cookie.domain, + "sameSite": sameSite, + "isSecure": cookie.isSecure, + "isHttpOnly": cookie.isHTTPOnly, + "path": cookie.path, + ]) + } + result(cookieList) + } + } + + public static func deleteCookie(url: String, name: String, path: String, domain: String?, result: @escaping FlutterResult, cookieStore: WKHTTPCookieStore = WKWebsiteDataStore.default().httpCookieStore) { + var domain = domain + cookieStore.getAllCookies { (cookies) in + for cookie in cookies { + var originURL = url + if cookie.properties![.originURL] is String { + originURL = cookie.properties![.originURL] as! String + } + else if cookie.properties![.originURL] is URL { + originURL = (cookie.properties![.originURL] as! URL).absoluteString + } + if domain == nil, let domainUrl = URL(string: originURL) { + if #available(iOS 16.0, *) { + domain = domainUrl.host() + } else { + domain = domainUrl.host + } + } + if let domain = domain, cookie.domain == domain, cookie.name == name, cookie.path == path { + cookieStore.delete(cookie, completionHandler: { + result(true) + }) + return + } + } + result(false) + } + } + + public static func deleteCookies(url: String, path: String, domain: String?, result: @escaping FlutterResult, cookieStore: WKHTTPCookieStore = WKWebsiteDataStore.default().httpCookieStore) { + var domain = domain + let dispatchGroup = DispatchGroup() + cookieStore.getAllCookies { (cookies) in + for cookie in cookies { + var originURL = url + if cookie.properties![.originURL] is String { + originURL = cookie.properties![.originURL] as! String + } + else if cookie.properties![.originURL] is URL { + originURL = (cookie.properties![.originURL] as! URL).absoluteString + } + if domain == nil, let domainUrl = URL(string: originURL) { + if #available(iOS 16.0, *) { + domain = domainUrl.host() + } else { + domain = domainUrl.host + } + } + if let domain = domain, cookie.domain == domain, cookie.path == path { + dispatchGroup.enter() + cookieStore.delete(cookie) { + dispatchGroup.leave() + } + } + } + dispatchGroup.notify(queue: .main) { + result(true) + } + } + } + + public static func deleteAllCookies(result: @escaping FlutterResult, cookieStore: WKHTTPCookieStore = WKWebsiteDataStore.default().httpCookieStore) { + cookieStore.getAllCookies { cookies in + let group = DispatchGroup() + for cookie in cookies { + group.enter() + cookieStore.delete(cookie) { group.leave() } + } + group.notify(queue: .main) { result(true) } + } + } + + public override func dispose() { + super.dispose() + plugin = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/MyWebStorageManager.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/MyWebStorageManager.swift new file mode 100755 index 00000000..a375fdfb --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/MyWebStorageManager.swift @@ -0,0 +1,96 @@ +// +// MyWebStorageManager.swift +// connectivity +// +// Created by Lorenzo Pichilli on 16/12/2019. +// + +import Foundation +import WebKit + +@available(iOS 9.0, *) +public class MyWebStorageManager: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_webstoragemanager" + static let websiteDataStore = WKWebsiteDataStore.default() + + private var plugin: SwiftFlutterPlugin? + + init(plugin: SwiftFlutterPlugin) { + super.init(channel: FlutterMethodChannel(name: MyWebStorageManager.METHOD_CHANNEL_NAME, binaryMessenger: plugin.registrar!.messenger())) + self.plugin = plugin + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + switch call.method { + case "fetchDataRecords": + let dataTypes = Set(arguments!["dataTypes"] as! [String]) + MyWebStorageManager.fetchDataRecords(dataTypes: dataTypes, result: result) + break + case "removeDataFor": + let dataTypes = Set(arguments!["dataTypes"] as! [String]) + let recordList = arguments!["recordList"] as! [[String: Any?]] + MyWebStorageManager.removeDataFor(dataTypes: dataTypes, recordList: recordList, result: result) + break + case "removeDataModifiedSince": + let dataTypes = Set(arguments!["dataTypes"] as! [String]) + let timestamp = arguments!["timestamp"] as! Int64 + MyWebStorageManager.removeDataModifiedSince(dataTypes: dataTypes, timestamp: timestamp, result: result) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public static func fetchDataRecords(dataTypes: Set, result: @escaping FlutterResult) { + var recordList: [[String: Any?]] = [] + + MyWebStorageManager.websiteDataStore.fetchDataRecords(ofTypes: dataTypes) { (data) in + for record in data { + recordList.append([ + "displayName": record.displayName, + "dataTypes": record.dataTypes.map({ (dataType) -> String in + return dataType + }) + ]) + } + result(recordList) + } + } + + public static func removeDataFor(dataTypes: Set, recordList: [[String: Any?]], result: @escaping FlutterResult) { + var records: [WKWebsiteDataRecord] = [] + + MyWebStorageManager.websiteDataStore.fetchDataRecords(ofTypes: dataTypes) { (data) in + for record in data { + for r in recordList { + let displayName = r["displayName"] as! String + if (record.displayName == displayName) { + records.append(record) + break + } + } + } + MyWebStorageManager.websiteDataStore.removeData(ofTypes: dataTypes, for: records) { + result(true) + } + } + } + + public static func removeDataModifiedSince(dataTypes: Set, timestamp: Int64, result: @escaping FlutterResult) { + let date = NSDate(timeIntervalSince1970: TimeInterval(timestamp)) + MyWebStorageManager.websiteDataStore.removeData(ofTypes: dataTypes, modifiedSince: date as Date) { + result(true) + } + } + + public override func dispose() { + super.dispose() + plugin = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PlatformUtil.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PlatformUtil.swift new file mode 100644 index 00000000..a49d5316 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PlatformUtil.swift @@ -0,0 +1,66 @@ +// +// PlatformUtil.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 01/03/21. +// + +import Foundation + +public class PlatformUtil: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_platformutil" + var plugin: SwiftFlutterPlugin? + + init(plugin: SwiftFlutterPlugin) { + super.init(channel: FlutterMethodChannel(name: PlatformUtil.METHOD_CHANNEL_NAME, binaryMessenger: plugin.registrar!.messenger())) + self.plugin = plugin + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "getSystemVersion": + let device = UIDevice.current + result(device.systemVersion) + break + case "formatDate": + let date = arguments!["date"] as! Int64 + let format = arguments!["format"] as! String + let locale = PlatformUtil.getLocaleFromString(locale: arguments!["locale"] as? String) + let timezone = TimeZone.init(abbreviation: arguments!["timezone"] as? String ?? "UTC")! + result(PlatformUtil.formatDate(date: date, format: format, locale: locale, timezone: timezone)) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + static public func getLocaleFromString(locale: String?) -> Locale { + guard let locale = locale else { + return Locale.init(identifier: "en_US") + } + return Locale.init(identifier: locale) + } + + static public func getDateFromMilliseconds(date: Int64) -> Date { + return Date(timeIntervalSince1970: TimeInterval(Double(date)/1000)) + } + + static public func formatDate(date: Int64, format: String, locale: Locale, timezone: TimeZone) -> String { + let formatter = DateFormatter() + formatter.dateFormat = format + formatter.timeZone = timezone + return formatter.string(from: PlatformUtil.getDateFromMilliseconds(date: date)) + } + + public override func dispose() { + super.dispose() + plugin = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/CallAsyncJavaScriptBelowIOS14WrapperJS.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/CallAsyncJavaScriptBelowIOS14WrapperJS.swift new file mode 100644 index 00000000..5622691a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/CallAsyncJavaScriptBelowIOS14WrapperJS.swift @@ -0,0 +1,21 @@ +// +// CallAsyncJavaScriptBelowIOS14WrapperJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let CALL_ASYNC_JAVASCRIPT_BELOW_IOS_14_WRAPPER_JS = """ +(function(obj) { + (async function(\(PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_NAMES) { + \(PluginScriptsUtil.VAR_FUNCTION_BODY) + })(\(PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_VALUES)).then(function(value) { + window.webkit.messageHandlers['onCallAsyncJavaScriptResultBelowIOS14Received'].postMessage({'value': value, 'error': null, 'resultUuid': '\(PluginScriptsUtil.VAR_RESULT_UUID)'}); + }).catch(function(error) { + window.webkit.messageHandlers['onCallAsyncJavaScriptResultBelowIOS14Received'].postMessage({'value': null, 'error': error + '', 'resultUuid': '\(PluginScriptsUtil.VAR_RESULT_UUID)'}); + }); + return null; +})(\(PluginScriptsUtil.VAR_FUNCTION_ARGUMENTS_OBJ)); +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/ConsoleLogJS.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/ConsoleLogJS.swift new file mode 100644 index 00000000..d244d899 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/ConsoleLogJS.swift @@ -0,0 +1,52 @@ +// +// ConsoleLogJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let CONSOLE_LOG_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_CONSOLE_LOG_JS_PLUGIN_SCRIPT" + +let CONSOLE_LOG_JS_PLUGIN_SCRIPT = PluginScript( + groupName: CONSOLE_LOG_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: CONSOLE_LOG_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: ["consoleLog", "consoleDebug", "consoleError", "consoleInfo", "consoleWarn"]) + +// the message needs to be concatenated with '' in order to have the same behavior like on Android +let CONSOLE_LOG_JS_SOURCE = """ +(function(console) { + + function _callHandler(oldLog, args) { + var message = ''; + for (var i in args) { + try { + message += message === '' ? args[i] : ' ' + args[i]; + } catch(ignored) {} + } + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + window.webkit.messageHandlers[oldLog].postMessage({'message': message, '_windowId': _windowId}); + } + + var oldLogs = { + 'consoleLog': console.log, + 'consoleDebug': console.debug, + 'consoleError': console.error, + 'consoleInfo': console.info, + 'consoleWarn': console.warn + }; + + for (var k in oldLogs) { + (function(oldLog) { + console[oldLog.replace('console', '').toLowerCase()] = function() { + oldLogs[oldLog].apply(null, arguments); + _callHandler(oldLog, arguments); + } + })(k); + } +})(window.console); +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/EnableViewportScaleJS.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/EnableViewportScaleJS.swift new file mode 100644 index 00000000..593ba00e --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/EnableViewportScaleJS.swift @@ -0,0 +1,36 @@ +// +// EnableViewportScaleJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT" + +let ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: ENABLE_VIEWPORT_SCALE_JS_SOURCE, + injectionTime: .atDocumentEnd, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let ENABLE_VIEWPORT_SCALE_JS_SOURCE = """ +(function() { + var meta = document.createElement('meta'); + meta.setAttribute('name', 'viewport'); + meta.setAttribute('content', 'width=device-width'); + document.getElementsByTagName('head')[0].appendChild(meta); +})() +""" + +let NOT_ENABLE_VIEWPORT_SCALE_JS_SOURCE = """ +(function() { + var meta = document.createElement('meta'); + meta.setAttribute('name', 'viewport'); + meta.setAttribute('content', window.\(JAVASCRIPT_BRIDGE_NAME)._originalViewPortMetaTagContent); + document.getElementsByTagName('head')[0].appendChild(meta); +})() +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/FindElementsAtPointJS.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/FindElementsAtPointJS.swift new file mode 100644 index 00000000..b3c22282 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/FindElementsAtPointJS.swift @@ -0,0 +1,73 @@ +// +// FindElementsAtPointJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let FIND_ELEMENTS_AT_POINT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_FIND_ELEMENTS_AT_POINT_JS_PLUGIN_SCRIPT" + +let FIND_ELEMENTS_AT_POINT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: FIND_ELEMENTS_AT_POINT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: FIND_ELEMENTS_AT_POINT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +/** + https://developer.android.com/reference/android/webkit/WebView.HitTestResult + */ +let FIND_ELEMENTS_AT_POINT_JS_SOURCE = """ +window.\(JAVASCRIPT_BRIDGE_NAME)._findElementsAtPoint = function(x, y) { + var hitTestResultType = { + UNKNOWN_TYPE: 0, + PHONE_TYPE: 2, + GEO_TYPE: 3, + EMAIL_TYPE: 4, + IMAGE_TYPE: 5, + SRC_ANCHOR_TYPE: 7, + SRC_IMAGE_ANCHOR_TYPE: 8, + EDIT_TEXT_TYPE: 9 + }; + var element = document.elementFromPoint(x, y); + var data = { + type: 0, + extra: null + }; + while (element) { + if (element.tagName === 'IMG' && element.src) { + if (element.parentNode && element.parentNode.tagName === 'A' && element.parentNode.href) { + data.type = hitTestResultType.SRC_IMAGE_ANCHOR_TYPE; + } else { + data.type = hitTestResultType.IMAGE_TYPE; + } + data.extra = element.src; + break; + } else if (element.tagName === 'A' && element.href) { + if (element.href.indexOf('mailto:') === 0) { + data.type = hitTestResultType.EMAIL_TYPE; + data.extra = element.href.replace('mailto:', ''); + } else if (element.href.indexOf('tel:') === 0) { + data.type = hitTestResultType.PHONE_TYPE; + data.extra = element.href.replace('tel:', ''); + } else if (element.href.indexOf('geo:') === 0) { + data.type = hitTestResultType.GEO_TYPE; + data.extra = element.href.replace('geo:', ''); + } else { + data.type = hitTestResultType.SRC_ANCHOR_TYPE; + data.extra = element.href; + } + break; + } else if ( + (element.tagName === 'INPUT' && ['text', 'email', 'password', 'number', 'search', 'tel', 'url'].indexOf(element.type) >= 0) || + element.tagName === 'TEXTAREA') { + data.type = hitTestResultType.EDIT_TEXT_TYPE + } + element = element.parentNode; + } + return data; +} +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/FindTextHighlightJS.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/FindTextHighlightJS.swift new file mode 100644 index 00000000..f0db6145 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/FindTextHighlightJS.swift @@ -0,0 +1,187 @@ +// +// FindTextHighlightJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let FIND_TEXT_HIGHLIGHT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_FIND_TEXT_HIGHLIGHT_JS_PLUGIN_SCRIPT" +let FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._searchResultCount" +let FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._currentHighlight" +let FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._isDoneCounting" + +let FIND_TEXT_HIGHLIGHT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: FIND_TEXT_HIGHLIGHT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: FIND_TEXT_HIGHLIGHT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: ["onFindResultReceived"]) + +let FIND_TEXT_HIGHLIGHT_JS_SOURCE = """ +\(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) = 0; +\(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE) = 0; +\(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) = false; +window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsyncForElement = function(element, keyword) { + if (element) { + if (element.nodeType == 3) { + // Text node + + var elementTmp = element; + while (true) { + var value = elementTmp.nodeValue; // Search for keyword in text node + var idx = value.toLowerCase().indexOf(keyword); + + if (idx < 0) break; + + var span = document.createElement("span"); + var text = document.createTextNode(value.substr(idx, keyword.length)); + span.appendChild(text); + + span.setAttribute( + "id", + "\(JAVASCRIPT_BRIDGE_NAME)_SEARCH_WORD_" + \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) + ); + span.setAttribute("class", "\(JAVASCRIPT_BRIDGE_NAME)_Highlight"); + var backgroundColor = \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) == 0 ? "#FF9732" : "#FFFF00"; + span.setAttribute("style", "color: #000 !important; background: " + backgroundColor + " !important; padding: 0px !important; margin: 0px !important; border: 0px !important;"); + + text = document.createTextNode(value.substr(idx + keyword.length)); + element.deleteData(idx, value.length - idx); + + var next = element.nextSibling; + element.parentNode.insertBefore(span, next); + element.parentNode.insertBefore(text, next); + element = text; + + \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE)++; + elementTmp = document.createTextNode( + value.substr(idx + keyword.length) + ); + + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + + window.webkit.messageHandlers["onFindResultReceived"].postMessage( + { + 'findResult': { + 'activeMatchOrdinal': \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE), + 'numberOfMatches': \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE), + 'isDoneCounting': \(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) + }, + '_windowId': _windowId + } + ); + } + } else if (element.nodeType == 1) { + // Element node + if ( + element.style.display != "none" && + element.nodeName.toLowerCase() != "select" + ) { + for (var i = element.childNodes.length - 1; i >= 0; i--) { + window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsyncForElement( + element.childNodes[element.childNodes.length - 1 - i], + keyword + ); + } + } + } + } +} + +// the main entry point to start the search +window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsync = function(keyword) { + window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatches(); + window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsyncForElement(document.body, keyword.toLowerCase()); + \(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) = true; + + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + + window.webkit.messageHandlers["onFindResultReceived"].postMessage( + { + 'findResult': { + 'activeMatchOrdinal': \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE), + 'numberOfMatches': \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE), + 'isDoneCounting': \(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) + }, + '_windowId': _windowId + } + ); +} + +// helper function, recursively removes the highlights in elements and their children +window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatchesForElement = function(element) { + if (element) { + if (element.nodeType == 1) { + if (element.getAttribute("class") == "\(JAVASCRIPT_BRIDGE_NAME)_Highlight") { + var text = element.removeChild(element.firstChild); + element.parentNode.insertBefore(text, element); + element.parentNode.removeChild(element); + return true; + } else { + var normalize = false; + for (var i = element.childNodes.length - 1; i >= 0; i--) { + if (window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatchesForElement(element.childNodes[i])) { + normalize = true; + } + } + if (normalize) { + element.normalize(); + } + } + } + } + return false; +} + +// the main entry point to remove the highlights +window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatches = function() { + \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) = 0; + \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE) = 0; + \(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) = false; + window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatchesForElement(document.body); +} + +window.\(JAVASCRIPT_BRIDGE_NAME)._findNext = function(forward) { + if (\(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) <= 0) return; + + var idx = \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE) + (forward ? +1 : -1); + idx = + idx < 0 + ? \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) - 1 + : idx >= \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) + ? 0 + : idx; + \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE) = idx; + + var scrollTo = document.getElementById("\(JAVASCRIPT_BRIDGE_NAME)_SEARCH_WORD_" + idx); + if (scrollTo) { + var highlights = document.getElementsByClassName("\(JAVASCRIPT_BRIDGE_NAME)_Highlight"); + for (var i = 0; i < highlights.length; i++) { + var span = highlights[i]; + span.style.backgroundColor = "#FFFF00"; + } + scrollTo.style.backgroundColor = "#FF9732"; + + scrollTo.scrollIntoView({ + behavior: "auto", + block: "center" + }); + + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + + window.webkit.messageHandlers["onFindResultReceived"].postMessage( + { + 'findResult': { + 'activeMatchOrdinal': \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE), + 'numberOfMatches': \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE), + 'isDoneCounting': \(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) + }, + '_windowId': _windowId + } + ); + } +} +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/InterceptAjaxRequestJS.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/InterceptAjaxRequestJS.swift new file mode 100644 index 00000000..28ebb58b --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/InterceptAjaxRequestJS.swift @@ -0,0 +1,267 @@ +// +// InterceptAjaxRequestsJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT" +let FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._useShouldInterceptAjaxRequest" + +let FLAG_VARIABLE_FOR_INTERCEPT_ONLY_ASYNC_AJAX_REQUESTS_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._interceptOnlyAsyncAjaxRequests"; + +let INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT = PluginScript( + groupName: INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: INTERCEPT_AJAX_REQUEST_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: []) + +func createInterceptOnlyAsyncAjaxRequestsPluginScript(onlyAsync: Bool) -> PluginScript { + return PluginScript(groupName: INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: "\(FLAG_VARIABLE_FOR_INTERCEPT_ONLY_ASYNC_AJAX_REQUESTS_JS_SOURCE) = \(onlyAsync);", + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: [] + ); +} + +let INTERCEPT_AJAX_REQUEST_JS_SOURCE = """ +\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) = true; +(function(ajax) { + 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; + if (\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) == null || \(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.\(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 canBeIntercepted = self._flutter_inappwebview_isAsync || \(FLAG_VARIABLE_FOR_INTERCEPT_ONLY_ASYNC_AJAX_REQUESTS_JS_SOURCE) === false; + if (canBeIntercepted && (\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) == null || \(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() { + if (\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) == null || \(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.\(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); + \(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.\(JAVASCRIPT_BRIDGE_NAME).callHandler('shouldInterceptAjaxRequest', ajaxRequest).then(function(result) { + if (result != null) { + switch (result) { + case 0: + self.abort(); + return; + }; + if (result.data != null && !\(JAVASCRIPT_UTIL_VAR_NAME).isString(result.data) && result.data.length > 0) { + var bodyString = \(JAVASCRIPT_UTIL_VAR_NAME).arrayBufferToString(result.data); + if (\(JAVASCRIPT_UTIL_VAR_NAME).isBodyFormData(bodyString)) { + var formDataContentType = \(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 (\(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; + }; + if (result.headers != null) { + 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); +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/InterceptFetchRequestJS.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/InterceptFetchRequestJS.swift new file mode 100644 index 00000000..14539811 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/InterceptFetchRequestJS.swift @@ -0,0 +1,153 @@ +// +// InterceptFetchRequestsJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT" +let FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._useShouldInterceptFetchRequest" + +let INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT = PluginScript( + groupName: INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: INTERCEPT_FETCH_REQUEST_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: []) + +let INTERCEPT_FETCH_REQUEST_JS_SOURCE = """ +\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE) = true; +(function(fetch) { + if (fetch == null) { + return; + } + window.fetch = async function(resource, init) { + if (\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE) == null || \(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 = \(JAVASCRIPT_UTIL_VAR_NAME).convertHeadersToJson(fetchRequest.headers); + } + fetchRequest.credentials = \(JAVASCRIPT_UTIL_VAR_NAME).convertCredentialsToJson(fetchRequest.credentials); + return \(JAVASCRIPT_UTIL_VAR_NAME).convertBodyRequest(fetchRequest.body).then(function(body) { + fetchRequest.body = body; + return window.\(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 && !\(JAVASCRIPT_UTIL_VAR_NAME).isString(result.body) && result.body.length > 0) { + var bodyString = \(JAVASCRIPT_UTIL_VAR_NAME).arrayBufferToString(result.body); + if (\(JAVASCRIPT_UTIL_VAR_NAME).isBodyFormData(bodyString)) { + var formDataContentType = \(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 = \(JAVASCRIPT_UTIL_VAR_NAME).convertJsonToHeaders(result.headers); + } + if (\(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 = \(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); +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/JavaScriptBridgeJS.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/JavaScriptBridgeJS.swift new file mode 100644 index 00000000..1549c28b --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/JavaScriptBridgeJS.swift @@ -0,0 +1,243 @@ +// +// javaScriptBridgeJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let JAVASCRIPT_BRIDGE_NAME = "flutter_inappwebview" +let JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT" + +let JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT = PluginScript( + groupName: JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: JAVASCRIPT_BRIDGE_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: ["callHandler"]) + +let JAVASCRIPT_BRIDGE_JS_SOURCE = """ +window.\(JAVASCRIPT_BRIDGE_NAME) = {}; +\(WEB_MESSAGE_CHANNELS_VARIABLE_NAME) = {}; +window.\(JAVASCRIPT_BRIDGE_NAME).callHandler = function() { + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + var _callHandlerID = setTimeout(function(){}); + window.webkit.messageHandlers['callHandler'].postMessage( {'handlerName': arguments[0], '_callHandlerID': _callHandlerID, 'args': JSON.stringify(Array.prototype.slice.call(arguments, 1)), '_windowId': _windowId} ); + return new Promise(function(resolve, reject) { + window.\(JAVASCRIPT_BRIDGE_NAME)[_callHandlerID] = {resolve: resolve, reject: reject}; + }); +}; +\(WEB_MESSAGE_LISTENER_JS_SOURCE) +\(UTIL_JS_SOURCE) +""" + +let PLATFORM_READY_JS_SOURCE = "window.dispatchEvent(new Event('flutterInAppWebViewPlatformReady'));"; + +let JAVASCRIPT_UTIL_VAR_NAME = "window.\(JAVASCRIPT_BRIDGE_NAME)._Util" + +/* + https://github.com/github/fetch/blob/master/fetch.js + */ +let 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((resolve, reject) => resolve(null)); + } + if (\(JAVASCRIPT_UTIL_VAR_NAME).isString(body) || (\(JAVASCRIPT_UTIL_VAR_NAME).support.searchParams && body instanceof URLSearchParams)) { + return new Promise((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; + } +}; +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/LastTouchedAnchorOrImageJS.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/LastTouchedAnchorOrImageJS.swift new file mode 100644 index 00000000..ef2e7d67 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/LastTouchedAnchorOrImageJS.swift @@ -0,0 +1,62 @@ +// +// LastTouchedAnchorOrImageJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_PLUGIN_SCRIPT" + +let LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_PLUGIN_SCRIPT = PluginScript( + groupName: LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let LAST_TOUCHED_ANCHOR_OR_IMAGE_JS_SOURCE = """ +window.\(JAVASCRIPT_BRIDGE_NAME)._lastAnchorOrImageTouched = null; +window.\(JAVASCRIPT_BRIDGE_NAME)._lastImageTouched = null; +(function() { + document.addEventListener('touchstart', function(event) { + var target = event.target; + while (target) { + if (target.tagName === 'IMG') { + var img = target; + window.\(JAVASCRIPT_BRIDGE_NAME)._lastImageTouched = { + url: img.src + }; + var parent = img.parentNode; + while (parent) { + if (parent.tagName === 'A') { + window.\(JAVASCRIPT_BRIDGE_NAME)._lastAnchorOrImageTouched = { + title: parent.textContent, + url: parent.href, + src: img.src + }; + break; + } + parent = parent.parentNode; + } + return; + } else if (target.tagName === 'A') { + var link = target; + var images = link.getElementsByTagName('img'); + var img = (images.length > 0) ? images[0] : null; + var imgSrc = (img != null) ? img.src : null; + window.\(JAVASCRIPT_BRIDGE_NAME)._lastImageTouched = (img != null) ? {url: imgSrc} : window.\(JAVASCRIPT_BRIDGE_NAME)._lastImageTouched; + window.\(JAVASCRIPT_BRIDGE_NAME)._lastAnchorOrImageTouched = { + title: link.textContent, + url: link.href, + src: imgSrc + }; + return; + } + target = target.parentNode; + } + }); +})(); +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/OnLoadResourceJS.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/OnLoadResourceJS.swift new file mode 100644 index 00000000..828de9ef --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/OnLoadResourceJS.swift @@ -0,0 +1,39 @@ +// +// resourceObserverJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT" +let FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._useOnLoadResource" + +let ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: ON_LOAD_RESOURCE_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let ON_LOAD_RESOURCE_JS_SOURCE = """ +\(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.\(JAVASCRIPT_BRIDGE_NAME).callHandler("onLoadResource", resource); + } + }); + }); + observer.observe({entryTypes: ['resource']}); +})(); +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/OnWindowBlurEventJS.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/OnWindowBlurEventJS.swift new file mode 100644 index 00000000..345b5ece --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/OnWindowBlurEventJS.swift @@ -0,0 +1,26 @@ +// +// OnWindowBlurEventJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT" + +let ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: ON_WINDOW_BLUR_EVENT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let ON_WINDOW_BLUR_EVENT_JS_SOURCE = """ +(function(){ + window.addEventListener('blur', function(e) { + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onWindowBlur'); + }); +})(); +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/OnWindowFocusEventJS.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/OnWindowFocusEventJS.swift new file mode 100644 index 00000000..9ebda3e9 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/OnWindowFocusEventJS.swift @@ -0,0 +1,26 @@ +// +// OnWindowFocusEventJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT" + +let ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ON_WINDOW_FOCUS_EVENT_JS_SOURCE, + source: ON_WINDOW_FOCUS_EVENT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let ON_WINDOW_FOCUS_EVENT_JS_SOURCE = """ +(function(){ + window.addEventListener('focus', function(e) { + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onWindowFocus'); + }); +})(); +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/OriginalViewPortMetaTagContentJS.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/OriginalViewPortMetaTagContentJS.swift new file mode 100644 index 00000000..a1c6637d --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/OriginalViewPortMetaTagContentJS.swift @@ -0,0 +1,31 @@ +// +// OriginalViewPortMetaTagContentJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_PLUGIN_SCRIPT" + +let ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_SOURCE, + injectionTime: .atDocumentEnd, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_SOURCE = """ +window.\(JAVASCRIPT_BRIDGE_NAME)._originalViewPortMetaTagContent = ""; +(function() { + var metaTagNodes = document.head.getElementsByTagName('meta'); + for (var i = 0; i < metaTagNodes.length; i++) { + var metaTagNode = metaTagNodes[i]; + if (metaTagNode.name === "viewport") { + window.\(JAVASCRIPT_BRIDGE_NAME)._originalViewPortMetaTagContent = metaTagNode.content; + } + } +})(); +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/PluginScriptsUtil.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/PluginScriptsUtil.swift new file mode 100644 index 00000000..51f9c867 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/PluginScriptsUtil.swift @@ -0,0 +1,31 @@ +// +// PluginScripts.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +public class PluginScriptsUtil { + public static let VAR_PLACEHOLDER_VALUE = "$IN_APP_WEBVIEW_PLACEHOLDER_VALUE" + public static let VAR_FUNCTION_ARGUMENT_NAMES = "$IN_APP_WEBVIEW_FUNCTION_ARGUMENT_NAMES" + public static let VAR_FUNCTION_ARGUMENT_VALUES = "$IN_APP_WEBVIEW_FUNCTION_ARGUMENT_VALUES" + public static let VAR_FUNCTION_ARGUMENTS_OBJ = "$IN_APP_WEBVIEW_FUNCTION_ARGUMENTS_OBJ" + public static let VAR_FUNCTION_BODY = "$IN_APP_WEBVIEW_FUNCTION_BODY" + public static let VAR_RESULT_UUID = "$IN_APP_WEBVIEW_RESULT_UUID" + + public static let 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; +})(); +""" +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/PrintJS.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/PrintJS.swift new file mode 100644 index 00000000..5ac12691 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/PrintJS.swift @@ -0,0 +1,24 @@ +// +// PrintJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let PRINT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_PRINT_JS_PLUGIN_SCRIPT" + +let PRINT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: PRINT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: PRINT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: []) + +let PRINT_JS_SOURCE = """ +window.print = function() { + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler("onPrintRequest", window.location.href); +} +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/PromisePolyfillJS.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/PromisePolyfillJS.swift new file mode 100644 index 00000000..d2ef3e2a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/PromisePolyfillJS.swift @@ -0,0 +1,27 @@ +// +// PromisePolyfillJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation +import WebKit + +let PROMISE_POLYFILL_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_PROMISE_POLYFILL_JS_PLUGIN_SCRIPT" + +let PROMISE_POLYFILL_JS_PLUGIN_SCRIPT = PluginScript( + groupName: PROMISE_POLYFILL_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: PROMISE_POLYFILL_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: []) + +// https://github.com/tildeio/rsvp.js +let 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;i2&&void 0!==arguments[2])||arguments[2],o=arguments[3];return function(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}(this,t.call(this,e,r,n,o))}return function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),e.prototype._init=function(t,e){this._result={},this._enumerate(e)},e.prototype._enumerate=function(t){var e=Object.keys(t),r=e.length,n=this.promise;this._remaining=r;for(var o=void 0,i=void 0,s=0;n._state===a&&s= 0) { + this.listeners.splice(index, 1); + } +}; + +window.\(JAVASCRIPT_BRIDGE_NAME)._normalizeIPv6 = function(ip_string) { + // replace ipv4 address if any + var ipv4 = ip_string.match(/(.*:)([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$)/); + if (ipv4) { + ip_string = ipv4[1]; + ipv4 = ipv4[2].match(/[0-9]+/g); + for (var i = 0;i < 4;i ++) { + var byte = parseInt(ipv4[i],10); + ipv4[i] = ("0" + byte.toString(16)).substr(-2); + } + ip_string += ipv4[0] + ipv4[1] + ':' + ipv4[2] + ipv4[3]; + } + + // take care of leading and trailing :: + ip_string = ip_string.replace(/^:|:$/g, ''); + + var ipv6 = ip_string.split(':'); + + for (var i = 0; i < ipv6.length; i ++) { + var hex = ipv6[i]; + if (hex != "") { + // normalize leading zeros + ipv6[i] = ("0000" + hex).substr(-4); + } + else { + // normalize grouped zeros :: + hex = []; + for (var j = ipv6.length; j <= 8; j ++) { + hex.push('0000'); + } + ipv6[i] = hex.join(':'); + } + } + + return ipv6.join(':'); +} + +window.\(JAVASCRIPT_BRIDGE_NAME)._isOriginAllowed = function(allowedOriginRules, scheme, host, port) { + for (var rule of allowedOriginRules) { + if (rule === "*") { + return true; + } + if (scheme == null || scheme === "") { + continue; + } + if ((scheme == null || scheme === "") && (host == null || host === "") && (port === 0 || port === "" || port == null)) { + continue; + } + var rulePort = rule.port == null || rule.port === 0 ? (rule.scheme == "https" ? 443 : 80) : rule.port; + var currentPort = port === 0 || port === "" || port == null ? (scheme == "https" ? 443 : 80) : port; + var IPv6 = null; + if (rule.host != null && rule.host[0] === "[") { + try { + IPv6 = window.\(JAVASCRIPT_BRIDGE_NAME)._normalizeIPv6(rule.host.substring(1, rule.host.length - 1)); + } catch {} + } + var hostIPv6 = null; + try { + hostIPv6 = window.\(JAVASCRIPT_BRIDGE_NAME)._normalizeIPv6(host); + } catch {} + + var schemeAllowed = scheme == rule.scheme; + + var hostAllowed = rule.host == null || + rule.host === "" || + host === rule.host || + (rule.host[0] === "*" && host != null && host.indexOf(rule.host.split("*")[1]) >= 0) || + (hostIPv6 != null && IPv6 != null && hostIPv6 === IPv6); + + var portAllowed = rulePort === currentPort; + + if (schemeAllowed && hostAllowed && portAllowed) { + return true; + } + } + return false; +} +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/WindowIdJS.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/WindowIdJS.swift new file mode 100644 index 00000000..8b3a1879 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PluginScriptsJS/WindowIdJS.swift @@ -0,0 +1,19 @@ +// +// WindowIdJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let WINDOW_ID_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_WINDOW_ID_JS_PLUGIN_SCRIPT" + +let WINDOW_ID_VARIABLE_JS_SOURCE = "window._\(JAVASCRIPT_BRIDGE_NAME)_windowId" + +let WINDOW_ID_INITIALIZE_JS_SOURCE = """ +(function() { + \(WINDOW_ID_VARIABLE_JS_SOURCE) = \(PluginScriptsUtil.VAR_PLACEHOLDER_VALUE); + return \(WINDOW_ID_VARIABLE_JS_SOURCE); +})() +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PrintJob/CustomUIPrintPageRenderer.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PrintJob/CustomUIPrintPageRenderer.swift new file mode 100644 index 00000000..4b003e56 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PrintJob/CustomUIPrintPageRenderer.swift @@ -0,0 +1,34 @@ +// +// CustomUIPrintPageRenderer.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/05/22. +// + +import Foundation + +public class CustomUIPrintPageRenderer: UIPrintPageRenderer { + private var _numberOfPages: Int? + private var forceRenderingQuality: Int? + + public init(numberOfPage: Int? = nil, forceRenderingQuality: Int? = nil) { + super.init() + self._numberOfPages = numberOfPage + self.forceRenderingQuality = forceRenderingQuality + } + + open override var numberOfPages: Int { + get { + return _numberOfPages ?? super.numberOfPages + } + } + + @available(iOS 14.5, *) + open override func currentRenderingQuality(forRequested requestedRenderingQuality: UIPrintRenderingQuality) -> UIPrintRenderingQuality { + if let forceRenderingQuality = forceRenderingQuality, + let quality = UIPrintRenderingQuality.init(rawValue: forceRenderingQuality) { + return quality + } + return super.currentRenderingQuality(forRequested: requestedRenderingQuality) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PrintJob/PrintAttributes.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PrintJob/PrintAttributes.swift new file mode 100644 index 00000000..ddd20d58 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PrintJob/PrintAttributes.swift @@ -0,0 +1,56 @@ +// +// PrintAttributes.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/05/22. +// + +import Foundation + +public class PrintAttributes: NSObject { + var orientation: UIPrintInfo.Orientation? + var duplex: UIPrintInfo.Duplex? + var outputType: UIPrintInfo.OutputType? + var margins: UIEdgeInsets? + var footerHeight: Double? + var headerHeight: Double? + var paperRect: CGRect? + var printableRect: CGRect? + var maximumContentHeight: Double? + var maximumContentWidth: Double? + + public init(fromPrintJobController: PrintJobController) { + super.init() + if let printPageRenderer = fromPrintJobController.printPageRenderer { + footerHeight = printPageRenderer.footerHeight + headerHeight = printPageRenderer.headerHeight + paperRect = printPageRenderer.paperRect + printableRect = printPageRenderer.printableRect + } + if let printFormatter = fromPrintJobController.printFormatter { + maximumContentHeight = printFormatter.maximumContentHeight + maximumContentWidth = printFormatter.maximumContentWidth + margins = printFormatter.perPageContentInsets + } + if let job = fromPrintJobController.job, let printInfo = job.printInfo { + orientation = printInfo.orientation + duplex = printInfo.duplex + outputType = printInfo.outputType + } + } + + public func toMap () -> [String:Any?] { + return [ + "footerHeight": footerHeight, + "headerHeight": headerHeight, + "paperRect": paperRect?.toMap(), + "printableRect": printableRect?.toMap(), + "margins": margins?.toMap(), + "maximumContentHeight": maximumContentHeight, + "maximumContentWidth": maximumContentWidth, + "orientation": orientation?.rawValue, + "duplex": duplex?.rawValue, + "outputType": outputType?.rawValue + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PrintJob/PrintJobChannelDelegate.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PrintJob/PrintJobChannelDelegate.swift new file mode 100644 index 00000000..337fae3a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PrintJob/PrintJobChannelDelegate.swift @@ -0,0 +1,68 @@ +// +// PrintJobChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 09/05/22. +// + +import Foundation + +public class PrintJobChannelDelegate: ChannelDelegate { + private weak var printJobController: PrintJobController? + + public init(printJobController: PrintJobController, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.printJobController = printJobController + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "dismiss": + if let job = printJobController?.job { + let animated = arguments!["animated"] as! Bool + job.dismiss(animated: animated) + result(true) + } else { + result(false) + } + break + case "getInfo": + if let printJobController = printJobController { + result(printJobController.getInfo()?.toMap()) + } else { + result(false) + } + break + case "dispose": + if let printJobController = printJobController { + printJobController.dispose() + result(true) + } else { + result(false) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onComplete(completed: Bool, error: Error?) { + let arguments: [String: Any?] = [ + "completed": completed, + "error": error?.localizedDescription + ] + channel?.invokeMethod("onComplete", arguments: arguments) + } + + public override func dispose() { + super.dispose() + printJobController = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PrintJob/PrintJobController.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PrintJob/PrintJobController.swift new file mode 100644 index 00000000..6b4c3210 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PrintJob/PrintJobController.swift @@ -0,0 +1,102 @@ +// +// PrintJob.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 09/05/22. +// + +import Foundation + +public enum PrintJobState: Int { + case created = 1 + case started = 3 + case completed = 5 + case failed = 6 + case canceled = 7 +} + +public class PrintJobController: NSObject, Disposable, UIPrintInteractionControllerDelegate { + static let METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_printjobcontroller_" + var id: String + var plugin: SwiftFlutterPlugin? + var job: UIPrintInteractionController? + var settings: PrintJobSettings? + var printFormatter: UIPrintFormatter? + var printPageRenderer: UIPrintPageRenderer? + var channelDelegate: PrintJobChannelDelegate? + var state = PrintJobState.created + var creationTime = Int64(Date().timeIntervalSince1970 * 1000) + + public init(plugin: SwiftFlutterPlugin, id: String, job: UIPrintInteractionController? = nil, settings: PrintJobSettings? = nil) { + self.id = id + self.plugin = plugin + super.init() + self.job = job + self.settings = settings + self.printFormatter = job?.printFormatter + self.printPageRenderer = job?.printPageRenderer + self.job?.delegate = self + if let registrar = plugin.registrar { + let channel = FlutterMethodChannel(name: PrintJobController.METHOD_CHANNEL_NAME_PREFIX + id, + binaryMessenger: registrar.messenger()) + self.channelDelegate = PrintJobChannelDelegate(printJobController: self, channel: channel) + } + } + + public func printInteractionControllerWillStartJob(_ printInteractionController: UIPrintInteractionController) { + state = .started + } + + public func present(animated: Bool, completionHandler: UIPrintInteractionController.CompletionHandler? = nil) { + guard let job = job else { + return + } + + job.present(animated: animated, completionHandler: { [weak self] (printController, completed, error) in + if !completed { + if let _ = error { + self?.state = .failed + } else { + self?.state = .canceled + } + } else { + self?.state = .completed + } + self?.channelDelegate?.onComplete(completed: completed, error: error) + if let completionHandler = completionHandler { + completionHandler(printController, completed, error) + } + }) + } + + public func getInfo() -> PrintJobInfo? { + guard let _ = job else { + return nil + } + + return PrintJobInfo.init(fromPrintJobController: self) + } + + public func disposeNoDismiss() { + channelDelegate?.dispose() + channelDelegate = nil + printFormatter = nil + printPageRenderer = nil + job?.delegate = nil + job = nil + plugin?.printJobManager?.jobs[id] = nil + plugin = nil + } + + public func dispose() { + channelDelegate?.dispose() + channelDelegate = nil + printFormatter = nil + printPageRenderer = nil + job?.delegate = nil + job?.dismiss(animated: false) + job = nil + plugin?.printJobManager?.jobs[id] = nil + plugin = nil + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PrintJob/PrintJobInfo.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PrintJob/PrintJobInfo.swift new file mode 100644 index 00000000..b94c1293 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PrintJob/PrintJobInfo.swift @@ -0,0 +1,45 @@ +// +// PrintJobInfo.swift +// flutter_downloader +// +// Created by Lorenzo Pichilli on 10/05/22. +// + +import Foundation + +public class PrintJobInfo: NSObject { + var state: PrintJobState + var attributes: PrintAttributes + var creationTime: Int64 + var numberOfPages: Int? + var label: String? + var printerId: String? + + public init(fromPrintJobController: PrintJobController) { + state = fromPrintJobController.state + creationTime = fromPrintJobController.creationTime + attributes = PrintAttributes.init(fromPrintJobController: fromPrintJobController) + super.init() + if let printPageRenderer = fromPrintJobController.printPageRenderer { + numberOfPages = printPageRenderer.numberOfPages + } + if let job = fromPrintJobController.job, let printInfo = job.printInfo { + label = printInfo.jobName + printerId = printInfo.printerID + } + } + + public func toMap () -> [String:Any?] { + return [ + "state": state.rawValue, + "attributes": attributes.toMap(), + "copies": nil, + "numberOfPages": numberOfPages, + "creationTime": creationTime, + "label": label, + "printer": [ + "id": printerId + ] + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PrintJob/PrintJobManager.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PrintJob/PrintJobManager.swift new file mode 100644 index 00000000..3a458c6e --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PrintJob/PrintJobManager.swift @@ -0,0 +1,31 @@ +// +// PrintJobManager.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 09/05/22. +// + +import Foundation + +public class PrintJobManager: NSObject, Disposable { + var plugin: SwiftFlutterPlugin? + var jobs: [String: PrintJobController?] = [:] + + public init(plugin: SwiftFlutterPlugin?) { + super.init() + self.plugin = plugin + } + + public func dispose() { + let jobValues = jobs.values + jobValues.forEach { (job: PrintJobController?) in + job?.dispose() + } + jobs.removeAll() + plugin = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PrintJob/PrintJobSettings.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PrintJob/PrintJobSettings.swift new file mode 100644 index 00000000..ab520c3c --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PrintJob/PrintJobSettings.swift @@ -0,0 +1,154 @@ +// +// PrintJobSettings.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 09/05/22. +// + +import Foundation + +@objcMembers +public class PrintJobSettings: ISettings { + + public var handledByClient = false + public var jobName: String? + public var animated = true + public var _orientation: NSNumber? + public var orientation: Int? { + get { + return _orientation?.intValue + } + set { + if let newValue = newValue { + _orientation = NSNumber.init(value: newValue) + } else { + _orientation = nil + } + } + } + public var _numberOfPages: NSNumber? + public var numberOfPages: Int? { + get { + return _numberOfPages?.intValue + } + set { + if let newValue = newValue { + _numberOfPages = NSNumber.init(value: newValue) + } else { + _numberOfPages = nil + } + } + } + public var _forceRenderingQuality: NSNumber? + public var forceRenderingQuality: Int? { + get { + return _forceRenderingQuality?.intValue + } + set { + if let newValue = newValue { + _forceRenderingQuality = NSNumber.init(value: newValue) + } else { + _forceRenderingQuality = nil + } + } + } + public var margins: UIEdgeInsets? + public var _duplexMode: NSNumber? + public var duplexMode: Int? { + get { + return _duplexMode?.intValue + } + set { + if let newValue = newValue { + _duplexMode = NSNumber.init(value: newValue) + } else { + _duplexMode = nil + } + } + } + public var _outputType: NSNumber? + public var outputType: Int? { + get { + return _outputType?.intValue + } + set { + if let newValue = newValue { + _outputType = NSNumber.init(value: newValue) + } else { + _outputType = nil + } + } + } + public var showsNumberOfCopies = true + public var showsPaperSelectionForLoadedPapers = false + public var showsPaperOrientation = true + public var _maximumContentHeight: NSNumber? + public var maximumContentHeight: Double? { + get { + return _maximumContentHeight?.doubleValue + } + set { + if let newValue = newValue { + _maximumContentHeight = NSNumber.init(value: newValue) + } else { + _maximumContentHeight = nil + } + } + } + public var _maximumContentWidth: NSNumber? + public var maximumContentWidth: Double? { + get { + return _maximumContentWidth?.doubleValue + } + set { + if let newValue = newValue { + _maximumContentWidth = NSNumber.init(value: newValue) + } else { + _maximumContentWidth = nil + } + } + } + public var _footerHeight: NSNumber? + public var footerHeight: Double? { + get { + return _footerHeight?.doubleValue + } + set { + if let newValue = newValue { + _footerHeight = NSNumber.init(value: newValue) + } else { + _footerHeight = nil + } + } + } + public var _headerHeight: NSNumber? + public var headerHeight: Double? { + get { + return _headerHeight?.doubleValue + } + set { + if let newValue = newValue { + _headerHeight = NSNumber.init(value: newValue) + } else { + _headerHeight = nil + } + } + } + + override init(){ + super.init() + } + + override func parse(settings: [String: Any?]) -> PrintJobSettings { + let _ = super.parse(settings: settings) + if let marginsMap = settings["margins"] as? [String : Double] { + margins = UIEdgeInsets.fromMap(map: marginsMap) + } + return self + } + + override func getRealSettings(obj: PrintJobController?) -> [String: Any?] { + let realOptions: [String: Any?] = toMap() + return realOptions + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PullToRefresh/PullToRefreshChannelDelegate.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PullToRefresh/PullToRefreshChannelDelegate.swift new file mode 100644 index 00000000..30490831 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PullToRefresh/PullToRefreshChannelDelegate.swift @@ -0,0 +1,108 @@ +// +// PullToRefreshChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 05/05/22. +// + +import Foundation + +public class PullToRefreshChannelDelegate: ChannelDelegate { + private weak var pullToRefreshControl: PullToRefreshControl? + + public init(pullToRefreshControl: PullToRefreshControl, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.pullToRefreshControl = pullToRefreshControl + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "setEnabled": + if let pullToRefreshView = pullToRefreshControl { + let enabled = arguments!["enabled"] as! Bool + if enabled { + pullToRefreshView.delegate?.enablePullToRefresh() + } else { + pullToRefreshView.delegate?.disablePullToRefresh() + } + result(true) + } else { + result(false) + } + break + case "isEnabled": + if let pullToRefreshView = pullToRefreshControl { + result(pullToRefreshView.delegate?.isPullToRefreshEnabled()) + } else { + result(false) + } + break + case "setRefreshing": + if let pullToRefreshView = pullToRefreshControl { + let refreshing = arguments!["refreshing"] as! Bool + if refreshing { + pullToRefreshView.beginRefreshing() + } else { + pullToRefreshView.endRefreshing() + } + result(true) + } else { + result(false) + } + break + case "isRefreshing": + if let pullToRefreshView = pullToRefreshControl { + result(pullToRefreshView.isRefreshing) + } else { + result(false) + } + break + case "setColor": + if let pullToRefreshView = pullToRefreshControl { + let color = arguments!["color"] as! String + pullToRefreshView.tintColor = UIColor(hexString: color) + result(true) + } else { + result(false) + } + break + case "setBackgroundColor": + if let pullToRefreshView = pullToRefreshControl { + let color = arguments!["color"] as! String + pullToRefreshView.backgroundColor = UIColor(hexString: color) + result(true) + } else { + result(false) + } + break + case "setStyledTitle": + if let pullToRefreshView = pullToRefreshControl { + let attributedTitleMap = arguments!["attributedTitle"] as! [String: Any?] + pullToRefreshView.attributedTitle = NSAttributedString.fromMap(map: attributedTitleMap) + result(true) + } else { + result(false) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onRefresh() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onRefresh", arguments: arguments) + } + + public override func dispose() { + super.dispose() + pullToRefreshControl = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PullToRefresh/PullToRefreshControl.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PullToRefresh/PullToRefreshControl.swift new file mode 100644 index 00000000..5df23c17 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PullToRefresh/PullToRefreshControl.swift @@ -0,0 +1,74 @@ +// +// File.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 03/03/21. +// + +import Foundation +import Flutter + +public class PullToRefreshControl: UIRefreshControl, Disposable { + static let METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_pull_to_refresh_" + + var plugin: SwiftFlutterPlugin? + var channelDelegate: PullToRefreshChannelDelegate? + var settings: PullToRefreshSettings? + var shouldCallOnRefresh = false + var delegate: PullToRefreshDelegate? + + public init(plugin: SwiftFlutterPlugin, id: Any, settings: PullToRefreshSettings?) { + super.init() + self.plugin = plugin + self.settings = settings + if let registrar = plugin.registrar { + let channel = FlutterMethodChannel(name: PullToRefreshControl.METHOD_CHANNEL_NAME_PREFIX + String(describing: id), + binaryMessenger: registrar.messenger()) + self.channelDelegate = PullToRefreshChannelDelegate(pullToRefreshControl: self, channel: channel) + } + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + public func prepare() { + if let settings = settings { + if settings.enabled { + delegate?.enablePullToRefresh() + } + if let color = settings.color, !color.isEmpty { + tintColor = UIColor(hexString: color) + } + if let backgroundTintColor = settings.backgroundColor, !backgroundTintColor.isEmpty { + backgroundColor = UIColor(hexString: backgroundTintColor) + } + if let attributedTitleMap = settings.attributedTitle { + attributedTitle = NSAttributedString.fromMap(map: attributedTitleMap) + } + } + addTarget(self, action: #selector(updateShouldCallOnRefresh), for: .valueChanged) + } + + public func onRefresh() { + shouldCallOnRefresh = false + channelDelegate?.onRefresh() + } + + @objc public func updateShouldCallOnRefresh() { + shouldCallOnRefresh = true + } + + public func dispose() { + channelDelegate?.dispose() + channelDelegate = nil + removeTarget(self, action: #selector(updateShouldCallOnRefresh), for: .valueChanged) + delegate = nil + plugin = nil + } + + deinit { + debugPrint("PullToRefreshControl - dealloc") + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PullToRefresh/PullToRefreshDelegate.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PullToRefresh/PullToRefreshDelegate.swift new file mode 100644 index 00000000..81d97a54 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PullToRefresh/PullToRefreshDelegate.swift @@ -0,0 +1,14 @@ +// +// PullToRefreshDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 04/03/21. +// + +import Foundation + +public protocol PullToRefreshDelegate { + func enablePullToRefresh() + func disablePullToRefresh() + func isPullToRefreshEnabled() -> Bool +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PullToRefresh/PullToRefreshSettings.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PullToRefresh/PullToRefreshSettings.swift new file mode 100644 index 00000000..2172bba1 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/PullToRefresh/PullToRefreshSettings.swift @@ -0,0 +1,33 @@ +// +// pullToRefreshSettings.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 03/03/21. +// + +import Foundation + +public class PullToRefreshSettings: ISettings { + + var enabled = true + var color: String? + var backgroundColor: String? + var attributedTitle: [String: Any?]? + + override init(){ + super.init() + } + + override func parse(settings: [String: Any?]) -> PullToRefreshSettings { + let _ = super.parse(settings: settings) + if let attributedTitle = settings["attributedTitle"] as? [String: Any?] { + self.attributedTitle = attributedTitle + } + return self + } + + override func getRealSettings(obj: PullToRefreshControl?) -> [String: Any?] { + let realSettings: [String: Any?] = toMap() + return realSettings + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/SafariViewController/ChromeSafariBrowserManager.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/SafariViewController/ChromeSafariBrowserManager.swift new file mode 100755 index 00000000..5887ea77 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/SafariViewController/ChromeSafariBrowserManager.swift @@ -0,0 +1,148 @@ +// +// ChromeSafariBrowserManager.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 18/12/2019. +// + +import Flutter +import UIKit +import WebKit +import Foundation +import AVFoundation +import SafariServices + +public class ChromeSafariBrowserManager: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_chromesafaribrowser" + var plugin: SwiftFlutterPlugin? + var browsers: [String: SafariViewController?] = [:] + var prewarmingTokens: [String: Any?] = [:] + + init(plugin: SwiftFlutterPlugin) { + super.init(channel: FlutterMethodChannel(name: ChromeSafariBrowserManager.METHOD_CHANNEL_NAME, binaryMessenger: plugin.registrar!.messenger())) + self.plugin = plugin + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "open": + let id = arguments!["id"] as! String + let url = arguments!["url"] as! String + let settings = arguments!["settings"] as! [String: Any?] + let menuItemList = arguments!["menuItemList"] as! [[String: Any]] + open(id: id, url: url, settings: settings, menuItemList: menuItemList, result: result) + break + case "isAvailable": + if #available(iOS 9.0, *) { + result(true) + } else { + result(false) + } + break + case "clearWebsiteData": + if #available(iOS 16.0, *) { + SFSafariViewController.DataStore.default.clearWebsiteData { + result(true) + } + } else { + result(false) + } + case "prewarmConnections": + if #available(iOS 15.0, *) { + let stringURLs = arguments!["URLs"] as! [String] + var URLs: [URL] = [] + for stringURL in stringURLs { + if let url = URL(string: stringURL) { + URLs.append(url) + } + } + let prewarmingToken = SFSafariViewController.prewarmConnections(to: URLs) + let prewarmingTokenId = NSUUID().uuidString + prewarmingTokens[prewarmingTokenId] = prewarmingToken + result([ + "id": prewarmingTokenId + ]) + } else { + result(nil) + } + case "invalidatePrewarmingToken": + if #available(iOS 15.0, *) { + let prewarmingToken = arguments!["prewarmingToken"] as! [String:Any?] + if let prewarmingTokenId = prewarmingToken["id"] as? String, + let prewarmingToken = prewarmingTokens[prewarmingTokenId] as? SFSafariViewController.PrewarmingToken? { + prewarmingToken?.invalidate() + prewarmingTokens[prewarmingTokenId] = nil + } + result(true) + } else { + result(false) + } + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func open(id: String, url: String, settings: [String: Any?], menuItemList: [[String: Any]], result: @escaping FlutterResult) { + let absoluteUrl = URL(string: url)!.absoluteURL + + if #available(iOS 9.0, *), let plugin = plugin { + + if let flutterViewController = UIApplication.shared.visibleViewController { + // flutterViewController could be casted to FlutterViewController if needed + + let safariSettings = SafariBrowserSettings() + let _ = safariSettings.parse(settings: settings) + + let safari: SafariViewController + + if #available(iOS 11.0, *) { + let config = SFSafariViewController.Configuration() + safari = SafariViewController(plugin: plugin, id: id, url: absoluteUrl, configuration: config, + menuItemList: menuItemList, safariSettings: safariSettings) + } else { + // Fallback on earlier versions + safari = SafariViewController(plugin: plugin, id: id, url: absoluteUrl, entersReaderIfAvailable: safariSettings.entersReaderIfAvailable, + menuItemList: menuItemList, safariSettings: safariSettings) + } + + safari.prepareSafariBrowser() + + flutterViewController.present(safari, animated: true) { + result(true) + } + + browsers[id] = safari + } + return + } + + result(FlutterError.init(code: "ChromeSafariBrowserManager", message: "SafariViewController is not available!", details: nil)) + } + + public override func dispose() { + super.dispose() + let browserValues = browsers.values + browserValues.forEach { (browser: SafariViewController?) in + browser?.close(result: nil) + browser?.dispose() + } + browsers.removeAll() + if #available(iOS 15.0, *) { + let prewarmingTokensValues = prewarmingTokens.values + prewarmingTokensValues.forEach { (prewarmingToken: Any?) in + if let prewarmingToken = prewarmingToken as? SFSafariViewController.PrewarmingToken? { + prewarmingToken?.invalidate() + } + } + prewarmingTokens.removeAll() + } + plugin = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/SafariViewController/CustomUIActivity.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/SafariViewController/CustomUIActivity.swift new file mode 100644 index 00000000..540a1265 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/SafariViewController/CustomUIActivity.swift @@ -0,0 +1,55 @@ +// +// CustomUIActivity.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/05/22. +// + +import Foundation + +class CustomUIActivity: UIActivity { + var plugin: SwiftFlutterPlugin + var viewId: String + var id: Int64 + var url: URL + var title: String? + var type: UIActivity.ActivityType? + var label: String? + var image: UIImage? + + init(plugin: SwiftFlutterPlugin, viewId: String, id: Int64, url: URL, title: String?, label: String?, type: UIActivity.ActivityType?, image: UIImage?) { + self.plugin = plugin + self.viewId = viewId + self.id = id + self.url = url + self.title = title + self.label = label + self.type = type + self.image = image + } + + override class var activityCategory: UIActivity.Category { + return .action + } + + override var activityType: UIActivity.ActivityType? { + return type + } + + override var activityTitle: String? { + return label + } + + override var activityImage: UIImage? { + return image + } + + override func canPerform(withActivityItems activityItems: [Any]) -> Bool { + return true + } + + override func perform() { + let browser = plugin.chromeSafariBrowserManager?.browsers[viewId] + browser??.channelDelegate?.onItemActionPerform(id: id, url: url, title: title) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/SafariViewController/SafariBrowserSettings.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/SafariViewController/SafariBrowserSettings.swift new file mode 100755 index 00000000..d7ec25eb --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/SafariViewController/SafariBrowserSettings.swift @@ -0,0 +1,52 @@ +// +// SafariBrowserOptions.swift +// flutter_inappwebview +// +// Created by Lorenzo on 26/09/18. +// + +import Foundation + +@available(iOS 9.0, *) +@objcMembers +public class SafariBrowserSettings: ISettings { + + var entersReaderIfAvailable = false + var barCollapsingEnabled = false + var dismissButtonStyle = 0 //done + var preferredBarTintColor: String? + var preferredControlTintColor: String? + var presentationStyle = 0 //fullscreen + var transitionStyle = 0 //coverVertical + var activityButton: [String:Any?]? + var eventAttribution: [String:Any?]? + + override init(){ + super.init() + } + + override func parse(settings: [String: Any?]) -> SafariBrowserSettings { + let _ = super.parse(settings: settings) + activityButton = settings["activityButton"] as? [String : Any?] + eventAttribution = settings["eventAttribution"] as? [String : Any?] + return self + } + + override func getRealSettings(obj: SafariViewController?) -> [String: Any?] { + var realOptions: [String: Any?] = toMap() + if let safariViewController = obj { + if #available(iOS 11.0, *) { + realOptions["entersReaderIfAvailable"] = safariViewController.configuration.entersReaderIfAvailable + realOptions["barCollapsingEnabled"] = safariViewController.configuration.barCollapsingEnabled + realOptions["dismissButtonStyle"] = safariViewController.dismissButtonStyle.rawValue + } + if #available(iOS 10.0, *) { + realOptions["preferredBarTintColor"] = safariViewController.preferredBarTintColor?.hexString + realOptions["preferredControlTintColor"] = safariViewController.preferredControlTintColor?.hexString + } + realOptions["presentationStyle"] = safariViewController.modalPresentationStyle.rawValue + realOptions["transitionStyle"] = safariViewController.modalTransitionStyle.rawValue + } + return realOptions + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/SafariViewController/SafariViewController.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/SafariViewController/SafariViewController.swift new file mode 100755 index 00000000..980e983e --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/SafariViewController/SafariViewController.swift @@ -0,0 +1,141 @@ +// +// SafariViewController.swift +// flutter_inappwebview +// +// Created by Lorenzo on 25/09/18. +// + +import Foundation +import SafariServices + +@available(iOS 9.0, *) +public class SafariViewController: SFSafariViewController, SFSafariViewControllerDelegate, Disposable { + static let METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_chromesafaribrowser_" + var channelDelegate: SafariViewControllerChannelDelegate? + var safariSettings: SafariBrowserSettings + var id: String + var plugin: SwiftFlutterPlugin? + var menuItemList: [[String: Any]] = [] + + @available(iOS 11.0, *) + public init(plugin: SwiftFlutterPlugin, id: String, url: URL, configuration: SFSafariViewController.Configuration, menuItemList: [[String: Any]] = [], safariSettings: SafariBrowserSettings) { + self.id = id + self.plugin = plugin + self.menuItemList = menuItemList + self.safariSettings = safariSettings + SafariViewController.prepareConfig(configuration: configuration, safariSettings: safariSettings) + super.init(url: url, configuration: configuration) + let channel = FlutterMethodChannel(name: SafariViewController.METHOD_CHANNEL_NAME_PREFIX + id, + binaryMessenger: plugin.registrar!.messenger()) + self.channelDelegate = SafariViewControllerChannelDelegate(safariViewController: self, channel: channel) + self.delegate = self + } + + public init(plugin: SwiftFlutterPlugin, id: String, url: URL, entersReaderIfAvailable: Bool, menuItemList: [[String: Any]] = [], safariSettings: SafariBrowserSettings) { + self.id = id + self.plugin = plugin + self.menuItemList = menuItemList + self.safariSettings = safariSettings + super.init(url: url, entersReaderIfAvailable: entersReaderIfAvailable) + let channel = FlutterMethodChannel(name: SafariViewController.METHOD_CHANNEL_NAME_PREFIX + id, + binaryMessenger: plugin.registrar!.messenger()) + self.channelDelegate = SafariViewControllerChannelDelegate(safariViewController: self, channel: channel) + self.delegate = self + } + + public static func prepareConfig(configuration: SFSafariViewController.Configuration, safariSettings: SafariBrowserSettings) { + configuration.entersReaderIfAvailable = safariSettings.entersReaderIfAvailable + configuration.barCollapsingEnabled = safariSettings.barCollapsingEnabled + if #available(iOS 15.0, *), let activityButtonMap = safariSettings.activityButton { + configuration.activityButton = .fromMap(map: activityButtonMap) + } + if #available(iOS 15.2, *), let eventAttributionMap = safariSettings.eventAttribution { + configuration.eventAttribution = .fromMap(map: eventAttributionMap) + } + } + + func prepareSafariBrowser() { + if #available(iOS 11.0, *) { + self.dismissButtonStyle = SFSafariViewController.DismissButtonStyle(rawValue: safariSettings.dismissButtonStyle)! + } + + if #available(iOS 10.0, *) { + if let preferredBarTintColor = safariSettings.preferredBarTintColor, !preferredBarTintColor.isEmpty { + self.preferredBarTintColor = UIColor(hexString: preferredBarTintColor) + } + if let preferredControlTintColor = safariSettings.preferredControlTintColor, !preferredControlTintColor.isEmpty { + self.preferredControlTintColor = UIColor(hexString: preferredControlTintColor) + } + } + + self.modalPresentationStyle = UIModalPresentationStyle(rawValue: safariSettings.presentationStyle)! + self.modalTransitionStyle = UIModalTransitionStyle(rawValue: safariSettings.transitionStyle)! + } + + public override func viewWillAppear(_ animated: Bool) { + // prepareSafariBrowser() + super.viewWillAppear(animated) + channelDelegate?.onOpened() + } + + public override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + channelDelegate?.onClosed() + self.dispose() + } + + func close(result: FlutterResult?) { + dismiss(animated: true) + + // wait for the animation + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(400), execute: {() -> Void in + if let result = result { + result(true) + } + }) + } + + public func safariViewControllerDidFinish(_ controller: SFSafariViewController) { + close(result: nil) + } + + public func safariViewController(_ controller: SFSafariViewController, + didCompleteInitialLoad didLoadSuccessfully: Bool) { + channelDelegate?.onCompletedInitialLoad(didLoadSuccessfully: didLoadSuccessfully) + } + + public func safariViewController(_ controller: SFSafariViewController, activityItemsFor URL: URL, title: String?) -> [UIActivity] { + guard let plugin = plugin else { + return [] + } + var uiActivities: [UIActivity] = [] + menuItemList.forEach { (menuItem) in + let activity = CustomUIActivity(plugin: plugin, viewId: id, id: menuItem["id"] as! Int64, url: URL, + title: title, label: menuItem["label"] as? String, type: nil, + image: .fromMap(map: menuItem["image"] as? [String:Any?])) + uiActivities.append(activity) + } + return uiActivities + } + + public func safariViewController(_ controller: SFSafariViewController, initialLoadDidRedirectTo url: URL) { + channelDelegate?.onInitialLoadDidRedirect(url: url) + } + + public func safariViewControllerWillOpenInBrowser(_ controller: SFSafariViewController) { + channelDelegate?.onWillOpenInBrowser() + } + + public func dispose() { + channelDelegate?.dispose() + channelDelegate = nil + delegate = nil + plugin?.chromeSafariBrowserManager?.browsers[id] = nil + plugin = nil + } + + deinit { + debugPrint("SafariViewController - dealloc") + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/SafariViewController/SafariViewControllerChannelDelegate.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/SafariViewController/SafariViewControllerChannelDelegate.swift new file mode 100644 index 00000000..ba5d99b5 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/SafariViewController/SafariViewControllerChannelDelegate.swift @@ -0,0 +1,80 @@ +// +// SafariViewControllerChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 05/05/22. +// + +import Foundation + +public class SafariViewControllerChannelDelegate: ChannelDelegate { + private weak var safariViewController: SafariViewController? + + public init(safariViewController: SafariViewController, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.safariViewController = safariViewController + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + // let arguments = call.arguments as? NSDictionary + switch call.method { + case "close": + if let safariViewController = safariViewController { + safariViewController.close(result: result) + } else { + result(false) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onOpened() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onOpened", arguments: arguments) + } + + public func onCompletedInitialLoad(didLoadSuccessfully: Bool) { + let arguments: [String: Any?] = [ + "didLoadSuccessfully": didLoadSuccessfully + ] + channel?.invokeMethod("onCompletedInitialLoad", arguments: arguments) + } + + public func onInitialLoadDidRedirect(url: URL) { + let arguments: [String: Any?] = [ + "url": url.absoluteString + ] + channel?.invokeMethod("onInitialLoadDidRedirect", arguments: arguments) + } + + public func onWillOpenInBrowser() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onWillOpenInBrowser", arguments: arguments) + } + + public func onClosed() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onClosed", arguments: arguments) + } + + public func onItemActionPerform(id: Int64, url: URL, title: String?) { + let arguments: [String: Any?] = [ + "id": id, + "url": url.absoluteString, + "title": title, + ] + channel?.invokeMethod("onItemActionPerform", arguments: arguments) + } + + public override func dispose() { + super.dispose() + safariViewController = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/SwiftFlutterPlugin.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/SwiftFlutterPlugin.swift new file mode 100755 index 00000000..22eaecdc --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/SwiftFlutterPlugin.swift @@ -0,0 +1,94 @@ +/* + 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. + */ + +import Flutter +import UIKit +import WebKit +import Foundation +import AVFoundation +import SafariServices + +public class SwiftFlutterPlugin: NSObject, FlutterPlugin { + + var registrar: FlutterPluginRegistrar? + var platformUtil: PlatformUtil? + var inAppWebViewManager: InAppWebViewManager? + var myCookieManager: Any? + var myWebStorageManager: Any? + var credentialDatabase: CredentialDatabase? + var inAppBrowserManager: InAppBrowserManager? + var headlessInAppWebViewManager: HeadlessInAppWebViewManager? + var chromeSafariBrowserManager: ChromeSafariBrowserManager? + var webAuthenticationSessionManager: WebAuthenticationSessionManager? + var printJobManager: PrintJobManager? + + var webViewControllers: [String: InAppBrowserWebViewController?] = [:] + var safariViewControllers: [String: Any?] = [:] + + public init(with registrar: FlutterPluginRegistrar) { + super.init() + + self.registrar = registrar + registrar.register(FlutterWebViewFactory(plugin: self) as FlutterPlatformViewFactory, withId: FlutterWebViewFactory.VIEW_TYPE_ID) + + platformUtil = PlatformUtil(plugin: self) + inAppBrowserManager = InAppBrowserManager(plugin: self) + headlessInAppWebViewManager = HeadlessInAppWebViewManager(plugin: self) + chromeSafariBrowserManager = ChromeSafariBrowserManager(plugin: self) + inAppWebViewManager = InAppWebViewManager(plugin: self) + credentialDatabase = CredentialDatabase(plugin: self) + if #available(iOS 11.0, *) { + myCookieManager = MyCookieManager(plugin: self) + } + if #available(iOS 9.0, *) { + myWebStorageManager = MyWebStorageManager(plugin: self) + } + webAuthenticationSessionManager = WebAuthenticationSessionManager(plugin: self) + printJobManager = PrintJobManager(plugin: self) + } + + public static func register(with registrar: FlutterPluginRegistrar) { + let _ = SwiftFlutterPlugin(with: registrar) + } + + public func detachFromEngine(for registrar: FlutterPluginRegistrar) { + platformUtil?.dispose() + platformUtil = nil + inAppBrowserManager?.dispose() + inAppBrowserManager = nil + headlessInAppWebViewManager?.dispose() + headlessInAppWebViewManager = nil + chromeSafariBrowserManager?.dispose() + chromeSafariBrowserManager = nil + inAppWebViewManager?.dispose() + inAppWebViewManager = nil + credentialDatabase?.dispose() + credentialDatabase = nil + if #available(iOS 11.0, *) { + (myCookieManager as! MyCookieManager?)?.dispose() + myCookieManager = nil + } + if #available(iOS 9.0, *) { + (myWebStorageManager as! MyWebStorageManager?)?.dispose() + myWebStorageManager = nil + } + webAuthenticationSessionManager?.dispose() + webAuthenticationSessionManager = nil + printJobManager?.dispose() + printJobManager = nil + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/ActivityButton.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/ActivityButton.swift new file mode 100644 index 00000000..85612318 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/ActivityButton.swift @@ -0,0 +1,24 @@ +// +// ActivityButton.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 26/10/22. +// + +import Foundation +import SafariServices + +@available(iOS 15.0, *) +extension SFSafariViewController.ActivityButton { + public static func fromMap(map: [String:Any?]?) -> SFSafariViewController.ActivityButton? { + guard let map = map else { + return nil + } + if let templateImageMap = map["templateImage"] as? [String:Any?], + let templateImage = UIImage.fromMap(map: templateImageMap), + let extensionIdentifier = map["extensionIdentifier"] as? String { + return .init(templateImage: templateImage, extensionIdentifier: extensionIdentifier) + } + return nil + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/BaseCallbackResult.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/BaseCallbackResult.swift new file mode 100644 index 00000000..aa79e499 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/BaseCallbackResult.swift @@ -0,0 +1,32 @@ +// +// BaseCallbackResult.swift +// shared-apple +// +// Created by Lorenzo Pichilli on 17/10/22. +// + +import Foundation + +public class BaseCallbackResult: CallbackResult { + + override init() { + super.init() + + self.success = { [weak self] (obj: Any?) in + let result: T? = self?.decodeResult(obj) + var shouldRunDefaultBehaviour = false + if let result = result { + shouldRunDefaultBehaviour = self?.nonNullSuccess(result) ?? shouldRunDefaultBehaviour + } else { + shouldRunDefaultBehaviour = self?.nullSuccess() ?? shouldRunDefaultBehaviour + } + if shouldRunDefaultBehaviour { + self?.defaultBehaviour(result) + } + } + + self.notImplemented = { [weak self] in + self?.defaultBehaviour(nil) + } + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/CGRect.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/CGRect.swift new file mode 100644 index 00000000..61cb7627 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/CGRect.swift @@ -0,0 +1,23 @@ +// +// CGRect.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/05/22. +// + +import Foundation + +extension CGRect { + public static func fromMap(map: [String: Double]) -> CGRect { + return CGRect(x: map["x"]!, y: map["y"]!, width: map["width"]!, height: map["height"]!) + } + + public func toMap () -> [String:Any?] { + return [ + "x": minX, + "y": minY, + "width": width, + "height": height + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/CGSize.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/CGSize.swift new file mode 100644 index 00000000..730861e7 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/CGSize.swift @@ -0,0 +1,21 @@ +// +// CGSize.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 28/10/22. +// + +import Foundation + +extension CGSize { + public static func fromMap(map: [String: Double]) -> CGSize { + return CGSize(width: map["width"]!, height: map["height"]!) + } + + public func toMap () -> [String:Any?] { + return [ + "width": width, + "height": height + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/CallbackResult.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/CallbackResult.swift new file mode 100644 index 00000000..ea677c6b --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/CallbackResult.swift @@ -0,0 +1,18 @@ +// +// CallbackResult.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 06/05/22. +// + +import Foundation + +public class CallbackResult: MethodChannelResult { + public var notImplemented: () -> Void = {} + public var success: (Any?) -> Void = {_ in } + public var error: (String, String?, Any?) -> Void = {_,_,_ in } + public var nonNullSuccess: (T) -> Bool = {_ in true} + public var nullSuccess: () -> Bool = {true} + public var defaultBehaviour: (T?) -> Void = {_ in } + public var decodeResult: (Any?) -> T? = {_ in nil} +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/ChannelDelegate.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/ChannelDelegate.swift new file mode 100644 index 00000000..40511fb3 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/ChannelDelegate.swift @@ -0,0 +1,27 @@ +// +// ChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 04/05/22. +// + +import Foundation + +public class ChannelDelegate: FlutterMethodCallDelegate, Disposable { + var channel: FlutterMethodChannel? + + public init(channel: FlutterMethodChannel) { + super.init() + self.channel = channel + self.channel?.setMethodCallHandler(handle) + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + + } + + public func dispose() { + channel?.setMethodCallHandler(nil) + channel = nil + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/ClientCertChallenge.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/ClientCertChallenge.swift new file mode 100644 index 00000000..f2381ee4 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/ClientCertChallenge.swift @@ -0,0 +1,22 @@ +// +// ClientCertChallenge.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/02/21. +// + +import Foundation + +public class ClientCertChallenge: NSObject { + var protectionSpace: URLProtectionSpace! + + public init(fromChallenge: URLAuthenticationChallenge) { + protectionSpace = fromChallenge.protectionSpace + } + + public func toMap () -> [String:Any?] { + return [ + "protectionSpace": protectionSpace.toMap(), + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/ClientCertResponse.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/ClientCertResponse.swift new file mode 100644 index 00000000..6273db74 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/ClientCertResponse.swift @@ -0,0 +1,33 @@ +// +// ClientCertResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class ClientCertResponse: NSObject { + var certificatePath: String + var certificatePassword: String? + var keyStoreType: String? + var action: Int? + + public init(certificatePath: String, certificatePassword: String? = nil, keyStoreType: String? = nil, action: Int? = nil) { + self.certificatePath = certificatePath + self.certificatePassword = certificatePassword + self.keyStoreType = keyStoreType + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> ClientCertResponse? { + guard let map = map else { + return nil + } + let certificatePath = map["certificatePath"] as! String + let certificatePassword = map["certificatePassword"] as? String + let keyStoreType = map["keyStoreType"] as? String + let action = map["action"] as? Int + return ClientCertResponse(certificatePath: certificatePath, certificatePassword: certificatePassword, keyStoreType: keyStoreType, action: action) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/CreateWindowAction.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/CreateWindowAction.swift new file mode 100644 index 00000000..f3b4681f --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/CreateWindowAction.swift @@ -0,0 +1,31 @@ +// +// CreateWindowAction.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation +import WebKit + +public class CreateWindowAction: NSObject { + var navigationAction: WKNavigationAction + var windowId: Int64 + var windowFeatures: WKWindowFeatures + var isDialog: Bool? + + public init(navigationAction: WKNavigationAction, windowId: Int64, windowFeatures: WKWindowFeatures, isDialog: Bool? = nil) { + self.navigationAction = navigationAction + self.windowId = windowId + self.windowFeatures = windowFeatures + self.isDialog = isDialog + } + + public func toMap () -> [String:Any?] { + var map = navigationAction.toMap() + map["windowId"] = windowId + map["windowFeatures"] = windowFeatures.toMap() + map["isDialog"] = isDialog + return map + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/CustomSchemeResponse.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/CustomSchemeResponse.swift new file mode 100644 index 00000000..c7ad4a75 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/CustomSchemeResponse.swift @@ -0,0 +1,30 @@ +// +// CustomSchemeResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class CustomSchemeResponse: NSObject { + var data: Data + var contentType: String + var contentEncoding: String + + public init(data: Data, contentType: String, contentEncoding: String) { + self.data = data + self.contentType = contentType + self.contentEncoding = contentEncoding + } + + public static func fromMap(map: [String:Any?]?) -> CustomSchemeResponse? { + guard let map = map else { + return nil + } + let data = map["data"] as! FlutterStandardTypedData + let contentType = map["contentType"] as! String + let contentEncoding = map["contentEncoding"] as! String + return CustomSchemeResponse(data: data.data, contentType: contentType, contentEncoding: contentEncoding) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/Disposable.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/Disposable.swift new file mode 100644 index 00000000..0a34a942 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/Disposable.swift @@ -0,0 +1,12 @@ +// +// Disposable.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 04/05/22. +// + +import Foundation + +public protocol Disposable { + func dispose() -> Void +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/DownloadStartRequest.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/DownloadStartRequest.swift new file mode 100644 index 00000000..808f9453 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/DownloadStartRequest.swift @@ -0,0 +1,42 @@ +// +// DownloadStartRequest.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 17/04/22. +// + +import Foundation + +public class DownloadStartRequest: NSObject { + var url: String + var userAgent: String? + var contentDisposition: String? + var mimeType: String? + var contentLength: Int64 + var suggestedFilename: String? + var textEncodingName: String? + + public init(url: String, userAgent: String?, contentDisposition: String?, + mimeType: String?, contentLength: Int64, + suggestedFilename: String?, textEncodingName: String?) { + self.url = url + self.userAgent = userAgent + self.contentDisposition = contentDisposition + self.mimeType = mimeType + self.contentLength = contentLength + self.suggestedFilename = suggestedFilename + self.textEncodingName = textEncodingName + } + + public func toMap () -> [String:Any?] { + return [ + "url": url, + "userAgent": userAgent, + "contentDisposition": contentDisposition, + "mimeType": mimeType, + "contentLength": contentLength, + "suggestedFilename": suggestedFilename, + "textEncodingName": textEncodingName + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/FlutterMethodCallDelegate.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/FlutterMethodCallDelegate.swift new file mode 100755 index 00000000..d4f15591 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/FlutterMethodCallDelegate.swift @@ -0,0 +1,18 @@ +// +// FlutterMethodCallDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/12/2019. +// + +import Foundation + +public class FlutterMethodCallDelegate: NSObject { + public override init() { + super.init() + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/FlutterMethodChannel.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/FlutterMethodChannel.swift new file mode 100644 index 00000000..a7409d58 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/FlutterMethodChannel.swift @@ -0,0 +1,26 @@ +// +// FlutterMethodChannel.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 06/05/22. +// + +import Foundation +import Flutter + +extension FlutterMethodChannel { + public func invokeMethod(_ method: String, arguments: Any, callback: MethodChannelResult) { + invokeMethod(method, arguments: arguments) {(result) -> Void in + if result is FlutterError { + let error = result as! FlutterError + callback.error(error.code, error.message, error.details) + } + else if (result as? NSObject) == FlutterMethodNotImplemented { + callback.notImplemented() + } + else { + callback.success(result) + } + } + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/HitTestResult.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/HitTestResult.swift new file mode 100644 index 00000000..53307789 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/HitTestResult.swift @@ -0,0 +1,44 @@ +// +// HitTestResult.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +public enum HitTestResultType: Int { + case unknownType = 0 + case phoneType = 2 + case geoType = 3 + case emailType = 4 + case imageType = 5 + case srcAnchorType = 7 + case srcImageAnchorType = 8 + case editTextType = 9 +} + +public class HitTestResult: NSObject { + var type: HitTestResultType + var extra: String? + + public init(type: HitTestResultType, extra: String?) { + self.type = type + self.extra = extra + } + + public static func fromMap(map: [String:Any?]?) -> HitTestResult? { + guard let map = map else { + return nil + } + let type = HitTestResultType.init(rawValue: map["type"] as? Int ?? HitTestResultType.unknownType.rawValue) ?? HitTestResultType.unknownType + return HitTestResult(type: type, extra: map["extra"] as? String) + } + + public func toMap () -> [String:Any?] { + return [ + "type": type.rawValue, + "extra": extra, + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/HttpAuthResponse.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/HttpAuthResponse.swift new file mode 100644 index 00000000..f77d3ab1 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/HttpAuthResponse.swift @@ -0,0 +1,33 @@ +// +// HttpAuthResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class HttpAuthResponse: NSObject { + var username: String + var password: String + var permanentPersistence: Bool + var action: Int? + + public init(username: String, password: String, permanentPersistence: Bool, action: Int? = nil) { + self.username = username + self.password = password + self.permanentPersistence = permanentPersistence + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> HttpAuthResponse? { + guard let map = map else { + return nil + } + let username = map["username"] as! String + let password = map["password"] as! String + let permanentPersistence = map["permanentPersistence"] as! Bool + let action = map["action"] as? Int + return HttpAuthResponse(username: username, password: password, permanentPersistence: permanentPersistence, action: action) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/HttpAuthenticationChallenge.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/HttpAuthenticationChallenge.swift new file mode 100644 index 00000000..b22a0092 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/HttpAuthenticationChallenge.swift @@ -0,0 +1,34 @@ +// +// HttpAuthenticationChallenge.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/02/21. +// + +import Foundation + +public class HttpAuthenticationChallenge: NSObject { + var protectionSpace: URLProtectionSpace! + var previousFailureCount: Int = 0 + var failureResponse: URLResponse? + var error: Error? + var proposedCredential: URLCredential? + + public init(fromChallenge: URLAuthenticationChallenge) { + protectionSpace = fromChallenge.protectionSpace + previousFailureCount = fromChallenge.previousFailureCount + failureResponse = fromChallenge.failureResponse + error = fromChallenge.error + proposedCredential = fromChallenge.proposedCredential + } + + public func toMap () -> [String:Any?] { + return [ + "protectionSpace": protectionSpace.toMap(), + "previousFailureCount": previousFailureCount, + "failureResponse": failureResponse?.toMap(), + "error": error?.localizedDescription, + "proposedCredential": proposedCredential?.toMap() + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/InAppBrowserMenuItem.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/InAppBrowserMenuItem.swift new file mode 100644 index 00000000..ef2af3c5 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/InAppBrowserMenuItem.swift @@ -0,0 +1,48 @@ +// +// InAppBrowserMenuItem.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 21/05/23. +// + +import Foundation + +public class InAppBrowserMenuItem: NSObject { + var id: Int64 + var title: String + var order: Int64? + var icon: UIImage? + var iconColor: UIColor? + var showAsAction = false + + public init(id: Int64, title: String, order: Int64?, icon: UIImage?, iconColor: UIColor?, showAsAction: Bool) { + self.id = id + self.title = title + self.order = order + self.icon = icon + self.iconColor = iconColor + self.showAsAction = showAsAction + if #available(iOS 13.0, *), let icon = icon, let iconColor = iconColor { + icon.withTintColor(iconColor, renderingMode: .alwaysOriginal) + } + } + + public static func fromMap(map: [String:Any?]?) -> InAppBrowserMenuItem? { + guard let map = map else { + return nil + } + let id = map["id"] as! Int64 + let title = map["title"] as! String + let order = map["order"] as? Int64 + var icon = UIImage.fromMap(map: map["icon"] as? [String : Any?]) + if let data = map["icon"] as? FlutterStandardTypedData { + icon = UIImage(data: data.data) + } + var iconColor: UIColor? = nil + if let hexString = map["iconColor"] as? String { + iconColor = UIColor(hexString: hexString) + } + let showAsAction = map["showAsAction"] as! Bool + return InAppBrowserMenuItem(id: id, title: title, order: order, icon: icon, iconColor: iconColor, showAsAction: showAsAction) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/JsAlertResponse.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/JsAlertResponse.swift new file mode 100644 index 00000000..3e6b3e78 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/JsAlertResponse.swift @@ -0,0 +1,33 @@ +// +// JsAlertResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 06/05/22. +// + +import Foundation + +public class JsAlertResponse: NSObject { + var message: String + var confirmButtonTitle: String + var handledByClient: Bool + var action: Int? + + public init(message: String, confirmButtonTitle: String, handledByClient: Bool, action: Int? = nil) { + self.message = message + self.confirmButtonTitle = confirmButtonTitle + self.handledByClient = handledByClient + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> JsAlertResponse? { + guard let map = map else { + return nil + } + let message = map["message"] as! String + let confirmButtonTitle = map["confirmButtonTitle"] as! String + let handledByClient = map["handledByClient"] as! Bool + let action = map["action"] as? Int + return JsAlertResponse(message: message, confirmButtonTitle: confirmButtonTitle, handledByClient: handledByClient, action: action) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/JsConfirmResponse.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/JsConfirmResponse.swift new file mode 100644 index 00000000..76bfbe20 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/JsConfirmResponse.swift @@ -0,0 +1,36 @@ +// +// JsConfirmResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class JsConfirmResponse: NSObject { + var message: String + var confirmButtonTitle: String + var cancelButtonTitle: String + var handledByClient: Bool + var action: Int? + + public init(message: String, confirmButtonTitle: String, cancelButtonTitle: String, handledByClient: Bool, action: Int? = nil) { + self.message = message + self.confirmButtonTitle = confirmButtonTitle + self.cancelButtonTitle = cancelButtonTitle + self.handledByClient = handledByClient + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> JsConfirmResponse? { + guard let map = map else { + return nil + } + let message = map["message"] as! String + let confirmButtonTitle = map["confirmButtonTitle"] as! String + let cancelButtonTitle = map["cancelButtonTitle"] as! String + let handledByClient = map["handledByClient"] as! Bool + let action = map["action"] as? Int + return JsConfirmResponse(message: message, confirmButtonTitle: confirmButtonTitle, cancelButtonTitle: cancelButtonTitle, handledByClient: handledByClient, action: action) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/JsPromptResponse.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/JsPromptResponse.swift new file mode 100644 index 00000000..5c40b492 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/JsPromptResponse.swift @@ -0,0 +1,43 @@ +// +// JsPromptResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class JsPromptResponse: NSObject { + var message: String + var defaultValue: String + var confirmButtonTitle: String + var cancelButtonTitle: String + var handledByClient: Bool + var value: String? + var action: Int? + + public init(message: String, defaultValue: String, confirmButtonTitle: String, cancelButtonTitle: String, handledByClient: Bool, value: String? = nil, action: Int? = nil) { + self.message = message + self.defaultValue = defaultValue + self.confirmButtonTitle = confirmButtonTitle + self.cancelButtonTitle = cancelButtonTitle + self.handledByClient = handledByClient + self.value = value + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> JsPromptResponse? { + guard let map = map else { + return nil + } + let message = map["message"] as! String + let defaultValue = map["defaultValue"] as! String + let confirmButtonTitle = map["confirmButtonTitle"] as! String + let cancelButtonTitle = map["cancelButtonTitle"] as! String + let handledByClient = map["handledByClient"] as! Bool + let value = map["value"] as? String + let action = map["action"] as? Int + return JsPromptResponse(message: message, defaultValue: defaultValue, confirmButtonTitle: confirmButtonTitle, cancelButtonTitle: cancelButtonTitle, + handledByClient: handledByClient, value: value, action: action) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/MethodChannelResult.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/MethodChannelResult.swift new file mode 100644 index 00000000..d1555e39 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/MethodChannelResult.swift @@ -0,0 +1,14 @@ +// +// MethodChannelResult.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 06/05/22. +// + +import Foundation + +public protocol MethodChannelResult { + var success: (_ obj: Any?) -> Void { get set } + var error: (_ code: String, _ message: String?, _ details: Any?) -> Void { get set } + var notImplemented: () -> Void { get set } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/NSAttributedString.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/NSAttributedString.swift new file mode 100644 index 00000000..64169abf --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/NSAttributedString.swift @@ -0,0 +1,63 @@ +// +// NSAttributedString.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 05/03/21. +// + +import Foundation + +extension NSAttributedString { + public static func fromMap(map: [String:Any?]?) -> NSAttributedString? { + guard let map = map, let string = map["string"] as? String else { + return nil + } + + var attributes: [NSAttributedString.Key : Any] = [:] + + if let backgroundColor = map["backgroundColor"] as? String { + attributes[.backgroundColor] = UIColor(hexString: backgroundColor) + } + if let baselineOffset = map["baselineOffset"] as? Double { + attributes[.baselineOffset] = baselineOffset + } + if let expansion = map["expansion"] as? Double { + attributes[.expansion] = expansion + } + if let foregroundColor = map["foregroundColor"] as? String { + attributes[.foregroundColor] = UIColor(hexString: foregroundColor) + } + if let kern = map["kern"] as? Double { + attributes[.kern] = kern + } + if let ligature = map["ligature"] as? Int64 { + attributes[.ligature] = ligature + } + if let obliqueness = map["obliqueness"] as? Double { + attributes[.obliqueness] = obliqueness + } + if let strikethroughColor = map["strikethroughColor"] as? String { + attributes[.strikethroughColor] = UIColor(hexString: strikethroughColor) + } + if let strikethroughStyle = map["strikethroughStyle"] as? Int64 { + attributes[.strikethroughStyle] = strikethroughStyle + } + if let strokeColor = map["strokeColor"] as? String { + attributes[.strokeColor] = UIColor(hexString: strokeColor) + } + if let strokeWidth = map["strokeWidth"] as? Double { + attributes[.strokeWidth] = strokeWidth + } + if let textEffect = map["textEffect"] as? String { + attributes[.textEffect] = textEffect + } + if let underlineColor = map["underlineColor"] as? String { + attributes[.underlineColor] = UIColor(hexString: underlineColor) + } + if let underlineStyle = map["underlineStyle"] as? Int64 { + attributes[.underlineStyle] = underlineStyle + } + + return NSAttributedString(string: string, attributes: attributes) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/PermissionRequest.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/PermissionRequest.swift new file mode 100644 index 00000000..a286a3f3 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/PermissionRequest.swift @@ -0,0 +1,29 @@ +// +// PermissionRequest.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 21/04/22. +// + +import Foundation +import WebKit + +public class PermissionRequest: NSObject { + var origin: String + var resources: [StringOrInt] + var frame: WKFrameInfo + + public init(origin: String, resources: [StringOrInt], frame: WKFrameInfo) { + self.origin = origin + self.resources = resources + self.frame = frame + } + + public func toMap () -> [String:Any?] { + return [ + "origin": origin, + "resources": resources, + "frame": frame.toMap() + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/PermissionResponse.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/PermissionResponse.swift new file mode 100644 index 00000000..171817bb --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/PermissionResponse.swift @@ -0,0 +1,27 @@ +// +// PermissionResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class PermissionResponse: NSObject { + var resources: [Any] + var action: Int? + + public init(resources: [Any], action: Int? = nil) { + self.resources = resources + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> PermissionResponse? { + guard let map = map else { + return nil + } + let resources = map["resources"] as! [Any] + let action = map["action"] as? Int + return PermissionResponse(resources: resources, action: action) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/PluginScript.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/PluginScript.swift new file mode 100644 index 00000000..9b6499f2 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/PluginScript.swift @@ -0,0 +1,109 @@ +// +// PluginScript.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 17/02/21. +// + +import Foundation +import WebKit + +public class PluginScript: UserScript { + var requiredInAllContentWorlds = false + var messageHandlerNames: [String] = [] + + public override init(source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly) + } + + public init(groupName: String, source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, requiredInAllContentWorlds: Bool = false, messageHandlerNames: [String] = []) { + super.init(groupName: groupName, source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly) + self.requiredInAllContentWorlds = requiredInAllContentWorlds + self.messageHandlerNames = messageHandlerNames + } + + @available(iOS 14.0, *) + public override init(source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, in contentWorld: WKContentWorld) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly, in: contentWorld) + self.contentWorld = contentWorld + } + + @available(iOS 14.0, *) + public init(source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, in contentWorld: WKContentWorld, requiredInAllContentWorlds: Bool = false, messageHandlerNames: [String] = []) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly, in: contentWorld) + self.requiredInAllContentWorlds = requiredInAllContentWorlds + self.messageHandlerNames = messageHandlerNames + } + + @available(iOS 14.0, *) + public init(groupName: String, source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, in contentWorld: WKContentWorld, requiredInAllContentWorlds: Bool = false, messageHandlerNames: [String] = []) { + super.init(groupName: groupName, source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly, in: contentWorld) + self.requiredInAllContentWorlds = requiredInAllContentWorlds + self.messageHandlerNames = messageHandlerNames + } + + public func copyAndSet(groupName: String? = nil, + source: String? = nil, + injectionTime: WKUserScriptInjectionTime? = nil, + forMainFrameOnly: Bool? = nil, + requiredInAllContentWorlds: Bool? = nil, + messageHandlerNames: [String]? = nil) -> PluginScript { + if #available(iOS 14.0, *) { + return PluginScript( + groupName: groupName ?? self.groupName!, + source: source ?? self.source, + injectionTime: injectionTime ?? self.injectionTime, + forMainFrameOnly: forMainFrameOnly ?? self.isForMainFrameOnly, + in: self.contentWorld, + requiredInAllContentWorlds: requiredInAllContentWorlds ?? self.requiredInAllContentWorlds, + messageHandlerNames: messageHandlerNames ?? self.messageHandlerNames + ) + } + return PluginScript( + groupName: groupName ?? self.groupName!, + source: source ?? self.source, + injectionTime: injectionTime ?? self.injectionTime, + forMainFrameOnly: forMainFrameOnly ?? self.isForMainFrameOnly, + requiredInAllContentWorlds: requiredInAllContentWorlds ?? self.requiredInAllContentWorlds, + messageHandlerNames: messageHandlerNames ?? self.messageHandlerNames + ) + } + + @available(iOS 14.0, *) + public func copyAndSet(groupName: String? = nil, + source: String? = nil, + injectionTime: WKUserScriptInjectionTime? = nil, + forMainFrameOnly: Bool? = nil, + contentWorld: WKContentWorld? = nil, + requiredInAllContentWorlds: Bool? = nil, + messageHandlerNames: [String]? = nil) -> PluginScript { + return PluginScript( + groupName: groupName ?? self.groupName!, + source: source ?? self.source, + injectionTime: injectionTime ?? self.injectionTime, + forMainFrameOnly: forMainFrameOnly ?? self.isForMainFrameOnly, + in: contentWorld ?? self.contentWorld, + requiredInAllContentWorlds: requiredInAllContentWorlds ?? self.requiredInAllContentWorlds, + messageHandlerNames: messageHandlerNames ?? self.messageHandlerNames + ) + } + + static func == (lhs: PluginScript, rhs: PluginScript) -> Bool { + if #available(iOS 14.0, *) { + return lhs.groupName == rhs.groupName && + lhs.source == rhs.source && + lhs.injectionTime == rhs.injectionTime && + lhs.isForMainFrameOnly == rhs.isForMainFrameOnly && + lhs.contentWorld == rhs.contentWorld && + lhs.requiredInAllContentWorlds == rhs.requiredInAllContentWorlds && + lhs.messageHandlerNames == rhs.messageHandlerNames + } else { + return lhs.groupName == rhs.groupName && + lhs.source == rhs.source && + lhs.injectionTime == rhs.injectionTime && + lhs.isForMainFrameOnly == rhs.isForMainFrameOnly && + lhs.requiredInAllContentWorlds == rhs.requiredInAllContentWorlds && + lhs.messageHandlerNames == rhs.messageHandlerNames + } + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/SecCertificate.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/SecCertificate.swift new file mode 100644 index 00000000..e7292f22 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/SecCertificate.swift @@ -0,0 +1,15 @@ +// +// SecCertificate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension SecCertificate { + var data: Data { + let serverCertificateCFData = SecCertificateCopyData(self) + return serverCertificateCFData as Data + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/ServerTrustAuthResponse.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/ServerTrustAuthResponse.swift new file mode 100644 index 00000000..2a998057 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/ServerTrustAuthResponse.swift @@ -0,0 +1,24 @@ +// +// ServerTrustAuthResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class ServerTrustAuthResponse: NSObject { + var action: Int? + + public init(action: Int? = nil) { + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> ServerTrustAuthResponse? { + guard let map = map else { + return nil + } + let action = map["action"] as? Int + return ServerTrustAuthResponse(action: action) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/ServerTrustChallenge.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/ServerTrustChallenge.swift new file mode 100644 index 00000000..190c831b --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/ServerTrustChallenge.swift @@ -0,0 +1,22 @@ +// +// ServerTrustChallenge.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/02/21. +// + +import Foundation + +public class ServerTrustChallenge: NSObject { + var protectionSpace: URLProtectionSpace! + + public init(fromChallenge: URLAuthenticationChallenge) { + protectionSpace = fromChallenge.protectionSpace + } + + public func toMap () -> [String:Any?] { + return [ + "protectionSpace": protectionSpace.toMap() + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/Size2D.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/Size2D.swift new file mode 100644 index 00000000..de13ffc2 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/Size2D.swift @@ -0,0 +1,35 @@ +// +// Size.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 26/03/21. +// + +import Foundation + +public class Size2D: NSObject { + var width: Double + var height: Double + + public init(width: Double, height: Double) { + self.width = width + self.height = height + } + + public static func fromMap(map: [String:Any?]?) -> Size2D? { + guard let map = map else { + return nil + } + return Size2D( + width: map["width"] as? Double ?? -1.0, + height: map["height"] as? Double ?? -1.0 + ) + } + + public func toMap() -> [String:Any?] { + return [ + "width": width, + "height": height + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/SslCertificate.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/SslCertificate.swift new file mode 100644 index 00000000..6b9d9a17 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/SslCertificate.swift @@ -0,0 +1,30 @@ +// +// SslCertificate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/02/21. +// + +import Foundation + +public class SslCertificate: NSObject { + var x509Certificate: Data + var issuedBy: Any? + var issuedTo: Any? + var validNotAfterDate: Any? + var validNotBeforeDate: Any? + + public init(x509Certificate: Data) { + self.x509Certificate = x509Certificate + } + + public func toMap () -> [String:Any?] { + return [ + "x509Certificate": x509Certificate, + "issuedBy": issuedBy, + "issuedTo": issuedTo, + "validNotAfterDate": validNotAfterDate, + "validNotBeforeDate": validNotBeforeDate + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/SslError.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/SslError.swift new file mode 100644 index 00000000..58e3cf94 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/SslError.swift @@ -0,0 +1,50 @@ +// +// SslError.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/02/21. +// + +import Foundation + +public class SslError: NSObject { + var errorType: SecTrustResultType? + var message: String? + + public init(errorType: SecTrustResultType?) { + self.errorType = errorType + + var sslErrorMessage: String? = nil + switch errorType { + case .deny: + sslErrorMessage = "Indicates a user-configured deny; do not proceed." + break + case .fatalTrustFailure: + sslErrorMessage = "Indicates a trust failure which cannot be overridden by the user." + break + case .invalid: + sslErrorMessage = "Indicates an invalid setting or result." + break + case .otherError: + sslErrorMessage = "Indicates a failure other than that of trust evaluation." + break + case .recoverableTrustFailure: + sslErrorMessage = "Indicates a trust policy failure which can be overridden by the user." + break + case .unspecified: + sslErrorMessage = "Indicates the evaluation succeeded and the certificate is implicitly trusted, but user intent was not explicitly specified." + break + default: + sslErrorMessage = nil + } + + self.message = sslErrorMessage + } + + public func toMap () -> [String:Any?] { + return [ + "code": errorType?.rawValue, + "message": message + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/StringOrInt.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/StringOrInt.swift new file mode 100644 index 00000000..f7cd40c7 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/StringOrInt.swift @@ -0,0 +1,13 @@ +// +// StringOrInt.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 21/04/22. +// + +import Foundation + +public protocol StringOrInt { } + +extension Int: StringOrInt { } +extension String: StringOrInt { } diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/UIColor.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/UIColor.swift new file mode 100644 index 00000000..4216b7e2 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/UIColor.swift @@ -0,0 +1,59 @@ +// +// UIColor.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension UIColor { + convenience init(hexString: String) { + let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) + var int = UInt64() + Scanner(string: hex).scanHexInt64(&int) + let a, r, g, b: UInt64 + switch hex.count { + case 3: // RGB (12-bit) + (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) + case 6: // RGB (24-bit) + (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) + case 8: // ARGB (32-bit) + (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) + default: + (a, r, g, b) = (255, 0, 0, 0) + } + self.init(red: CGFloat(r) / 255, green: CGFloat(g) / 255, blue: CGFloat(b) / 255, alpha: CGFloat(a) / 255) + } + + var hexString: String? { + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + var alpha: CGFloat = 0 + + let multiplier = CGFloat(255.999999) + + guard self.getRed(&red, green: &green, blue: &blue, alpha: &alpha) else { + return nil + } + + if alpha == 1.0 { + return String( + format: "#%02lX%02lX%02lX", + Int(red * multiplier), + Int(green * multiplier), + Int(blue * multiplier) + ) + } + else { + return String( + format: "#%02lX%02lX%02lX%02lX", + Int(red * multiplier), + Int(green * multiplier), + Int(blue * multiplier), + Int(alpha * multiplier) + ) + } + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/UIEdgeInsets.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/UIEdgeInsets.swift new file mode 100644 index 00000000..5bf3130a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/UIEdgeInsets.swift @@ -0,0 +1,23 @@ +// +// UIEdgeInsets.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 11/05/22. +// + +import Foundation + +extension UIEdgeInsets { + public static func fromMap(map: [String: Double]) -> UIEdgeInsets { + return UIEdgeInsets.init(top: map["top"]!, left: map["left"]!, bottom: map["bottom"]!, right: map["right"]!) + } + + public func toMap () -> [String:Any?] { + return [ + "top": top, + "right": self.right, + "bottom": bottom, + "left": self.left + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/UIEventAttribution.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/UIEventAttribution.swift new file mode 100644 index 00000000..71c87e2b --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/UIEventAttribution.swift @@ -0,0 +1,28 @@ +// +// UIEventAttribution.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 26/10/22. +// + +import Foundation + +@available(iOS 14.5, *) +extension UIEventAttribution { + public static func fromMap(map: [String:Any?]?) -> UIEventAttribution? { + guard let map = map else { + return nil + } + if let sourceIdentifier = map["sourceIdentifier"] as? UInt8, + let destinationURLString = map["destinationURL"] as? String, + let destinationURL = URL(string: destinationURLString), + let sourceDescription = map["sourceDescription"] as? String, + let purchaser = map["purchaser"] as? String { + return UIEventAttribution(sourceIdentifier: sourceIdentifier, + destinationURL: destinationURL, + sourceDescription: sourceDescription, + purchaser: purchaser) + } + return nil + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/UIFindSession.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/UIFindSession.swift new file mode 100644 index 00000000..be1e84d1 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/UIFindSession.swift @@ -0,0 +1,46 @@ +// +// UIFindSession.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/10/22. +// + +import Foundation + +public class FindSession: NSObject { + var resultCount: Int + var highlightedResultIndex: Int + var searchResultDisplayStyle: Int + + public init(resultCount: Int, highlightedResultIndex: Int, searchResultDisplayStyle: Int) { + self.resultCount = resultCount + self.highlightedResultIndex = highlightedResultIndex + self.searchResultDisplayStyle = searchResultDisplayStyle + } + + @available(iOS 16.0, *) + public static func fromUIFindSession(uiFindSession: UIFindSession) -> FindSession { + return FindSession(resultCount: uiFindSession.resultCount, + highlightedResultIndex: uiFindSession.highlightedResultIndex, + searchResultDisplayStyle: uiFindSession.searchResultDisplayStyle.rawValue) + } + + public func toMap () -> [String:Any?] { + return [ + "resultCount": resultCount, + "highlightedResultIndex": highlightedResultIndex, + "searchResultDisplayStyle": searchResultDisplayStyle + ] + } +} + +@available(iOS 16.0, *) +extension UIFindSession { + public func toMap () -> [String:Any?] { + return [ + "resultCount": resultCount, + "highlightedResultIndex": highlightedResultIndex, + "searchResultDisplayStyle": searchResultDisplayStyle.rawValue + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/UIImage.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/UIImage.swift new file mode 100644 index 00000000..20405c95 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/UIImage.swift @@ -0,0 +1,27 @@ +// +// UIImage.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 26/10/22. +// + +import Foundation +import Flutter + +extension UIImage { + public static func fromMap(map: [String:Any?]?) -> UIImage? { + guard let map = map else { + return nil + } + if let name = map["name"] as? String { + return UIImage(named: name) + } + if #available(iOS 13.0, *), let systemName = map["systemName"] as? String { + return UIImage(systemName: systemName) + } + if let data = map["data"] as? FlutterStandardTypedData { + return UIImage(data: data.data) + } + return nil + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/URLAuthenticationChallenge.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/URLAuthenticationChallenge.swift new file mode 100644 index 00000000..39b21fce --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/URLAuthenticationChallenge.swift @@ -0,0 +1,20 @@ +// +// URLAuthenticationChallenge.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension URLAuthenticationChallenge { + public func toMap () -> [String:Any?] { + return [ + "protectionSpace": protectionSpace.toMap(), + "previousFailureCount": previousFailureCount, + "failureResponse": failureResponse?.toMap(), + "error": error?.localizedDescription, + "proposedCredential": proposedCredential?.toMap(), + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/URLCredential.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/URLCredential.swift new file mode 100644 index 00000000..469f1e15 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/URLCredential.swift @@ -0,0 +1,26 @@ +// +// URLCredential.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension URLCredential { + public func toMap () -> [String:Any?] { + var x509Certificates: [Data] = [] + // certificates could be nil!!! + if certificates != nil { + for certificate in certificates { + x509Certificates.append((certificate as! SecCertificate).data) + } + } + return [ + "password": password, + "username": user, + "certificates": x509Certificates, + "persistence": persistence.rawValue + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/URLProtectionSpace.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/URLProtectionSpace.swift new file mode 100644 index 00000000..3d6b9022 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/URLProtectionSpace.swift @@ -0,0 +1,63 @@ +// +// URLProtectionSpace.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension URLProtectionSpace { + + var x509Certificate: Data? { + guard let serverTrust = serverTrust else { + return nil + } + + var secResult = SecTrustResultType.invalid + let secTrustEvaluateStatus = SecTrustEvaluate(serverTrust, &secResult); + + if secTrustEvaluateStatus == errSecSuccess, let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) { + return serverCertificate.data + } + return nil + } + + var sslCertificate: SslCertificate? { + var sslCertificate: SslCertificate? = nil + if let x509Certificate = x509Certificate { + sslCertificate = SslCertificate(x509Certificate: x509Certificate) + } + return sslCertificate + } + + var sslError: SslError? { + guard let serverTrust = serverTrust else { + return nil + } + + var secResult = SecTrustResultType.invalid + SecTrustEvaluate(serverTrust, &secResult); + + guard let sslErrorType = secResult != SecTrustResultType.proceed ? secResult : nil else { + return nil + } + + return SslError(errorType: sslErrorType) + } + + public func toMap () -> [String:Any?] { + return [ + "host": host, + "protocol": self.protocol, + "realm": realm, + "port": port, + "sslCertificate": sslCertificate?.toMap(), + "sslError": sslError?.toMap(), + "authenticationMethod": authenticationMethod, + "distinguishedNames": distinguishedNames, + "receivesCredentialSecurely": receivesCredentialSecurely, + "proxyType": proxyType + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/URLRequest.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/URLRequest.swift new file mode 100644 index 00000000..b8e9430b --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/URLRequest.swift @@ -0,0 +1,98 @@ +// +// URLRequest.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension URLRequest { + public init(fromPluginMap: [String:Any?]) { + if let urlString = fromPluginMap["url"] as? String, let url = URL(string: urlString) { + self.init(url: url) + } else { + self.init(url: URL(string: "about:blank")!) + } + + if let method = fromPluginMap["method"] as? String { + httpMethod = method + } + if let body = fromPluginMap["body"] as? FlutterStandardTypedData { + httpBody = body.data + } + if let headers = fromPluginMap["headers"] as? [String:String] { + for (key, value) in headers { + setValue(value, forHTTPHeaderField: key) + } + } + if let _allowsCellularAccess = fromPluginMap["allowsCellularAccess"] as? Bool { + allowsCellularAccess = _allowsCellularAccess + } + if #available(iOS 13.0, *), let _allowsConstrainedNetworkAccess = fromPluginMap["allowsConstrainedNetworkAccess"] as? Bool { + allowsConstrainedNetworkAccess = _allowsConstrainedNetworkAccess + } + if #available(iOS 13.0, *), let _allowsExpensiveNetworkAccess = fromPluginMap["allowsExpensiveNetworkAccess"] as? Bool { + allowsExpensiveNetworkAccess = _allowsExpensiveNetworkAccess + } + if let _cachePolicy = fromPluginMap["cachePolicy"] as? Int { + cachePolicy = CachePolicy.init(rawValue: UInt(_cachePolicy)) ?? .useProtocolCachePolicy + } + if let _httpShouldHandleCookies = fromPluginMap["httpShouldHandleCookies"] as? Bool { + httpShouldHandleCookies = _httpShouldHandleCookies + } + if let _httpShouldUsePipelining = fromPluginMap["httpShouldUsePipelining"] as? Bool { + httpShouldUsePipelining = _httpShouldUsePipelining + } + if let _networkServiceType = fromPluginMap["networkServiceType"] as? Int { + networkServiceType = NetworkServiceType.init(rawValue: UInt(_networkServiceType)) ?? .default + } + if let _timeoutInterval = fromPluginMap["timeoutInterval"] as? Double { + timeoutInterval = _timeoutInterval + } + if let _mainDocumentURL = fromPluginMap["mainDocumentURL"] as? String { + mainDocumentURL = URL(string: _mainDocumentURL)! + } + if #available(iOS 14.5, *), let _assumesHTTP3Capable = fromPluginMap["assumesHTTP3Capable"] as? Bool { + assumesHTTP3Capable = _assumesHTTP3Capable + } + if #available(iOS 15.0, *), let attributionRawValue = fromPluginMap["attribution"] as? UInt, + let _attribution = URLRequest.Attribution(rawValue: attributionRawValue) { + attribution = _attribution + } + } + + public func toMap () -> [String:Any?] { + var _allowsConstrainedNetworkAccess: Bool? = nil + var _allowsExpensiveNetworkAccess: Bool? = nil + if #available(iOS 13.0, *) { + _allowsConstrainedNetworkAccess = allowsConstrainedNetworkAccess + _allowsExpensiveNetworkAccess = allowsExpensiveNetworkAccess + } + var _assumesHTTP3Capable: Bool? = nil + if #available(iOS 14.5, *) { + _assumesHTTP3Capable = assumesHTTP3Capable + } + var _attribution: UInt? = nil + if #available(iOS 15.0, *) { + _attribution = attribution.rawValue + } + return [ + "url": url?.absoluteString, + "method": httpMethod, + "headers": allHTTPHeaderFields, + "body": httpBody.map(FlutterStandardTypedData.init(bytes:)), + "allowsCellularAccess": allowsCellularAccess, + "allowsConstrainedNetworkAccess": _allowsConstrainedNetworkAccess, + "allowsExpensiveNetworkAccess": _allowsExpensiveNetworkAccess, + "cachePolicy": cachePolicy.rawValue, + "httpShouldHandleCookies": httpShouldHandleCookies, + "httpShouldUsePipelining": httpShouldUsePipelining, + "networkServiceType": networkServiceType.rawValue, + "timeoutInterval": timeoutInterval, + "mainDocumentURL": mainDocumentURL?.absoluteString, + "assumesHTTP3Capable": _assumesHTTP3Capable, + "attribution": _attribution + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/URLResponse.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/URLResponse.swift new file mode 100644 index 00000000..0a6a4835 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/URLResponse.swift @@ -0,0 +1,31 @@ +// +// URLResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension URLResponse { + public convenience init?(fromPluginMap: [String:Any?]) { + let url = URL(string: fromPluginMap["url"] as? String ?? "about:blank")! + let mimeType = fromPluginMap["mimeType"] as? String + let expectedContentLength = fromPluginMap["expectedContentLength"] as? Int64 ?? 0 + let textEncodingName = fromPluginMap["textEncodingName"] as? String + self.init(url: url, mimeType: mimeType, expectedContentLength: Int(expectedContentLength), textEncodingName: textEncodingName) + } + + public func toMap () -> [String:Any?] { + let httpResponse: HTTPURLResponse? = self as? HTTPURLResponse + return [ + "expectedContentLength": expectedContentLength, + "mimeType": mimeType, + "suggestedFilename": suggestedFilename, + "textEncodingName": textEncodingName, + "url": url?.absoluteString, + "headers": httpResponse?.allHeaderFields, + "statusCode": httpResponse?.statusCode + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/UserScript.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/UserScript.swift new file mode 100644 index 00000000..e18c588d --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/UserScript.swift @@ -0,0 +1,71 @@ +// +// InAppWebViewUserScript.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation +import WebKit + +public class UserScript: WKUserScript { + var groupName: String? + + private var contentWorldWrapper: Any? + @available(iOS 14.0, *) + var contentWorld: WKContentWorld { + get { + if let value = contentWorldWrapper as? WKContentWorld { + return value + } + return .page + } + set { contentWorldWrapper = newValue } + } + + public override init(source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly) + } + + public init(groupName: String?, source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly) + self.groupName = groupName + } + + @available(iOS 14.0, *) + public override init(source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, in contentWorld: WKContentWorld) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly, in: contentWorld) + self.contentWorld = contentWorld + } + + @available(iOS 14.0, *) + public init(groupName: String?, source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, in contentWorld: WKContentWorld) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly, in: contentWorld) + self.groupName = groupName + self.contentWorld = contentWorld + } + + public static func fromMap(map: [String:Any?]?, windowId: Int64?) -> UserScript? { + guard let map = map else { + return nil + } + + let contentWorldMap = map["contentWorld"] as? [String:Any?] + if #available(iOS 14.0, *), let contentWorldMap = contentWorldMap { + let contentWorld = WKContentWorld.fromMap(map: contentWorldMap, windowId: windowId)! + return UserScript( + groupName: map["groupName"] as? String, + source: map["source"] as! String, + injectionTime: WKUserScriptInjectionTime.init(rawValue: map["injectionTime"] as! Int) ?? .atDocumentStart, + forMainFrameOnly: map["forMainFrameOnly"] as! Bool, + in: contentWorld + ) + } + return UserScript( + groupName: map["groupName"] as? String, + source: map["source"] as! String, + injectionTime: WKUserScriptInjectionTime.init(rawValue: map["injectionTime"] as! Int) ?? .atDocumentStart, + forMainFrameOnly: map["forMainFrameOnly"] as! Bool + ) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WKContentWorld.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WKContentWorld.swift new file mode 100644 index 00000000..9ebade3d --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WKContentWorld.swift @@ -0,0 +1,41 @@ +// +// WKContentWorld.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +@available(iOS 14.0, *) +extension WKContentWorld { + // Workaround to create stored properties in an extension: + // https://valv0.medium.com/computed-properties-and-extensions-a-pure-swift-approach-64733768112c + + private static var _windowId = [String: Int64?]() + + var windowId: Int64? { + get { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + return WKContentWorld._windowId[tmpAddress] ?? nil + } + set(newValue) { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + WKContentWorld._windowId[tmpAddress] = newValue + } + } + + public static func fromMap(map: [String:Any?]?, windowId: Int64?) -> WKContentWorld? { + guard let map = map else { + return nil + } + var name = map["name"] as! String + name = windowId != nil && name != "page" ? + WKUserContentController.WINDOW_ID_PREFIX + String(windowId!) + "-" + name : + name + let contentWorld = Util.getContentWorld(name: name) + contentWorld.windowId = name != "page" ? windowId : nil + return contentWorld + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WKFrameInfo.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WKFrameInfo.swift new file mode 100644 index 00000000..042fe419 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WKFrameInfo.swift @@ -0,0 +1,26 @@ +// +// WKFrameInfo.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +extension WKFrameInfo { + + public func toMap () -> [String:Any?] { + var securityOrigin: [String:Any?]? = nil + if #available(iOS 9.0, *) { + securityOrigin = self.securityOrigin.toMap() + } + // fix: self.request throws EXC_BREAKPOINT when coming from WKNavigationAction.sourceFrame + let request: URLRequest? = self.value(forKey: "request") as? URLRequest + return [ + "isMainFrame": isMainFrame, + "request": request?.toMap(), + "securityOrigin": securityOrigin + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WKNavigationAction.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WKNavigationAction.swift new file mode 100644 index 00000000..1fdc5c17 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WKNavigationAction.swift @@ -0,0 +1,28 @@ +// +// WKNavigationAction.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +extension WKNavigationAction { + public func toMap () -> [String:Any?] { + var shouldPerformDownload: Bool? = nil + if #available(iOS 14.5, *) { + shouldPerformDownload = self.shouldPerformDownload + } + return [ + "request": request.toMap(), + "isForMainFrame": targetFrame?.isMainFrame ?? false, + "hasGesture": nil, + "isRedirect": nil, + "navigationType": navigationType.rawValue, + "sourceFrame": sourceFrame.toMap(), + "targetFrame": targetFrame?.toMap(), + "shouldPerformDownload": shouldPerformDownload + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WKNavigationResponse.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WKNavigationResponse.swift new file mode 100644 index 00000000..0e685291 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WKNavigationResponse.swift @@ -0,0 +1,19 @@ +// +// WKNavigationResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +extension WKNavigationResponse { + public func toMap () -> [String:Any?] { + return [ + "response": response.toMap(), + "isForMainFrame": isForMainFrame, + "canShowMIMEType": canShowMIMEType, + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WKSecurityOrigin.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WKSecurityOrigin.swift new file mode 100644 index 00000000..a7e8c714 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WKSecurityOrigin.swift @@ -0,0 +1,20 @@ +// +// WKSecurityOrigin.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +@available(iOS 9.0, *) +extension WKSecurityOrigin { + public func toMap () -> [String:Any?] { + return [ + "host": host, + "port": port, + "protocol": self.protocol + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WKUserContentController.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WKUserContentController.swift new file mode 100644 index 00000000..633f1e1b --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WKUserContentController.swift @@ -0,0 +1,375 @@ +// +// UserContentController.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 17/02/21. +// + +import Foundation +import WebKit +import OrderedSet + +extension WKUserContentController { + static var WINDOW_ID_PREFIX = "WINDOW-ID-" + + // Workaround to create stored properties in an extension: + // https://valv0.medium.com/computed-properties-and-extensions-a-pure-swift-approach-64733768112c + + @available(iOS 14.0, *) + private static var _contentWorlds = [String: Set]() + @available(iOS 14.0, *) + var contentWorlds: Set { + get { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + return WKUserContentController._contentWorlds[tmpAddress] ?? [] + } + set(newValue) { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + WKUserContentController._contentWorlds[tmpAddress] = newValue + } + } + + private static var _userOnlyScripts = [String: [WKUserScriptInjectionTime:OrderedSet]]() + var userOnlyScripts: [WKUserScriptInjectionTime:OrderedSet] { + get { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + return WKUserContentController._userOnlyScripts[tmpAddress] ?? [:] + } + set(newValue) { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + WKUserContentController._userOnlyScripts[tmpAddress] = newValue + } + } + + private static var _pluginScripts = [String: [WKUserScriptInjectionTime:OrderedSet]]() + var pluginScripts: [WKUserScriptInjectionTime:OrderedSet] { + get { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + return WKUserContentController._pluginScripts[tmpAddress] ?? [:] + } + set(newValue) { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + WKUserContentController._pluginScripts[tmpAddress] = newValue + } + } + + public func initialize () { + if #available(iOS 14.0, *) { + contentWorlds = Set([WKContentWorld.page]) + } + pluginScripts = [ + .atDocumentStart: OrderedSet(sequence: []), + .atDocumentEnd: OrderedSet(sequence: []), + ] + userOnlyScripts = [ + .atDocumentStart: OrderedSet(sequence: []), + .atDocumentEnd: OrderedSet(sequence: []), + ] + } + + public func dispose (windowId: Int64?) { + if windowId == nil { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + if #available(iOS 14.0, *) { + contentWorlds.removeAll() + WKUserContentController._contentWorlds.removeValue(forKey: tmpAddress) + } + pluginScripts.removeAll() + WKUserContentController._pluginScripts.removeValue(forKey: tmpAddress) + userOnlyScripts.removeAll() + WKUserContentController._userOnlyScripts.removeValue(forKey: tmpAddress) + } + else if #available(iOS 14.0, *), let windowId = windowId { + let contentWorldsToRemove = contentWorlds.filter({ $0.windowId == windowId }) + for contentWorld in contentWorldsToRemove { + contentWorlds.remove(contentWorld) + removeAllScriptMessageHandlers(from: contentWorld) + } + } + } + + public func sync(scriptMessageHandler: WKScriptMessageHandler) { + let pluginScriptsList = pluginScripts.compactMap({ $0.value }).joined() + for pluginScript in pluginScriptsList { + if !containsPluginScript(pluginScript: pluginScript) { + addUserScript(pluginScript) + for messageHandlerName in pluginScript.messageHandlerNames { + removeScriptMessageHandler(forName: messageHandlerName) + add(scriptMessageHandler, name: messageHandlerName) + } + } + if #available(iOS 14.0, *), pluginScript.requiredInAllContentWorlds { + for contentWorld in contentWorlds { + if !containsPluginScript(pluginScript: pluginScript, in: contentWorld) { + let pluginScriptWithContentWorld = pluginScript.copyAndSet(contentWorld: contentWorld) + addUserScript(pluginScriptWithContentWorld) + for messageHandlerName in pluginScriptWithContentWorld.messageHandlerNames { + removeScriptMessageHandler(forName: messageHandlerName, contentWorld: contentWorld) + add(scriptMessageHandler, contentWorld: contentWorld, name: messageHandlerName) + } + } + } + } + } + + let userOnlyScriptsList = userOnlyScripts.compactMap({ $0.value }).joined() + for userOnlyScript in userOnlyScriptsList { + if !userScripts.contains(userOnlyScript) { + addUserScript(userOnlyScript) + } + } + } + + public func addUserOnlyScript(_ userOnlyScript: UserScript) { + if #available(iOS 14.0, *) { + contentWorlds.insert(userOnlyScript.contentWorld) + } + userOnlyScripts[userOnlyScript.injectionTime]!.append(userOnlyScript) + } + + public func addUserOnlyScripts(_ userOnlyScripts: [UserScript]) { + for userOnlyScript in userOnlyScripts { + addUserOnlyScript(userOnlyScript) + } + } + + public func addPluginScript(_ pluginScript: PluginScript) { + if #available(iOS 14.0, *) { + contentWorlds.insert(pluginScript.contentWorld) + } + pluginScripts[pluginScript.injectionTime]!.append(pluginScript) + } + + public func addPluginScripts(_ pluginScripts: [PluginScript]) { + for pluginScript in pluginScripts { + addPluginScript(pluginScript) + } + } + + public func getPluginScriptsRequiredInAllContentWorlds() -> [PluginScript] { + return pluginScripts.compactMap({ $0.value }) + .joined() + .filter({ $0.injectionTime == .atDocumentStart && $0.requiredInAllContentWorlds }) + } + + @available(iOS 14.0, *) + public func generateCodeForScriptEvaluation(scriptMessageHandler: WKScriptMessageHandler, source: String, contentWorld: WKContentWorld) -> String { + let (inserted, _) = contentWorlds.insert(contentWorld) + if inserted { + var generatedCode = "" + let pluginScriptsRequired = getPluginScriptsRequiredInAllContentWorlds() + for pluginScript in pluginScriptsRequired { + generatedCode += pluginScript.source + "\n" + for messageHandlerName in pluginScript.messageHandlerNames { + removeScriptMessageHandler(forName: messageHandlerName, contentWorld: contentWorld) + add(scriptMessageHandler, contentWorld: contentWorld, name: messageHandlerName) + } + } + if let windowId = contentWorld.windowId { + generatedCode += "\(WINDOW_ID_VARIABLE_JS_SOURCE) = \(String(windowId));\n" + } + return generatedCode + "\n" + source + } + return source + } + + public func removeUserOnlyScript(_ userOnlyScript: UserScript) { + userOnlyScripts[userOnlyScript.injectionTime]!.remove(userOnlyScript) + removeUserScript(scriptToRemove: userOnlyScript) + } + + public func removeUserOnlyScript(at index: Int, injectionTime: WKUserScriptInjectionTime) { + let scriptToRemove = userOnlyScripts[injectionTime]![index] + userOnlyScripts[injectionTime]!.removeObject(at: index) + removeUserScript(scriptToRemove: scriptToRemove) + } + + public func removeAllUserOnlyScripts() { + let allUserOnlyScripts = Array(userOnlyScripts.compactMap({ $0.value }).joined()) + + userOnlyScripts[.atDocumentStart]!.removeAllObjects() + userOnlyScripts[.atDocumentEnd]!.removeAllObjects() + + removeUserScripts(scriptsToRemove: allUserOnlyScripts) + } + + public func removePluginScript(_ pluginScript: PluginScript) { + pluginScripts[pluginScript.injectionTime]!.remove(pluginScript) + for messageHandlerName in pluginScript.messageHandlerNames { + removeScriptMessageHandler(forName: messageHandlerName) + if #available(iOS 14.0, *) { + for contentWorld in contentWorlds { + removeScriptMessageHandler(forName: messageHandlerName, contentWorld: contentWorld) + } + } + } + removeUserScript(scriptToRemove: pluginScript) + } + + public func removeAllPluginScripts() { + let allPluginScripts = Array(pluginScripts.compactMap({ $0.value }).joined()) + + pluginScripts[.atDocumentStart]!.removeAllObjects() + pluginScripts[.atDocumentEnd]!.removeAllObjects() + + removeUserScripts(scriptsToRemove: allPluginScripts) + } + + public func removeAllPluginScriptMessageHandlers() { + let allPluginScripts = pluginScripts.compactMap({ $0.value }).joined() + for pluginScript in allPluginScripts { + for messageHandlerName in pluginScript.messageHandlerNames { + removeScriptMessageHandler(forName: messageHandlerName) + } + } + if #available(iOS 14.0, *) { + removeAllScriptMessageHandlers() + for contentWorld in contentWorlds { + removeAllScriptMessageHandlers(from: contentWorld) + } + } + } + + @available(iOS 14.0, *) + public func resetContentWorlds(windowId: Int64?) { + let allUserOnlyScripts = userOnlyScripts.compactMap({ $0.value }).joined() + let contentWorldsFiltered = contentWorlds.filter({ $0.windowId == windowId && $0 != WKContentWorld.page }) + for contentWorld in contentWorldsFiltered { + var found = false + for script in allUserOnlyScripts { + if script.contentWorld == contentWorld { + found = true + break + } + } + if !found { + contentWorlds.remove(contentWorld) + } + } + } + + private func removeUserScript(scriptToRemove: WKUserScript, shouldAddPreviousScripts: Bool = true) -> Void { + // there isn't a way to remove a specific user script using WKUserContentController, + // so we remove all the user scripts and, then, we add them again without the one that has been removed + let userScripts = useCopyOfUserScripts() + + var userScriptsUpdated: [WKUserScript] = [] + for script in userScripts { + if script != scriptToRemove { + userScriptsUpdated.append(script) + } + } + + removeAllUserScripts() + + if shouldAddPreviousScripts { + for script in userScriptsUpdated { + addUserScript(script) + } + } + } + + private func removeUserScripts(scriptsToRemove: [WKUserScript], shouldAddPreviousScripts: Bool = true) -> Void { + // there isn't a way to remove a specific user script using WKUserContentController, + // so we remove all the user scripts and, then, we add them again without the one that has been removed + let userScripts = useCopyOfUserScripts() + + var userScriptsUpdated: [WKUserScript] = [] + for script in userScripts { + if !scriptsToRemove.contains(script) { + userScriptsUpdated.append(script) + } + } + + removeAllUserScripts() + + if shouldAddPreviousScripts { + for script in userScriptsUpdated { + addUserScript(script) + } + } + } + + public func removeUserOnlyScripts(with groupName: String, shouldAddPreviousScripts: Bool = true) -> Void { + let allUserOnlyScripts = userOnlyScripts.compactMap({ $0.value }).joined() + var scriptsToRemove: [UserScript] = [] + for script in allUserOnlyScripts { + if let scriptName = script.groupName, scriptName == groupName { + scriptsToRemove.append(script) + userOnlyScripts[script.injectionTime]!.remove(script) + } + } + removeUserScripts(scriptsToRemove: scriptsToRemove, shouldAddPreviousScripts: shouldAddPreviousScripts) + } + + public func removePluginScripts(with groupName: String, shouldAddPreviousScripts: Bool = true) -> Void { + let allPluginScripts = pluginScripts.compactMap({ $0.value }).joined() + var scriptsToRemove: [PluginScript] = [] + for script in allPluginScripts { + if let scriptName = script.groupName, scriptName == groupName { + scriptsToRemove.append(script) + pluginScripts[script.injectionTime]!.remove(script) + } + } + removeUserScripts(scriptsToRemove: scriptsToRemove, shouldAddPreviousScripts: shouldAddPreviousScripts) + } + + public func containsPluginScript(pluginScript: PluginScript) -> Bool { + let userScripts = useCopyOfUserScripts() + for script in userScripts { + if let script = script as? PluginScript, script == pluginScript { + return true + } + } + return false + } + + public func containsPluginScript(with groupName: String) -> Bool { + let userScripts = useCopyOfUserScripts() + for script in userScripts { + if let script = script as? PluginScript, script.groupName == groupName { + return true + } + } + return false + } + + @available(iOS 14.0, *) + public func containsPluginScript(pluginScript: PluginScript, in contentWorld: WKContentWorld) -> Bool { + let userScripts = useCopyOfUserScripts() + for script in userScripts { + if let script = script as? PluginScript, script == pluginScript, script.contentWorld == contentWorld { + return true + } + } + return false + } + + @available(iOS 14.0, *) + public func containsPluginScript(with groupName: String, in contentWorld: WKContentWorld) -> Bool { + let userScripts = useCopyOfUserScripts() + for script in userScripts { + if let script = script as? PluginScript, script.groupName == groupName, script.contentWorld == contentWorld { + return true + } + } + return false + } + + @available(iOS 14.0, *) + public func getContentWorlds(with windowId: Int64?) -> Set { + var contentWorldsFiltered = Set([WKContentWorld.page]) + let contentWorlds = Array(self.contentWorlds) + for contentWorld in contentWorlds { + if contentWorld.windowId == windowId { + contentWorldsFiltered.insert(contentWorld) + } + } + return contentWorldsFiltered + } + + // use a copy of self.userScripts to avoid EXC_BREAKPOINT at runtime if self.userScripts gets removed when another code is looping them + private func useCopyOfUserScripts() -> [WKUserScript] { + return Array(self.userScripts) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WKWindowFeatures.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WKWindowFeatures.swift new file mode 100644 index 00000000..469e2be1 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WKWindowFeatures.swift @@ -0,0 +1,24 @@ +// +// WKWindowFeatures.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +extension WKWindowFeatures { + public func toMap () -> [String:Any?] { + return [ + "allowsResizing": allowsResizing, + "height": height, + "menuBarVisibility": menuBarVisibility, + "statusBarVisibility": statusBarVisibility, + "toolbarsVisibility": toolbarsVisibility, + "width": width, + "x": x, + "y": y + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WebMessage.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WebMessage.swift new file mode 100644 index 00000000..5a49f611 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WebMessage.swift @@ -0,0 +1,70 @@ +// +// WebMessage.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/03/21. +// + +import Foundation + +public class WebMessage: NSObject, Disposable { + var data: Any? + var type: WebMessageType + var ports: [WebMessagePort]? + + var jsData: String { + var jsData: String = "null" + if let messageData = data { + if type == .arrayBuffer, let messageDataArrayBuffer = messageData as? FlutterStandardTypedData { + jsData = "new Uint8Array(\(Array(messageDataArrayBuffer.data))).buffer" + } else if let messageDataString = messageData as? String { + jsData = "'\(messageDataString.replacingOccurrences(of: "\'", with: "\\'"))'" + } + } + return jsData + } + + public init(data: Any?, type: WebMessageType, ports: [WebMessagePort]?) { + self.type = type + super.init() + self.data = data + self.ports = ports + } + + public static func fromMap(map: [String: Any?]) -> WebMessage { + let portMapList = map["ports"] as? [[String: Any?]] + var ports: [WebMessagePort]? = nil + if let portMapList = portMapList, !portMapList.isEmpty { + ports = [] + portMapList.forEach { (portMap) in + ports?.append(WebMessagePort.fromMap(map: portMap)) + } + } + + return WebMessage( + data: map["data"] as? Any, + type: WebMessageType.init(rawValue: map["type"] as! Int)!, + ports: ports) + } + + public func toMap () -> [String: Any?] { + return [ + "data": type == .arrayBuffer && data is [UInt8] ? Data(data as! [UInt8]) : data, + "type": type.rawValue + ] + } + + public func dispose() { + ports?.removeAll() + } + + deinit { + debugPrint("WebMessage - dealloc") + dispose() + } +} + +public enum WebMessageType: Int { + case string = 0 + case arrayBuffer = 1 +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WebMessagePort.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WebMessagePort.swift new file mode 100644 index 00000000..b7a6e53c --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WebMessagePort.swift @@ -0,0 +1,147 @@ +// +// WebMessagePort.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/03/21. +// + +import Foundation + +public class WebMessagePort: NSObject { + var name: String + var index: Int64 + var webMessageChannelId: String + var webMessageChannel: WebMessageChannel? + var isClosed = false + var isTransferred = false + var isStarted = false + + public init(name: String, index: Int64, webMessageChannelId: String, webMessageChannel: WebMessageChannel?) { + self.name = name + self.index = index + self.webMessageChannelId = webMessageChannelId + super.init() + self.webMessageChannel = webMessageChannel + } + + public func setWebMessageCallback(completionHandler: ((Any?) -> Void)? = nil) throws { + if isClosed || isTransferred { + throw NSError(domain: "Port is already closed or transferred", code: 0) + } + self.isStarted = true + if let webMessageChannel = webMessageChannel, let webView = webMessageChannel.webView { + let index = name == "port1" ? 0 : 1 + webView.evaluateJavascript(source: """ + (function() { + var webMessageChannel = \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(webMessageChannel.id)"]; + if (webMessageChannel != null) { + webMessageChannel.\(self.name).onmessage = function (event) { + window.webkit.messageHandlers["onWebMessagePortMessageReceived"].postMessage({ + "webMessageChannelId": "\(webMessageChannel.id)", + "index": \(String(index)), + "message": { + "data": window.ArrayBuffer != null && event.data instanceof ArrayBuffer ? Array.from(new Uint8Array(event.data)) : (event.data != null ? event.data.toString() : null), + "type": window.ArrayBuffer != null && event.data instanceof ArrayBuffer ? 1 : 0 + } + }); + } + } + })(); + """) { (_) in + completionHandler?(nil) + } + } else { + completionHandler?(nil) + } + } + + public func postMessage(message: WebMessage, completionHandler: ((Any?) -> Void)? = nil) throws { + if isClosed || isTransferred { + throw NSError(domain: "Port is already closed or transferred", code: 0) + } + if let webMessageChannel = webMessageChannel, let webView = webMessageChannel.webView { + var portsString = "null" + if let ports = message.ports { + var portArrayString: [String] = [] + for port in ports { + if port == self { + throw NSError(domain: "Source port cannot be transferred", code: 0) + } + if port.isStarted { + throw NSError(domain: "Port is already started", code: 0) + } + if port.isClosed || port.isTransferred { + throw NSError(domain: "Port is already closed or transferred", code: 0) + } + port.isTransferred = true + portArrayString.append("\(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)['\(port.webMessageChannel!.id)'].\(port.name)") + } + portsString = "[" + portArrayString.joined(separator: ", ") + "]" + } + + let source = """ + (function() { + var webMessageChannel = \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(webMessageChannel.id)"]; + if (webMessageChannel != null) { + webMessageChannel.\(self.name).postMessage(\(message.jsData), \(portsString)); + } + })(); + """ + webView.evaluateJavascript(source: source) { (_) in + completionHandler?(nil) + } + } else { + completionHandler?(nil) + } + message.dispose() + } + + public func close(completionHandler: ((Any?) -> Void)? = nil) throws { + if isTransferred { + throw NSError(domain: "Port is already transferred", code: 0) + } + isClosed = true + if let webMessageChannel = webMessageChannel, let webView = webMessageChannel.webView { + let source = """ + (function() { + var webMessageChannel = \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(webMessageChannel.id)"]; + if (webMessageChannel != null) { + webMessageChannel.\(self.name).close(); + } + })(); + """ + webView.evaluateJavascript(source: source) { (_) in + completionHandler?(nil) + } + } else { + completionHandler?(nil) + } + } + + public static func fromMap(map: [String: Any?]) -> WebMessagePort { + let index = map["index"] as! Int64 + return WebMessagePort( + name: "port\(String(index + 1))", + index: index, + webMessageChannelId: map["webMessageChannelId"] as! String, + webMessageChannel: nil) + } + + public func toMap () -> [String: Any?] { + return [ + "name": name, + "index": index, + "webMessageChannelId": webMessageChannelId + ] + } + + public func dispose() { + isClosed = true + webMessageChannel = nil + } + + deinit { + debugPrint("WebMessagePort - dealloc") + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WebResourceError.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WebResourceError.swift new file mode 100644 index 00000000..3183944b --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WebResourceError.swift @@ -0,0 +1,25 @@ +// +// WebResourceError.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 01/05/22. +// + +import Foundation + +public class WebResourceError: NSObject { + var type: Int + var errorDescription: String + + public init(type: Int, errorDescription: String) { + self.type = type + self.errorDescription = errorDescription + } + + public func toMap () -> [String:Any?] { + return [ + "type": type, + "description": errorDescription + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WebResourceRequest.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WebResourceRequest.swift new file mode 100644 index 00000000..ad7635c4 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WebResourceRequest.swift @@ -0,0 +1,53 @@ +// +// WebResourceRequest.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 01/05/22. +// + +import Foundation +import WebKit + +public class WebResourceRequest: NSObject { + var url: URL + var headers: [AnyHashable:Any]? + var isRedirect = false + var hasGesture = false + var isForMainFrame = true + var method = "GET" + + public init(url: URL, headers: [AnyHashable:Any]?) { + self.url = url + self.headers = headers + } + + public init(url: URL, headers: [AnyHashable:Any]?, isForMainFrame: Bool) { + self.url = url + self.headers = headers + self.isForMainFrame = isForMainFrame + } + + public init(fromURLRequest: URLRequest) { + self.url = fromURLRequest.url ?? URL(string: "about:blank")! + self.headers = fromURLRequest.allHTTPHeaderFields + self.method = fromURLRequest.httpMethod ?? "GET" + } + + public init(fromWKNavigationResponse: WKNavigationResponse) { + let response = fromWKNavigationResponse.response as? HTTPURLResponse + self.url = response?.url ?? URL(string: "about:blank")! + self.headers = response?.allHeaderFields + self.isForMainFrame = fromWKNavigationResponse.isForMainFrame + } + + public func toMap () -> [String:Any?] { + return [ + "url": url.absoluteString, + "headers": headers, + "isRedirect": isRedirect, + "hasGesture": hasGesture, + "isForMainFrame": isForMainFrame, + "method": method + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WebResourceResponse.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WebResourceResponse.swift new file mode 100644 index 00000000..4fcf13e7 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WebResourceResponse.swift @@ -0,0 +1,47 @@ +// +// WebResourceResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 01/05/22. +// + +import Foundation +import WebKit + +public class WebResourceResponse: NSObject { + var contentType: String + var contentEncoding: String + var data: Data? + var headers: [AnyHashable:Any]? + var statusCode: Int? + var reasonPhrase: String? + + public init(contentType: String, contentEncoding: String, data: Data?, + headers: [AnyHashable:Any]?, statusCode: Int?, reasonPhrase: String?) { + self.contentType = contentType + self.contentEncoding = contentEncoding + self.data = data + self.headers = headers + self.statusCode = statusCode + self.reasonPhrase = reasonPhrase + } + + public init(fromWKNavigationResponse: WKNavigationResponse) { + let response = fromWKNavigationResponse.response as? HTTPURLResponse + self.contentType = response?.mimeType ?? "" + self.contentEncoding = response?.textEncodingName ?? "" + self.headers = response?.allHeaderFields + self.statusCode = response?.statusCode + } + + public func toMap () -> [String:Any?] { + return [ + "contentType": contentType, + "contentEncoding": contentEncoding, + "data": data, + "headers": headers, + "statusCode": statusCode, + "reasonPhrase": reasonPhrase + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WebViewTransport.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WebViewTransport.swift new file mode 100644 index 00000000..7e21f7e6 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Types/WebViewTransport.swift @@ -0,0 +1,18 @@ +// +// WebViewTransport.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +public class WebViewTransport: NSObject { + var webView: InAppWebView + var request: URLRequest + + init(webView: InAppWebView, request: URLRequest) { + self.webView = webView + self.request = request + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/UIApplication/VisibleViewController.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/UIApplication/VisibleViewController.swift new file mode 100644 index 00000000..e6d549ca --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/UIApplication/VisibleViewController.swift @@ -0,0 +1,31 @@ +// +// VisibleViewController.swift +// flutter_inappwebview +// +// Created by Alexandru Terente on 02.08.2023. +// + +import UIKit + +extension UIApplication { + + var visibleViewController: UIViewController? { + guard let rootViewController = keyWindow?.rootViewController else { + return nil + } + return getVisibleViewController(rootViewController) + } + + private func getVisibleViewController(_ rootViewController: UIViewController) -> UIViewController? { + if let presentedViewController = rootViewController.presentedViewController { + return getVisibleViewController(presentedViewController) + } + if let navigationController = rootViewController as? UINavigationController { + return navigationController.visibleViewController + } + if let tabBarController = rootViewController as? UITabBarController { + return tabBarController.selectedViewController + } + return rootViewController + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Util.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Util.swift new file mode 100644 index 00000000..9992ece9 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/Util.swift @@ -0,0 +1,226 @@ +// +// Util.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 12/02/21. +// + +import Foundation +import WebKit + +var SharedLastTouchPointTimestamp: [InAppWebView: Int64] = [:] + +public class Util { + public static func getUrlAsset(plugin: SwiftFlutterPlugin, assetFilePath: String) throws -> URL { + guard let key = plugin.registrar?.lookupKey(forAsset: assetFilePath), + let assetURL = Bundle.main.url(forResource: key, withExtension: nil) else { + throw NSError(domain: assetFilePath + " asset file cannot be found!", code: 0) + } + return assetURL + } + + public static func getAbsPathAsset(plugin: SwiftFlutterPlugin, assetFilePath: String) throws -> String { + guard let key = plugin.registrar?.lookupKey(forAsset: assetFilePath), + let assetAbsPath = Bundle.main.path(forResource: key, ofType: nil) else { + throw NSError(domain: assetFilePath + " asset file cannot be found!", code: 0) + } + return assetAbsPath + } + + public static func convertToDictionary(text: String) -> [String: Any]? { + if let data = text.data(using: .utf8) { + do { + return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] + } catch { + print(error.localizedDescription) + } + } + return nil + } + + public static func JSONStringify(value: Any, prettyPrinted: Bool = false) -> String { + let options: JSONSerialization.WritingOptions = prettyPrinted ? .prettyPrinted : .init(rawValue: 0) + if JSONSerialization.isValidJSONObject(value) { + let data = try? JSONSerialization.data(withJSONObject: value, options: options) + if data != nil { + if let string = String(data: data!, encoding: .utf8) { + return string + } + } + } + return "" + } + + @available(iOS 14.0, *) + public static func getContentWorld(name: String) -> WKContentWorld { + switch name { + case "defaultClient": + return WKContentWorld.defaultClient + case "page": + return WKContentWorld.page + default: + return WKContentWorld.world(name: name) + } + } + + @available(iOS 10.0, *) + public static func getDataDetectorType(type: String) -> WKDataDetectorTypes { + switch type { + case "NONE": + return WKDataDetectorTypes.init(rawValue: 0) + case "PHONE_NUMBER": + return .phoneNumber + case "LINK": + return .link + case "ADDRESS": + return .address + case "CALENDAR_EVENT": + return .calendarEvent + case "TRACKING_NUMBER": + return .trackingNumber + case "FLIGHT_NUMBER": + return .flightNumber + case "LOOKUP_SUGGESTION": + return .lookupSuggestion + case "SPOTLIGHT_SUGGESTION": + return .spotlightSuggestion + case "ALL": + return .all + default: + return WKDataDetectorTypes.init(rawValue: 0) + } + } + + @available(iOS 10.0, *) + public static func getDataDetectorTypeString(type: WKDataDetectorTypes) -> [String] { + var dataDetectorTypeString: [String] = [] + if type.contains(.all) { + dataDetectorTypeString.append("ALL") + } else { + if type.contains(.phoneNumber) { + dataDetectorTypeString.append("PHONE_NUMBER") + } + if type.contains(.link) { + dataDetectorTypeString.append("LINK") + } + if type.contains(.address) { + dataDetectorTypeString.append("ADDRESS") + } + if type.contains(.calendarEvent) { + dataDetectorTypeString.append("CALENDAR_EVENT") + } + if type.contains(.trackingNumber) { + dataDetectorTypeString.append("TRACKING_NUMBER") + } + if type.contains(.flightNumber) { + dataDetectorTypeString.append("FLIGHT_NUMBER") + } + if type.contains(.lookupSuggestion) { + dataDetectorTypeString.append("LOOKUP_SUGGESTION") + } + if type.contains(.spotlightSuggestion) { + dataDetectorTypeString.append("SPOTLIGHT_SUGGESTION") + } + } + if dataDetectorTypeString.count == 0 { + dataDetectorTypeString = ["NONE"] + } + return dataDetectorTypeString + } + + public static func getDecelerationRate(type: String) -> UIScrollView.DecelerationRate { + switch type { + case "NORMAL": + return .normal + case "FAST": + return .fast + default: + return .normal + } + } + + public static func getDecelerationRateString(type: UIScrollView.DecelerationRate) -> String { + switch type { + case .normal: + return "NORMAL" + case .fast: + return "FAST" + default: + return "NORMAL" + } + } + + public static func isIPv4(address: String) -> Bool { + var sin = sockaddr_in() + return address.withCString({ cstring in inet_pton(AF_INET, cstring, &sin.sin_addr) }) == 1 + } + + public static func isIPv6(address: String) -> Bool { + var sin6 = sockaddr_in6() + return address.withCString({ cstring in inet_pton(AF_INET6, cstring, &sin6.sin6_addr) }) == 1 + } + + public static func isIpAddress(address: String) -> Bool { + return Util.isIPv6(address: address) || Util.isIPv4(address: address) + } + + public static func normalizeIPv6(address: String) throws -> String { + if !Util.isIPv6(address: address) { + throw NSError(domain: "Invalid address: \(address)", code: 0) + } + var ipString = address + // replace ipv4 address if any + let ipv4Regex = try! NSRegularExpression(pattern: "(.*:)([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$)") + if let match = ipv4Regex.firstMatch(in: address, options: [], range: NSRange(location: 0, length: address.utf16.count)) { + if let ipv6PartRange = Range(match.range(at: 1), in: address) { + ipString = String(address[ipv6PartRange]) + } + if let ipv4Range = Range(match.range(at: 2), in: address) { + let ipv4 = address[ipv4Range] + let ipv4Split = ipv4.split(separator: ".") + var ipv4Converted = Array(repeating: "0000", count: 4) + for i in 0...3 { + let byte = Int(ipv4Split[i])! + let hex = ("0" + String(byte, radix: 16)) + var offset = hex.count - 3 + offset = offset < 0 ? 0 : offset + let fromIndex = hex.index(hex.startIndex, offsetBy: offset) + let toIndex = hex.index(hex.startIndex, offsetBy: hex.count - 1) + let indexRange = Range(uncheckedBounds: (lower: fromIndex, upper: toIndex)) + ipv4Converted[i] = String(hex[indexRange]) + } + ipString += ipv4Converted[0] + ipv4Converted[1] + ":" + ipv4Converted[2] + ipv4Converted[3] + } + } + + // take care of leading and trailing :: + let regex = try! NSRegularExpression(pattern: "^:|:$") + ipString = regex.stringByReplacingMatches(in: ipString, options: [], range: NSRange(location: 0, length: ipString.count), withTemplate: "") + + let ipv6 = ipString.split(separator: ":", omittingEmptySubsequences: false) + var fullIPv6 = Array(repeating: "0000", count: ipv6.count) + + for (i, hex) in ipv6.enumerated() { + if !hex.isEmpty { + // normalize leading zeros + let hexString = String("0000" + hex) + var offset = hexString.count - 5 + offset = offset < 0 ? 0 : offset + let fromIndex = hexString.index(hexString.startIndex, offsetBy: offset) + let toIndex = hexString.index(hexString.startIndex, offsetBy: hexString.count - 1) + let indexRange = Range(uncheckedBounds: (lower: fromIndex, upper: toIndex)) + fullIPv6[i] = String(hexString[indexRange]) + } else { + // normalize grouped zeros :: + var zeros: [String] = [] + for _ in ipv6.count...8 { + zeros.append("0000") + } + fullIPv6[i] = zeros.joined(separator: ":") + } + } + + return fullIPv6.joined(separator: ":") + } + +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/WKProcessPoolManager.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/WKProcessPoolManager.swift new file mode 100755 index 00000000..038fb4d7 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/WKProcessPoolManager.swift @@ -0,0 +1,13 @@ +// +// WKProcessPoolManager.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/11/2019. +// + +import Foundation +import WebKit + +public class WKProcessPoolManager { + static let sharedProcessPool = WKProcessPool() +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/WebAuthenticationSession/WebAuthenticationSession.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/WebAuthenticationSession/WebAuthenticationSession.swift new file mode 100644 index 00000000..99409b11 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/WebAuthenticationSession/WebAuthenticationSession.swift @@ -0,0 +1,109 @@ +// +// WebAuthenticationSession.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/05/22. +// + +import Foundation +import AuthenticationServices +import SafariServices + +public class WebAuthenticationSession: NSObject, ASWebAuthenticationPresentationContextProviding, Disposable { + static let METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_webauthenticationsession_" + var id: String + var plugin: SwiftFlutterPlugin? + var url: URL + var callbackURLScheme: String? + var settings: WebAuthenticationSessionSettings + var session: Any? + var channelDelegate: WebAuthenticationSessionChannelDelegate? + private var _canStart = true + + public init(plugin: SwiftFlutterPlugin, id: String, url: URL, callbackURLScheme: String?, settings: WebAuthenticationSessionSettings) { + self.id = id + self.plugin = plugin + self.url = url + self.settings = settings + super.init() + self.callbackURLScheme = callbackURLScheme + if #available(iOS 12.0, *) { + let session = ASWebAuthenticationSession(url: self.url, callbackURLScheme: self.callbackURLScheme, completionHandler: self.completionHandler) + if #available(iOS 13.0, *) { + session.presentationContextProvider = self + } + self.session = session + } else if #available(iOS 11.0, *) { + self.session = SFAuthenticationSession(url: self.url, callbackURLScheme: self.callbackURLScheme, completionHandler: self.completionHandler) + } + let channel = FlutterMethodChannel(name: WebAuthenticationSession.METHOD_CHANNEL_NAME_PREFIX + id, + binaryMessenger: plugin.registrar!.messenger()) + self.channelDelegate = WebAuthenticationSessionChannelDelegate(webAuthenticationSession: self, channel: channel) + } + + public func prepare() { + if #available(iOS 13.0, *), let session = session as? ASWebAuthenticationSession { + session.prefersEphemeralWebBrowserSession = settings.prefersEphemeralWebBrowserSession + } + } + + public func completionHandler(url: URL?, error: Error?) -> Void { + channelDelegate?.onComplete(url: url, errorCode: error?._code) + } + + public func canStart() -> Bool { + guard let session = session else { + return false + } + if #available(iOS 13.4, *), let session = session as? ASWebAuthenticationSession { + return session.canStart + } + return _canStart + } + + public func start() -> Bool { + guard let session = session else { + return false + } + var started = false + if #available(iOS 12.0, *), let session = session as? ASWebAuthenticationSession { + started = session.start() + } else if #available(iOS 11.0, *), let session = session as? SFAuthenticationSession { + started = session.start() + } + if started { + _canStart = false + } + return started + } + + public func cancel() { + guard let session = session else { + return + } + if #available(iOS 12.0, *), let session = session as? ASWebAuthenticationSession { + session.cancel() + } else if #available(iOS 11.0, *), let session = session as? SFAuthenticationSession { + session.cancel() + } + } + + @available(iOS 12.0, *) + public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { + return UIApplication.shared.windows.first { $0.isKeyWindow } ?? ASPresentationAnchor() + } + + public func dispose() { + cancel() + channelDelegate?.dispose() + channelDelegate = nil + session = nil + plugin?.webAuthenticationSessionManager?.sessions[id] = nil + plugin = nil + } + + deinit { + debugPrint("WebAuthenticationSession - dealloc") + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/WebAuthenticationSession/WebAuthenticationSessionChannelDelegate.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/WebAuthenticationSession/WebAuthenticationSessionChannelDelegate.swift new file mode 100644 index 00000000..5bbc023a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/WebAuthenticationSession/WebAuthenticationSessionChannelDelegate.swift @@ -0,0 +1,73 @@ +// +// WebAuthenticationSessionChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/05/22. +// + +import Foundation + +public class WebAuthenticationSessionChannelDelegate: ChannelDelegate { + private weak var webAuthenticationSession: WebAuthenticationSession? + + public init(webAuthenticationSession: WebAuthenticationSession, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.webAuthenticationSession = webAuthenticationSession + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + // let arguments = call.arguments as? NSDictionary + switch call.method { + case "canStart": + if let webAuthenticationSession = webAuthenticationSession { + result(webAuthenticationSession.canStart()) + } else { + result(false) + } + break + case "start": + if let webAuthenticationSession = webAuthenticationSession { + result(webAuthenticationSession.start()) + } else { + result(false) + } + break + case "cancel": + if let webAuthenticationSession = webAuthenticationSession { + webAuthenticationSession.cancel() + result(true) + } else { + result(false) + } + break + case "dispose": + if let webAuthenticationSession = webAuthenticationSession { + webAuthenticationSession.dispose() + result(true) + } else { + result(false) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onComplete(url: URL?, errorCode: Int?) { + let arguments: [String: Any?] = [ + "url": url?.absoluteString, + "errorCode": errorCode + ] + channel?.invokeMethod("onComplete", arguments: arguments) + } + + public override func dispose() { + super.dispose() + webAuthenticationSession = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/WebAuthenticationSession/WebAuthenticationSessionManager.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/WebAuthenticationSession/WebAuthenticationSessionManager.swift new file mode 100644 index 00000000..6ba8238d --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/WebAuthenticationSession/WebAuthenticationSessionManager.swift @@ -0,0 +1,78 @@ +// +// WebAuthenticationSessionManager.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/05/22. +// + +import Flutter +import UIKit +import WebKit +import Foundation +import AVFoundation +import SafariServices + +public class WebAuthenticationSessionManager: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_webauthenticationsession" + var plugin: SwiftFlutterPlugin? + var sessions: [String: WebAuthenticationSession?] = [:] + + init(plugin: SwiftFlutterPlugin) { + super.init(channel: FlutterMethodChannel(name: WebAuthenticationSessionManager.METHOD_CHANNEL_NAME, binaryMessenger: plugin.registrar!.messenger())) + self.plugin = plugin + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "create": + let id = arguments!["id"] as! String + let url = arguments!["url"] as! String + let callbackURLScheme = arguments!["callbackURLScheme"] as? String + let initialSettings = arguments!["initialSettings"] as! [String: Any?] + create(id: id, url: url, callbackURLScheme: callbackURLScheme, settings: initialSettings, result: result) + break + case "isAvailable": + if #available(iOS 11.0, *) { + result(true) + } else { + result(false) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func create(id: String, url: String, callbackURLScheme: String?, settings: [String: Any?], result: @escaping FlutterResult) { + if #available(iOS 11.0, *), let plugin = plugin { + let sessionUrl = URL(string: url) ?? URL(string: "about:blank")! + let initialSettings = WebAuthenticationSessionSettings() + let _ = initialSettings.parse(settings: settings) + let session = WebAuthenticationSession(plugin: plugin, id: id, url: sessionUrl, callbackURLScheme: callbackURLScheme, settings: initialSettings) + session.prepare() + sessions[id] = session + result(true) + return + } + + result(FlutterError.init(code: "WebAuthenticationSessionManager", message: "WebAuthenticationSession is not available!", details: nil)) + } + + public override func dispose() { + super.dispose() + let sessionValues = sessions.values + sessionValues.forEach { (session: WebAuthenticationSession?) in + session?.cancel() + session?.dispose() + } + sessions.removeAll() + plugin = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/WebAuthenticationSession/WebAuthenticationSessionSettings.swift b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/WebAuthenticationSession/WebAuthenticationSessionSettings.swift new file mode 100644 index 00000000..b737381e --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Classes/WebAuthenticationSession/WebAuthenticationSessionSettings.swift @@ -0,0 +1,30 @@ +// +// WebAuthenticationSessionSettings.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/05/22. +// + +import Foundation +import AuthenticationServices +import SafariServices + +@objcMembers +public class WebAuthenticationSessionSettings: ISettings { + + var prefersEphemeralWebBrowserSession = false + + override init(){ + super.init() + } + + override func getRealSettings(obj: WebAuthenticationSession?) -> [String: Any?] { + var realOptions: [String: Any?] = toMap() + if #available(iOS 12.0, *), let session = obj?.session as? ASWebAuthenticationSession { + if #available(iOS 13.0, *) { + realOptions["prefersEphemeralWebBrowserSession"] = session.prefersEphemeralWebBrowserSession + } + } + return realOptions + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Resources/PrivacyInfo.xcprivacy b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 00000000..0eca193e --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyTrackingDomains + + NSPrivacyAccessedAPITypes + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + \ No newline at end of file diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/Storyboards/WebView.storyboard b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Storyboards/WebView.storyboard new file mode 100755 index 00000000..51a8d4f4 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/Storyboards/WebView.storyboard @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/ios/flutter_inappwebview_ios.podspec b/ui/flutter/third_party/flutter_inappwebview_ios/ios/flutter_inappwebview_ios.podspec new file mode 100755 index 00000000..d0aec268 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/ios/flutter_inappwebview_ios.podspec @@ -0,0 +1,41 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint flutterplugintest.podspec' to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'flutter_inappwebview_ios' + s.version = '0.0.1' + s.summary = 'A new Flutter plugin.' + s.description = <<-DESC +A new Flutter plugin. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.resources = 'Storyboards/**/*.storyboard' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + s.resource_bundles = {'flutter_inappwebview_ios_privacy' => ['Resources/PrivacyInfo.xcprivacy']} + + # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } + + s.libraries = 'swiftCoreGraphics' + + s.xcconfig = { + 'LIBRARY_SEARCH_PATHS' => '$(SDKROOT)/usr/lib/swift', + } + + s.swift_version = '5.0' + + s.platforms = { :ios => '12.0' } + s.dependency 'OrderedSet', '~>6.0.3' + + s.default_subspec = 'Core' + + s.subspec 'Core' do |core| + core.platform = :ios, '12.0' + end +end diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/flutter_inappwebview_ios.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/flutter_inappwebview_ios.dart new file mode 100644 index 00000000..9390a49d --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/flutter_inappwebview_ios.dart @@ -0,0 +1,3 @@ +library flutter_inappwebview_ios; + +export 'src/main.dart'; diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/chrome_safari_browser/chrome_safari_browser.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/chrome_safari_browser/chrome_safari_browser.dart new file mode 100755 index 00000000..80b5933b --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/chrome_safari_browser/chrome_safari_browser.dart @@ -0,0 +1,218 @@ +import 'dart:async'; +import 'dart:collection'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [IOSChromeSafariBrowser]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformChromeSafariBrowserCreationParams] for +/// more information. +@immutable +class IOSChromeSafariBrowserCreationParams + extends PlatformChromeSafariBrowserCreationParams { + /// Creates a new [IOSChromeSafariBrowserCreationParams] instance. + const IOSChromeSafariBrowserCreationParams(); + + /// Creates a [IOSChromeSafariBrowserCreationParams] instance based on [PlatformChromeSafariBrowserCreationParams]. + factory IOSChromeSafariBrowserCreationParams.fromPlatformChromeSafariBrowserCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformChromeSafariBrowserCreationParams params) { + return IOSChromeSafariBrowserCreationParams(); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformChromeSafariBrowser} +class IOSChromeSafariBrowser extends PlatformChromeSafariBrowser + with ChannelController { + @override + final String id = IdGenerator.generate(); + + /// Constructs a [IOSChromeSafariBrowser]. + IOSChromeSafariBrowser(PlatformChromeSafariBrowserCreationParams params) + : super.implementation( + params is IOSChromeSafariBrowserCreationParams + ? params + : IOSChromeSafariBrowserCreationParams + .fromPlatformChromeSafariBrowserCreationParams(params), + ); + + static final IOSChromeSafariBrowser _staticValue = + IOSChromeSafariBrowser(IOSChromeSafariBrowserCreationParams()); + + /// Provide static access. + factory IOSChromeSafariBrowser.static() { + return _staticValue; + } + + Map _menuItems = new HashMap(); + bool _isOpened = false; + static const MethodChannel _staticChannel = + const MethodChannel('com.pichillilorenzo/flutter_chromesafaribrowser'); + + _init() { + channel = + MethodChannel('com.pichillilorenzo/flutter_chromesafaribrowser_$id'); + handler = _handleMethod; + initMethodCallHandler(); + } + + _debugLog(String method, dynamic args) { + debugLog( + className: this.runtimeType.toString(), + id: id, + debugLoggingSettings: PlatformChromeSafariBrowser.debugLoggingSettings, + method: method, + args: args); + } + + Future _handleMethod(MethodCall call) async { + _debugLog(call.method, call.arguments); + + switch (call.method) { + case "onOpened": + eventHandler?.onOpened(); + break; + case "onCompletedInitialLoad": + final bool? didLoadSuccessfully = call.arguments["didLoadSuccessfully"]; + eventHandler?.onCompletedInitialLoad(didLoadSuccessfully); + break; + case "onInitialLoadDidRedirect": + final String? url = call.arguments["url"]; + final WebUri? uri = url != null ? WebUri(url) : null; + eventHandler?.onInitialLoadDidRedirect(uri); + break; + case "onWillOpenInBrowser": + eventHandler?.onWillOpenInBrowser(); + break; + case "onClosed": + _isOpened = false; + final onClosed = eventHandler?.onClosed; + dispose(); + onClosed?.call(); + break; + case "onItemActionPerform": + String url = call.arguments["url"]; + String title = call.arguments["title"]; + int id = call.arguments["id"].toInt(); + if (this._menuItems[id] != null) { + if (this._menuItems[id]?.action != null) { + this._menuItems[id]?.action!(url, title); + } + if (this._menuItems[id]?.onClick != null) { + this._menuItems[id]?.onClick!(WebUri(url), title); + } + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + } + + @override + Future open( + {WebUri? url, + Map? headers, + List? otherLikelyURLs, + WebUri? referrer, + @Deprecated('Use settings instead') + // ignore: deprecated_member_use_from_same_package + ChromeSafariBrowserClassOptions? options, + ChromeSafariBrowserSettings? settings}) async { + assert(!_isOpened, 'The browser is already opened.'); + _isOpened = true; + + assert(url != null, 'The specified URL must not be null on iOS.'); + assert(['http', 'https'].contains(url!.scheme), + 'The specified URL has an unsupported scheme. Only HTTP and HTTPS URLs are supported on iOS.'); + if (url != null) { + assert(url.toString().isNotEmpty, 'The specified URL must not be empty.'); + } + + _init(); + + List> menuItemList = []; + _menuItems.forEach((key, value) { + menuItemList.add(value.toMap()); + }); + + var initialSettings = settings?.toMap() ?? + options?.toMap() ?? + ChromeSafariBrowserSettings().toMap(); + + Map args = {}; + args.putIfAbsent('id', () => id); + args.putIfAbsent('url', () => url?.toString()); + args.putIfAbsent('headers', () => headers); + args.putIfAbsent('otherLikelyURLs', + () => otherLikelyURLs?.map((e) => e.toString()).toList()); + args.putIfAbsent('referrer', () => referrer?.toString()); + args.putIfAbsent('settings', () => initialSettings); + args.putIfAbsent('menuItemList', () => menuItemList); + await _staticChannel.invokeMethod('open', args); + } + + @override + Future close() async { + Map args = {}; + await channel?.invokeMethod("close", args); + } + + @override + void addMenuItem(ChromeSafariBrowserMenuItem menuItem) { + this._menuItems[menuItem.id] = menuItem; + } + + @override + void addMenuItems(List menuItems) { + menuItems.forEach((menuItem) { + this._menuItems[menuItem.id] = menuItem; + }); + } + + @override + Future isAvailable() async { + Map args = {}; + return await _staticChannel.invokeMethod("isAvailable", args) ?? + false; + } + + @override + Future clearWebsiteData() async { + Map args = {}; + await _staticChannel.invokeMethod("clearWebsiteData", args); + } + + @override + Future prewarmConnections(List URLs) async { + Map args = {}; + args.putIfAbsent('URLs', () => URLs.map((e) => e.toString()).toList()); + Map? result = + (await _staticChannel.invokeMethod("prewarmConnections", args)) + ?.cast(); + return PrewarmingToken.fromMap(result); + } + + @override + Future invalidatePrewarmingToken( + PrewarmingToken prewarmingToken) async { + Map args = {}; + args.putIfAbsent('prewarmingToken', () => prewarmingToken.toMap()); + await _staticChannel.invokeMethod("invalidatePrewarmingToken", args); + } + + @override + bool isOpened() { + return _isOpened; + } + + @override + @mustCallSuper + void dispose() { + super.dispose(); + disposeChannel(); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/chrome_safari_browser/main.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/chrome_safari_browser/main.dart new file mode 100644 index 00000000..9a6238b7 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/chrome_safari_browser/main.dart @@ -0,0 +1 @@ +export 'chrome_safari_browser.dart'; diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/cookie_manager.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/cookie_manager.dart new file mode 100755 index 00000000..e640c9f0 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/cookie_manager.dart @@ -0,0 +1,433 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +import 'in_app_webview/headless_in_app_webview.dart'; +import 'platform_util.dart'; + +/// Object specifying creation parameters for creating a [IOSCookieManager]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformCookieManagerCreationParams] for +/// more information. +@immutable +class IOSCookieManagerCreationParams + extends PlatformCookieManagerCreationParams { + /// Creates a new [IOSCookieManagerCreationParams] instance. + const IOSCookieManagerCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformCookieManagerCreationParams params, + ) : super(); + + /// Creates a [IOSCookieManagerCreationParams] instance based on [PlatformCookieManagerCreationParams]. + factory IOSCookieManagerCreationParams.fromPlatformCookieManagerCreationParams( + PlatformCookieManagerCreationParams params) { + return IOSCookieManagerCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformCookieManager} +class IOSCookieManager extends PlatformCookieManager with ChannelController { + /// Creates a new [IOSCookieManager]. + IOSCookieManager(PlatformCookieManagerCreationParams params) + : super.implementation( + params is IOSCookieManagerCreationParams + ? params + : IOSCookieManagerCreationParams + .fromPlatformCookieManagerCreationParams(params), + ) { + channel = const MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_cookiemanager'); + handler = handleMethod; + initMethodCallHandler(); + } + + static IOSCookieManager? _instance; + + ///Gets the [IOSCookieManager] shared instance. + static IOSCookieManager instance() { + return (_instance != null) ? _instance! : _init(); + } + + static IOSCookieManager _init() { + _instance = IOSCookieManager(IOSCookieManagerCreationParams( + const PlatformCookieManagerCreationParams())); + return _instance!; + } + + Future _handleMethod(MethodCall call) async {} + + @override + Future setCookie( + {required WebUri url, + required String name, + required String value, + String path = "/", + String? domain, + int? expiresDate, + int? maxAge, + bool? isSecure, + bool? isHttpOnly, + HTTPCookieSameSitePolicy? sameSite, + @Deprecated("Use webViewController instead") + PlatformInAppWebViewController? iosBelow11WebViewController, + PlatformInAppWebViewController? webViewController}) async { + webViewController = webViewController ?? iosBelow11WebViewController; + + assert(url.toString().isNotEmpty); + assert(name.isNotEmpty); + assert(value.isNotEmpty); + assert(path.isNotEmpty); + + if (await _shouldUseJavascript()) { + await _setCookieWithJavaScript( + url: url, + name: name, + value: value, + domain: domain, + path: path, + expiresDate: expiresDate, + maxAge: maxAge, + isSecure: isSecure, + sameSite: sameSite, + webViewController: webViewController); + return true; + } + + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + args.putIfAbsent('name', () => name); + args.putIfAbsent('value', () => value); + args.putIfAbsent('domain', () => domain); + args.putIfAbsent('path', () => path); + args.putIfAbsent('expiresDate', () => expiresDate?.toString()); + args.putIfAbsent('maxAge', () => maxAge); + args.putIfAbsent('isSecure', () => isSecure); + args.putIfAbsent('isHttpOnly', () => isHttpOnly); + args.putIfAbsent('sameSite', () => sameSite?.toNativeValue()); + + return await channel?.invokeMethod('setCookie', args) ?? false; + } + + Future _setCookieWithJavaScript( + {required WebUri url, + required String name, + required String value, + String path = "/", + String? domain, + int? expiresDate, + int? maxAge, + bool? isSecure, + HTTPCookieSameSitePolicy? sameSite, + PlatformInAppWebViewController? webViewController}) async { + var cookieValue = name + "=" + value + "; Path=" + path; + + if (domain != null) cookieValue += "; Domain=" + domain; + + if (expiresDate != null) + cookieValue += "; Expires=" + await _getCookieExpirationDate(expiresDate); + + if (maxAge != null) cookieValue += "; Max-Age=" + maxAge.toString(); + + if (isSecure != null && isSecure) cookieValue += "; Secure"; + + if (sameSite != null) + cookieValue += "; SameSite=" + sameSite.toNativeValue(); + + cookieValue += ";"; + + if (webViewController != null) { + final javaScriptEnabled = + (await webViewController.getSettings())?.javaScriptEnabled ?? false; + if (javaScriptEnabled) { + await webViewController.evaluateJavascript( + source: 'document.cookie="$cookieValue"'); + return; + } + } + + final setCookieCompleter = Completer(); + final headlessWebView = + IOSHeadlessInAppWebView(IOSHeadlessInAppWebViewCreationParams( + initialUrlRequest: URLRequest(url: url), + onLoadStop: (controller, url) async { + await controller.evaluateJavascript( + source: 'document.cookie="$cookieValue"'); + setCookieCompleter.complete(); + })); + await headlessWebView.run(); + await setCookieCompleter.future; + await headlessWebView.dispose(); + } + + @override + Future> getCookies( + {required WebUri url, + @Deprecated("Use webViewController instead") + PlatformInAppWebViewController? iosBelow11WebViewController, + PlatformInAppWebViewController? webViewController}) async { + assert(url.toString().isNotEmpty); + + webViewController = webViewController ?? iosBelow11WebViewController; + + if (await _shouldUseJavascript()) { + return await _getCookiesWithJavaScript( + url: url, webViewController: webViewController); + } + + List cookies = []; + + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + List cookieListMap = + await channel?.invokeMethod('getCookies', args) ?? []; + cookieListMap = cookieListMap.cast>(); + + cookieListMap.forEach((cookieMap) { + cookies.add(Cookie( + name: cookieMap["name"], + value: cookieMap["value"], + expiresDate: cookieMap["expiresDate"], + isSessionOnly: cookieMap["isSessionOnly"], + domain: cookieMap["domain"], + sameSite: + HTTPCookieSameSitePolicy.fromNativeValue(cookieMap["sameSite"]), + isSecure: cookieMap["isSecure"], + isHttpOnly: cookieMap["isHttpOnly"], + path: cookieMap["path"])); + }); + return cookies; + } + + Future> _getCookiesWithJavaScript( + {required WebUri url, + PlatformInAppWebViewController? webViewController}) async { + assert(url.toString().isNotEmpty); + + List cookies = []; + + if (webViewController != null) { + final javaScriptEnabled = + (await webViewController.getSettings())?.javaScriptEnabled ?? false; + if (javaScriptEnabled) { + List documentCookies = (await webViewController + .evaluateJavascript(source: 'document.cookie') as String) + .split(';') + .map((documentCookie) => documentCookie.trim()) + .toList(); + documentCookies.forEach((documentCookie) { + List cookie = documentCookie.split('='); + if (cookie.length > 1) { + cookies.add(Cookie( + name: cookie[0], + value: cookie[1], + )); + } + }); + return cookies; + } + } + + final pageLoaded = Completer(); + final headlessWebView = + IOSHeadlessInAppWebView(IOSHeadlessInAppWebViewCreationParams( + initialUrlRequest: URLRequest(url: url), + onLoadStop: (controller, url) async { + pageLoaded.complete(); + }, + )); + await headlessWebView.run(); + await pageLoaded.future; + + List documentCookies = (await headlessWebView.webViewController! + .evaluateJavascript(source: 'document.cookie') as String) + .split(';') + .map((documentCookie) => documentCookie.trim()) + .toList(); + documentCookies.forEach((documentCookie) { + List cookie = documentCookie.split('='); + if (cookie.length > 1) { + cookies.add(Cookie( + name: cookie[0], + value: cookie[1], + )); + } + }); + await headlessWebView.dispose(); + return cookies; + } + + @override + Future getCookie( + {required WebUri url, + required String name, + @Deprecated("Use webViewController instead") + PlatformInAppWebViewController? iosBelow11WebViewController, + PlatformInAppWebViewController? webViewController}) async { + assert(url.toString().isNotEmpty); + assert(name.isNotEmpty); + + webViewController = webViewController ?? iosBelow11WebViewController; + + if (await _shouldUseJavascript()) { + List cookies = await _getCookiesWithJavaScript( + url: url, webViewController: webViewController); + return cookies + .cast() + .firstWhere((cookie) => cookie!.name == name, orElse: () => null); + } + + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + List cookies = + await channel?.invokeMethod('getCookies', args) ?? []; + cookies = cookies.cast>(); + for (var i = 0; i < cookies.length; i++) { + cookies[i] = cookies[i].cast(); + if (cookies[i]["name"] == name) + return Cookie( + name: cookies[i]["name"], + value: cookies[i]["value"], + expiresDate: cookies[i]["expiresDate"], + isSessionOnly: cookies[i]["isSessionOnly"], + domain: cookies[i]["domain"], + sameSite: HTTPCookieSameSitePolicy.fromNativeValue( + cookies[i]["sameSite"]), + isSecure: cookies[i]["isSecure"], + isHttpOnly: cookies[i]["isHttpOnly"], + path: cookies[i]["path"]); + } + return null; + } + + @override + Future deleteCookie( + {required WebUri url, + required String name, + String path = "/", + String? domain, + @Deprecated("Use webViewController instead") + PlatformInAppWebViewController? iosBelow11WebViewController, + PlatformInAppWebViewController? webViewController}) async { + assert(url.toString().isNotEmpty); + assert(name.isNotEmpty); + + webViewController = webViewController ?? iosBelow11WebViewController; + + if (await _shouldUseJavascript()) { + await _setCookieWithJavaScript( + url: url, + name: name, + value: "", + path: path, + domain: domain, + maxAge: -1, + webViewController: webViewController); + return true; + } + + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + args.putIfAbsent('name', () => name); + args.putIfAbsent('domain', () => domain); + args.putIfAbsent('path', () => path); + return await channel?.invokeMethod('deleteCookie', args) ?? false; + } + + @override + Future deleteCookies( + {required WebUri url, + String path = "/", + String? domain, + @Deprecated("Use webViewController instead") + PlatformInAppWebViewController? iosBelow11WebViewController, + PlatformInAppWebViewController? webViewController}) async { + assert(url.toString().isNotEmpty); + + webViewController = webViewController ?? iosBelow11WebViewController; + + if (await _shouldUseJavascript()) { + List cookies = await _getCookiesWithJavaScript( + url: url, webViewController: webViewController); + for (var i = 0; i < cookies.length; i++) { + await _setCookieWithJavaScript( + url: url, + name: cookies[i].name, + value: "", + path: path, + domain: domain, + maxAge: -1, + webViewController: webViewController); + } + return true; + } + + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + args.putIfAbsent('domain', () => domain); + args.putIfAbsent('path', () => path); + return await channel?.invokeMethod('deleteCookies', args) ?? false; + } + + @override + Future deleteAllCookies() async { + Map args = {}; + return await channel?.invokeMethod('deleteAllCookies', args) ?? false; + } + + @override + Future> getAllCookies() async { + List cookies = []; + + Map args = {}; + List cookieListMap = + await channel?.invokeMethod('getAllCookies', args) ?? []; + cookieListMap = cookieListMap.cast>(); + + cookieListMap.forEach((cookieMap) { + cookies.add(Cookie( + name: cookieMap["name"], + value: cookieMap["value"], + expiresDate: cookieMap["expiresDate"], + isSessionOnly: cookieMap["isSessionOnly"], + domain: cookieMap["domain"], + sameSite: + HTTPCookieSameSitePolicy.fromNativeValue(cookieMap["sameSite"]), + isSecure: cookieMap["isSecure"], + isHttpOnly: cookieMap["isHttpOnly"], + path: cookieMap["path"])); + }); + return cookies; + } + + Future _getCookieExpirationDate(int expiresDate) async { + var platformUtil = PlatformUtil.instance(); + var dateTime = DateTime.fromMillisecondsSinceEpoch(expiresDate).toUtc(); + return !kIsWeb + ? await platformUtil.formatDate( + date: dateTime, + format: 'EEE, dd MMM yyyy hh:mm:ss z', + locale: 'en_US', + timezone: 'GMT') + : await platformUtil.getWebCookieExpirationDate(date: dateTime); + } + + Future _shouldUseJavascript() async { + final platformUtil = PlatformUtil.instance(); + final systemVersion = await platformUtil.getSystemVersion(); + return systemVersion.compareTo("10.13") == -1; + } + + @override + void dispose() { + // empty + } +} + +extension InternalCookieManager on IOSCookieManager { + get handleMethod => _handleMethod; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/find_interaction/find_interaction_controller.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/find_interaction/find_interaction_controller.dart new file mode 100644 index 00000000..b262a12f --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/find_interaction/find_interaction_controller.dart @@ -0,0 +1,148 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [IOSFindInteractionController]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformFindInteractionControllerCreationParams] for +/// more information. +@immutable +class IOSFindInteractionControllerCreationParams + extends PlatformFindInteractionControllerCreationParams { + /// Creates a new [IOSFindInteractionControllerCreationParams] instance. + const IOSFindInteractionControllerCreationParams( + {super.onFindResultReceived}); + + /// Creates a [IOSFindInteractionControllerCreationParams] instance based on [PlatformFindInteractionControllerCreationParams]. + factory IOSFindInteractionControllerCreationParams.fromPlatformFindInteractionControllerCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformFindInteractionControllerCreationParams params) { + return IOSFindInteractionControllerCreationParams( + onFindResultReceived: params.onFindResultReceived); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController} +class IOSFindInteractionController extends PlatformFindInteractionController + with ChannelController { + /// Constructs a [IOSFindInteractionController]. + IOSFindInteractionController( + PlatformFindInteractionControllerCreationParams params) + : super.implementation( + params is IOSFindInteractionControllerCreationParams + ? params + : IOSFindInteractionControllerCreationParams + .fromPlatformFindInteractionControllerCreationParams(params), + ); + + _debugLog(String method, dynamic args) { + debugLog( + className: this.runtimeType.toString(), + debugLoggingSettings: + PlatformFindInteractionController.debugLoggingSettings, + method: method, + args: args); + } + + Future _handleMethod(MethodCall call) async { + _debugLog(call.method, call.arguments); + + switch (call.method) { + case "onFindResultReceived": + if (onFindResultReceived != null) { + int activeMatchOrdinal = call.arguments["activeMatchOrdinal"]; + int numberOfMatches = call.arguments["numberOfMatches"]; + bool isDoneCounting = call.arguments["isDoneCounting"]; + onFindResultReceived!( + this, activeMatchOrdinal, numberOfMatches, isDoneCounting); + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.findAll} + Future findAll({String? find}) async { + Map args = {}; + args.putIfAbsent('find', () => find); + await channel?.invokeMethod('findAll', args); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.findNext} + Future findNext({bool forward = true}) async { + Map args = {}; + args.putIfAbsent('forward', () => forward); + await channel?.invokeMethod('findNext', args); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.clearMatches} + Future clearMatches() async { + Map args = {}; + await channel?.invokeMethod('clearMatches', args); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.setSearchText} + Future setSearchText(String? searchText) async { + Map args = {}; + args.putIfAbsent('searchText', () => searchText); + await channel?.invokeMethod('setSearchText', args); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.getSearchText} + Future getSearchText() async { + Map args = {}; + return await channel?.invokeMethod('getSearchText', args); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.isFindNavigatorVisible} + Future isFindNavigatorVisible() async { + Map args = {}; + return await channel?.invokeMethod('isFindNavigatorVisible', args); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.updateResultCount} + Future updateResultCount() async { + Map args = {}; + await channel?.invokeMethod('updateResultCount', args); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.presentFindNavigator} + Future presentFindNavigator() async { + Map args = {}; + await channel?.invokeMethod('presentFindNavigator', args); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.dismissFindNavigator} + Future dismissFindNavigator() async { + Map args = {}; + await channel?.invokeMethod('dismissFindNavigator', args); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.getActiveFindSession} + Future getActiveFindSession() async { + Map args = {}; + Map? result = + (await channel?.invokeMethod('getActiveFindSession', args)) + ?.cast(); + return FindSession.fromMap(result); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.dispose} + @override + void dispose({bool isKeepAlive = false}) { + disposeChannel(removeMethodCallHandler: !isKeepAlive); + } +} + +extension InternalFindInteractionController on IOSFindInteractionController { + void init(dynamic id) { + channel = MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_find_interaction_$id'); + handler = _handleMethod; + initMethodCallHandler(); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/find_interaction/main.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/find_interaction/main.dart new file mode 100644 index 00000000..a7adaacf --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/find_interaction/main.dart @@ -0,0 +1,2 @@ +export 'find_interaction_controller.dart' + hide InternalFindInteractionController; diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/http_auth_credentials_database.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/http_auth_credentials_database.dart new file mode 100755 index 00000000..cc2dc8df --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/http_auth_credentials_database.dart @@ -0,0 +1,154 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [IOSHttpAuthCredentialDatabase]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformHttpAuthCredentialDatabaseCreationParams] for +/// more information. +@immutable +class IOSHttpAuthCredentialDatabaseCreationParams + extends PlatformHttpAuthCredentialDatabaseCreationParams { + /// Creates a new [IOSHttpAuthCredentialDatabaseCreationParams] instance. + const IOSHttpAuthCredentialDatabaseCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformHttpAuthCredentialDatabaseCreationParams params, + ) : super(); + + /// Creates a [IOSHttpAuthCredentialDatabaseCreationParams] instance based on [PlatformHttpAuthCredentialDatabaseCreationParams]. + factory IOSHttpAuthCredentialDatabaseCreationParams.fromPlatformHttpAuthCredentialDatabaseCreationParams( + PlatformHttpAuthCredentialDatabaseCreationParams params) { + return IOSHttpAuthCredentialDatabaseCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformHttpAuthCredentialDatabase} +class IOSHttpAuthCredentialDatabase extends PlatformHttpAuthCredentialDatabase + with ChannelController { + /// Creates a new [IOSHttpAuthCredentialDatabase]. + IOSHttpAuthCredentialDatabase( + PlatformHttpAuthCredentialDatabaseCreationParams params) + : super.implementation( + params is IOSHttpAuthCredentialDatabaseCreationParams + ? params + : IOSHttpAuthCredentialDatabaseCreationParams + .fromPlatformHttpAuthCredentialDatabaseCreationParams(params), + ) { + channel = const MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_credential_database'); + handler = handleMethod; + initMethodCallHandler(); + } + + static IOSHttpAuthCredentialDatabase? _instance; + + ///Gets the database shared instance. + static IOSHttpAuthCredentialDatabase instance() { + return (_instance != null) ? _instance! : _init(); + } + + static IOSHttpAuthCredentialDatabase _init() { + _instance = IOSHttpAuthCredentialDatabase( + IOSHttpAuthCredentialDatabaseCreationParams( + const PlatformHttpAuthCredentialDatabaseCreationParams())); + return _instance!; + } + + Future _handleMethod(MethodCall call) async {} + + @override + Future> + getAllAuthCredentials() async { + Map args = {}; + List allCredentials = + await channel?.invokeMethod('getAllAuthCredentials', args) ?? []; + + List result = []; + + for (Map map in allCredentials) { + var element = URLProtectionSpaceHttpAuthCredentials.fromMap( + map.cast()); + if (element != null) { + result.add(element); + } + } + return result; + } + + @override + Future> getHttpAuthCredentials( + {required URLProtectionSpace protectionSpace}) async { + Map args = {}; + args.putIfAbsent("host", () => protectionSpace.host); + args.putIfAbsent("protocol", () => protectionSpace.protocol); + args.putIfAbsent("realm", () => protectionSpace.realm); + args.putIfAbsent("port", () => protectionSpace.port); + List credentialList = + await channel?.invokeMethod('getHttpAuthCredentials', args) ?? []; + List credentials = []; + for (Map map in credentialList) { + var credential = URLCredential.fromMap(map.cast()); + if (credential != null) { + credentials.add(credential); + } + } + return credentials; + } + + @override + Future setHttpAuthCredential( + {required URLProtectionSpace protectionSpace, + required URLCredential credential}) async { + Map args = {}; + args.putIfAbsent("host", () => protectionSpace.host); + args.putIfAbsent("protocol", () => protectionSpace.protocol); + args.putIfAbsent("realm", () => protectionSpace.realm); + args.putIfAbsent("port", () => protectionSpace.port); + args.putIfAbsent("username", () => credential.username); + args.putIfAbsent("password", () => credential.password); + await channel?.invokeMethod('setHttpAuthCredential', args); + } + + @override + Future removeHttpAuthCredential( + {required URLProtectionSpace protectionSpace, + required URLCredential credential}) async { + Map args = {}; + args.putIfAbsent("host", () => protectionSpace.host); + args.putIfAbsent("protocol", () => protectionSpace.protocol); + args.putIfAbsent("realm", () => protectionSpace.realm); + args.putIfAbsent("port", () => protectionSpace.port); + args.putIfAbsent("username", () => credential.username); + args.putIfAbsent("password", () => credential.password); + await channel?.invokeMethod('removeHttpAuthCredential', args); + } + + @override + Future removeHttpAuthCredentials( + {required URLProtectionSpace protectionSpace}) async { + Map args = {}; + args.putIfAbsent("host", () => protectionSpace.host); + args.putIfAbsent("protocol", () => protectionSpace.protocol); + args.putIfAbsent("realm", () => protectionSpace.realm); + args.putIfAbsent("port", () => protectionSpace.port); + await channel?.invokeMethod('removeHttpAuthCredentials', args); + } + + @override + Future clearAllAuthCredentials() async { + Map args = {}; + await channel?.invokeMethod('clearAllAuthCredentials', args); + } + + @override + void dispose() { + // empty + } +} + +extension InternalHttpAuthCredentialDatabase on IOSHttpAuthCredentialDatabase { + get handleMethod => _handleMethod; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/in_app_browser/in_app_browser.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/in_app_browser/in_app_browser.dart new file mode 100755 index 00000000..e8da5864 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/in_app_browser/in_app_browser.dart @@ -0,0 +1,374 @@ +import 'dart:async'; +import 'dart:collection'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +import '../find_interaction/find_interaction_controller.dart'; +import '../in_app_webview/in_app_webview_controller.dart'; +import '../pull_to_refresh/pull_to_refresh_controller.dart'; + +/// Object specifying creation parameters for creating a [IOSInAppBrowser]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformInAppBrowserCreationParams] for +/// more information. +class IOSInAppBrowserCreationParams extends PlatformInAppBrowserCreationParams { + /// Creates a new [IOSInAppBrowserCreationParams] instance. + IOSInAppBrowserCreationParams( + {super.contextMenu, + this.pullToRefreshController, + this.findInteractionController, + super.initialUserScripts, + super.windowId}); + + /// Creates a [IOSInAppBrowserCreationParams] instance based on [PlatformInAppBrowserCreationParams]. + factory IOSInAppBrowserCreationParams.fromPlatformInAppBrowserCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformInAppBrowserCreationParams params) { + return IOSInAppBrowserCreationParams( + contextMenu: params.contextMenu, + pullToRefreshController: + params.pullToRefreshController as IOSPullToRefreshController?, + findInteractionController: + params.findInteractionController as IOSFindInteractionController?, + initialUserScripts: params.initialUserScripts, + windowId: params.windowId); + } + + @override + final IOSFindInteractionController? findInteractionController; + + @override + final IOSPullToRefreshController? pullToRefreshController; +} + +///{@macro flutter_inappwebview_platform_interface.PlatformInAppBrowser} +class IOSInAppBrowser extends PlatformInAppBrowser with ChannelController { + @override + final String id = IdGenerator.generate(); + + /// Constructs a [IOSInAppBrowser]. + IOSInAppBrowser(PlatformInAppBrowserCreationParams params) + : super.implementation( + params is IOSInAppBrowserCreationParams + ? params + : IOSInAppBrowserCreationParams + .fromPlatformInAppBrowserCreationParams(params), + ) { + _contextMenu = params.contextMenu; + } + + static final IOSInAppBrowser _staticValue = + IOSInAppBrowser(IOSInAppBrowserCreationParams()); + + /// Provide static access. + factory IOSInAppBrowser.static() { + return _staticValue; + } + + IOSInAppBrowserCreationParams get _iosParams => + params as IOSInAppBrowserCreationParams; + + static const MethodChannel _staticChannel = + const MethodChannel('com.pichillilorenzo/flutter_inappbrowser'); + + ContextMenu? _contextMenu; + + @override + ContextMenu? get contextMenu => _contextMenu; + + Map _menuItems = HashMap(); + bool _isOpened = false; + IOSInAppWebViewController? _webViewController; + + @override + IOSInAppWebViewController? get webViewController { + return _isOpened ? _webViewController : null; + } + + _init() { + channel = MethodChannel('com.pichillilorenzo/flutter_inappbrowser_$id'); + handler = _handleMethod; + initMethodCallHandler(); + + _webViewController = IOSInAppWebViewController.fromInAppBrowser( + IOSInAppWebViewControllerCreationParams(id: id), + channel!, + this, + this.initialUserScripts); + _iosParams.pullToRefreshController?.init(id); + _iosParams.findInteractionController?.init(id); + } + + _debugLog(String method, dynamic args) { + debugLog( + className: this.runtimeType.toString(), + id: id, + debugLoggingSettings: PlatformInAppBrowser.debugLoggingSettings, + method: method, + args: args); + } + + Future _handleMethod(MethodCall call) async { + switch (call.method) { + case "onBrowserCreated": + _debugLog(call.method, call.arguments); + eventHandler?.onBrowserCreated(); + break; + case "onMenuItemClicked": + _debugLog(call.method, call.arguments); + int id = call.arguments["id"].toInt(); + if (this._menuItems[id] != null) { + if (this._menuItems[id]?.onClick != null) { + this._menuItems[id]?.onClick!(); + } + } + break; + case "onExit": + _debugLog(call.method, call.arguments); + _isOpened = false; + final onExit = eventHandler?.onExit; + dispose(); + onExit?.call(); + break; + default: + return _webViewController?.handleMethod(call); + } + } + + Map _prepareOpenRequest( + {@Deprecated('Use settings instead') InAppBrowserClassOptions? options, + InAppBrowserClassSettings? settings}) { + assert(!_isOpened, 'The browser is already opened.'); + _isOpened = true; + _init(); + + var initialSettings = settings?.toMap() ?? + options?.toMap() ?? + InAppBrowserClassSettings().toMap(); + + Map pullToRefreshSettings = + pullToRefreshController?.settings.toMap() ?? + pullToRefreshController?.options.toMap() ?? + PullToRefreshSettings(enabled: false).toMap(); + + List> menuItemList = []; + _menuItems.forEach((key, value) { + menuItemList.add(value.toMap()); + }); + + Map args = {}; + args.putIfAbsent('id', () => id); + args.putIfAbsent('settings', () => initialSettings); + args.putIfAbsent('contextMenu', () => contextMenu?.toMap() ?? {}); + args.putIfAbsent('windowId', () => windowId); + args.putIfAbsent('initialUserScripts', + () => initialUserScripts?.map((e) => e.toMap()).toList() ?? []); + args.putIfAbsent('pullToRefreshSettings', () => pullToRefreshSettings); + args.putIfAbsent('menuItems', () => menuItemList); + return args; + } + + @override + Future openUrlRequest( + {required URLRequest urlRequest, + // ignore: deprecated_member_use_from_same_package + @Deprecated('Use settings instead') InAppBrowserClassOptions? options, + InAppBrowserClassSettings? settings}) async { + assert(urlRequest.url != null && urlRequest.url.toString().isNotEmpty); + + Map args = + _prepareOpenRequest(options: options, settings: settings); + args.putIfAbsent('urlRequest', () => urlRequest.toMap()); + await _staticChannel.invokeMethod('open', args); + } + + @override + Future openFile( + {required String assetFilePath, + // ignore: deprecated_member_use_from_same_package + @Deprecated('Use settings instead') InAppBrowserClassOptions? options, + InAppBrowserClassSettings? settings}) async { + assert(assetFilePath.isNotEmpty); + + Map args = + _prepareOpenRequest(options: options, settings: settings); + args.putIfAbsent('assetFilePath', () => assetFilePath); + await _staticChannel.invokeMethod('open', args); + } + + @override + Future openData( + {required String data, + String mimeType = "text/html", + String encoding = "utf8", + WebUri? baseUrl, + @Deprecated("Use historyUrl instead") Uri? androidHistoryUrl, + WebUri? historyUrl, + // ignore: deprecated_member_use_from_same_package + @Deprecated('Use settings instead') InAppBrowserClassOptions? options, + InAppBrowserClassSettings? settings}) async { + Map args = + _prepareOpenRequest(options: options, settings: settings); + args.putIfAbsent('data', () => data); + args.putIfAbsent('mimeType', () => mimeType); + args.putIfAbsent('encoding', () => encoding); + args.putIfAbsent('baseUrl', () => baseUrl?.toString() ?? "about:blank"); + args.putIfAbsent('historyUrl', + () => (historyUrl ?? androidHistoryUrl)?.toString() ?? "about:blank"); + await _staticChannel.invokeMethod('open', args); + } + + @override + Future openWithSystemBrowser({required WebUri url}) async { + assert(url.toString().isNotEmpty); + + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + return await _staticChannel.invokeMethod('openWithSystemBrowser', args); + } + + @override + void addMenuItem(InAppBrowserMenuItem menuItem) { + _menuItems[menuItem.id] = menuItem; + } + + @override + void addMenuItems(List menuItems) { + menuItems.forEach((menuItem) { + _menuItems[menuItem.id] = menuItem; + }); + } + + @override + bool removeMenuItem(InAppBrowserMenuItem menuItem) { + return _menuItems.remove(menuItem.id) != null; + } + + @override + void removeMenuItems(List menuItems) { + for (final menuItem in menuItems) { + removeMenuItem(menuItem); + } + } + + @override + void removeAllMenuItem() { + _menuItems.clear(); + } + + @override + bool hasMenuItem(InAppBrowserMenuItem menuItem) { + return _menuItems.containsKey(menuItem.id); + } + + @override + Future show() async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + await channel?.invokeMethod('show', args); + } + + @override + Future hide() async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + await channel?.invokeMethod('hide', args); + } + + @override + Future close() async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + await channel?.invokeMethod('close', args); + } + + @override + Future isHidden() async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + return await channel?.invokeMethod('isHidden', args) ?? false; + } + + @override + @Deprecated('Use setSettings instead') + Future setOptions({required InAppBrowserClassOptions options}) async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + args.putIfAbsent('settings', () => options.toMap()); + await channel?.invokeMethod('setSettings', args); + } + + @override + @Deprecated('Use getSettings instead') + Future getOptions() async { + assert(_isOpened, 'The browser is not opened.'); + Map args = {}; + + Map? options = + await channel?.invokeMethod('getSettings', args); + if (options != null) { + options = options.cast(); + return InAppBrowserClassOptions.fromMap(options as Map); + } + + return null; + } + + @override + Future setSettings( + {required InAppBrowserClassSettings settings}) async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + args.putIfAbsent('settings', () => settings.toMap()); + await channel?.invokeMethod('setSettings', args); + } + + @override + Future getSettings() async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + + Map? settings = + await channel?.invokeMethod('getSettings', args); + if (settings != null) { + settings = settings.cast(); + return InAppBrowserClassSettings.fromMap( + settings as Map); + } + + return null; + } + + @override + bool isOpened() { + return this._isOpened; + } + + @override + @mustCallSuper + void dispose() { + super.dispose(); + disposeChannel(); + _webViewController?.dispose(); + _webViewController = null; + pullToRefreshController?.dispose(); + findInteractionController?.dispose(); + } +} + +extension InternalInAppBrowser on IOSInAppBrowser { + void setContextMenu(ContextMenu? contextMenu) { + _contextMenu = contextMenu; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/in_app_browser/main.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/in_app_browser/main.dart new file mode 100644 index 00000000..e11eb8b1 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/in_app_browser/main.dart @@ -0,0 +1 @@ +export 'in_app_browser.dart' hide InternalInAppBrowser; diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/in_app_webview/_static_channel.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/in_app_webview/_static_channel.dart new file mode 100644 index 00000000..beb7de70 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/in_app_webview/_static_channel.dart @@ -0,0 +1,4 @@ +import 'package:flutter/services.dart'; + +const IN_APP_WEBVIEW_STATIC_CHANNEL = + MethodChannel('com.pichillilorenzo/flutter_inappwebview_manager'); diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/in_app_webview/headless_in_app_webview.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/in_app_webview/headless_in_app_webview.dart new file mode 100644 index 00000000..0e9da653 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/in_app_webview/headless_in_app_webview.dart @@ -0,0 +1,439 @@ +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; +import '../find_interaction/find_interaction_controller.dart'; +import '../pull_to_refresh/pull_to_refresh_controller.dart'; +import 'in_app_webview_controller.dart'; + +/// Object specifying creation parameters for creating a [IOSHeadlessInAppWebView]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformHeadlessInAppWebViewCreationParams] for +/// more information. +@immutable +class IOSHeadlessInAppWebViewCreationParams + extends PlatformHeadlessInAppWebViewCreationParams { + /// Creates a new [IOSHeadlessInAppWebViewCreationParams] instance. + IOSHeadlessInAppWebViewCreationParams( + {super.controllerFromPlatform, + super.initialSize, + super.windowId, + super.onWebViewCreated, + super.onLoadStart, + super.onLoadStop, + @Deprecated('Use onReceivedError instead') super.onLoadError, + super.onReceivedError, + @Deprecated("Use onReceivedHttpError instead") super.onLoadHttpError, + super.onReceivedHttpError, + super.onProgressChanged, + super.onConsoleMessage, + super.shouldOverrideUrlLoading, + super.onLoadResource, + super.onScrollChanged, + @Deprecated('Use onDownloadStartRequest instead') super.onDownloadStart, + super.onDownloadStartRequest, + @Deprecated('Use onLoadResourceWithCustomScheme instead') + super.onLoadResourceCustomScheme, + super.onLoadResourceWithCustomScheme, + super.onCreateWindow, + super.onCloseWindow, + super.onJsAlert, + super.onJsConfirm, + super.onJsPrompt, + super.onReceivedHttpAuthRequest, + super.onReceivedServerTrustAuthRequest, + super.onReceivedClientCertRequest, + @Deprecated('Use FindInteractionController.onFindResultReceived instead') + super.onFindResultReceived, + super.shouldInterceptAjaxRequest, + super.onAjaxReadyStateChange, + super.onAjaxProgress, + super.shouldInterceptFetchRequest, + super.onUpdateVisitedHistory, + @Deprecated("Use onPrintRequest instead") super.onPrint, + super.onPrintRequest, + super.onLongPressHitTestResult, + super.onEnterFullscreen, + super.onExitFullscreen, + super.onPageCommitVisible, + super.onTitleChanged, + super.onWindowFocus, + super.onWindowBlur, + super.onOverScrolled, + super.onZoomScaleChanged, + @Deprecated('Use onSafeBrowsingHit instead') + super.androidOnSafeBrowsingHit, + super.onSafeBrowsingHit, + @Deprecated('Use onPermissionRequest instead') + super.androidOnPermissionRequest, + super.onPermissionRequest, + @Deprecated('Use onGeolocationPermissionsShowPrompt instead') + super.androidOnGeolocationPermissionsShowPrompt, + super.onGeolocationPermissionsShowPrompt, + @Deprecated('Use onGeolocationPermissionsHidePrompt instead') + super.androidOnGeolocationPermissionsHidePrompt, + super.onGeolocationPermissionsHidePrompt, + @Deprecated('Use shouldInterceptRequest instead') + super.androidShouldInterceptRequest, + super.shouldInterceptRequest, + @Deprecated('Use onRenderProcessGone instead') + super.androidOnRenderProcessGone, + super.onRenderProcessGone, + @Deprecated('Use onRenderProcessResponsive instead') + super.androidOnRenderProcessResponsive, + super.onRenderProcessResponsive, + @Deprecated('Use onRenderProcessUnresponsive instead') + super.androidOnRenderProcessUnresponsive, + super.onRenderProcessUnresponsive, + @Deprecated('Use onFormResubmission instead') + super.androidOnFormResubmission, + super.onFormResubmission, + @Deprecated('Use onZoomScaleChanged instead') super.androidOnScaleChanged, + @Deprecated('Use onReceivedIcon instead') super.androidOnReceivedIcon, + super.onReceivedIcon, + @Deprecated('Use onReceivedTouchIconUrl instead') + super.androidOnReceivedTouchIconUrl, + super.onReceivedTouchIconUrl, + @Deprecated('Use onJsBeforeUnload instead') super.androidOnJsBeforeUnload, + super.onJsBeforeUnload, + @Deprecated('Use onReceivedLoginRequest instead') + super.androidOnReceivedLoginRequest, + super.onReceivedLoginRequest, + super.onPermissionRequestCanceled, + super.onRequestFocus, + @Deprecated('Use onWebContentProcessDidTerminate instead') + super.iosOnWebContentProcessDidTerminate, + super.onWebContentProcessDidTerminate, + @Deprecated( + 'Use onDidReceiveServerRedirectForProvisionalNavigation instead') + super.iosOnDidReceiveServerRedirectForProvisionalNavigation, + super.onDidReceiveServerRedirectForProvisionalNavigation, + @Deprecated('Use onNavigationResponse instead') + super.iosOnNavigationResponse, + super.onNavigationResponse, + @Deprecated('Use shouldAllowDeprecatedTLS instead') + super.iosShouldAllowDeprecatedTLS, + super.shouldAllowDeprecatedTLS, + super.onCameraCaptureStateChanged, + super.onMicrophoneCaptureStateChanged, + super.onContentSizeChanged, + super.initialUrlRequest, + super.initialFile, + super.initialData, + @Deprecated('Use initialSettings instead') super.initialOptions, + super.initialSettings, + super.contextMenu, + super.initialUserScripts, + this.pullToRefreshController, + this.findInteractionController}); + + /// Creates a [IOSHeadlessInAppWebViewCreationParams] instance based on [PlatformHeadlessInAppWebViewCreationParams]. + IOSHeadlessInAppWebViewCreationParams.fromPlatformHeadlessInAppWebViewCreationParams( + PlatformHeadlessInAppWebViewCreationParams params) + : this( + controllerFromPlatform: params.controllerFromPlatform, + initialSize: params.initialSize, + windowId: params.windowId, + onWebViewCreated: params.onWebViewCreated, + onLoadStart: params.onLoadStart, + onLoadStop: params.onLoadStop, + onLoadError: params.onLoadError, + onReceivedError: params.onReceivedError, + onLoadHttpError: params.onLoadHttpError, + onReceivedHttpError: params.onReceivedHttpError, + onProgressChanged: params.onProgressChanged, + onConsoleMessage: params.onConsoleMessage, + shouldOverrideUrlLoading: params.shouldOverrideUrlLoading, + onLoadResource: params.onLoadResource, + onScrollChanged: params.onScrollChanged, + onDownloadStart: params.onDownloadStart, + onDownloadStartRequest: params.onDownloadStartRequest, + onLoadResourceCustomScheme: params.onLoadResourceCustomScheme, + onLoadResourceWithCustomScheme: + params.onLoadResourceWithCustomScheme, + onCreateWindow: params.onCreateWindow, + onCloseWindow: params.onCloseWindow, + onJsAlert: params.onJsAlert, + onJsConfirm: params.onJsConfirm, + onJsPrompt: params.onJsPrompt, + onReceivedHttpAuthRequest: params.onReceivedHttpAuthRequest, + onReceivedServerTrustAuthRequest: + params.onReceivedServerTrustAuthRequest, + onReceivedClientCertRequest: params.onReceivedClientCertRequest, + onFindResultReceived: params.onFindResultReceived, + shouldInterceptAjaxRequest: params.shouldInterceptAjaxRequest, + onAjaxReadyStateChange: params.onAjaxReadyStateChange, + onAjaxProgress: params.onAjaxProgress, + shouldInterceptFetchRequest: params.shouldInterceptFetchRequest, + onUpdateVisitedHistory: params.onUpdateVisitedHistory, + onPrint: params.onPrint, + onPrintRequest: params.onPrintRequest, + onLongPressHitTestResult: params.onLongPressHitTestResult, + onEnterFullscreen: params.onEnterFullscreen, + onExitFullscreen: params.onExitFullscreen, + onPageCommitVisible: params.onPageCommitVisible, + onTitleChanged: params.onTitleChanged, + onWindowFocus: params.onWindowFocus, + onWindowBlur: params.onWindowBlur, + onOverScrolled: params.onOverScrolled, + onZoomScaleChanged: params.onZoomScaleChanged, + androidOnSafeBrowsingHit: params.androidOnSafeBrowsingHit, + onSafeBrowsingHit: params.onSafeBrowsingHit, + androidOnPermissionRequest: params.androidOnPermissionRequest, + onPermissionRequest: params.onPermissionRequest, + androidOnGeolocationPermissionsShowPrompt: + params.androidOnGeolocationPermissionsShowPrompt, + onGeolocationPermissionsShowPrompt: + params.onGeolocationPermissionsShowPrompt, + androidOnGeolocationPermissionsHidePrompt: + params.androidOnGeolocationPermissionsHidePrompt, + onGeolocationPermissionsHidePrompt: + params.onGeolocationPermissionsHidePrompt, + androidShouldInterceptRequest: params.androidShouldInterceptRequest, + shouldInterceptRequest: params.shouldInterceptRequest, + androidOnRenderProcessGone: params.androidOnRenderProcessGone, + onRenderProcessGone: params.onRenderProcessGone, + androidOnRenderProcessResponsive: + params.androidOnRenderProcessResponsive, + onRenderProcessResponsive: params.onRenderProcessResponsive, + androidOnRenderProcessUnresponsive: + params.androidOnRenderProcessUnresponsive, + onRenderProcessUnresponsive: params.onRenderProcessUnresponsive, + androidOnFormResubmission: params.androidOnFormResubmission, + onFormResubmission: params.onFormResubmission, + androidOnScaleChanged: params.androidOnScaleChanged, + androidOnReceivedIcon: params.androidOnReceivedIcon, + onReceivedIcon: params.onReceivedIcon, + androidOnReceivedTouchIconUrl: params.androidOnReceivedTouchIconUrl, + onReceivedTouchIconUrl: params.onReceivedTouchIconUrl, + androidOnJsBeforeUnload: params.androidOnJsBeforeUnload, + onJsBeforeUnload: params.onJsBeforeUnload, + androidOnReceivedLoginRequest: params.androidOnReceivedLoginRequest, + onReceivedLoginRequest: params.onReceivedLoginRequest, + onPermissionRequestCanceled: params.onPermissionRequestCanceled, + onRequestFocus: params.onRequestFocus, + iosOnWebContentProcessDidTerminate: + params.iosOnWebContentProcessDidTerminate, + onWebContentProcessDidTerminate: + params.onWebContentProcessDidTerminate, + iosOnDidReceiveServerRedirectForProvisionalNavigation: + params.iosOnDidReceiveServerRedirectForProvisionalNavigation, + onDidReceiveServerRedirectForProvisionalNavigation: + params.onDidReceiveServerRedirectForProvisionalNavigation, + iosOnNavigationResponse: params.iosOnNavigationResponse, + onNavigationResponse: params.onNavigationResponse, + iosShouldAllowDeprecatedTLS: params.iosShouldAllowDeprecatedTLS, + shouldAllowDeprecatedTLS: params.shouldAllowDeprecatedTLS, + onCameraCaptureStateChanged: params.onCameraCaptureStateChanged, + onMicrophoneCaptureStateChanged: + params.onMicrophoneCaptureStateChanged, + onContentSizeChanged: params.onContentSizeChanged, + initialUrlRequest: params.initialUrlRequest, + initialFile: params.initialFile, + initialData: params.initialData, + initialOptions: params.initialOptions, + initialSettings: params.initialSettings, + contextMenu: params.contextMenu, + initialUserScripts: params.initialUserScripts, + pullToRefreshController: + params.pullToRefreshController as IOSPullToRefreshController?, + findInteractionController: params.findInteractionController + as IOSFindInteractionController?); + + @override + final IOSFindInteractionController? findInteractionController; + + @override + final IOSPullToRefreshController? pullToRefreshController; +} + +///{@macro flutter_inappwebview_platform_interface.PlatformHeadlessInAppWebView} +class IOSHeadlessInAppWebView extends PlatformHeadlessInAppWebView + with ChannelController { + @override + late final String id; + + bool _started = false; + bool _running = false; + + static const MethodChannel _sharedChannel = + const MethodChannel('com.pichillilorenzo/flutter_headless_inappwebview'); + + IOSInAppWebViewController? _webViewController; + + /// Constructs a [IOSHeadlessInAppWebView]. + IOSHeadlessInAppWebView(PlatformHeadlessInAppWebViewCreationParams params) + : super.implementation( + params is IOSHeadlessInAppWebViewCreationParams + ? params + : IOSHeadlessInAppWebViewCreationParams + .fromPlatformHeadlessInAppWebViewCreationParams(params), + ) { + id = IdGenerator.generate(); + } + + @override + IOSInAppWebViewController? get webViewController => _webViewController; + + dynamic _controllerFromPlatform; + + IOSHeadlessInAppWebViewCreationParams get _iosParams => + params as IOSHeadlessInAppWebViewCreationParams; + + _init() { + _webViewController = IOSInAppWebViewController( + IOSInAppWebViewControllerCreationParams(id: id, webviewParams: params), + ); + _controllerFromPlatform = + params.controllerFromPlatform?.call(_webViewController!) ?? + _webViewController!; + _iosParams.pullToRefreshController?.init(id); + _iosParams.findInteractionController?.init(id); + channel = + MethodChannel('com.pichillilorenzo/flutter_headless_inappwebview_$id'); + handler = _handleMethod; + initMethodCallHandler(); + } + + Future _handleMethod(MethodCall call) async { + switch (call.method) { + case "onWebViewCreated": + if (params.onWebViewCreated != null && _webViewController != null) { + params.onWebViewCreated!(_controllerFromPlatform); + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + Future run() async { + if (_started) { + return; + } + _started = true; + _init(); + + final initialSettings = params.initialSettings ?? InAppWebViewSettings(); + _inferInitialSettings(initialSettings); + + Map settingsMap = + (params.initialSettings != null ? initialSettings.toMap() : null) ?? + params.initialOptions?.toMap() ?? + initialSettings.toMap(); + + Map pullToRefreshSettings = + _iosParams.pullToRefreshController?.params.settings.toMap() ?? + _iosParams.pullToRefreshController?.params.options.toMap() ?? + PullToRefreshSettings(enabled: false).toMap(); + + Map args = {}; + args.putIfAbsent('id', () => id); + args.putIfAbsent( + 'params', + () => { + 'initialUrlRequest': params.initialUrlRequest?.toMap(), + 'initialFile': params.initialFile, + 'initialData': params.initialData?.toMap(), + 'initialSettings': settingsMap, + 'contextMenu': params.contextMenu?.toMap() ?? {}, + 'windowId': params.windowId, + 'initialUserScripts': + params.initialUserScripts?.map((e) => e.toMap()).toList() ?? + [], + 'pullToRefreshSettings': pullToRefreshSettings, + 'initialSize': params.initialSize.toMap() + }); + await _sharedChannel.invokeMethod('run', args); + _running = true; + } + + void _inferInitialSettings(InAppWebViewSettings settings) { + if (params.shouldOverrideUrlLoading != null && + settings.useShouldOverrideUrlLoading == null) { + settings.useShouldOverrideUrlLoading = true; + } + if (params.onLoadResource != null && settings.useOnLoadResource == null) { + settings.useOnLoadResource = true; + } + if (params.onDownloadStartRequest != null && + settings.useOnDownloadStart == null) { + settings.useOnDownloadStart = true; + } + if (params.shouldInterceptAjaxRequest != null && + settings.useShouldInterceptAjaxRequest == null) { + settings.useShouldInterceptAjaxRequest = true; + } + if (params.shouldInterceptFetchRequest != null && + settings.useShouldInterceptFetchRequest == null) { + settings.useShouldInterceptFetchRequest = true; + } + if (params.shouldInterceptRequest != null && + settings.useShouldInterceptRequest == null) { + settings.useShouldInterceptRequest = true; + } + if (params.onRenderProcessGone != null && + settings.useOnRenderProcessGone == null) { + settings.useOnRenderProcessGone = true; + } + if (params.onNavigationResponse != null && + settings.useOnNavigationResponse == null) { + settings.useOnNavigationResponse = true; + } + } + + @override + bool isRunning() { + return _running; + } + + @override + Future setSize(Size size) async { + if (!_running) { + return; + } + + Map args = {}; + args.putIfAbsent('size', () => size.toMap()); + await channel?.invokeMethod('setSize', args); + } + + @override + Future getSize() async { + if (!_running) { + return null; + } + + Map args = {}; + Map sizeMap = + (await channel?.invokeMethod('getSize', args))?.cast(); + return MapSize.fromMap(sizeMap); + } + + @override + Future dispose() async { + if (!_running) { + return; + } + Map args = {}; + await channel?.invokeMethod('dispose', args); + disposeChannel(); + _started = false; + _running = false; + _webViewController?.dispose(); + _webViewController = null; + _controllerFromPlatform = null; + _iosParams.pullToRefreshController?.dispose(); + _iosParams.findInteractionController?.dispose(); + } +} + +extension InternalHeadlessInAppWebView on IOSHeadlessInAppWebView { + Future internalDispose() async { + _started = false; + _running = false; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/in_app_webview/in_app_webview.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/in_app_webview/in_app_webview.dart new file mode 100755 index 00000000..235d4ed1 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/in_app_webview/in_app_webview.dart @@ -0,0 +1,416 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; +import 'headless_in_app_webview.dart'; + +import '../find_interaction/find_interaction_controller.dart'; +import 'in_app_webview_controller.dart'; +import '../pull_to_refresh/main.dart'; +import '../pull_to_refresh/pull_to_refresh_controller.dart'; + +/// Object specifying creation parameters for creating a [PlatformInAppWebViewWidget]. +/// +/// Platform specific implementations can add additional fields by extending +/// this class. +class IOSInAppWebViewWidgetCreationParams + extends PlatformInAppWebViewWidgetCreationParams { + IOSInAppWebViewWidgetCreationParams( + {super.controllerFromPlatform, + super.key, + super.layoutDirection, + super.gestureRecognizers, + super.headlessWebView, + super.keepAlive, + super.preventGestureDelay, + super.windowId, + super.onWebViewCreated, + super.onLoadStart, + super.onLoadStop, + @Deprecated('Use onReceivedError instead') super.onLoadError, + super.onReceivedError, + @Deprecated("Use onReceivedHttpError instead") super.onLoadHttpError, + super.onReceivedHttpError, + super.onProgressChanged, + super.onConsoleMessage, + super.shouldOverrideUrlLoading, + super.onLoadResource, + super.onScrollChanged, + @Deprecated('Use onDownloadStartRequest instead') super.onDownloadStart, + super.onDownloadStartRequest, + @Deprecated('Use onLoadResourceWithCustomScheme instead') + super.onLoadResourceCustomScheme, + super.onLoadResourceWithCustomScheme, + super.onCreateWindow, + super.onCloseWindow, + super.onJsAlert, + super.onJsConfirm, + super.onJsPrompt, + super.onReceivedHttpAuthRequest, + super.onReceivedServerTrustAuthRequest, + super.onReceivedClientCertRequest, + @Deprecated('Use FindInteractionController.onFindResultReceived instead') + super.onFindResultReceived, + super.shouldInterceptAjaxRequest, + super.onAjaxReadyStateChange, + super.onAjaxProgress, + super.shouldInterceptFetchRequest, + super.onUpdateVisitedHistory, + @Deprecated("Use onPrintRequest instead") super.onPrint, + super.onPrintRequest, + super.onLongPressHitTestResult, + super.onEnterFullscreen, + super.onExitFullscreen, + super.onPageCommitVisible, + super.onTitleChanged, + super.onWindowFocus, + super.onWindowBlur, + super.onOverScrolled, + super.onZoomScaleChanged, + @Deprecated('Use onSafeBrowsingHit instead') + super.androidOnSafeBrowsingHit, + super.onSafeBrowsingHit, + @Deprecated('Use onPermissionRequest instead') + super.androidOnPermissionRequest, + super.onPermissionRequest, + @Deprecated('Use onGeolocationPermissionsShowPrompt instead') + super.androidOnGeolocationPermissionsShowPrompt, + super.onGeolocationPermissionsShowPrompt, + @Deprecated('Use onGeolocationPermissionsHidePrompt instead') + super.androidOnGeolocationPermissionsHidePrompt, + super.onGeolocationPermissionsHidePrompt, + @Deprecated('Use shouldInterceptRequest instead') + super.androidShouldInterceptRequest, + super.shouldInterceptRequest, + @Deprecated('Use onRenderProcessGone instead') + super.androidOnRenderProcessGone, + super.onRenderProcessGone, + @Deprecated('Use onRenderProcessResponsive instead') + super.androidOnRenderProcessResponsive, + super.onRenderProcessResponsive, + @Deprecated('Use onRenderProcessUnresponsive instead') + super.androidOnRenderProcessUnresponsive, + super.onRenderProcessUnresponsive, + @Deprecated('Use onFormResubmission instead') + super.androidOnFormResubmission, + super.onFormResubmission, + @Deprecated('Use onZoomScaleChanged instead') super.androidOnScaleChanged, + @Deprecated('Use onReceivedIcon instead') super.androidOnReceivedIcon, + super.onReceivedIcon, + @Deprecated('Use onReceivedTouchIconUrl instead') + super.androidOnReceivedTouchIconUrl, + super.onReceivedTouchIconUrl, + @Deprecated('Use onJsBeforeUnload instead') super.androidOnJsBeforeUnload, + super.onJsBeforeUnload, + @Deprecated('Use onReceivedLoginRequest instead') + super.androidOnReceivedLoginRequest, + super.onReceivedLoginRequest, + super.onPermissionRequestCanceled, + super.onRequestFocus, + @Deprecated('Use onWebContentProcessDidTerminate instead') + super.iosOnWebContentProcessDidTerminate, + super.onWebContentProcessDidTerminate, + @Deprecated( + 'Use onDidReceiveServerRedirectForProvisionalNavigation instead') + super.iosOnDidReceiveServerRedirectForProvisionalNavigation, + super.onDidReceiveServerRedirectForProvisionalNavigation, + @Deprecated('Use onNavigationResponse instead') + super.iosOnNavigationResponse, + super.onNavigationResponse, + @Deprecated('Use shouldAllowDeprecatedTLS instead') + super.iosShouldAllowDeprecatedTLS, + super.shouldAllowDeprecatedTLS, + super.onCameraCaptureStateChanged, + super.onMicrophoneCaptureStateChanged, + super.onContentSizeChanged, + super.initialUrlRequest, + super.initialFile, + super.initialData, + @Deprecated('Use initialSettings instead') super.initialOptions, + super.initialSettings, + super.contextMenu, + super.initialUserScripts, + this.pullToRefreshController, + this.findInteractionController}); + + /// Constructs a [IOSInAppWebViewWidgetCreationParams] using a + /// [PlatformInAppWebViewWidgetCreationParams]. + IOSInAppWebViewWidgetCreationParams.fromPlatformInAppWebViewWidgetCreationParams( + PlatformInAppWebViewWidgetCreationParams params) + : this( + controllerFromPlatform: params.controllerFromPlatform, + key: params.key, + layoutDirection: params.layoutDirection, + gestureRecognizers: params.gestureRecognizers, + headlessWebView: params.headlessWebView, + keepAlive: params.keepAlive, + preventGestureDelay: params.preventGestureDelay, + windowId: params.windowId, + onWebViewCreated: params.onWebViewCreated, + onLoadStart: params.onLoadStart, + onLoadStop: params.onLoadStop, + onLoadError: params.onLoadError, + onReceivedError: params.onReceivedError, + onLoadHttpError: params.onLoadHttpError, + onReceivedHttpError: params.onReceivedHttpError, + onProgressChanged: params.onProgressChanged, + onConsoleMessage: params.onConsoleMessage, + shouldOverrideUrlLoading: params.shouldOverrideUrlLoading, + onLoadResource: params.onLoadResource, + onScrollChanged: params.onScrollChanged, + onDownloadStart: params.onDownloadStart, + onDownloadStartRequest: params.onDownloadStartRequest, + onLoadResourceCustomScheme: params.onLoadResourceCustomScheme, + onLoadResourceWithCustomScheme: + params.onLoadResourceWithCustomScheme, + onCreateWindow: params.onCreateWindow, + onCloseWindow: params.onCloseWindow, + onJsAlert: params.onJsAlert, + onJsConfirm: params.onJsConfirm, + onJsPrompt: params.onJsPrompt, + onReceivedHttpAuthRequest: params.onReceivedHttpAuthRequest, + onReceivedServerTrustAuthRequest: + params.onReceivedServerTrustAuthRequest, + onReceivedClientCertRequest: params.onReceivedClientCertRequest, + onFindResultReceived: params.onFindResultReceived, + shouldInterceptAjaxRequest: params.shouldInterceptAjaxRequest, + onAjaxReadyStateChange: params.onAjaxReadyStateChange, + onAjaxProgress: params.onAjaxProgress, + shouldInterceptFetchRequest: params.shouldInterceptFetchRequest, + onUpdateVisitedHistory: params.onUpdateVisitedHistory, + onPrint: params.onPrint, + onPrintRequest: params.onPrintRequest, + onLongPressHitTestResult: params.onLongPressHitTestResult, + onEnterFullscreen: params.onEnterFullscreen, + onExitFullscreen: params.onExitFullscreen, + onPageCommitVisible: params.onPageCommitVisible, + onTitleChanged: params.onTitleChanged, + onWindowFocus: params.onWindowFocus, + onWindowBlur: params.onWindowBlur, + onOverScrolled: params.onOverScrolled, + onZoomScaleChanged: params.onZoomScaleChanged, + androidOnSafeBrowsingHit: params.androidOnSafeBrowsingHit, + onSafeBrowsingHit: params.onSafeBrowsingHit, + androidOnPermissionRequest: params.androidOnPermissionRequest, + onPermissionRequest: params.onPermissionRequest, + androidOnGeolocationPermissionsShowPrompt: + params.androidOnGeolocationPermissionsShowPrompt, + onGeolocationPermissionsShowPrompt: + params.onGeolocationPermissionsShowPrompt, + androidOnGeolocationPermissionsHidePrompt: + params.androidOnGeolocationPermissionsHidePrompt, + onGeolocationPermissionsHidePrompt: + params.onGeolocationPermissionsHidePrompt, + androidShouldInterceptRequest: params.androidShouldInterceptRequest, + shouldInterceptRequest: params.shouldInterceptRequest, + androidOnRenderProcessGone: params.androidOnRenderProcessGone, + onRenderProcessGone: params.onRenderProcessGone, + androidOnRenderProcessResponsive: + params.androidOnRenderProcessResponsive, + onRenderProcessResponsive: params.onRenderProcessResponsive, + androidOnRenderProcessUnresponsive: + params.androidOnRenderProcessUnresponsive, + onRenderProcessUnresponsive: params.onRenderProcessUnresponsive, + androidOnFormResubmission: params.androidOnFormResubmission, + onFormResubmission: params.onFormResubmission, + androidOnScaleChanged: params.androidOnScaleChanged, + androidOnReceivedIcon: params.androidOnReceivedIcon, + onReceivedIcon: params.onReceivedIcon, + androidOnReceivedTouchIconUrl: params.androidOnReceivedTouchIconUrl, + onReceivedTouchIconUrl: params.onReceivedTouchIconUrl, + androidOnJsBeforeUnload: params.androidOnJsBeforeUnload, + onJsBeforeUnload: params.onJsBeforeUnload, + androidOnReceivedLoginRequest: params.androidOnReceivedLoginRequest, + onReceivedLoginRequest: params.onReceivedLoginRequest, + onPermissionRequestCanceled: params.onPermissionRequestCanceled, + onRequestFocus: params.onRequestFocus, + iosOnWebContentProcessDidTerminate: + params.iosOnWebContentProcessDidTerminate, + onWebContentProcessDidTerminate: + params.onWebContentProcessDidTerminate, + iosOnDidReceiveServerRedirectForProvisionalNavigation: + params.iosOnDidReceiveServerRedirectForProvisionalNavigation, + onDidReceiveServerRedirectForProvisionalNavigation: + params.onDidReceiveServerRedirectForProvisionalNavigation, + iosOnNavigationResponse: params.iosOnNavigationResponse, + onNavigationResponse: params.onNavigationResponse, + iosShouldAllowDeprecatedTLS: params.iosShouldAllowDeprecatedTLS, + shouldAllowDeprecatedTLS: params.shouldAllowDeprecatedTLS, + onCameraCaptureStateChanged: params.onCameraCaptureStateChanged, + onMicrophoneCaptureStateChanged: + params.onMicrophoneCaptureStateChanged, + onContentSizeChanged: params.onContentSizeChanged, + initialUrlRequest: params.initialUrlRequest, + initialFile: params.initialFile, + initialData: params.initialData, + initialOptions: params.initialOptions, + initialSettings: params.initialSettings, + contextMenu: params.contextMenu, + initialUserScripts: params.initialUserScripts, + pullToRefreshController: + params.pullToRefreshController as IOSPullToRefreshController?, + findInteractionController: params.findInteractionController + as IOSFindInteractionController?); + + @override + final IOSFindInteractionController? findInteractionController; + + @override + final IOSPullToRefreshController? pullToRefreshController; +} + +///{@macro flutter_inappwebview_platform_interface.PlatformInAppWebViewWidget} +class IOSInAppWebViewWidget extends PlatformInAppWebViewWidget { + /// Constructs a [IOSInAppWebViewWidget]. + /// + ///{@macro flutter_inappwebview_platform_interface.PlatformInAppWebViewWidget} + IOSInAppWebViewWidget(PlatformInAppWebViewWidgetCreationParams params) + : super.implementation( + params is IOSInAppWebViewWidgetCreationParams + ? params + : IOSInAppWebViewWidgetCreationParams + .fromPlatformInAppWebViewWidgetCreationParams(params), + ); + + IOSInAppWebViewWidgetCreationParams get _iosParams => + params as IOSInAppWebViewWidgetCreationParams; + + IOSInAppWebViewController? _controller; + + IOSHeadlessInAppWebView? get _iosHeadlessInAppWebView => + params.headlessWebView as IOSHeadlessInAppWebView?; + + @override + Widget build(BuildContext context) { + final initialSettings = params.initialSettings ?? InAppWebViewSettings(); + _inferInitialSettings(initialSettings); + + Map settingsMap = + (params.initialSettings != null ? initialSettings.toMap() : null) ?? + // ignore: deprecated_member_use_from_same_package + params.initialOptions?.toMap() ?? + initialSettings.toMap(); + + Map pullToRefreshSettings = + params.pullToRefreshController?.params.settings.toMap() ?? + // ignore: deprecated_member_use_from_same_package + params.pullToRefreshController?.params.options.toMap() ?? + PullToRefreshSettings(enabled: false).toMap(); + + if ((params.headlessWebView?.isRunning() ?? false) && + params.keepAlive != null) { + final headlessId = params.headlessWebView?.id; + if (headlessId != null) { + // force keep alive id to match headless webview id + params.keepAlive?.id = headlessId; + } + } + + return UiKitView( + viewType: 'com.pichillilorenzo/flutter_inappwebview', + onPlatformViewCreated: _onPlatformViewCreated, + gestureRecognizers: params.gestureRecognizers, + creationParams: { + 'initialUrlRequest': params.initialUrlRequest?.toMap(), + 'initialFile': params.initialFile, + 'initialData': params.initialData?.toMap(), + 'initialSettings': settingsMap, + 'contextMenu': params.contextMenu?.toMap() ?? {}, + 'windowId': params.windowId, + 'headlessWebViewId': params.headlessWebView?.isRunning() ?? false + ? params.headlessWebView?.id + : null, + 'initialUserScripts': + params.initialUserScripts?.map((e) => e.toMap()).toList() ?? [], + 'pullToRefreshSettings': pullToRefreshSettings, + 'keepAliveId': params.keepAlive?.id, + 'preventGestureDelay': params.preventGestureDelay + }, + creationParamsCodec: const StandardMessageCodec(), + ); + } + + void _onPlatformViewCreated(int id) { + dynamic viewId = id; + if (params.headlessWebView?.isRunning() ?? false) { + viewId = params.headlessWebView?.id; + } + viewId = params.keepAlive?.id ?? viewId ?? id; + _iosHeadlessInAppWebView?.internalDispose(); + _controller = IOSInAppWebViewController( + PlatformInAppWebViewControllerCreationParams( + id: viewId, webviewParams: params)); + _iosParams.pullToRefreshController?.init(viewId); + _iosParams.findInteractionController?.init(viewId); + debugLog( + className: runtimeType.toString(), + id: viewId?.toString(), + debugLoggingSettings: + PlatformInAppWebViewController.debugLoggingSettings, + method: "onWebViewCreated", + args: []); + if (params.onWebViewCreated != null) { + params.onWebViewCreated!( + params.controllerFromPlatform?.call(_controller!) ?? _controller!); + } + } + + void _inferInitialSettings(InAppWebViewSettings settings) { + if (params.shouldOverrideUrlLoading != null && + settings.useShouldOverrideUrlLoading == null) { + settings.useShouldOverrideUrlLoading = true; + } + if (params.onLoadResource != null && settings.useOnLoadResource == null) { + settings.useOnLoadResource = true; + } + if (params.onDownloadStartRequest != null && + settings.useOnDownloadStart == null) { + settings.useOnDownloadStart = true; + } + if ((params.shouldInterceptAjaxRequest != null || + params.onAjaxProgress != null || + params.onAjaxReadyStateChange != null) && + settings.useShouldInterceptAjaxRequest == null) { + settings.useShouldInterceptAjaxRequest = true; + } + if (params.shouldInterceptFetchRequest != null && + settings.useShouldInterceptFetchRequest == null) { + settings.useShouldInterceptFetchRequest = true; + } + if (params.shouldInterceptRequest != null && + settings.useShouldInterceptRequest == null) { + settings.useShouldInterceptRequest = true; + } + if (params.onRenderProcessGone != null && + settings.useOnRenderProcessGone == null) { + settings.useOnRenderProcessGone = true; + } + if (params.onNavigationResponse != null && + settings.useOnNavigationResponse == null) { + settings.useOnNavigationResponse = true; + } + } + + @override + void dispose() { + dynamic viewId = _controller?.getViewId(); + debugLog( + className: runtimeType.toString(), + id: viewId?.toString(), + debugLoggingSettings: + PlatformInAppWebViewController.debugLoggingSettings, + method: "dispose", + args: []); + final isKeepAlive = params.keepAlive != null; + _controller?.dispose(isKeepAlive: isKeepAlive); + _controller = null; + params.pullToRefreshController?.dispose(isKeepAlive: isKeepAlive); + params.findInteractionController?.dispose(isKeepAlive: isKeepAlive); + } + + @override + T controllerFromPlatform(PlatformInAppWebViewController controller) { + // unused + throw UnimplementedError(); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/in_app_webview/in_app_webview_controller.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/in_app_webview/in_app_webview_controller.dart new file mode 100644 index 00000000..7e847f14 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/in_app_webview/in_app_webview_controller.dart @@ -0,0 +1,2753 @@ +import 'dart:io'; +import 'dart:collection'; +import 'dart:convert'; +import 'dart:core'; +import 'dart:developer' as developer; +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +import '../web_message/main.dart'; + +import '../in_app_browser/in_app_browser.dart'; +import '../web_storage/web_storage.dart'; + +import 'headless_in_app_webview.dart'; +import '_static_channel.dart'; + +import '../print_job/main.dart'; + +///List of forbidden names for JavaScript handlers. +// ignore: non_constant_identifier_names +final _JAVASCRIPT_HANDLER_FORBIDDEN_NAMES = UnmodifiableListView([ + "onLoadResource", + "shouldInterceptAjaxRequest", + "onAjaxReadyStateChange", + "onAjaxProgress", + "shouldInterceptFetchRequest", + "onPrintRequest", + "onWindowFocus", + "onWindowBlur", + "callAsyncJavaScript", + "evaluateJavaScriptWithContentWorld" +]); + +/// Object specifying creation parameters for creating a [IOSInAppWebViewController]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformInAppWebViewControllerCreationParams] for +/// more information. +@immutable +class IOSInAppWebViewControllerCreationParams + extends PlatformInAppWebViewControllerCreationParams { + /// Creates a new [IOSInAppWebViewControllerCreationParams] instance. + const IOSInAppWebViewControllerCreationParams( + {required super.id, super.webviewParams}); + + /// Creates a [IOSInAppWebViewControllerCreationParams] instance based on [PlatformInAppWebViewControllerCreationParams]. + factory IOSInAppWebViewControllerCreationParams.fromPlatformInAppWebViewControllerCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformInAppWebViewControllerCreationParams params) { + return IOSInAppWebViewControllerCreationParams( + id: params.id, webviewParams: params.webviewParams); + } +} + +///Controls a WebView, such as an [InAppWebView] widget instance, a [IOSHeadlessInAppWebView] instance or [IOSInAppBrowser] WebView instance. +/// +///If you are using the [InAppWebView] widget, an [InAppWebViewController] instance can be obtained by setting the [InAppWebView.onWebViewCreated] +///callback. Instead, if you are using an [IOSInAppBrowser] instance, you can get it through the [IOSInAppBrowser.webViewController] attribute. +class IOSInAppWebViewController extends PlatformInAppWebViewController + with ChannelController { + static final MethodChannel _staticChannel = IN_APP_WEBVIEW_STATIC_CHANNEL; + + // List of properties to be saved and restored for keep alive feature + Map _javaScriptHandlersMap = + HashMap(); + Map> _userScripts = { + UserScriptInjectionTime.AT_DOCUMENT_START: [], + UserScriptInjectionTime.AT_DOCUMENT_END: [] + }; + Set _webMessageListenerObjNames = Set(); + Map _injectedScriptsFromURL = {}; + Set _webMessageChannels = Set(); + Set _webMessageListeners = Set(); + + // static map that contains the properties to be saved and restored for keep alive feature + static final Map + _keepAliveMap = {}; + + IOSInAppBrowser? _inAppBrowser; + + PlatformInAppBrowserEvents? get _inAppBrowserEventHandler => + _inAppBrowser?.eventHandler; + + dynamic _controllerFromPlatform; + + @override + late IOSWebStorage webStorage; + + IOSInAppWebViewController(PlatformInAppWebViewControllerCreationParams params) + : super.implementation(params is IOSInAppWebViewControllerCreationParams + ? params + : IOSInAppWebViewControllerCreationParams + .fromPlatformInAppWebViewControllerCreationParams(params)) { + channel = MethodChannel('com.pichillilorenzo/flutter_inappwebview_$id'); + handler = handleMethod; + initMethodCallHandler(); + + final initialUserScripts = webviewParams?.initialUserScripts; + if (initialUserScripts != null) { + for (final userScript in initialUserScripts) { + if (userScript.injectionTime == + UserScriptInjectionTime.AT_DOCUMENT_START) { + this + ._userScripts[UserScriptInjectionTime.AT_DOCUMENT_START] + ?.add(userScript); + } else { + this + ._userScripts[UserScriptInjectionTime.AT_DOCUMENT_END] + ?.add(userScript); + } + } + } + + this._init(params); + } + + static final IOSInAppWebViewController _staticValue = + IOSInAppWebViewController( + IOSInAppWebViewControllerCreationParams(id: null)); + + factory IOSInAppWebViewController.static() { + return _staticValue; + } + + IOSInAppWebViewController.fromInAppBrowser( + PlatformInAppWebViewControllerCreationParams params, + MethodChannel channel, + IOSInAppBrowser inAppBrowser, + UnmodifiableListView? initialUserScripts) + : super.implementation(params is IOSInAppWebViewControllerCreationParams + ? params + : IOSInAppWebViewControllerCreationParams + .fromPlatformInAppWebViewControllerCreationParams(params)) { + this.channel = channel; + this._inAppBrowser = inAppBrowser; + + if (initialUserScripts != null) { + for (final userScript in initialUserScripts) { + if (userScript.injectionTime == + UserScriptInjectionTime.AT_DOCUMENT_START) { + this + ._userScripts[UserScriptInjectionTime.AT_DOCUMENT_START] + ?.add(userScript); + } else { + this + ._userScripts[UserScriptInjectionTime.AT_DOCUMENT_END] + ?.add(userScript); + } + } + } + this._init(params); + } + + void _init(PlatformInAppWebViewControllerCreationParams params) { + _controllerFromPlatform = + params.webviewParams?.controllerFromPlatform?.call(this) ?? this; + + webStorage = IOSWebStorage(IOSWebStorageCreationParams( + localStorage: IOSLocalStorage.defaultStorage(controller: this), + sessionStorage: IOSSessionStorage.defaultStorage(controller: this))); + + if (params.webviewParams is PlatformInAppWebViewWidgetCreationParams) { + final keepAlive = + (params.webviewParams as PlatformInAppWebViewWidgetCreationParams) + .keepAlive; + if (keepAlive != null) { + InAppWebViewControllerKeepAliveProps? props = _keepAliveMap[keepAlive]; + if (props == null) { + // save controller properties to restore it later + _keepAliveMap[keepAlive] = InAppWebViewControllerKeepAliveProps( + injectedScriptsFromURL: _injectedScriptsFromURL, + javaScriptHandlersMap: _javaScriptHandlersMap, + userScripts: _userScripts, + webMessageListenerObjNames: _webMessageListenerObjNames, + webMessageChannels: _webMessageChannels, + webMessageListeners: _webMessageListeners); + } else { + // restore controller properties + _injectedScriptsFromURL = props.injectedScriptsFromURL; + _javaScriptHandlersMap = props.javaScriptHandlersMap; + _userScripts = props.userScripts; + _webMessageListenerObjNames = props.webMessageListenerObjNames; + _webMessageChannels = + props.webMessageChannels as Set; + _webMessageListeners = + props.webMessageListeners as Set; + } + } + } + } + + _debugLog(String method, dynamic args) { + debugLog( + className: this.runtimeType.toString(), + name: _inAppBrowser == null + ? "WebView" + : _inAppBrowser.runtimeType.toString(), + id: (getViewId() ?? _inAppBrowser?.id).toString(), + debugLoggingSettings: + PlatformInAppWebViewController.debugLoggingSettings, + method: method, + args: args); + } + + Future _handleMethod(MethodCall call) async { + if (PlatformInAppWebViewController.debugLoggingSettings.enabled && + call.method != "onCallJsHandler") { + _debugLog(call.method, call.arguments); + } + + switch (call.method) { + case "onLoadStart": + _injectedScriptsFromURL.clear(); + if ((webviewParams != null && webviewParams!.onLoadStart != null) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + WebUri? uri = url != null ? WebUri(url) : null; + if (webviewParams != null && webviewParams!.onLoadStart != null) + webviewParams!.onLoadStart!(_controllerFromPlatform, uri); + else + _inAppBrowserEventHandler!.onLoadStart(uri); + } + break; + case "onLoadStop": + if ((webviewParams != null && webviewParams!.onLoadStop != null) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + WebUri? uri = url != null ? WebUri(url) : null; + if (webviewParams != null && webviewParams!.onLoadStop != null) + webviewParams!.onLoadStop!(_controllerFromPlatform, uri); + else + _inAppBrowserEventHandler!.onLoadStop(uri); + } + break; + case "onReceivedError": + if ((webviewParams != null && + (webviewParams!.onReceivedError != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.onLoadError != null)) || + _inAppBrowserEventHandler != null) { + WebResourceRequest request = WebResourceRequest.fromMap( + call.arguments["request"].cast())!; + WebResourceError error = WebResourceError.fromMap( + call.arguments["error"].cast())!; + var isForMainFrame = request.isForMainFrame ?? false; + + if (webviewParams != null) { + if (webviewParams!.onReceivedError != null) + webviewParams!.onReceivedError!( + _controllerFromPlatform, request, error); + else if (isForMainFrame) { + // ignore: deprecated_member_use_from_same_package + webviewParams!.onLoadError!(_controllerFromPlatform, request.url, + error.type.toNativeValue() ?? -1, error.description); + } + } else { + if (isForMainFrame) { + _inAppBrowserEventHandler!.onLoadError(request.url, + error.type.toNativeValue() ?? -1, error.description); + } + _inAppBrowserEventHandler!.onReceivedError(request, error); + } + } + break; + case "onReceivedHttpError": + if ((webviewParams != null && + (webviewParams!.onReceivedHttpError != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.onLoadHttpError != null)) || + _inAppBrowserEventHandler != null) { + WebResourceRequest request = WebResourceRequest.fromMap( + call.arguments["request"].cast())!; + WebResourceResponse errorResponse = WebResourceResponse.fromMap( + call.arguments["errorResponse"].cast())!; + var isForMainFrame = request.isForMainFrame ?? false; + + if (webviewParams != null) { + if (webviewParams!.onReceivedHttpError != null) + webviewParams!.onReceivedHttpError!( + _controllerFromPlatform, request, errorResponse); + else if (isForMainFrame) { + // ignore: deprecated_member_use_from_same_package + webviewParams!.onLoadHttpError!( + _controllerFromPlatform, + request.url, + errorResponse.statusCode ?? -1, + errorResponse.reasonPhrase ?? ''); + } + } else { + if (isForMainFrame) { + _inAppBrowserEventHandler!.onLoadHttpError( + request.url, + errorResponse.statusCode ?? -1, + errorResponse.reasonPhrase ?? ''); + } + _inAppBrowserEventHandler! + .onReceivedHttpError(request, errorResponse); + } + } + break; + case "onProgressChanged": + if ((webviewParams != null && + webviewParams!.onProgressChanged != null) || + _inAppBrowserEventHandler != null) { + int progress = call.arguments["progress"]; + if (webviewParams != null && webviewParams!.onProgressChanged != null) + webviewParams!.onProgressChanged!( + _controllerFromPlatform, progress); + else + _inAppBrowserEventHandler!.onProgressChanged(progress); + } + break; + case "shouldOverrideUrlLoading": + if ((webviewParams != null && + webviewParams!.shouldOverrideUrlLoading != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + NavigationAction navigationAction = + NavigationAction.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.shouldOverrideUrlLoading != null) + return (await webviewParams!.shouldOverrideUrlLoading!( + _controllerFromPlatform, navigationAction)) + ?.toNativeValue(); + return (await _inAppBrowserEventHandler! + .shouldOverrideUrlLoading(navigationAction)) + ?.toNativeValue(); + } + break; + case "onConsoleMessage": + if ((webviewParams != null && + webviewParams!.onConsoleMessage != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + ConsoleMessage consoleMessage = ConsoleMessage.fromMap(arguments)!; + if (webviewParams != null && webviewParams!.onConsoleMessage != null) + webviewParams!.onConsoleMessage!( + _controllerFromPlatform, consoleMessage); + else + _inAppBrowserEventHandler!.onConsoleMessage(consoleMessage); + } + break; + case "onScrollChanged": + if ((webviewParams != null && webviewParams!.onScrollChanged != null) || + _inAppBrowserEventHandler != null) { + int x = call.arguments["x"]; + int y = call.arguments["y"]; + if (webviewParams != null && webviewParams!.onScrollChanged != null) + webviewParams!.onScrollChanged!(_controllerFromPlatform, x, y); + else + _inAppBrowserEventHandler!.onScrollChanged(x, y); + } + break; + case "onDownloadStartRequest": + if ((webviewParams != null && + // ignore: deprecated_member_use_from_same_package + (webviewParams!.onDownloadStart != null || + webviewParams!.onDownloadStartRequest != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + DownloadStartRequest downloadStartRequest = + DownloadStartRequest.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.onDownloadStartRequest != null) + webviewParams!.onDownloadStartRequest!( + _controllerFromPlatform, downloadStartRequest); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.onDownloadStart!( + _controllerFromPlatform, downloadStartRequest.url); + } + } else { + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler! + .onDownloadStart(downloadStartRequest.url); + _inAppBrowserEventHandler! + .onDownloadStartRequest(downloadStartRequest); + } + } + break; + case "onLoadResourceWithCustomScheme": + if ((webviewParams != null && + (webviewParams!.onLoadResourceWithCustomScheme != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.onLoadResourceCustomScheme != null)) || + _inAppBrowserEventHandler != null) { + Map requestMap = + call.arguments["request"].cast(); + WebResourceRequest request = WebResourceRequest.fromMap(requestMap)!; + + if (webviewParams != null) { + if (webviewParams!.onLoadResourceWithCustomScheme != null) + return (await webviewParams!.onLoadResourceWithCustomScheme!( + _controllerFromPlatform, request)) + ?.toMap(); + else { + return (await params + .webviewParams! + // ignore: deprecated_member_use_from_same_package + .onLoadResourceCustomScheme!( + _controllerFromPlatform, request.url)) + ?.toMap(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onLoadResourceWithCustomScheme(request)) ?? + (await _inAppBrowserEventHandler! + .onLoadResourceCustomScheme(request.url))) + ?.toMap(); + } + } + break; + case "onCreateWindow": + if ((webviewParams != null && webviewParams!.onCreateWindow != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + CreateWindowAction createWindowAction = + CreateWindowAction.fromMap(arguments)!; + + if (webviewParams != null && webviewParams!.onCreateWindow != null) + return await webviewParams!.onCreateWindow!( + _controllerFromPlatform, createWindowAction); + else + return await _inAppBrowserEventHandler! + .onCreateWindow(createWindowAction); + } + break; + case "onCloseWindow": + if (webviewParams != null && webviewParams!.onCloseWindow != null) + webviewParams!.onCloseWindow!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onCloseWindow(); + break; + case "onTitleChanged": + if ((webviewParams != null && webviewParams!.onTitleChanged != null) || + _inAppBrowserEventHandler != null) { + String? title = call.arguments["title"]; + if (webviewParams != null && webviewParams!.onTitleChanged != null) + webviewParams!.onTitleChanged!(_controllerFromPlatform, title); + else + _inAppBrowserEventHandler!.onTitleChanged(title); + } + break; + case "onGeolocationPermissionsShowPrompt": + if ((webviewParams != null && + (webviewParams!.onGeolocationPermissionsShowPrompt != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnGeolocationPermissionsShowPrompt != + null)) || + _inAppBrowserEventHandler != null) { + String origin = call.arguments["origin"]; + + if (webviewParams != null) { + if (webviewParams!.onGeolocationPermissionsShowPrompt != null) + return (await webviewParams!.onGeolocationPermissionsShowPrompt!( + _controllerFromPlatform, origin)) + ?.toMap(); + else { + return (await params + .webviewParams! + // ignore: deprecated_member_use_from_same_package + .androidOnGeolocationPermissionsShowPrompt!( + _controllerFromPlatform, origin)) + ?.toMap(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onGeolocationPermissionsShowPrompt(origin)) ?? + (await _inAppBrowserEventHandler! + .androidOnGeolocationPermissionsShowPrompt(origin))) + ?.toMap(); + } + } + break; + case "onGeolocationPermissionsHidePrompt": + if (webviewParams != null && + (webviewParams!.onGeolocationPermissionsHidePrompt != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnGeolocationPermissionsHidePrompt != + null)) { + if (webviewParams!.onGeolocationPermissionsHidePrompt != null) + webviewParams! + .onGeolocationPermissionsHidePrompt!(_controllerFromPlatform); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnGeolocationPermissionsHidePrompt!( + _controllerFromPlatform); + } + } else if (_inAppBrowserEventHandler != null) { + _inAppBrowserEventHandler!.onGeolocationPermissionsHidePrompt(); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler! + .androidOnGeolocationPermissionsHidePrompt(); + } + break; + case "shouldInterceptRequest": + if ((webviewParams != null && + (webviewParams!.shouldInterceptRequest != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidShouldInterceptRequest != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + WebResourceRequest request = WebResourceRequest.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.shouldInterceptRequest != null) + return (await webviewParams!.shouldInterceptRequest!( + _controllerFromPlatform, request)) + ?.toMap(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.androidShouldInterceptRequest!( + _controllerFromPlatform, request)) + ?.toMap(); + } + } else { + return ((await _inAppBrowserEventHandler! + .shouldInterceptRequest(request)) ?? + (await _inAppBrowserEventHandler! + .androidShouldInterceptRequest(request))) + ?.toMap(); + } + } + break; + case "onRenderProcessUnresponsive": + if ((webviewParams != null && + (webviewParams!.onRenderProcessUnresponsive != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnRenderProcessUnresponsive != + null)) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + WebUri? uri = url != null ? WebUri(url) : null; + + if (webviewParams != null) { + if (webviewParams!.onRenderProcessUnresponsive != null) + return (await webviewParams!.onRenderProcessUnresponsive!( + _controllerFromPlatform, uri)) + ?.toNativeValue(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.androidOnRenderProcessUnresponsive!( + _controllerFromPlatform, uri)) + ?.toNativeValue(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onRenderProcessUnresponsive(uri)) ?? + (await _inAppBrowserEventHandler! + .androidOnRenderProcessUnresponsive(uri))) + ?.toNativeValue(); + } + } + break; + case "onRenderProcessResponsive": + if ((webviewParams != null && + (webviewParams!.onRenderProcessResponsive != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnRenderProcessResponsive != null)) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + WebUri? uri = url != null ? WebUri(url) : null; + + if (webviewParams != null) { + if (webviewParams!.onRenderProcessResponsive != null) + return (await webviewParams!.onRenderProcessResponsive!( + _controllerFromPlatform, uri)) + ?.toNativeValue(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.androidOnRenderProcessResponsive!( + _controllerFromPlatform, uri)) + ?.toNativeValue(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onRenderProcessResponsive(uri)) ?? + (await _inAppBrowserEventHandler! + .androidOnRenderProcessResponsive(uri))) + ?.toNativeValue(); + } + } + break; + case "onRenderProcessGone": + if ((webviewParams != null && + (webviewParams!.onRenderProcessGone != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnRenderProcessGone != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + RenderProcessGoneDetail detail = + RenderProcessGoneDetail.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.onRenderProcessGone != null) + webviewParams!.onRenderProcessGone!( + _controllerFromPlatform, detail); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnRenderProcessGone!( + _controllerFromPlatform, detail); + } + } else if (_inAppBrowserEventHandler != null) { + _inAppBrowserEventHandler!.onRenderProcessGone(detail); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler!.androidOnRenderProcessGone(detail); + } + } + break; + case "onFormResubmission": + if ((webviewParams != null && + (webviewParams!.onFormResubmission != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnFormResubmission != null)) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + WebUri? uri = url != null ? WebUri(url) : null; + + if (webviewParams != null) { + if (webviewParams!.onFormResubmission != null) + return (await webviewParams!.onFormResubmission!( + _controllerFromPlatform, uri)) + ?.toNativeValue(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.androidOnFormResubmission!( + _controllerFromPlatform, uri)) + ?.toNativeValue(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onFormResubmission(uri)) ?? + // ignore: deprecated_member_use_from_same_package + (await _inAppBrowserEventHandler! + .androidOnFormResubmission(uri))) + ?.toNativeValue(); + } + } + break; + case "onZoomScaleChanged": + if ((webviewParams != null && + // ignore: deprecated_member_use_from_same_package + (webviewParams!.androidOnScaleChanged != null || + webviewParams!.onZoomScaleChanged != null)) || + _inAppBrowserEventHandler != null) { + double oldScale = call.arguments["oldScale"]; + double newScale = call.arguments["newScale"]; + + if (webviewParams != null) { + if (webviewParams!.onZoomScaleChanged != null) + webviewParams!.onZoomScaleChanged!( + _controllerFromPlatform, oldScale, newScale); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnScaleChanged!( + _controllerFromPlatform, oldScale, newScale); + } + } else { + _inAppBrowserEventHandler!.onZoomScaleChanged(oldScale, newScale); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler! + .androidOnScaleChanged(oldScale, newScale); + } + } + break; + case "onReceivedIcon": + if ((webviewParams != null && + (webviewParams!.onReceivedIcon != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnReceivedIcon != null)) || + _inAppBrowserEventHandler != null) { + Uint8List icon = + Uint8List.fromList(call.arguments["icon"].cast()); + + if (webviewParams != null) { + if (webviewParams!.onReceivedIcon != null) + webviewParams!.onReceivedIcon!(_controllerFromPlatform, icon); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnReceivedIcon!( + _controllerFromPlatform, icon); + } + } else { + _inAppBrowserEventHandler!.onReceivedIcon(icon); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler!.androidOnReceivedIcon(icon); + } + } + break; + case "onReceivedTouchIconUrl": + if ((webviewParams != null && + (webviewParams!.onReceivedTouchIconUrl != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnReceivedTouchIconUrl != null)) || + _inAppBrowserEventHandler != null) { + String url = call.arguments["url"]; + bool precomposed = call.arguments["precomposed"]; + WebUri uri = WebUri(url); + + if (webviewParams != null) { + if (webviewParams!.onReceivedTouchIconUrl != null) + webviewParams!.onReceivedTouchIconUrl!( + _controllerFromPlatform, uri, precomposed); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnReceivedTouchIconUrl!( + _controllerFromPlatform, uri, precomposed); + } + } else { + _inAppBrowserEventHandler!.onReceivedTouchIconUrl(uri, precomposed); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler! + .androidOnReceivedTouchIconUrl(uri, precomposed); + } + } + break; + case "onJsAlert": + if ((webviewParams != null && webviewParams!.onJsAlert != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + JsAlertRequest jsAlertRequest = JsAlertRequest.fromMap(arguments)!; + + if (webviewParams != null && webviewParams!.onJsAlert != null) + return (await webviewParams!.onJsAlert!( + _controllerFromPlatform, jsAlertRequest)) + ?.toMap(); + else + return (await _inAppBrowserEventHandler!.onJsAlert(jsAlertRequest)) + ?.toMap(); + } + break; + case "onJsConfirm": + if ((webviewParams != null && webviewParams!.onJsConfirm != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + JsConfirmRequest jsConfirmRequest = + JsConfirmRequest.fromMap(arguments)!; + + if (webviewParams != null && webviewParams!.onJsConfirm != null) + return (await webviewParams!.onJsConfirm!( + _controllerFromPlatform, jsConfirmRequest)) + ?.toMap(); + else + return (await _inAppBrowserEventHandler! + .onJsConfirm(jsConfirmRequest)) + ?.toMap(); + } + break; + case "onJsPrompt": + if ((webviewParams != null && webviewParams!.onJsPrompt != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + JsPromptRequest jsPromptRequest = JsPromptRequest.fromMap(arguments)!; + + if (webviewParams != null && webviewParams!.onJsPrompt != null) + return (await webviewParams!.onJsPrompt!( + _controllerFromPlatform, jsPromptRequest)) + ?.toMap(); + else + return (await _inAppBrowserEventHandler! + .onJsPrompt(jsPromptRequest)) + ?.toMap(); + } + break; + case "onJsBeforeUnload": + if ((webviewParams != null && + (webviewParams!.onJsBeforeUnload != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnJsBeforeUnload != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + JsBeforeUnloadRequest jsBeforeUnloadRequest = + JsBeforeUnloadRequest.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.onJsBeforeUnload != null) + return (await webviewParams!.onJsBeforeUnload!( + _controllerFromPlatform, jsBeforeUnloadRequest)) + ?.toMap(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.androidOnJsBeforeUnload!( + _controllerFromPlatform, jsBeforeUnloadRequest)) + ?.toMap(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onJsBeforeUnload(jsBeforeUnloadRequest)) ?? + (await _inAppBrowserEventHandler! + .androidOnJsBeforeUnload(jsBeforeUnloadRequest))) + ?.toMap(); + } + } + break; + case "onSafeBrowsingHit": + if ((webviewParams != null && + (webviewParams!.onSafeBrowsingHit != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnSafeBrowsingHit != null)) || + _inAppBrowserEventHandler != null) { + String url = call.arguments["url"]; + SafeBrowsingThreat? threatType = + SafeBrowsingThreat.fromNativeValue(call.arguments["threatType"]); + WebUri uri = WebUri(url); + + if (webviewParams != null) { + if (webviewParams!.onSafeBrowsingHit != null) + return (await webviewParams!.onSafeBrowsingHit!( + _controllerFromPlatform, uri, threatType)) + ?.toMap(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.androidOnSafeBrowsingHit!( + _controllerFromPlatform, uri, threatType)) + ?.toMap(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onSafeBrowsingHit(uri, threatType)) ?? + (await _inAppBrowserEventHandler! + .androidOnSafeBrowsingHit(uri, threatType))) + ?.toMap(); + } + } + break; + case "onReceivedLoginRequest": + if ((webviewParams != null && + (webviewParams!.onReceivedLoginRequest != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnReceivedLoginRequest != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + LoginRequest loginRequest = LoginRequest.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.onReceivedLoginRequest != null) + webviewParams!.onReceivedLoginRequest!( + _controllerFromPlatform, loginRequest); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnReceivedLoginRequest!( + _controllerFromPlatform, loginRequest); + } + } else { + _inAppBrowserEventHandler!.onReceivedLoginRequest(loginRequest); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler! + .androidOnReceivedLoginRequest(loginRequest); + } + } + break; + case "onPermissionRequestCanceled": + if ((webviewParams != null && + webviewParams!.onPermissionRequestCanceled != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + PermissionRequest permissionRequest = + PermissionRequest.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onPermissionRequestCanceled != null) + webviewParams!.onPermissionRequestCanceled!( + _controllerFromPlatform, permissionRequest); + else + _inAppBrowserEventHandler! + .onPermissionRequestCanceled(permissionRequest); + } + break; + case "onRequestFocus": + if ((webviewParams != null && webviewParams!.onRequestFocus != null) || + _inAppBrowserEventHandler != null) { + if (webviewParams != null && webviewParams!.onRequestFocus != null) + webviewParams!.onRequestFocus!(_controllerFromPlatform); + else + _inAppBrowserEventHandler!.onRequestFocus(); + } + break; + case "onReceivedHttpAuthRequest": + if ((webviewParams != null && + webviewParams!.onReceivedHttpAuthRequest != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + HttpAuthenticationChallenge challenge = + HttpAuthenticationChallenge.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onReceivedHttpAuthRequest != null) + return (await webviewParams!.onReceivedHttpAuthRequest!( + _controllerFromPlatform, challenge)) + ?.toMap(); + else + return (await _inAppBrowserEventHandler! + .onReceivedHttpAuthRequest(challenge)) + ?.toMap(); + } + break; + case "onReceivedServerTrustAuthRequest": + if ((webviewParams != null && + webviewParams!.onReceivedServerTrustAuthRequest != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + ServerTrustChallenge challenge = + ServerTrustChallenge.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onReceivedServerTrustAuthRequest != null) + return (await webviewParams!.onReceivedServerTrustAuthRequest!( + _controllerFromPlatform, challenge)) + ?.toMap(); + else + return (await _inAppBrowserEventHandler! + .onReceivedServerTrustAuthRequest(challenge)) + ?.toMap(); + } + break; + case "onReceivedClientCertRequest": + if ((webviewParams != null && + webviewParams!.onReceivedClientCertRequest != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + ClientCertChallenge challenge = + ClientCertChallenge.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onReceivedClientCertRequest != null) + return (await webviewParams!.onReceivedClientCertRequest!( + _controllerFromPlatform, challenge)) + ?.toMap(); + else + return (await _inAppBrowserEventHandler! + .onReceivedClientCertRequest(challenge)) + ?.toMap(); + } + break; + case "onFindResultReceived": + if ((webviewParams != null && + (webviewParams!.onFindResultReceived != null || + (webviewParams!.findInteractionController != null && + webviewParams!.findInteractionController!.params + .onFindResultReceived != + null))) || + _inAppBrowserEventHandler != null) { + int activeMatchOrdinal = call.arguments["activeMatchOrdinal"]; + int numberOfMatches = call.arguments["numberOfMatches"]; + bool isDoneCounting = call.arguments["isDoneCounting"]; + if (webviewParams != null) { + if (webviewParams!.findInteractionController != null && + webviewParams!.findInteractionController!.params + .onFindResultReceived != + null) + webviewParams! + .findInteractionController!.params.onFindResultReceived!( + webviewParams!.findInteractionController!, + activeMatchOrdinal, + numberOfMatches, + isDoneCounting); + else + webviewParams!.onFindResultReceived!(_controllerFromPlatform, + activeMatchOrdinal, numberOfMatches, isDoneCounting); + } else { + if (_inAppBrowser!.findInteractionController != null && + _inAppBrowser! + .findInteractionController!.onFindResultReceived != + null) + _inAppBrowser!.findInteractionController!.onFindResultReceived!( + webviewParams!.findInteractionController!, + activeMatchOrdinal, + numberOfMatches, + isDoneCounting); + else + _inAppBrowserEventHandler!.onFindResultReceived( + activeMatchOrdinal, numberOfMatches, isDoneCounting); + } + } + break; + case "onPermissionRequest": + if ((webviewParams != null && + (webviewParams!.onPermissionRequest != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnPermissionRequest != null)) || + _inAppBrowserEventHandler != null) { + String origin = call.arguments["origin"]; + List resources = call.arguments["resources"].cast(); + + Map arguments = + call.arguments.cast(); + PermissionRequest permissionRequest = + PermissionRequest.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.onPermissionRequest != null) + return (await webviewParams!.onPermissionRequest!( + _controllerFromPlatform, permissionRequest)) + ?.toMap(); + else { + return (await webviewParams!.androidOnPermissionRequest!( + _controllerFromPlatform, origin, resources)) + ?.toMap(); + } + } else { + return (await _inAppBrowserEventHandler! + .onPermissionRequest(permissionRequest)) + ?.toMap() ?? + (await _inAppBrowserEventHandler! + .androidOnPermissionRequest(origin, resources)) + ?.toMap(); + } + } + break; + case "onUpdateVisitedHistory": + if ((webviewParams != null && + webviewParams!.onUpdateVisitedHistory != null) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + bool? isReload = call.arguments["isReload"]; + WebUri? uri = url != null ? WebUri(url) : null; + if (webviewParams != null && + webviewParams!.onUpdateVisitedHistory != null) + webviewParams!.onUpdateVisitedHistory!( + _controllerFromPlatform, uri, isReload); + else + _inAppBrowserEventHandler!.onUpdateVisitedHistory(uri, isReload); + } + break; + case "onWebContentProcessDidTerminate": + if (webviewParams != null && + (webviewParams!.onWebContentProcessDidTerminate != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.iosOnWebContentProcessDidTerminate != null)) { + if (webviewParams!.onWebContentProcessDidTerminate != null) + webviewParams! + .onWebContentProcessDidTerminate!(_controllerFromPlatform); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams! + .iosOnWebContentProcessDidTerminate!(_controllerFromPlatform); + } + } else if (_inAppBrowserEventHandler != null) { + _inAppBrowserEventHandler!.onWebContentProcessDidTerminate(); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler!.iosOnWebContentProcessDidTerminate(); + } + break; + case "onPageCommitVisible": + if ((webviewParams != null && + webviewParams!.onPageCommitVisible != null) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + WebUri? uri = url != null ? WebUri(url) : null; + if (webviewParams != null && + webviewParams!.onPageCommitVisible != null) + webviewParams!.onPageCommitVisible!(_controllerFromPlatform, uri); + else + _inAppBrowserEventHandler!.onPageCommitVisible(uri); + } + break; + case "onDidReceiveServerRedirectForProvisionalNavigation": + if (webviewParams != null && + (webviewParams! + .onDidReceiveServerRedirectForProvisionalNavigation != + null || + params + .webviewParams! + // ignore: deprecated_member_use_from_same_package + .iosOnDidReceiveServerRedirectForProvisionalNavigation != + null)) { + if (webviewParams! + .onDidReceiveServerRedirectForProvisionalNavigation != + null) + webviewParams!.onDidReceiveServerRedirectForProvisionalNavigation!( + _controllerFromPlatform); + else { + params + .webviewParams! + // ignore: deprecated_member_use_from_same_package + .iosOnDidReceiveServerRedirectForProvisionalNavigation!( + _controllerFromPlatform); + } + } else if (_inAppBrowserEventHandler != null) { + _inAppBrowserEventHandler! + .onDidReceiveServerRedirectForProvisionalNavigation(); + _inAppBrowserEventHandler! + .iosOnDidReceiveServerRedirectForProvisionalNavigation(); + } + break; + case "onNavigationResponse": + if ((webviewParams != null && + (webviewParams!.onNavigationResponse != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.iosOnNavigationResponse != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + // ignore: deprecated_member_use_from_same_package + IOSWKNavigationResponse iosOnNavigationResponse = + // ignore: deprecated_member_use_from_same_package + IOSWKNavigationResponse.fromMap(arguments)!; + + NavigationResponse navigationResponse = + NavigationResponse.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.onNavigationResponse != null) + return (await webviewParams!.onNavigationResponse!( + _controllerFromPlatform, navigationResponse)) + ?.toNativeValue(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.iosOnNavigationResponse!( + _controllerFromPlatform, iosOnNavigationResponse)) + ?.toNativeValue(); + } + } else { + return (await _inAppBrowserEventHandler! + .onNavigationResponse(navigationResponse)) + ?.toNativeValue() ?? + (await _inAppBrowserEventHandler! + .iosOnNavigationResponse(iosOnNavigationResponse)) + ?.toNativeValue(); + } + } + break; + case "shouldAllowDeprecatedTLS": + if ((webviewParams != null && + (webviewParams!.shouldAllowDeprecatedTLS != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.iosShouldAllowDeprecatedTLS != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + URLAuthenticationChallenge challenge = + URLAuthenticationChallenge.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.shouldAllowDeprecatedTLS != null) + return (await webviewParams!.shouldAllowDeprecatedTLS!( + _controllerFromPlatform, challenge)) + ?.toNativeValue(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.iosShouldAllowDeprecatedTLS!( + _controllerFromPlatform, challenge)) + ?.toNativeValue(); + } + } else { + return (await _inAppBrowserEventHandler! + .shouldAllowDeprecatedTLS(challenge)) + ?.toNativeValue() ?? + // ignore: deprecated_member_use_from_same_package + (await _inAppBrowserEventHandler! + .iosShouldAllowDeprecatedTLS(challenge)) + ?.toNativeValue(); + } + } + break; + case "onLongPressHitTestResult": + if ((webviewParams != null && + webviewParams!.onLongPressHitTestResult != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + InAppWebViewHitTestResult hitTestResult = + InAppWebViewHitTestResult.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onLongPressHitTestResult != null) + webviewParams!.onLongPressHitTestResult!( + _controllerFromPlatform, hitTestResult); + else + _inAppBrowserEventHandler!.onLongPressHitTestResult(hitTestResult); + } + break; + case "onCreateContextMenu": + ContextMenu? contextMenu; + if (webviewParams != null && webviewParams!.contextMenu != null) { + contextMenu = webviewParams!.contextMenu; + } else if (_inAppBrowserEventHandler != null && + _inAppBrowser!.contextMenu != null) { + contextMenu = _inAppBrowser!.contextMenu; + } + + if (contextMenu != null && contextMenu.onCreateContextMenu != null) { + Map arguments = + call.arguments.cast(); + InAppWebViewHitTestResult hitTestResult = + InAppWebViewHitTestResult.fromMap(arguments)!; + + contextMenu.onCreateContextMenu!(hitTestResult); + } + break; + case "onHideContextMenu": + ContextMenu? contextMenu; + if (webviewParams != null && webviewParams!.contextMenu != null) { + contextMenu = webviewParams!.contextMenu; + } else if (_inAppBrowserEventHandler != null && + _inAppBrowser!.contextMenu != null) { + contextMenu = _inAppBrowser!.contextMenu; + } + + if (contextMenu != null && contextMenu.onHideContextMenu != null) { + contextMenu.onHideContextMenu!(); + } + break; + case "onContextMenuActionItemClicked": + ContextMenu? contextMenu; + if (webviewParams != null && webviewParams!.contextMenu != null) { + contextMenu = webviewParams!.contextMenu; + } else if (_inAppBrowserEventHandler != null && + _inAppBrowser!.contextMenu != null) { + contextMenu = _inAppBrowser!.contextMenu; + } + + if (contextMenu != null) { + int? androidId = call.arguments["androidId"]; + String? iosId = call.arguments["iosId"]; + dynamic id = call.arguments["id"]; + String title = call.arguments["title"]; + + ContextMenuItem menuItemClicked = ContextMenuItem( + id: id, + // ignore: deprecated_member_use_from_same_package + androidId: androidId, + // ignore: deprecated_member_use_from_same_package + iosId: iosId, + title: title, + action: null); + + for (var menuItem in contextMenu.menuItems) { + if (menuItem.id == id) { + menuItemClicked = menuItem; + if (menuItem.action != null) { + menuItem.action!(); + } + break; + } + } + + if (contextMenu.onContextMenuActionItemClicked != null) { + contextMenu.onContextMenuActionItemClicked!(menuItemClicked); + } + } + break; + case "onEnterFullscreen": + if (webviewParams != null && webviewParams!.onEnterFullscreen != null) + webviewParams!.onEnterFullscreen!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onEnterFullscreen(); + break; + case "onExitFullscreen": + if (webviewParams != null && webviewParams!.onExitFullscreen != null) + webviewParams!.onExitFullscreen!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onExitFullscreen(); + break; + case "onOverScrolled": + if ((webviewParams != null && webviewParams!.onOverScrolled != null) || + _inAppBrowserEventHandler != null) { + int x = call.arguments["x"]; + int y = call.arguments["y"]; + bool clampedX = call.arguments["clampedX"]; + bool clampedY = call.arguments["clampedY"]; + + if (webviewParams != null && webviewParams!.onOverScrolled != null) + webviewParams!.onOverScrolled!( + _controllerFromPlatform, x, y, clampedX, clampedY); + else + _inAppBrowserEventHandler!.onOverScrolled(x, y, clampedX, clampedY); + } + break; + case "onWindowFocus": + if (webviewParams != null && webviewParams!.onWindowFocus != null) + webviewParams!.onWindowFocus!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onWindowFocus(); + break; + case "onWindowBlur": + if (webviewParams != null && webviewParams!.onWindowBlur != null) + webviewParams!.onWindowBlur!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onWindowBlur(); + break; + case "onPrintRequest": + if ((webviewParams != null && + (webviewParams!.onPrintRequest != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.onPrint != null)) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + String? printJobId = call.arguments["printJobId"]; + WebUri? uri = url != null ? WebUri(url) : null; + IOSPrintJobController? printJob = printJobId != null + ? IOSPrintJobController( + IOSPrintJobControllerCreationParams(id: printJobId)) + : null; + + if (webviewParams != null) { + if (webviewParams!.onPrintRequest != null) + return await webviewParams!.onPrintRequest!( + _controllerFromPlatform, uri, printJob); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.onPrint!(_controllerFromPlatform, uri); + return false; + } + } else { + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler!.onPrint(uri); + return await _inAppBrowserEventHandler! + .onPrintRequest(uri, printJob); + } + } + break; + case "onInjectedScriptLoaded": + String id = call.arguments[0]; + var onLoadCallback = _injectedScriptsFromURL[id]?.onLoad; + if ((webviewParams != null || _inAppBrowserEventHandler != null) && + onLoadCallback != null) { + onLoadCallback(); + } + break; + case "onInjectedScriptError": + String id = call.arguments[0]; + var onErrorCallback = _injectedScriptsFromURL[id]?.onError; + if ((webviewParams != null || _inAppBrowserEventHandler != null) && + onErrorCallback != null) { + onErrorCallback(); + } + break; + case "onCameraCaptureStateChanged": + if ((webviewParams != null && + webviewParams!.onCameraCaptureStateChanged != null) || + _inAppBrowserEventHandler != null) { + var oldState = + MediaCaptureState.fromNativeValue(call.arguments["oldState"]); + var newState = + MediaCaptureState.fromNativeValue(call.arguments["newState"]); + + if (webviewParams != null && + webviewParams!.onCameraCaptureStateChanged != null) + webviewParams!.onCameraCaptureStateChanged!( + _controllerFromPlatform, oldState, newState); + else + _inAppBrowserEventHandler! + .onCameraCaptureStateChanged(oldState, newState); + } + break; + case "onMicrophoneCaptureStateChanged": + if ((webviewParams != null && + webviewParams!.onMicrophoneCaptureStateChanged != null) || + _inAppBrowserEventHandler != null) { + var oldState = + MediaCaptureState.fromNativeValue(call.arguments["oldState"]); + var newState = + MediaCaptureState.fromNativeValue(call.arguments["newState"]); + + if (webviewParams != null && + webviewParams!.onMicrophoneCaptureStateChanged != null) + webviewParams!.onMicrophoneCaptureStateChanged!( + _controllerFromPlatform, oldState, newState); + else + _inAppBrowserEventHandler! + .onMicrophoneCaptureStateChanged(oldState, newState); + } + break; + case "onContentSizeChanged": + if ((webviewParams != null && + webviewParams!.onContentSizeChanged != null) || + _inAppBrowserEventHandler != null) { + var oldContentSize = MapSize.fromMap( + call.arguments["oldContentSize"]?.cast())!; + var newContentSize = MapSize.fromMap( + call.arguments["newContentSize"]?.cast())!; + + if (webviewParams != null && + webviewParams!.onContentSizeChanged != null) + webviewParams!.onContentSizeChanged!( + _controllerFromPlatform, oldContentSize, newContentSize); + else + _inAppBrowserEventHandler! + .onContentSizeChanged(oldContentSize, newContentSize); + } + break; + case "onCallJsHandler": + String handlerName = call.arguments["handlerName"]; + // decode args to json + List args = jsonDecode(call.arguments["args"]); + + _debugLog(handlerName, args); + + switch (handlerName) { + case "onLoadResource": + if ((webviewParams != null && + webviewParams!.onLoadResource != null) || + _inAppBrowserEventHandler != null) { + Map arguments = args[0].cast(); + arguments["startTime"] = arguments["startTime"] is int + ? arguments["startTime"].toDouble() + : arguments["startTime"]; + arguments["duration"] = arguments["duration"] is int + ? arguments["duration"].toDouble() + : arguments["duration"]; + + var response = LoadedResource.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onLoadResource != null) + webviewParams!.onLoadResource!( + _controllerFromPlatform, response); + else + _inAppBrowserEventHandler!.onLoadResource(response); + } + return null; + case "shouldInterceptAjaxRequest": + if ((webviewParams != null && + webviewParams!.shouldInterceptAjaxRequest != null) || + _inAppBrowserEventHandler != null) { + Map arguments = args[0].cast(); + AjaxRequest request = AjaxRequest.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.shouldInterceptAjaxRequest != null) + return jsonEncode( + await params.webviewParams!.shouldInterceptAjaxRequest!( + _controllerFromPlatform, request)); + else + return jsonEncode(await _inAppBrowserEventHandler! + .shouldInterceptAjaxRequest(request)); + } + return null; + case "onAjaxReadyStateChange": + if ((webviewParams != null && + webviewParams!.onAjaxReadyStateChange != null) || + _inAppBrowserEventHandler != null) { + Map arguments = args[0].cast(); + AjaxRequest request = AjaxRequest.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onAjaxReadyStateChange != null) + return (await webviewParams!.onAjaxReadyStateChange!( + _controllerFromPlatform, request)) + ?.toNativeValue(); + else + return (await _inAppBrowserEventHandler! + .onAjaxReadyStateChange(request)) + ?.toNativeValue(); + } + return null; + case "onAjaxProgress": + if ((webviewParams != null && + webviewParams!.onAjaxProgress != null) || + _inAppBrowserEventHandler != null) { + Map arguments = args[0].cast(); + AjaxRequest request = AjaxRequest.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onAjaxProgress != null) + return (await webviewParams!.onAjaxProgress!( + _controllerFromPlatform, request)) + ?.toNativeValue(); + else + return (await _inAppBrowserEventHandler! + .onAjaxProgress(request)) + ?.toNativeValue(); + } + return null; + case "shouldInterceptFetchRequest": + if ((webviewParams != null && + webviewParams!.shouldInterceptFetchRequest != null) || + _inAppBrowserEventHandler != null) { + Map arguments = args[0].cast(); + FetchRequest request = FetchRequest.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.shouldInterceptFetchRequest != null) + return jsonEncode( + await webviewParams!.shouldInterceptFetchRequest!( + _controllerFromPlatform, request)); + else + return jsonEncode(await _inAppBrowserEventHandler! + .shouldInterceptFetchRequest(request)); + } + return null; + case "onWindowFocus": + if (webviewParams != null && webviewParams!.onWindowFocus != null) + webviewParams!.onWindowFocus!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onWindowFocus(); + return null; + case "onWindowBlur": + if (webviewParams != null && webviewParams!.onWindowBlur != null) + webviewParams!.onWindowBlur!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onWindowBlur(); + return null; + case "onInjectedScriptLoaded": + String id = args[0]; + var onLoadCallback = _injectedScriptsFromURL[id]?.onLoad; + if ((webviewParams != null || _inAppBrowserEventHandler != null) && + onLoadCallback != null) { + onLoadCallback(); + } + return null; + case "onInjectedScriptError": + String id = args[0]; + var onErrorCallback = _injectedScriptsFromURL[id]?.onError; + if ((webviewParams != null || _inAppBrowserEventHandler != null) && + onErrorCallback != null) { + onErrorCallback(); + } + return null; + } + + if (_javaScriptHandlersMap.containsKey(handlerName)) { + // convert result to json + try { + return jsonEncode(await _javaScriptHandlersMap[handlerName]!(args)); + } catch (error, stacktrace) { + developer.log(error.toString() + '\n' + stacktrace.toString(), + name: 'JavaScript Handler "$handlerName"'); + throw Exception(error.toString().replaceFirst('Exception: ', '')); + } + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + @override + Future getUrl() async { + Map args = {}; + String? url = await channel?.invokeMethod('getUrl', args); + return url != null ? WebUri(url) : null; + } + + @override + Future getTitle() async { + Map args = {}; + return await channel?.invokeMethod('getTitle', args); + } + + @override + Future getProgress() async { + Map args = {}; + return await channel?.invokeMethod('getProgress', args); + } + + @override + Future getHtml() async { + String? html; + + InAppWebViewSettings? settings = await getSettings(); + if (settings != null && settings.javaScriptEnabled == true) { + html = await evaluateJavascript( + source: "window.document.getElementsByTagName('html')[0].outerHTML;"); + if (html != null && html.isNotEmpty) return html; + } + + var webviewUrl = await getUrl(); + if (webviewUrl == null) { + return html; + } + + if (webviewUrl.isScheme("file")) { + var assetPathSplit = webviewUrl.toString().split("/flutter_assets/"); + var assetPath = assetPathSplit[assetPathSplit.length - 1]; + try { + var bytes = await rootBundle.load(assetPath); + html = utf8.decode(bytes.buffer.asUint8List()); + } catch (e) {} + } else { + try { + HttpClient client = HttpClient(); + var htmlRequest = await client.getUrl(webviewUrl); + html = + await (await htmlRequest.close()).transform(Utf8Decoder()).join(); + } catch (e) { + developer.log(e.toString(), name: this.runtimeType.toString()); + } + } + + return html; + } + + @override + Future> getFavicons() async { + List favicons = []; + + var webviewUrl = await getUrl(); + + if (webviewUrl == null) { + return favicons; + } + + String? manifestUrl; + + var html = await getHtml(); + if (html == null || html.isEmpty) { + return favicons; + } + var assetPathBase; + + if (webviewUrl.isScheme("file")) { + var assetPathSplit = webviewUrl.toString().split("/flutter_assets/"); + assetPathBase = assetPathSplit[0] + "/flutter_assets/"; + } + + InAppWebViewSettings? settings = await getSettings(); + if (settings != null && settings.javaScriptEnabled == true) { + List> links = (await evaluateJavascript(source: """ +(function() { + var linkNodes = document.head.getElementsByTagName("link"); + var links = []; + for (var i = 0; i < linkNodes.length; i++) { + var linkNode = linkNodes[i]; + if (linkNode.rel === 'manifest') { + links.push( + { + rel: linkNode.rel, + href: linkNode.href, + sizes: null + } + ); + } else if (linkNode.rel != null && linkNode.rel.indexOf('icon') >= 0) { + links.push( + { + rel: linkNode.rel, + href: linkNode.href, + sizes: linkNode.sizes != null && linkNode.sizes.value != "" ? linkNode.sizes.value : null + } + ); + } + } + return links; +})(); +"""))?.cast>() ?? []; + for (var link in links) { + if (link["rel"] == "manifest") { + manifestUrl = link["href"]; + if (!_isUrlAbsolute(manifestUrl!)) { + if (manifestUrl.startsWith("/")) { + manifestUrl = manifestUrl.substring(1); + } + manifestUrl = ((assetPathBase == null) + ? webviewUrl.scheme + "://" + webviewUrl.host + "/" + : assetPathBase) + + manifestUrl; + } + continue; + } + favicons.addAll(_createFavicons(webviewUrl, assetPathBase, link["href"], + link["rel"], link["sizes"], false)); + } + } + + // try to get /favicon.ico + try { + HttpClient client = HttpClient(); + var faviconUrl = + webviewUrl.scheme + "://" + webviewUrl.host + "/favicon.ico"; + var faviconUri = WebUri(faviconUrl); + var headRequest = await client.headUrl(faviconUri); + var headResponse = await headRequest.close(); + if (headResponse.statusCode == 200) { + favicons.add(Favicon(url: faviconUri, rel: "shortcut icon")); + } + } catch (e) { + developer.log("/favicon.ico file not found: " + e.toString(), + name: runtimeType.toString()); + } + + // try to get the manifest file + HttpClientRequest? manifestRequest; + HttpClientResponse? manifestResponse; + bool manifestFound = false; + if (manifestUrl == null) { + manifestUrl = + webviewUrl.scheme + "://" + webviewUrl.host + "/manifest.json"; + } + try { + HttpClient client = HttpClient(); + manifestRequest = await client.getUrl(Uri.parse(manifestUrl)); + manifestResponse = await manifestRequest.close(); + manifestFound = manifestResponse.statusCode == 200 && + manifestResponse.headers.contentType?.mimeType == "application/json"; + } catch (e) { + developer.log("Manifest file not found: " + e.toString(), + name: this.runtimeType.toString()); + } + + if (manifestFound) { + try { + Map manifest = json + .decode(await manifestResponse!.transform(Utf8Decoder()).join()); + if (manifest.containsKey("icons")) { + for (Map icon in manifest["icons"]) { + favicons.addAll(_createFavicons(webviewUrl, assetPathBase, + icon["src"], icon["rel"], icon["sizes"], true)); + } + } + } catch (e) { + developer.log( + "Cannot get favicons from Manifest file. It might not have a valid format: " + + e.toString(), + error: e, + name: runtimeType.toString()); + } + } + + return favicons; + } + + bool _isUrlAbsolute(String url) { + return url.startsWith("http://") || url.startsWith("https://"); + } + + List _createFavicons(WebUri url, String? assetPathBase, + String urlIcon, String? rel, String? sizes, bool isManifest) { + List favicons = []; + + List urlSplit = urlIcon.split("/"); + if (!_isUrlAbsolute(urlIcon)) { + if (urlIcon.startsWith("/")) { + urlIcon = urlIcon.substring(1); + } + urlIcon = ((assetPathBase == null) + ? url.scheme + "://" + url.host + "/" + : assetPathBase) + + urlIcon; + } + if (isManifest) { + rel = (sizes != null) + ? urlSplit[urlSplit.length - 1] + .replaceFirst("-" + sizes, "") + .split(" ")[0] + .split(".")[0] + : null; + } + if (sizes != null && sizes.isNotEmpty && sizes != "any") { + List sizesSplit = sizes.split(" "); + for (String size in sizesSplit) { + int width = int.parse(size.split("x")[0]); + int height = int.parse(size.split("x")[1]); + favicons.add(Favicon( + url: WebUri(urlIcon), rel: rel, width: width, height: height)); + } + } else { + favicons.add( + Favicon(url: WebUri(urlIcon), rel: rel, width: null, height: null)); + } + + return favicons; + } + + @override + Future loadUrl( + {required URLRequest urlRequest, + @Deprecated('Use allowingReadAccessTo instead') + Uri? iosAllowingReadAccessTo, + WebUri? allowingReadAccessTo}) async { + assert(urlRequest.url != null && urlRequest.url.toString().isNotEmpty); + assert( + allowingReadAccessTo == null || allowingReadAccessTo.isScheme("file")); + assert(iosAllowingReadAccessTo == null || + iosAllowingReadAccessTo.isScheme("file")); + + Map args = {}; + args.putIfAbsent('urlRequest', () => urlRequest.toMap()); + args.putIfAbsent( + 'allowingReadAccessTo', + () => + allowingReadAccessTo?.toString() ?? + iosAllowingReadAccessTo?.toString()); + await channel?.invokeMethod('loadUrl', args); + } + + @override + Future postUrl( + {required WebUri url, required Uint8List postData}) async { + assert(url.toString().isNotEmpty); + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + args.putIfAbsent('postData', () => postData); + await channel?.invokeMethod('postUrl', args); + } + + @override + Future loadData( + {required String data, + String mimeType = "text/html", + String encoding = "utf8", + WebUri? baseUrl, + @Deprecated('Use historyUrl instead') Uri? androidHistoryUrl, + WebUri? historyUrl, + @Deprecated('Use allowingReadAccessTo instead') + Uri? iosAllowingReadAccessTo, + WebUri? allowingReadAccessTo}) async { + assert( + allowingReadAccessTo == null || allowingReadAccessTo.isScheme("file")); + assert(iosAllowingReadAccessTo == null || + iosAllowingReadAccessTo.isScheme("file")); + + Map args = {}; + args.putIfAbsent('data', () => data); + args.putIfAbsent('mimeType', () => mimeType); + args.putIfAbsent('encoding', () => encoding); + args.putIfAbsent('baseUrl', () => baseUrl?.toString() ?? "about:blank"); + args.putIfAbsent( + 'historyUrl', + () => + historyUrl?.toString() ?? + androidHistoryUrl?.toString() ?? + "about:blank"); + args.putIfAbsent( + 'allowingReadAccessTo', + () => + allowingReadAccessTo?.toString() ?? + iosAllowingReadAccessTo?.toString()); + await channel?.invokeMethod('loadData', args); + } + + @override + Future loadFile({required String assetFilePath}) async { + assert(assetFilePath.isNotEmpty); + Map args = {}; + args.putIfAbsent('assetFilePath', () => assetFilePath); + await channel?.invokeMethod('loadFile', args); + } + + @override + Future reload() async { + Map args = {}; + await channel?.invokeMethod('reload', args); + } + + @override + Future goBack() async { + Map args = {}; + await channel?.invokeMethod('goBack', args); + } + + @override + Future canGoBack() async { + Map args = {}; + return await channel?.invokeMethod('canGoBack', args) ?? false; + } + + @override + Future goForward() async { + Map args = {}; + await channel?.invokeMethod('goForward', args); + } + + @override + Future canGoForward() async { + Map args = {}; + return await channel?.invokeMethod('canGoForward', args) ?? false; + } + + @override + Future goBackOrForward({required int steps}) async { + Map args = {}; + args.putIfAbsent('steps', () => steps); + await channel?.invokeMethod('goBackOrForward', args); + } + + @override + Future canGoBackOrForward({required int steps}) async { + Map args = {}; + args.putIfAbsent('steps', () => steps); + return await channel?.invokeMethod('canGoBackOrForward', args) ?? + false; + } + + @override + Future goTo({required WebHistoryItem historyItem}) async { + var steps = historyItem.offset; + if (steps != null) { + await goBackOrForward(steps: steps); + } + } + + @override + Future isLoading() async { + Map args = {}; + return await channel?.invokeMethod('isLoading', args) ?? false; + } + + @override + Future stopLoading() async { + Map args = {}; + await channel?.invokeMethod('stopLoading', args); + } + + @override + Future evaluateJavascript( + {required String source, ContentWorld? contentWorld}) async { + Map args = {}; + args.putIfAbsent('source', () => source); + args.putIfAbsent('contentWorld', () => contentWorld?.toMap()); + var data = await channel?.invokeMethod('evaluateJavascript', args); + return data; + } + + @override + Future injectJavascriptFileFromUrl( + {required WebUri urlFile, + ScriptHtmlTagAttributes? scriptHtmlTagAttributes}) async { + assert(urlFile.toString().isNotEmpty); + var id = scriptHtmlTagAttributes?.id; + if (scriptHtmlTagAttributes != null && id != null) { + _injectedScriptsFromURL[id] = scriptHtmlTagAttributes; + } + Map args = {}; + args.putIfAbsent('urlFile', () => urlFile.toString()); + args.putIfAbsent( + 'scriptHtmlTagAttributes', () => scriptHtmlTagAttributes?.toMap()); + await channel?.invokeMethod('injectJavascriptFileFromUrl', args); + } + + @override + Future injectJavascriptFileFromAsset( + {required String assetFilePath}) async { + String source = await rootBundle.loadString(assetFilePath); + return await evaluateJavascript(source: source); + } + + @override + Future injectCSSCode({required String source}) async { + Map args = {}; + args.putIfAbsent('source', () => source); + await channel?.invokeMethod('injectCSSCode', args); + } + + @override + Future injectCSSFileFromUrl( + {required WebUri urlFile, + CSSLinkHtmlTagAttributes? cssLinkHtmlTagAttributes}) async { + assert(urlFile.toString().isNotEmpty); + Map args = {}; + args.putIfAbsent('urlFile', () => urlFile.toString()); + args.putIfAbsent( + 'cssLinkHtmlTagAttributes', () => cssLinkHtmlTagAttributes?.toMap()); + await channel?.invokeMethod('injectCSSFileFromUrl', args); + } + + @override + Future injectCSSFileFromAsset({required String assetFilePath}) async { + String source = await rootBundle.loadString(assetFilePath); + await injectCSSCode(source: source); + } + + @override + void addJavaScriptHandler( + {required String handlerName, + required JavaScriptHandlerCallback callback}) { + assert(!_JAVASCRIPT_HANDLER_FORBIDDEN_NAMES.contains(handlerName), + '"$handlerName" is a forbidden name!'); + this._javaScriptHandlersMap[handlerName] = (callback); + } + + @override + JavaScriptHandlerCallback? removeJavaScriptHandler( + {required String handlerName}) { + return this._javaScriptHandlersMap.remove(handlerName); + } + + @override + bool hasJavaScriptHandler({required String handlerName}) { + return this._javaScriptHandlersMap.containsKey(handlerName); + } + + @override + Future takeScreenshot( + {ScreenshotConfiguration? screenshotConfiguration}) async { + Map args = {}; + args.putIfAbsent( + 'screenshotConfiguration', () => screenshotConfiguration?.toMap()); + return await channel?.invokeMethod('takeScreenshot', args); + } + + @override + @Deprecated('Use setSettings instead') + Future setOptions({required InAppWebViewGroupOptions options}) async { + InAppWebViewSettings settings = + InAppWebViewSettings.fromMap(options.toMap()) ?? InAppWebViewSettings(); + await setSettings(settings: settings); + } + + @override + @Deprecated('Use getSettings instead') + Future getOptions() async { + InAppWebViewSettings? settings = await getSettings(); + + Map? options = settings?.toMap(); + if (options != null) { + options = options.cast(); + return InAppWebViewGroupOptions.fromMap(options as Map); + } + + return null; + } + + @override + Future setSettings({required InAppWebViewSettings settings}) async { + Map args = {}; + + args.putIfAbsent('settings', () => settings.toMap()); + await channel?.invokeMethod('setSettings', args); + } + + @override + Future getSettings() async { + Map args = {}; + + Map? settings = + await channel?.invokeMethod('getSettings', args); + if (settings != null) { + settings = settings.cast(); + return InAppWebViewSettings.fromMap(settings as Map); + } + + return null; + } + + @override + Future getCopyBackForwardList() async { + Map args = {}; + Map? result = + (await channel?.invokeMethod('getCopyBackForwardList', args)) + ?.cast(); + return WebHistory.fromMap(result); + } + + @override + @Deprecated("Use InAppWebViewController.clearAllCache instead") + Future clearCache() async { + Map args = {}; + await channel?.invokeMethod('clearCache', args); + } + + @override + @Deprecated("Use FindInteractionController.findAll instead") + Future findAllAsync({required String find}) async { + Map args = {}; + args.putIfAbsent('find', () => find); + await channel?.invokeMethod('findAll', args); + } + + @override + @Deprecated("Use FindInteractionController.findNext instead") + Future findNext({required bool forward}) async { + Map args = {}; + args.putIfAbsent('forward', () => forward); + await channel?.invokeMethod('findNext', args); + } + + @override + @Deprecated("Use FindInteractionController.clearMatches instead") + Future clearMatches() async { + Map args = {}; + await channel?.invokeMethod('clearMatches', args); + } + + @override + @Deprecated("Use tRexRunnerHtml instead") + Future getTRexRunnerHtml() async { + return await tRexRunnerHtml; + } + + @override + @Deprecated("Use tRexRunnerCss instead") + Future getTRexRunnerCss() async { + return await tRexRunnerCss; + } + + @override + Future scrollTo( + {required int x, required int y, bool animated = false}) async { + Map args = {}; + args.putIfAbsent('x', () => x); + args.putIfAbsent('y', () => y); + args.putIfAbsent('animated', () => animated); + await channel?.invokeMethod('scrollTo', args); + } + + @override + Future scrollBy( + {required int x, required int y, bool animated = false}) async { + Map args = {}; + args.putIfAbsent('x', () => x); + args.putIfAbsent('y', () => y); + args.putIfAbsent('animated', () => animated); + await channel?.invokeMethod('scrollBy', args); + } + + @override + Future pauseTimers() async { + Map args = {}; + await channel?.invokeMethod('pauseTimers', args); + } + + @override + Future resumeTimers() async { + Map args = {}; + await channel?.invokeMethod('resumeTimers', args); + } + + @override + Future printCurrentPage( + {PrintJobSettings? settings}) async { + Map args = {}; + args.putIfAbsent("settings", () => settings?.toMap()); + String? jobId = + await channel?.invokeMethod('printCurrentPage', args); + if (jobId != null) { + return IOSPrintJobController( + PlatformPrintJobControllerCreationParams(id: jobId)); + } + return null; + } + + @override + Future getContentHeight() async { + Map args = {}; + var height = await channel?.invokeMethod('getContentHeight', args); + if (height == null || height == 0) { + // try to use javascript + var scrollHeight = await evaluateJavascript( + source: "document.documentElement.scrollHeight;"); + if (scrollHeight != null && scrollHeight is num) { + height = scrollHeight.toInt(); + } + } + return height; + } + + @override + Future getContentWidth() async { + Map args = {}; + var height = await channel?.invokeMethod('getContentWidth', args); + if (height == null || height == 0) { + // try to use javascript + var scrollHeight = await evaluateJavascript( + source: "document.documentElement.scrollWidth;"); + if (scrollHeight != null && scrollHeight is num) { + height = scrollHeight.toInt(); + } + } + return height; + } + + @override + Future zoomBy( + {required double zoomFactor, + @Deprecated('Use animated instead') bool? iosAnimated, + bool animated = false}) async { + Map args = {}; + args.putIfAbsent('zoomFactor', () => zoomFactor); + args.putIfAbsent('animated', () => iosAnimated ?? animated); + return await channel?.invokeMethod('zoomBy', args); + } + + @override + Future getOriginalUrl() async { + Map args = {}; + String? url = await channel?.invokeMethod('getOriginalUrl', args); + return url != null ? WebUri(url) : null; + } + + @override + Future getZoomScale() async { + Map args = {}; + return await channel?.invokeMethod('getZoomScale', args); + } + + @override + @Deprecated('Use getZoomScale instead') + Future getScale() async { + return await getZoomScale(); + } + + @override + Future getSelectedText() async { + Map args = {}; + return await channel?.invokeMethod('getSelectedText', args); + } + + @override + Future getHitTestResult() async { + Map args = {}; + Map? hitTestResultMap = + await channel?.invokeMethod('getHitTestResult', args); + + if (hitTestResultMap == null) { + return null; + } + + hitTestResultMap = hitTestResultMap.cast(); + + InAppWebViewHitTestResultType? type = + InAppWebViewHitTestResultType.fromNativeValue( + hitTestResultMap["type"]?.toInt()); + String? extra = hitTestResultMap["extra"]; + return InAppWebViewHitTestResult(type: type, extra: extra); + } + + @override + Future clearFocus() async { + Map args = {}; + return await channel?.invokeMethod('clearFocus', args); + } + + @override + Future setContextMenu(ContextMenu? contextMenu) async { + Map args = {}; + args.putIfAbsent("contextMenu", () => contextMenu?.toMap()); + await channel?.invokeMethod('setContextMenu', args); + _inAppBrowser?.setContextMenu(contextMenu); + } + + @override + Future requestFocusNodeHref() async { + Map args = {}; + Map? result = + await channel?.invokeMethod('requestFocusNodeHref', args); + return result != null + ? RequestFocusNodeHrefResult( + url: result['url'] != null ? WebUri(result['url']) : null, + title: result['title'], + src: result['src'], + ) + : null; + } + + @override + Future requestImageRef() async { + Map args = {}; + Map? result = + await channel?.invokeMethod('requestImageRef', args); + return result != null + ? RequestImageRefResult( + url: result['url'] != null ? WebUri(result['url']) : null, + ) + : null; + } + + @override + Future> getMetaTags() async { + List metaTags = []; + + List>? metaTagList = + (await evaluateJavascript(source: """ +(function() { + var metaTags = []; + var metaTagNodes = document.head.getElementsByTagName('meta'); + for (var i = 0; i < metaTagNodes.length; i++) { + var metaTagNode = metaTagNodes[i]; + + var otherAttributes = metaTagNode.getAttributeNames(); + var nameIndex = otherAttributes.indexOf("name"); + if (nameIndex !== -1) otherAttributes.splice(nameIndex, 1); + var contentIndex = otherAttributes.indexOf("content"); + if (contentIndex !== -1) otherAttributes.splice(contentIndex, 1); + + var attrs = []; + for (var j = 0; j < otherAttributes.length; j++) { + var otherAttribute = otherAttributes[j]; + attrs.push( + { + name: otherAttribute, + value: metaTagNode.getAttribute(otherAttribute) + } + ); + } + + metaTags.push( + { + name: metaTagNode.name, + content: metaTagNode.content, + attrs: attrs + } + ); + } + return metaTags; +})(); + """))?.cast>(); + + if (metaTagList == null) { + return metaTags; + } + + for (var metaTag in metaTagList) { + var attrs = []; + + for (var metaTagAttr in metaTag["attrs"]) { + attrs.add(MetaTagAttribute( + name: metaTagAttr["name"], value: metaTagAttr["value"])); + } + + metaTags.add(MetaTag( + name: metaTag["name"], content: metaTag["content"], attrs: attrs)); + } + + return metaTags; + } + + @override + Future getMetaThemeColor() async { + Color? themeColor; + + try { + Map args = {}; + themeColor = UtilColor.fromStringRepresentation( + await channel?.invokeMethod('getMetaThemeColor', args)); + return themeColor; + } catch (e) { + // not implemented + } + + // try using javascript + var metaTags = await getMetaTags(); + MetaTag? metaTagThemeColor; + + for (var metaTag in metaTags) { + if (metaTag.name == "theme-color") { + metaTagThemeColor = metaTag; + break; + } + } + + if (metaTagThemeColor == null) { + return null; + } + + var colorValue = metaTagThemeColor.content; + + themeColor = colorValue != null + ? UtilColor.fromStringRepresentation(colorValue) + : null; + + return themeColor; + } + + @override + Future getScrollX() async { + Map args = {}; + return await channel?.invokeMethod('getScrollX', args); + } + + @override + Future getScrollY() async { + Map args = {}; + return await channel?.invokeMethod('getScrollY', args); + } + + @override + Future getCertificate() async { + Map args = {}; + Map? sslCertificateMap = + (await channel?.invokeMethod('getCertificate', args)) + ?.cast(); + return SslCertificate.fromMap(sslCertificateMap); + } + + @override + Future addUserScript({required UserScript userScript}) async { + assert(webviewParams?.windowId == null); + + Map args = {}; + args.putIfAbsent('userScript', () => userScript.toMap()); + if (!(_userScripts[userScript.injectionTime]?.contains(userScript) ?? + false)) { + _userScripts[userScript.injectionTime]?.add(userScript); + await channel?.invokeMethod('addUserScript', args); + } + } + + @override + Future addUserScripts({required List userScripts}) async { + assert(webviewParams?.windowId == null); + + for (var i = 0; i < userScripts.length; i++) { + await addUserScript(userScript: userScripts[i]); + } + } + + @override + Future removeUserScript({required UserScript userScript}) async { + assert(webviewParams?.windowId == null); + + var index = _userScripts[userScript.injectionTime]?.indexOf(userScript); + if (index == null || index == -1) { + return false; + } + + _userScripts[userScript.injectionTime]?.remove(userScript); + Map args = {}; + args.putIfAbsent('userScript', () => userScript.toMap()); + args.putIfAbsent('index', () => index); + await channel?.invokeMethod('removeUserScript', args); + + return true; + } + + @override + Future removeUserScriptsByGroupName({required String groupName}) async { + assert(webviewParams?.windowId == null); + + final List userScriptsAtDocumentStart = List.from( + _userScripts[UserScriptInjectionTime.AT_DOCUMENT_START] ?? []); + for (final userScript in userScriptsAtDocumentStart) { + if (userScript.groupName == groupName) { + _userScripts[userScript.injectionTime]?.remove(userScript); + } + } + + final List userScriptsAtDocumentEnd = + List.from(_userScripts[UserScriptInjectionTime.AT_DOCUMENT_END] ?? []); + for (final userScript in userScriptsAtDocumentEnd) { + if (userScript.groupName == groupName) { + _userScripts[userScript.injectionTime]?.remove(userScript); + } + } + + Map args = {}; + args.putIfAbsent('groupName', () => groupName); + await channel?.invokeMethod('removeUserScriptsByGroupName', args); + } + + @override + Future removeUserScripts( + {required List userScripts}) async { + assert(webviewParams?.windowId == null); + + for (final userScript in userScripts) { + await removeUserScript(userScript: userScript); + } + } + + @override + Future removeAllUserScripts() async { + assert(webviewParams?.windowId == null); + + _userScripts[UserScriptInjectionTime.AT_DOCUMENT_START]?.clear(); + _userScripts[UserScriptInjectionTime.AT_DOCUMENT_END]?.clear(); + + Map args = {}; + await channel?.invokeMethod('removeAllUserScripts', args); + } + + @override + bool hasUserScript({required UserScript userScript}) { + return _userScripts[userScript.injectionTime]?.contains(userScript) ?? + false; + } + + @override + Future callAsyncJavaScript( + {required String functionBody, + Map arguments = const {}, + ContentWorld? contentWorld}) async { + Map args = {}; + args.putIfAbsent('functionBody', () => functionBody); + args.putIfAbsent('arguments', () => arguments); + args.putIfAbsent('contentWorld', () => contentWorld?.toMap()); + var data = await channel?.invokeMethod('callAsyncJavaScript', args); + if (data == null) { + return null; + } + return CallAsyncJavaScriptResult( + value: data["value"], error: data["error"]); + } + + @override + Future saveWebArchive( + {required String filePath, bool autoname = false}) async { + if (!autoname) { + assert( + filePath.endsWith("." + WebArchiveFormat.WEBARCHIVE.toNativeValue())); + } + + Map args = {}; + args.putIfAbsent("filePath", () => filePath); + args.putIfAbsent("autoname", () => autoname); + return await channel?.invokeMethod('saveWebArchive', args); + } + + @override + Future isSecureContext() async { + Map args = {}; + return await channel?.invokeMethod('isSecureContext', args) ?? false; + } + + @override + Future createWebMessageChannel() async { + Map args = {}; + Map? result = + (await channel?.invokeMethod('createWebMessageChannel', args)) + ?.cast(); + final webMessageChannel = IOSWebMessageChannel.static().fromMap(result); + if (webMessageChannel != null) { + _webMessageChannels.add(webMessageChannel); + } + return webMessageChannel; + } + + @override + Future postWebMessage( + {required WebMessage message, WebUri? targetOrigin}) async { + if (targetOrigin == null) { + targetOrigin = WebUri(''); + } + Map args = {}; + args.putIfAbsent('message', () => message.toMap()); + args.putIfAbsent('targetOrigin', () => targetOrigin.toString()); + await channel?.invokeMethod('postWebMessage', args); + } + + @override + Future addWebMessageListener( + PlatformWebMessageListener webMessageListener) async { + assert(!_webMessageListeners.contains(webMessageListener), + "${webMessageListener} was already added."); + assert( + !_webMessageListenerObjNames + .contains(webMessageListener.params.jsObjectName), + "jsObjectName ${webMessageListener.params.jsObjectName} was already added."); + _webMessageListeners.add(webMessageListener as IOSWebMessageListener); + _webMessageListenerObjNames.add(webMessageListener.params.jsObjectName); + + Map args = {}; + args.putIfAbsent('webMessageListener', () => webMessageListener.toMap()); + await channel?.invokeMethod('addWebMessageListener', args); + } + + @override + bool hasWebMessageListener(PlatformWebMessageListener webMessageListener) { + return _webMessageListeners.contains(webMessageListener) || + _webMessageListenerObjNames + .contains(webMessageListener.params.jsObjectName); + } + + @override + Future canScrollVertically() async { + Map args = {}; + return await channel?.invokeMethod('canScrollVertically', args) ?? + false; + } + + @override + Future canScrollHorizontally() async { + Map args = {}; + return await channel?.invokeMethod('canScrollHorizontally', args) ?? + false; + } + + @override + Future reloadFromOrigin() async { + Map args = {}; + await channel?.invokeMethod('reloadFromOrigin', args); + } + + @override + Future createPdf( + {@Deprecated("Use pdfConfiguration instead") + // ignore: deprecated_member_use_from_same_package + IOSWKPDFConfiguration? iosWKPdfConfiguration, + PDFConfiguration? pdfConfiguration}) async { + Map args = {}; + args.putIfAbsent('pdfConfiguration', + () => pdfConfiguration?.toMap() ?? iosWKPdfConfiguration?.toMap()); + return await channel?.invokeMethod('createPdf', args); + } + + @override + Future createWebArchiveData() async { + Map args = {}; + return await channel?.invokeMethod('createWebArchiveData', args); + } + + @override + Future hasOnlySecureContent() async { + Map args = {}; + return await channel?.invokeMethod('hasOnlySecureContent', args) ?? + false; + } + + @override + Future pauseAllMediaPlayback() async { + Map args = {}; + return await channel?.invokeMethod('pauseAllMediaPlayback', args); + } + + @override + Future setAllMediaPlaybackSuspended({required bool suspended}) async { + Map args = {}; + args.putIfAbsent("suspended", () => suspended); + return await channel?.invokeMethod('setAllMediaPlaybackSuspended', args); + } + + @override + Future closeAllMediaPresentations() async { + Map args = {}; + return await channel?.invokeMethod('closeAllMediaPresentations', args); + } + + @override + Future requestMediaPlaybackState() async { + Map args = {}; + return MediaPlaybackState.fromNativeValue( + await channel?.invokeMethod('requestMediaPlaybackState', args)); + } + + @override + Future isInFullscreen() async { + Map args = {}; + return await channel?.invokeMethod('isInFullscreen', args) ?? false; + } + + @override + Future getCameraCaptureState() async { + Map args = {}; + return MediaCaptureState.fromNativeValue( + await channel?.invokeMethod('getCameraCaptureState', args)); + } + + @override + Future setCameraCaptureState({required MediaCaptureState state}) async { + Map args = {}; + args.putIfAbsent('state', () => state.toNativeValue()); + await channel?.invokeMethod('setCameraCaptureState', args); + } + + @override + Future getMicrophoneCaptureState() async { + Map args = {}; + return MediaCaptureState.fromNativeValue( + await channel?.invokeMethod('getMicrophoneCaptureState', args)); + } + + @override + Future setMicrophoneCaptureState( + {required MediaCaptureState state}) async { + Map args = {}; + args.putIfAbsent('state', () => state.toNativeValue()); + await channel?.invokeMethod('setMicrophoneCaptureState', args); + } + + @override + Future loadSimulatedRequest( + {required URLRequest urlRequest, + required Uint8List data, + URLResponse? urlResponse}) async { + Map args = {}; + args.putIfAbsent('urlRequest', () => urlRequest.toMap()); + args.putIfAbsent('data', () => data); + args.putIfAbsent('urlResponse', () => urlResponse?.toMap()); + await channel?.invokeMethod('loadSimulatedRequest', args); + } + + @override + Future getDefaultUserAgent() async { + Map args = {}; + return await _staticChannel.invokeMethod( + 'getDefaultUserAgent', args) ?? + ''; + } + + @override + Future handlesURLScheme(String urlScheme) async { + Map args = {}; + args.putIfAbsent('urlScheme', () => urlScheme); + return await _staticChannel.invokeMethod('handlesURLScheme', args); + } + + @override + Future disposeKeepAlive(InAppWebViewKeepAlive keepAlive) async { + Map args = {}; + args.putIfAbsent('keepAliveId', () => keepAlive.id); + await _staticChannel.invokeMethod('disposeKeepAlive', args); + _keepAliveMap[keepAlive] = null; + } + + @override + Future clearAllCache({bool includeDiskFiles = true}) async { + Map args = {}; + args.putIfAbsent('includeDiskFiles', () => includeDiskFiles); + await _staticChannel.invokeMethod('clearAllCache', args); + } + + @override + Future get tRexRunnerHtml async => await rootBundle.loadString( + 'packages/flutter_inappwebview/assets/t_rex_runner/t-rex.html'); + + @override + Future get tRexRunnerCss async => await rootBundle.loadString( + 'packages/flutter_inappwebview/assets/t_rex_runner/t-rex.css'); + + @override + dynamic getViewId() { + return id; + } + + @override + void dispose({bool isKeepAlive = false}) { + disposeChannel(removeMethodCallHandler: !isKeepAlive); + _inAppBrowser = null; + webStorage.dispose(); + if (!isKeepAlive) { + _controllerFromPlatform = null; + _javaScriptHandlersMap.clear(); + _userScripts.clear(); + _webMessageListenerObjNames.clear(); + _injectedScriptsFromURL.clear(); + for (final webMessageChannel in _webMessageChannels) { + webMessageChannel.dispose(); + } + _webMessageChannels.clear(); + for (final webMessageListener in _webMessageListeners) { + webMessageListener.dispose(); + } + _webMessageListeners.clear(); + } + } +} + +extension InternalInAppWebViewController on IOSInAppWebViewController { + get handleMethod => _handleMethod; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/in_app_webview/main.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/in_app_webview/main.dart new file mode 100644 index 00000000..b83b0611 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/in_app_webview/main.dart @@ -0,0 +1,3 @@ +export 'in_app_webview_controller.dart' hide InternalInAppWebViewController; +export 'in_app_webview.dart'; +export 'headless_in_app_webview.dart' hide InternalHeadlessInAppWebView; diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/inappwebview_platform.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/inappwebview_platform.dart new file mode 100644 index 00000000..fc27a56d --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/inappwebview_platform.dart @@ -0,0 +1,273 @@ +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +import 'chrome_safari_browser/chrome_safari_browser.dart'; +import 'cookie_manager.dart'; +import 'http_auth_credentials_database.dart'; +import 'find_interaction/main.dart'; +import 'in_app_browser/in_app_browser.dart'; +import 'in_app_webview/main.dart'; +import 'print_job/main.dart'; +import 'pull_to_refresh/main.dart'; +import 'web_message/main.dart'; +import 'web_storage/main.dart'; +import 'web_authentication_session/main.dart'; + +/// Implementation of [InAppWebViewPlatform] using the WebKit API. +class IOSInAppWebViewPlatform extends InAppWebViewPlatform { + /// Registers this class as the default instance of [InAppWebViewPlatform]. + static void registerWith() { + InAppWebViewPlatform.instance = IOSInAppWebViewPlatform(); + } + + /// Creates a new [IOSCookieManager]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [CookieManager] in `flutter_inappwebview` instead. + @override + IOSCookieManager createPlatformCookieManager( + PlatformCookieManagerCreationParams params, + ) { + return IOSCookieManager(params); + } + + /// Creates a new [IOSInAppWebViewController]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [InAppWebViewController] in `flutter_inappwebview` instead. + @override + IOSInAppWebViewController createPlatformInAppWebViewController( + PlatformInAppWebViewControllerCreationParams params, + ) { + return IOSInAppWebViewController(params); + } + + /// Creates a new empty [IOSInAppWebViewController] to access static methods. + /// + /// This function should only be called by the app-facing package. + /// Look at using [InAppWebViewController] in `flutter_inappwebview` instead. + @override + IOSInAppWebViewController createPlatformInAppWebViewControllerStatic() { + return IOSInAppWebViewController.static(); + } + + /// Creates a new [IOSInAppWebViewWidget]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [InAppWebView] in `flutter_inappwebview` instead. + @override + IOSInAppWebViewWidget createPlatformInAppWebViewWidget( + PlatformInAppWebViewWidgetCreationParams params, + ) { + return IOSInAppWebViewWidget(params); + } + + /// Creates a new [IOSFindInteractionController]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [FindInteractionController] in `flutter_inappwebview` instead. + @override + IOSFindInteractionController createPlatformFindInteractionController( + PlatformFindInteractionControllerCreationParams params, + ) { + return IOSFindInteractionController(params); + } + + /// Creates a new [IOSPrintJobController]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [PrintJobController] in `flutter_inappwebview` instead. + @override + IOSPrintJobController createPlatformPrintJobController( + PlatformPrintJobControllerCreationParams params, + ) { + return IOSPrintJobController(params); + } + + /// Creates a new [IOSPullToRefreshController]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [PullToRefreshController] in `flutter_inappwebview` instead. + @override + IOSPullToRefreshController createPlatformPullToRefreshController( + PlatformPullToRefreshControllerCreationParams params, + ) { + return IOSPullToRefreshController(params); + } + + /// Creates a new [IOSWebMessageChannel]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebMessageChannel] in `flutter_inappwebview` instead. + @override + IOSWebMessageChannel createPlatformWebMessageChannel( + PlatformWebMessageChannelCreationParams params, + ) { + return IOSWebMessageChannel(params); + } + + /// Creates a new empty [IOSWebMessageChannel] to access static methods. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebMessageChannel] in `flutter_inappwebview` instead. + @override + IOSWebMessageChannel createPlatformWebMessageChannelStatic() { + return IOSWebMessageChannel.static(); + } + + /// Creates a new [IOSWebMessageListener]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebMessageListener] in `flutter_inappwebview` instead. + @override + IOSWebMessageListener createPlatformWebMessageListener( + PlatformWebMessageListenerCreationParams params, + ) { + return IOSWebMessageListener(params); + } + + /// Creates a new [IOSJavaScriptReplyProxy]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [JavaScriptReplyProxy] in `flutter_inappwebview` instead. + @override + IOSJavaScriptReplyProxy createPlatformJavaScriptReplyProxy( + PlatformJavaScriptReplyProxyCreationParams params, + ) { + return IOSJavaScriptReplyProxy(params); + } + + /// Creates a new [IOSWebMessagePort]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebMessagePort] in `flutter_inappwebview` instead. + @override + IOSWebMessagePort createPlatformWebMessagePort( + PlatformWebMessagePortCreationParams params, + ) { + return IOSWebMessagePort(params); + } + + /// Creates a new [IOSWebStorage]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebStorage] in `flutter_inappwebview` instead. + @override + IOSWebStorage createPlatformWebStorage( + PlatformWebStorageCreationParams params, + ) { + return IOSWebStorage(params); + } + + /// Creates a new [IOSLocalStorage]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [LocalStorage] in `flutter_inappwebview` instead. + @override + IOSLocalStorage createPlatformLocalStorage( + PlatformLocalStorageCreationParams params, + ) { + return IOSLocalStorage(params); + } + + /// Creates a new [IOSSessionStorage]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [SessionStorage] in `flutter_inappwebview` instead. + @override + IOSSessionStorage createPlatformSessionStorage( + PlatformSessionStorageCreationParams params, + ) { + return IOSSessionStorage(params); + } + + /// Creates a new [IOSHeadlessInAppWebView]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [HeadlessInAppWebView] in `flutter_inappwebview` instead. + @override + IOSHeadlessInAppWebView createPlatformHeadlessInAppWebView( + PlatformHeadlessInAppWebViewCreationParams params, + ) { + return IOSHeadlessInAppWebView(params); + } + + /// Creates a new [IOSHttpAuthCredentialDatabase]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [HttpAuthCredentialDatabase] in `flutter_inappwebview` instead. + @override + IOSHttpAuthCredentialDatabase createPlatformHttpAuthCredentialDatabase( + PlatformHttpAuthCredentialDatabaseCreationParams params, + ) { + return IOSHttpAuthCredentialDatabase(params); + } + + /// Creates a new [IOSInAppBrowser]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [InAppBrowser] in `flutter_inappwebview` instead. + @override + IOSInAppBrowser createPlatformInAppBrowser( + PlatformInAppBrowserCreationParams params, + ) { + return IOSInAppBrowser(params); + } + + /// Creates a new empty [IOSInAppBrowser] to access static methods. + /// + /// This function should only be called by the app-facing package. + /// Look at using [InAppBrowser] in `flutter_inappwebview` instead. + @override + IOSInAppBrowser createPlatformInAppBrowserStatic() { + return IOSInAppBrowser.static(); + } + + /// Creates a new [IOSChromeSafariBrowser]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [ChromeSafariBrowser] in `flutter_inappwebview` instead. + @override + IOSChromeSafariBrowser createPlatformChromeSafariBrowser( + PlatformChromeSafariBrowserCreationParams params, + ) { + return IOSChromeSafariBrowser(params); + } + + /// Creates a new empty [IOSChromeSafariBrowser] to access static methods. + /// + /// This function should only be called by the app-facing package. + /// Look at using [ChromeSafariBrowser] in `flutter_inappwebview` instead. + @override + IOSChromeSafariBrowser createPlatformChromeSafariBrowserStatic() { + return IOSChromeSafariBrowser.static(); + } + + /// Creates a new empty [IOSWebStorageManager] to access static methods. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebStorageManager] in `flutter_inappwebview` instead. + @override + IOSWebStorageManager createPlatformWebStorageManager( + PlatformWebStorageManagerCreationParams params) { + return IOSWebStorageManager(params); + } + + /// Creates a new [IOSWebAuthenticationSession]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebAuthenticationSession] in `flutter_inappwebview` instead. + @override + IOSWebAuthenticationSession createPlatformWebAuthenticationSession( + PlatformWebAuthenticationSessionCreationParams params) { + return IOSWebAuthenticationSession(params); + } + + /// Creates a new empty [IOSWebAuthenticationSession] to access static methods. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebAuthenticationSession] in `flutter_inappwebview` instead. + @override + IOSWebAuthenticationSession createPlatformWebAuthenticationSessionStatic() { + return IOSWebAuthenticationSession.static(); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/main.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/main.dart new file mode 100644 index 00000000..17f32781 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/main.dart @@ -0,0 +1,13 @@ +export 'inappwebview_platform.dart'; +export 'in_app_webview/main.dart'; +export 'in_app_browser/main.dart'; +export 'chrome_safari_browser/main.dart'; +export 'web_storage/main.dart'; +export 'cookie_manager.dart' hide InternalCookieManager; +export 'http_auth_credentials_database.dart' + hide InternalHttpAuthCredentialDatabase; +export 'pull_to_refresh/main.dart'; +export 'web_message/main.dart'; +export 'print_job/main.dart'; +export 'find_interaction/main.dart'; +export 'web_authentication_session/main.dart'; diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/platform_util.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/platform_util.dart new file mode 100644 index 00000000..27e96478 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/platform_util.dart @@ -0,0 +1,64 @@ +import 'package:flutter/services.dart'; + +///Platform native utilities +class PlatformUtil { + static PlatformUtil? _instance; + static const MethodChannel _channel = + MethodChannel('com.pichillilorenzo/flutter_inappwebview_platformutil'); + + PlatformUtil._(); + + ///Get [PlatformUtil] instance. + static PlatformUtil instance() { + return (_instance != null) ? _instance! : _init(); + } + + static PlatformUtil _init() { + _channel.setMethodCallHandler((call) async { + try { + return await _handleMethod(call); + } on Error catch (e) { + print(e); + print(e.stackTrace); + } + }); + _instance = PlatformUtil._(); + return _instance!; + } + + static Future _handleMethod(MethodCall call) async {} + + String? _cachedSystemVersion; + + ///Get current platform system version. + Future getSystemVersion() async { + if (_cachedSystemVersion != null) { + return _cachedSystemVersion!; + } + Map args = {}; + _cachedSystemVersion = + await _channel.invokeMethod('getSystemVersion', args); + return _cachedSystemVersion!; + } + + ///Format date. + Future formatDate( + {required DateTime date, + required String format, + String locale = "en_US", + String timezone = "UTC"}) async { + Map args = {}; + args.putIfAbsent('date', () => date.millisecondsSinceEpoch); + args.putIfAbsent('format', () => format); + args.putIfAbsent('locale', () => locale); + args.putIfAbsent('timezone', () => timezone); + return await _channel.invokeMethod('formatDate', args); + } + + ///Get cookie expiration date used by Web platform. + Future getWebCookieExpirationDate({required DateTime date}) async { + Map args = {}; + args.putIfAbsent('date', () => date.millisecondsSinceEpoch); + return await _channel.invokeMethod('getWebCookieExpirationDate', args); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/print_job/main.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/print_job/main.dart new file mode 100644 index 00000000..4e70ad94 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/print_job/main.dart @@ -0,0 +1 @@ +export 'print_job_controller.dart'; diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/print_job/print_job_controller.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/print_job/print_job_controller.dart new file mode 100644 index 00000000..e2385f9c --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/print_job/print_job_controller.dart @@ -0,0 +1,80 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [IOSPrintJobController]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformPrintJobControllerCreationParams] for +/// more information. +@immutable +class IOSPrintJobControllerCreationParams + extends PlatformPrintJobControllerCreationParams { + /// Creates a new [IOSPrintJobControllerCreationParams] instance. + const IOSPrintJobControllerCreationParams( + {required super.id, super.onComplete}); + + /// Creates a [IOSPrintJobControllerCreationParams] instance based on [PlatformPrintJobControllerCreationParams]. + factory IOSPrintJobControllerCreationParams.fromPlatformPrintJobControllerCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformPrintJobControllerCreationParams params) { + return IOSPrintJobControllerCreationParams( + id: params.id, onComplete: params.onComplete); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformPrintJobController} +class IOSPrintJobController extends PlatformPrintJobController + with ChannelController { + /// Constructs a [IOSPrintJobController]. + IOSPrintJobController(PlatformPrintJobControllerCreationParams params) + : super.implementation( + params is IOSPrintJobControllerCreationParams + ? params + : IOSPrintJobControllerCreationParams + .fromPlatformPrintJobControllerCreationParams(params), + ) { + onComplete = params.onComplete; + channel = MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_printjobcontroller_${params.id}'); + handler = _handleMethod; + initMethodCallHandler(); + } + + Future _handleMethod(MethodCall call) async { + switch (call.method) { + case "onComplete": + bool completed = call.arguments["completed"]; + String? error = call.arguments["error"]; + if (onComplete != null) { + onComplete!(completed, error); + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + } + + @override + Future dismiss({bool animated = true}) async { + Map args = {}; + args.putIfAbsent("animated", () => animated); + await channel?.invokeMethod('dismiss', args); + } + + @override + Future getInfo() async { + Map args = {}; + Map? infoMap = + (await channel?.invokeMethod('getInfo', args))?.cast(); + return PrintJobInfo.fromMap(infoMap); + } + + @override + Future dispose() async { + Map args = {}; + await channel?.invokeMethod('dispose', args); + disposeChannel(); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/pull_to_refresh/main.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/pull_to_refresh/main.dart new file mode 100644 index 00000000..586af9d8 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/pull_to_refresh/main.dart @@ -0,0 +1 @@ +export 'pull_to_refresh_controller.dart' hide InternalPullToRefreshController; diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/pull_to_refresh/pull_to_refresh_controller.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/pull_to_refresh/pull_to_refresh_controller.dart new file mode 100644 index 00000000..fae128c5 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/pull_to_refresh/pull_to_refresh_controller.dart @@ -0,0 +1,141 @@ +import 'dart:ui'; + +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [IOSPullToRefreshController]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformPullToRefreshControllerCreationParams] for +/// more information. +class IOSPullToRefreshControllerCreationParams + extends PlatformPullToRefreshControllerCreationParams { + /// Creates a new [IOSPullToRefreshControllerCreationParams] instance. + IOSPullToRefreshControllerCreationParams( + {super.onRefresh, super.options, super.settings}); + + /// Creates a [IOSPullToRefreshControllerCreationParams] instance based on [PlatformPullToRefreshControllerCreationParams]. + factory IOSPullToRefreshControllerCreationParams.fromPlatformPullToRefreshControllerCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformPullToRefreshControllerCreationParams params) { + return IOSPullToRefreshControllerCreationParams( + onRefresh: params.onRefresh, + options: params.options, + settings: params.settings); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformPullToRefreshController} +class IOSPullToRefreshController extends PlatformPullToRefreshController + with ChannelController { + /// Constructs a [IOSPullToRefreshController]. + IOSPullToRefreshController( + PlatformPullToRefreshControllerCreationParams params) + : super.implementation( + params is IOSPullToRefreshControllerCreationParams + ? params + : IOSPullToRefreshControllerCreationParams + .fromPlatformPullToRefreshControllerCreationParams(params), + ); + + _debugLog(String method, dynamic args) { + debugLog( + className: this.runtimeType.toString(), + debugLoggingSettings: + PlatformPullToRefreshController.debugLoggingSettings, + method: method, + args: args); + } + + Future _handleMethod(MethodCall call) async { + _debugLog(call.method, call.arguments); + + switch (call.method) { + case "onRefresh": + if (params.onRefresh != null) params.onRefresh!(); + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + @override + Future setEnabled(bool enabled) async { + Map args = {}; + args.putIfAbsent('enabled', () => enabled); + await channel?.invokeMethod('setEnabled', args); + } + + @override + Future isEnabled() async { + Map args = {}; + return await channel?.invokeMethod('isEnabled', args) ?? false; + } + + Future _setRefreshing(bool refreshing) async { + Map args = {}; + args.putIfAbsent('refreshing', () => refreshing); + await channel?.invokeMethod('setRefreshing', args); + } + + @override + Future beginRefreshing() async { + return await _setRefreshing(true); + } + + @override + Future endRefreshing() async { + await _setRefreshing(false); + } + + @override + Future isRefreshing() async { + Map args = {}; + return await channel?.invokeMethod('isRefreshing', args) ?? false; + } + + @override + Future setColor(Color color) async { + Map args = {}; + args.putIfAbsent('color', () => color.toHex()); + await channel?.invokeMethod('setColor', args); + } + + @override + Future setBackgroundColor(Color color) async { + Map args = {}; + args.putIfAbsent('color', () => color.toHex()); + await channel?.invokeMethod('setBackgroundColor', args); + } + + @Deprecated("Use setStyledTitle instead") + @override + Future setAttributedTitle(IOSNSAttributedString attributedTitle) async { + Map args = {}; + args.putIfAbsent('attributedTitle', () => attributedTitle.toMap()); + await channel?.invokeMethod('setStyledTitle', args); + } + + @override + Future setStyledTitle(AttributedString attributedTitle) async { + Map args = {}; + args.putIfAbsent('attributedTitle', () => attributedTitle.toMap()); + await channel?.invokeMethod('setStyledTitle', args); + } + + @override + void dispose({bool isKeepAlive = false}) { + disposeChannel(removeMethodCallHandler: !isKeepAlive); + } +} + +extension InternalPullToRefreshController on IOSPullToRefreshController { + void init(dynamic id) { + channel = MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_pull_to_refresh_$id'); + handler = _handleMethod; + initMethodCallHandler(); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_authentication_session/main.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_authentication_session/main.dart new file mode 100644 index 00000000..37ca0c24 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_authentication_session/main.dart @@ -0,0 +1 @@ +export 'web_authenticate_session.dart'; diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_authentication_session/web_authenticate_session.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_authentication_session/web_authenticate_session.dart new file mode 100755 index 00000000..da6eb9c1 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_authentication_session/web_authenticate_session.dart @@ -0,0 +1,163 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [IOSWebAuthenticationSession]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebAuthenticationSessionCreationParams] for +/// more information. +class IOSWebAuthenticationSessionCreationParams + extends PlatformWebAuthenticationSessionCreationParams { + /// Creates a new [IOSWebAuthenticationSessionCreationParams] instance. + const IOSWebAuthenticationSessionCreationParams(); + + /// Creates a [IOSWebAuthenticationSessionCreationParams] instance based on [PlatformWebAuthenticationSessionCreationParams]. + factory IOSWebAuthenticationSessionCreationParams.fromPlatformWebAuthenticationSessionCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformWebAuthenticationSessionCreationParams params) { + return IOSWebAuthenticationSessionCreationParams(); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformWebAuthenticationSession} +class IOSWebAuthenticationSession extends PlatformWebAuthenticationSession + with ChannelController { + /// Constructs a [IOSWebAuthenticationSession]. + IOSWebAuthenticationSession( + PlatformWebAuthenticationSessionCreationParams params) + : super.implementation( + params is IOSWebAuthenticationSessionCreationParams + ? params + : IOSWebAuthenticationSessionCreationParams + .fromPlatformWebAuthenticationSessionCreationParams(params), + ); + + static final IOSWebAuthenticationSession _staticValue = + IOSWebAuthenticationSession(IOSWebAuthenticationSessionCreationParams()); + + /// Provide static access. + factory IOSWebAuthenticationSession.static() { + return _staticValue; + } + + @override + final String id = IdGenerator.generate(); + + @override + late final WebUri url; + + @override + late final String? callbackURLScheme; + + @override + late final WebAuthenticationSessionSettings? initialSettings; + + @override + late final WebAuthenticationSessionCompletionHandler onComplete; + + static const MethodChannel _staticChannel = const MethodChannel( + 'com.pichillilorenzo/flutter_webauthenticationsession'); + + @override + Future create( + {required WebUri url, + String? callbackURLScheme, + WebAuthenticationSessionCompletionHandler onComplete, + WebAuthenticationSessionSettings? initialSettings}) async { + var session = IOSWebAuthenticationSession._create( + url: url, + callbackURLScheme: callbackURLScheme, + onComplete: onComplete, + initialSettings: initialSettings); + initialSettings = + session.initialSettings ?? WebAuthenticationSessionSettings(); + Map args = {}; + args.putIfAbsent("id", () => session.id); + args.putIfAbsent("url", () => session.url.toString()); + args.putIfAbsent("callbackURLScheme", () => session.callbackURLScheme); + args.putIfAbsent("initialSettings", () => initialSettings?.toMap()); + await _staticChannel.invokeMethod('create', args); + return session; + } + + IOSWebAuthenticationSession._create( + {required this.url, + this.callbackURLScheme, + this.onComplete, + WebAuthenticationSessionSettings? initialSettings}) + : super.implementation(IOSWebAuthenticationSessionCreationParams()) { + assert(url.toString().isNotEmpty); + assert(['http', 'https'].contains(url.scheme), + 'The specified URL has an unsupported scheme. Only HTTP and HTTPS URLs are supported on iOS.'); + + this.initialSettings = + initialSettings ?? WebAuthenticationSessionSettings(); + channel = MethodChannel( + 'com.pichillilorenzo/flutter_webauthenticationsession_$id'); + handler = _handleMethod; + initMethodCallHandler(); + } + + _debugLog(String method, dynamic args) { + debugLog( + className: this.runtimeType.toString(), + debugLoggingSettings: + PlatformWebAuthenticationSession.debugLoggingSettings, + id: id, + method: method, + args: args); + } + + Future _handleMethod(MethodCall call) async { + _debugLog(call.method, call.arguments); + + switch (call.method) { + case "onComplete": + String? url = call.arguments["url"]; + WebUri? uri = url != null ? WebUri(url) : null; + var error = WebAuthenticationSessionError.fromNativeValue( + call.arguments["errorCode"]); + if (onComplete != null) { + onComplete!(uri, error); + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + } + + @override + Future canStart() async { + Map args = {}; + return await channel?.invokeMethod('canStart', args) ?? false; + } + + @override + Future start() async { + Map args = {}; + return await channel?.invokeMethod('start', args) ?? false; + } + + @override + Future cancel() async { + Map args = {}; + await channel?.invokeMethod("cancel", args); + } + + @override + Future dispose() async { + Map args = {}; + await channel?.invokeMethod("dispose", args); + disposeChannel(); + } + + @override + Future isAvailable() async { + Map args = {}; + return await _staticChannel.invokeMethod("isAvailable", args) ?? + false; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_message/main.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_message/main.dart new file mode 100644 index 00000000..d41e30c7 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_message/main.dart @@ -0,0 +1,3 @@ +export 'web_message_port.dart' hide InternalWebMessagePort; +export 'web_message_channel.dart' hide InternalWebMessageChannel; +export 'web_message_listener.dart'; diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_message/web_message_channel.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_message/web_message_channel.dart new file mode 100644 index 00000000..9f3ce2d2 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_message/web_message_channel.dart @@ -0,0 +1,117 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; +import 'web_message_port.dart'; + +/// Object specifying creation parameters for creating a [IOSWebMessageChannel]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebMessageChannelCreationParams] for +/// more information. +@immutable +class IOSWebMessageChannelCreationParams + extends PlatformWebMessageChannelCreationParams { + /// Creates a new [IOSWebMessageChannelCreationParams] instance. + const IOSWebMessageChannelCreationParams( + {required super.id, required super.port1, required super.port2}); + + /// Creates a [IOSWebMessageChannelCreationParams] instance based on [PlatformWebMessageChannelCreationParams]. + factory IOSWebMessageChannelCreationParams.fromPlatformWebMessageChannelCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformWebMessageChannelCreationParams params) { + return IOSWebMessageChannelCreationParams( + id: params.id, port1: params.port1, port2: params.port2); + } + + @override + String toString() { + return 'IOSWebMessageChannelCreationParams{id: $id, port1: $port1, port2: $port2}'; + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformWebMessageChannel} +class IOSWebMessageChannel extends PlatformWebMessageChannel + with ChannelController { + /// Constructs a [IOSWebMessageChannel]. + IOSWebMessageChannel(PlatformWebMessageChannelCreationParams params) + : super.implementation( + params is IOSWebMessageChannelCreationParams + ? params + : IOSWebMessageChannelCreationParams + .fromPlatformWebMessageChannelCreationParams(params), + ) { + channel = MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_web_message_channel_${params.id}'); + handler = _handleMethod; + initMethodCallHandler(); + } + + static final IOSWebMessageChannel _staticValue = IOSWebMessageChannel( + IOSWebMessageChannelCreationParams( + id: '', + port1: IOSWebMessagePort(IOSWebMessagePortCreationParams(index: 0)), + port2: IOSWebMessagePort(IOSWebMessagePortCreationParams(index: 1)))); + + /// Provide static access. + factory IOSWebMessageChannel.static() { + return _staticValue; + } + + IOSWebMessagePort get _iosPort1 => port1 as IOSWebMessagePort; + + IOSWebMessagePort get _iosPort2 => port2 as IOSWebMessagePort; + + static IOSWebMessageChannel? _fromMap(Map? map) { + if (map == null) { + return null; + } + var webMessageChannel = IOSWebMessageChannel( + IOSWebMessageChannelCreationParams( + id: map["id"], + port1: IOSWebMessagePort(IOSWebMessagePortCreationParams(index: 0)), + port2: + IOSWebMessagePort(IOSWebMessagePortCreationParams(index: 1)))); + webMessageChannel._iosPort1.webMessageChannel = webMessageChannel; + webMessageChannel._iosPort2.webMessageChannel = webMessageChannel; + return webMessageChannel; + } + + Future _handleMethod(MethodCall call) async { + switch (call.method) { + case "onMessage": + int index = call.arguments["index"]; + var port = index == 0 ? _iosPort1 : _iosPort2; + if (port.onMessage != null) { + WebMessage? message = call.arguments["message"] != null + ? WebMessage.fromMap( + call.arguments["message"].cast()) + : null; + port.onMessage!(message); + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + @override + IOSWebMessageChannel? fromMap(Map? map) { + return _fromMap(map); + } + + @override + void dispose() { + disposeChannel(); + } + + @override + String toString() { + return 'IOSWebMessageChannel{id: $id, port1: $port1, port2: $port2}'; + } +} + +extension InternalWebMessageChannel on IOSWebMessageChannel { + MethodChannel? get internalChannel => channel; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_message/web_message_listener.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_message/web_message_listener.dart new file mode 100644 index 00000000..38a27880 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_message/web_message_listener.dart @@ -0,0 +1,164 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [IOSWebMessageListener]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebMessageListenerCreationParams] for +/// more information. +@immutable +class IOSWebMessageListenerCreationParams + extends PlatformWebMessageListenerCreationParams { + /// Creates a new [IOSWebMessageListenerCreationParams] instance. + const IOSWebMessageListenerCreationParams( + {required this.allowedOriginRules, + required super.jsObjectName, + super.onPostMessage}); + + /// Creates a [IOSWebMessageListenerCreationParams] instance based on [PlatformWebMessageListenerCreationParams]. + factory IOSWebMessageListenerCreationParams.fromPlatformWebMessageListenerCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformWebMessageListenerCreationParams params) { + return IOSWebMessageListenerCreationParams( + allowedOriginRules: params.allowedOriginRules ?? Set.from(["*"]), + jsObjectName: params.jsObjectName, + onPostMessage: params.onPostMessage); + } + + @override + final Set allowedOriginRules; + + @override + String toString() { + return 'IOSWebMessageListenerCreationParams{jsObjectName: $jsObjectName, allowedOriginRules: $allowedOriginRules, onPostMessage: $onPostMessage}'; + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformWebMessageListener} +class IOSWebMessageListener extends PlatformWebMessageListener + with ChannelController { + /// Constructs a [IOSWebMessageListener]. + IOSWebMessageListener(PlatformWebMessageListenerCreationParams params) + : super.implementation( + params is IOSWebMessageListenerCreationParams + ? params + : IOSWebMessageListenerCreationParams + .fromPlatformWebMessageListenerCreationParams(params), + ) { + assert(!this._iosParams.allowedOriginRules.contains(""), + "allowedOriginRules cannot contain empty strings"); + channel = MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_web_message_listener_${_id}_${params.jsObjectName}'); + handler = _handleMethod; + initMethodCallHandler(); + } + + ///Message Listener ID used internally. + final String _id = IdGenerator.generate(); + + IOSJavaScriptReplyProxy? _replyProxy; + + IOSWebMessageListenerCreationParams get _iosParams => + params as IOSWebMessageListenerCreationParams; + + Future _handleMethod(MethodCall call) async { + switch (call.method) { + case "onPostMessage": + if (_replyProxy == null) { + _replyProxy = IOSJavaScriptReplyProxy( + PlatformJavaScriptReplyProxyCreationParams( + webMessageListener: this)); + } + if (onPostMessage != null) { + WebMessage? message = call.arguments["message"] != null + ? WebMessage.fromMap( + call.arguments["message"].cast()) + : null; + WebUri? sourceOrigin = call.arguments["sourceOrigin"] != null + ? WebUri(call.arguments["sourceOrigin"]) + : null; + bool isMainFrame = call.arguments["isMainFrame"]; + onPostMessage!(message, sourceOrigin, isMainFrame, _replyProxy!); + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + @override + void dispose() { + disposeChannel(); + } + + @override + Map toMap() { + return { + "id": _id, + "jsObjectName": params.jsObjectName, + "allowedOriginRules": _iosParams.allowedOriginRules.toList(), + }; + } + + @override + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return 'IOSWebMessageListener{id: ${_id}, jsObjectName: ${params.jsObjectName}, allowedOriginRules: ${params.allowedOriginRules}, replyProxy: $_replyProxy}'; + } +} + +/// Object specifying creation parameters for creating a [IOSJavaScriptReplyProxy]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformJavaScriptReplyProxyCreationParams] for +/// more information. +@immutable +class IOSJavaScriptReplyProxyCreationParams + extends PlatformJavaScriptReplyProxyCreationParams { + /// Creates a new [IOSJavaScriptReplyProxyCreationParams] instance. + const IOSJavaScriptReplyProxyCreationParams( + {required super.webMessageListener}); + + /// Creates a [IOSJavaScriptReplyProxyCreationParams] instance based on [PlatformJavaScriptReplyProxyCreationParams]. + factory IOSJavaScriptReplyProxyCreationParams.fromPlatformJavaScriptReplyProxyCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformJavaScriptReplyProxyCreationParams params) { + return IOSJavaScriptReplyProxyCreationParams( + webMessageListener: params.webMessageListener); + } +} + +///{@macro flutter_inappwebview_platform_interface.JavaScriptReplyProxy} +class IOSJavaScriptReplyProxy extends PlatformJavaScriptReplyProxy { + /// Constructs a [IOSWebMessageListener]. + IOSJavaScriptReplyProxy(PlatformJavaScriptReplyProxyCreationParams params) + : super.implementation( + params is IOSJavaScriptReplyProxyCreationParams + ? params + : IOSJavaScriptReplyProxyCreationParams + .fromPlatformJavaScriptReplyProxyCreationParams(params), + ); + + IOSWebMessageListener get _iosWebMessageListener => + params.webMessageListener as IOSWebMessageListener; + + @override + Future postMessage(WebMessage message) async { + Map args = {}; + args.putIfAbsent('message', () => message.toMap()); + await _iosWebMessageListener.channel?.invokeMethod('postMessage', args); + } + + @override + String toString() { + return 'IOSJavaScriptReplyProxy{}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_message/web_message_port.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_message/web_message_port.dart new file mode 100644 index 00000000..b16b3b75 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_message/web_message_port.dart @@ -0,0 +1,95 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +import 'web_message_channel.dart'; + +/// Object specifying creation parameters for creating a [IOSWebMessagePort]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebMessagePortCreationParams] for +/// more information. +@immutable +class IOSWebMessagePortCreationParams + extends PlatformWebMessagePortCreationParams { + /// Creates a new [IOSWebMessagePortCreationParams] instance. + const IOSWebMessagePortCreationParams({required super.index}); + + /// Creates a [IOSWebMessagePortCreationParams] instance based on [PlatformWebMessagePortCreationParams]. + factory IOSWebMessagePortCreationParams.fromPlatformWebMessagePortCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformWebMessagePortCreationParams params) { + return IOSWebMessagePortCreationParams(index: params.index); + } + + @override + String toString() { + return 'IOSWebMessagePortCreationParams{index: $index}'; + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformWebMessagePort} +class IOSWebMessagePort extends PlatformWebMessagePort { + WebMessageCallback? _onMessage; + late IOSWebMessageChannel _webMessageChannel; + + /// Constructs a [IOSWebMessagePort]. + IOSWebMessagePort(PlatformWebMessagePortCreationParams params) + : super.implementation( + params is IOSWebMessagePortCreationParams + ? params + : IOSWebMessagePortCreationParams + .fromPlatformWebMessagePortCreationParams(params), + ); + + @override + Future setWebMessageCallback(WebMessageCallback? onMessage) async { + Map args = {}; + args.putIfAbsent('index', () => params.index); + await _webMessageChannel.internalChannel + ?.invokeMethod('setWebMessageCallback', args); + this._onMessage = onMessage; + } + + @override + Future postMessage(WebMessage message) async { + Map args = {}; + args.putIfAbsent('index', () => params.index); + args.putIfAbsent('message', () => message.toMap()); + await _webMessageChannel.internalChannel?.invokeMethod('postMessage', args); + } + + @override + Future close() async { + Map args = {}; + args.putIfAbsent('index', () => params.index); + await _webMessageChannel.internalChannel?.invokeMethod('close', args); + } + + @override + Map toMap() { + return { + "index": params.index, + "webMessageChannelId": this._webMessageChannel.params.id + }; + } + + @override + Map toJson() { + return toMap(); + } + + @override + String toString() { + return 'IOSWebMessagePort{index: ${params.index}}'; + } +} + +extension InternalWebMessagePort on IOSWebMessagePort { + WebMessageCallback? get onMessage => _onMessage; + void set onMessage(WebMessageCallback? value) => _onMessage = value; + + IOSWebMessageChannel get webMessageChannel => _webMessageChannel; + void set webMessageChannel(IOSWebMessageChannel value) => + _webMessageChannel = value; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_storage/main.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_storage/main.dart new file mode 100644 index 00000000..7265ae52 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_storage/main.dart @@ -0,0 +1,2 @@ +export 'web_storage.dart'; +export 'web_storage_manager.dart' hide InternalWebStorageManager; diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_storage/web_storage.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_storage/web_storage.dart new file mode 100644 index 00000000..ca5accd3 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_storage/web_storage.dart @@ -0,0 +1,256 @@ +import 'dart:convert'; + +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +import '../in_app_webview/in_app_webview_controller.dart'; + +/// Object specifying creation parameters for creating a [IOSWebStorage]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebStorageCreationParams] for +/// more information. +class IOSWebStorageCreationParams extends PlatformWebStorageCreationParams { + /// Creates a new [IOSWebStorageCreationParams] instance. + IOSWebStorageCreationParams( + {required super.localStorage, required super.sessionStorage}); + + /// Creates a [IOSWebStorageCreationParams] instance based on [PlatformWebStorageCreationParams]. + factory IOSWebStorageCreationParams.fromPlatformWebStorageCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformWebStorageCreationParams params) { + return IOSWebStorageCreationParams( + localStorage: params.localStorage, + sessionStorage: params.sessionStorage); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformWebStorage} +class IOSWebStorage extends PlatformWebStorage { + /// Constructs a [IOSWebStorage]. + IOSWebStorage(PlatformWebStorageCreationParams params) + : super.implementation( + params is IOSWebStorageCreationParams + ? params + : IOSWebStorageCreationParams + .fromPlatformWebStorageCreationParams(params), + ); + + @override + PlatformLocalStorage get localStorage => params.localStorage; + + @override + PlatformSessionStorage get sessionStorage => params.sessionStorage; + + @override + void dispose() { + localStorage.dispose(); + sessionStorage.dispose(); + } +} + +/// Object specifying creation parameters for creating a [IOSStorage]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformStorageCreationParams] for +/// more information. +class IOSStorageCreationParams extends PlatformStorageCreationParams { + /// Creates a new [IOSStorageCreationParams] instance. + IOSStorageCreationParams( + {required super.controller, required super.webStorageType}); + + /// Creates a [IOSStorageCreationParams] instance based on [PlatformStorageCreationParams]. + factory IOSStorageCreationParams.fromPlatformStorageCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformStorageCreationParams params) { + return IOSStorageCreationParams( + controller: params.controller, webStorageType: params.webStorageType); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformStorage} +abstract mixin class IOSStorage implements PlatformStorage { + @override + IOSInAppWebViewController? controller; + + @override + Future length() async { + var result = await controller?.evaluateJavascript(source: """ + window.$webStorageType.length; + """); + return result != null ? int.parse(json.decode(result)) : null; + } + + @override + Future setItem({required String key, required dynamic value}) async { + var encodedValue = json.encode(value); + await controller?.evaluateJavascript(source: """ + window.$webStorageType.setItem("$key", ${value is String ? encodedValue : "JSON.stringify($encodedValue)"}); + """); + } + + @override + Future getItem({required String key}) async { + var itemValue = await controller?.evaluateJavascript(source: """ + window.$webStorageType.getItem("$key"); + """); + + if (itemValue == null) { + return null; + } + + try { + return json.decode(itemValue); + } catch (e) {} + + return itemValue; + } + + @override + Future removeItem({required String key}) async { + await controller?.evaluateJavascript(source: """ + window.$webStorageType.removeItem("$key"); + """); + } + + @override + Future> getItems() async { + var webStorageItems = []; + + List>? items = + (await controller?.evaluateJavascript(source: """ +(function() { + var webStorageItems = []; + for(var i = 0; i < window.$webStorageType.length; i++){ + var key = window.$webStorageType.key(i); + webStorageItems.push( + { + key: key, + value: window.$webStorageType.getItem(key) + } + ); + } + return webStorageItems; +})(); + """))?.cast>(); + + if (items == null) { + return webStorageItems; + } + + for (var item in items) { + webStorageItems + .add(WebStorageItem(key: item["key"], value: item["value"])); + } + + return webStorageItems; + } + + @override + Future clear() async { + await controller?.evaluateJavascript(source: """ + window.$webStorageType.clear(); + """); + } + + @override + Future key({required int index}) async { + var result = await controller?.evaluateJavascript(source: """ + window.$webStorageType.key($index); + """); + return result != null ? json.decode(result) : null; + } + + @override + void dispose() { + controller = null; + } +} + +/// Object specifying creation parameters for creating a [IOSLocalStorage]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformLocalStorageCreationParams] for +/// more information. +class IOSLocalStorageCreationParams extends PlatformLocalStorageCreationParams { + /// Creates a new [IOSLocalStorageCreationParams] instance. + IOSLocalStorageCreationParams(super.params); + + /// Creates a [IOSLocalStorageCreationParams] instance based on [PlatformLocalStorageCreationParams]. + factory IOSLocalStorageCreationParams.fromPlatformLocalStorageCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformLocalStorageCreationParams params) { + return IOSLocalStorageCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformLocalStorage} +class IOSLocalStorage extends PlatformLocalStorage with IOSStorage { + /// Constructs a [IOSLocalStorage]. + IOSLocalStorage(PlatformLocalStorageCreationParams params) + : super.implementation( + params is IOSLocalStorageCreationParams + ? params + : IOSLocalStorageCreationParams + .fromPlatformLocalStorageCreationParams(params), + ); + + /// Default storage + factory IOSLocalStorage.defaultStorage( + {required PlatformInAppWebViewController? controller}) { + return IOSLocalStorage(IOSLocalStorageCreationParams( + PlatformLocalStorageCreationParams(PlatformStorageCreationParams( + controller: controller, + webStorageType: WebStorageType.LOCAL_STORAGE)))); + } + + @override + IOSInAppWebViewController? get controller => + params.controller as IOSInAppWebViewController?; +} + +/// Object specifying creation parameters for creating a [IOSSessionStorage]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformSessionStorageCreationParams] for +/// more information. +class IOSSessionStorageCreationParams + extends PlatformSessionStorageCreationParams { + /// Creates a new [IOSSessionStorageCreationParams] instance. + IOSSessionStorageCreationParams(super.params); + + /// Creates a [IOSSessionStorageCreationParams] instance based on [PlatformSessionStorageCreationParams]. + factory IOSSessionStorageCreationParams.fromPlatformSessionStorageCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformSessionStorageCreationParams params) { + return IOSSessionStorageCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformSessionStorage} +class IOSSessionStorage extends PlatformSessionStorage with IOSStorage { + /// Constructs a [IOSSessionStorage]. + IOSSessionStorage(PlatformSessionStorageCreationParams params) + : super.implementation( + params is IOSSessionStorageCreationParams + ? params + : IOSSessionStorageCreationParams + .fromPlatformSessionStorageCreationParams(params), + ); + + /// Default storage + factory IOSSessionStorage.defaultStorage( + {required PlatformInAppWebViewController? controller}) { + return IOSSessionStorage(IOSSessionStorageCreationParams( + PlatformSessionStorageCreationParams(PlatformStorageCreationParams( + controller: controller, + webStorageType: WebStorageType.SESSION_STORAGE)))); + } + + @override + IOSInAppWebViewController? get controller => + params.controller as IOSInAppWebViewController?; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_storage/web_storage_manager.dart b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_storage/web_storage_manager.dart new file mode 100755 index 00000000..bb259cc3 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/lib/src/web_storage/web_storage_manager.dart @@ -0,0 +1,134 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [IOSWebStorageManager]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebStorageManagerCreationParams] for +/// more information. +@immutable +class IOSWebStorageManagerCreationParams + extends PlatformWebStorageManagerCreationParams { + /// Creates a new [IOSWebStorageManagerCreationParams] instance. + const IOSWebStorageManagerCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformWebStorageManagerCreationParams params, + ) : super(); + + /// Creates a [IOSWebStorageManagerCreationParams] instance based on [PlatformWebStorageManagerCreationParams]. + factory IOSWebStorageManagerCreationParams.fromPlatformWebStorageManagerCreationParams( + PlatformWebStorageManagerCreationParams params) { + return IOSWebStorageManagerCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformWebStorageManager} +class IOSWebStorageManager extends PlatformWebStorageManager + with ChannelController { + /// Creates a new [IOSWebStorageManager]. + IOSWebStorageManager(PlatformWebStorageManagerCreationParams params) + : super.implementation( + params is IOSWebStorageManagerCreationParams + ? params + : IOSWebStorageManagerCreationParams + .fromPlatformWebStorageManagerCreationParams(params), + ) { + channel = const MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_webstoragemanager'); + handler = handleMethod; + initMethodCallHandler(); + } + + static IOSWebStorageManager? _instance; + + ///Gets the WebStorage manager shared instance. + static IOSWebStorageManager instance() { + return (_instance != null) ? _instance! : _init(); + } + + static IOSWebStorageManager _init() { + _instance = IOSWebStorageManager(IOSWebStorageManagerCreationParams( + const PlatformWebStorageManagerCreationParams())); + return _instance!; + } + + Future _handleMethod(MethodCall call) async {} + + @override + Future> fetchDataRecords( + {required Set dataTypes}) async { + List recordList = []; + List dataTypesList = []; + for (var dataType in dataTypes) { + dataTypesList.add(dataType.toNativeValue()); + } + Map args = {}; + args.putIfAbsent("dataTypes", () => dataTypesList); + List> records = + (await channel?.invokeMethod('fetchDataRecords', args)) + ?.cast>() ?? + []; + for (var record in records) { + List dataTypesString = record["dataTypes"].cast(); + Set dataTypes = Set(); + for (var dataTypeValue in dataTypesString) { + var dataType = WebsiteDataType.fromNativeValue(dataTypeValue); + if (dataType != null) { + dataTypes.add(dataType); + } + } + recordList.add(WebsiteDataRecord( + displayName: record["displayName"], dataTypes: dataTypes)); + } + return recordList; + } + + @override + Future removeDataFor( + {required Set dataTypes, + required List dataRecords}) async { + List dataTypesList = []; + for (var dataType in dataTypes) { + dataTypesList.add(dataType.toNativeValue()); + } + + List> recordList = []; + for (var record in dataRecords) { + recordList.add(record.toMap()); + } + + Map args = {}; + args.putIfAbsent("dataTypes", () => dataTypesList); + args.putIfAbsent("recordList", () => recordList); + await channel?.invokeMethod('removeDataFor', args); + } + + @override + Future removeDataModifiedSince( + {required Set dataTypes, required DateTime date}) async { + List dataTypesList = []; + for (var dataType in dataTypes) { + dataTypesList.add(dataType.toNativeValue()); + } + + var timestamp = date.millisecondsSinceEpoch; + + Map args = {}; + args.putIfAbsent("dataTypes", () => dataTypesList); + args.putIfAbsent("timestamp", () => timestamp); + await channel?.invokeMethod('removeDataModifiedSince', args); + } + + @override + void dispose() { + // empty + } +} + +extension InternalWebStorageManager on IOSWebStorageManager { + get handleMethod => _handleMethod; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_ios/pubspec.yaml b/ui/flutter/third_party/flutter_inappwebview_ios/pubspec.yaml new file mode 100644 index 00000000..06afb905 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_ios/pubspec.yaml @@ -0,0 +1,82 @@ +name: flutter_inappwebview_ios +description: iOS implementation of the flutter_inappwebview plugin. +version: 1.1.2 +homepage: https://inappwebview.dev/ +repository: https://github.com/pichillilorenzo/flutter_inappwebview/tree/master/flutter_inappwebview_ios +issue_tracker: https://github.com/pichillilorenzo/flutter_inappwebview/issues +funding: + - https://inappwebview.dev/donate/ +topics: + - html + - webview + - webview-flutter + - inappwebview + - browser + +environment: + sdk: ^3.5.0 + flutter: ">=3.24.0" + +dependencies: + flutter: + sdk: flutter + flutter_inappwebview_platform_interface: ^1.3.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^4.0.0 + plugin_platform_interface: ^2.1.8 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + implements: flutter_inappwebview + platforms: + ios: + pluginClass: InAppWebViewFlutterPlugin + dartPluginClass: IOSInAppWebViewPlatform + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/CHANGELOG.md b/ui/flutter/third_party/flutter_inappwebview_macos/CHANGELOG.md new file mode 100644 index 00000000..7bd1ae74 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/CHANGELOG.md @@ -0,0 +1,83 @@ +## 1.1.2 + +- Updated flutter_inappwebview_platform_interface version to ^1.3.0 + +## 1.1.1 + +- Updated flutter_inappwebview_platform_interface version to ^1.2.0 + +## 1.1.0+3 + +- Updated flutter_inappwebview_platform_interface version + +## ## 1.1.0+2 + +- Updated pubspec.yaml + +## 1.1.0+1 + +- Fixed "v6.1.0 fails to compile on Xcode 15" [#2288](https://github.com/pichillilorenzo/flutter_inappwebview/issues/2288) + +## 1.1.0 + +- Added `InAppWebView` support +- Added privacy manifest +- Updates minimum supported SDK version to Flutter 3.24/Dart 3.5. +- Fixed "[MACOS] launching InAppBrowser with 'hidden: true' calls onExit immediately" [#1939](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1939) +- Fixed XCode 16 build + +## 1.0.11 + +- Updated `flutter_inappwebview_platform_interface` version dependency to `^1.0.10` + +## 1.0.10 + +- Updated `flutter_inappwebview_platform_interface` version dependency to `^1.0.9` +- Fix typos and other code improvements (thanks to [michalsrutek](https://github.com/michalsrutek)) +- Fixed "runtime issue of SecTrustCopyExceptions 'This method should not be called on the main thread as it may lead to UI unresponsiveness.' when using onReceivedServerTrustAuthRequest" [#1924](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1924) + +## 1.0.9 + +- Updated `flutter_inappwebview_platform_interface` version dependency to `^1.0.8` + +## 1.0.8 + +- Updated `flutter_inappwebview_platform_interface` version dependency to `^1.0.7` +- Implemented `InAppBrowser.onMainWindowWillClose` event + +## 1.0.7 + +- Implemented `InAppWebViewSettings.interceptOnlyAsyncAjaxRequests` +- Updated `useShouldInterceptAjaxRequest` automatic infer logic +- Updated `CookieManager` methods return value +- Fixed "iOS crash at public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)" [#1912](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1912) + +## 1.0.6 + +- Fixed error in InterceptAjaxRequestJS 'Failed to set responseType property' +- Fixed shouldInterceptAjaxRequest javascript code when overriding XMLHttpRequest.open method parameters + +## 1.0.5 + +- Fixed "getFavicons: _TypeError: type '_Map' is not a subtype of type 'Iterable'" [#1897](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1897) + +## 1.0.4 + +- Possible fix for "iOS Fatal Crash" [#1894](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1894) + +## 1.0.3 + +- Call `super.dispose();` on `InAppBrowser` implementation + +## 1.0.2 + +- Fixed "Cloudflare Turnstile failure" [#1738](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1738) + +## 1.0.1 + +- Added `PlatformPrintJobController.onComplete` setter +- Updated `flutter_inappwebview_platform_interface` version dependency to `^1.0.2` + +## 1.0.0 + +Initial release. diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/LICENSE b/ui/flutter/third_party/flutter_inappwebview_macos/LICENSE new file mode 100644 index 00000000..6ccd8da4 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/LICENSE @@ -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. diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/README.md b/ui/flutter/third_party/flutter_inappwebview_macos/README.md new file mode 100644 index 00000000..7af883d5 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/README.md @@ -0,0 +1,13 @@ +# flutter\_inappwebview\_macos + +The Apple macOS WKWebView 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. \ No newline at end of file diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/lib/flutter_inappwebview_macos.dart b/ui/flutter/third_party/flutter_inappwebview_macos/lib/flutter_inappwebview_macos.dart new file mode 100644 index 00000000..b82fc696 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/lib/flutter_inappwebview_macos.dart @@ -0,0 +1,3 @@ +library flutter_inappwebview_macos; + +export 'src/main.dart'; diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/cookie_manager.dart b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/cookie_manager.dart new file mode 100755 index 00000000..bf9d5a9b --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/cookie_manager.dart @@ -0,0 +1,433 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +import 'in_app_webview/headless_in_app_webview.dart'; +import 'platform_util.dart'; + +/// Object specifying creation parameters for creating a [MacOSCookieManager]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformCookieManagerCreationParams] for +/// more information. +@immutable +class MacOSCookieManagerCreationParams + extends PlatformCookieManagerCreationParams { + /// Creates a new [MacOSCookieManagerCreationParams] instance. + const MacOSCookieManagerCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformCookieManagerCreationParams params, + ) : super(); + + /// Creates a [MacOSCookieManagerCreationParams] instance based on [PlatformCookieManagerCreationParams]. + factory MacOSCookieManagerCreationParams.fromPlatformCookieManagerCreationParams( + PlatformCookieManagerCreationParams params) { + return MacOSCookieManagerCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformCookieManager} +class MacOSCookieManager extends PlatformCookieManager with ChannelController { + /// Creates a new [MacOSCookieManager]. + MacOSCookieManager(PlatformCookieManagerCreationParams params) + : super.implementation( + params is MacOSCookieManagerCreationParams + ? params + : MacOSCookieManagerCreationParams + .fromPlatformCookieManagerCreationParams(params), + ) { + channel = const MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_cookiemanager'); + handler = handleMethod; + initMethodCallHandler(); + } + + static MacOSCookieManager? _instance; + + ///Gets the [MacOSCookieManager] shared instance. + static MacOSCookieManager instance() { + return (_instance != null) ? _instance! : _init(); + } + + static MacOSCookieManager _init() { + _instance = MacOSCookieManager(MacOSCookieManagerCreationParams( + const PlatformCookieManagerCreationParams())); + return _instance!; + } + + Future _handleMethod(MethodCall call) async {} + + @override + Future setCookie( + {required WebUri url, + required String name, + required String value, + String path = "/", + String? domain, + int? expiresDate, + int? maxAge, + bool? isSecure, + bool? isHttpOnly, + HTTPCookieSameSitePolicy? sameSite, + @Deprecated("Use webViewController instead") + PlatformInAppWebViewController? iosBelow11WebViewController, + PlatformInAppWebViewController? webViewController}) async { + webViewController = webViewController ?? iosBelow11WebViewController; + + assert(url.toString().isNotEmpty); + assert(name.isNotEmpty); + assert(value.isNotEmpty); + assert(path.isNotEmpty); + + if (await _shouldUseJavascript()) { + await _setCookieWithJavaScript( + url: url, + name: name, + value: value, + domain: domain, + path: path, + expiresDate: expiresDate, + maxAge: maxAge, + isSecure: isSecure, + sameSite: sameSite, + webViewController: webViewController); + return true; + } + + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + args.putIfAbsent('name', () => name); + args.putIfAbsent('value', () => value); + args.putIfAbsent('domain', () => domain); + args.putIfAbsent('path', () => path); + args.putIfAbsent('expiresDate', () => expiresDate?.toString()); + args.putIfAbsent('maxAge', () => maxAge); + args.putIfAbsent('isSecure', () => isSecure); + args.putIfAbsent('isHttpOnly', () => isHttpOnly); + args.putIfAbsent('sameSite', () => sameSite?.toNativeValue()); + + return await channel?.invokeMethod('setCookie', args) ?? false; + } + + Future _setCookieWithJavaScript( + {required WebUri url, + required String name, + required String value, + String path = "/", + String? domain, + int? expiresDate, + int? maxAge, + bool? isSecure, + HTTPCookieSameSitePolicy? sameSite, + PlatformInAppWebViewController? webViewController}) async { + var cookieValue = name + "=" + value + "; Path=" + path; + + if (domain != null) cookieValue += "; Domain=" + domain; + + if (expiresDate != null) + cookieValue += "; Expires=" + await _getCookieExpirationDate(expiresDate); + + if (maxAge != null) cookieValue += "; Max-Age=" + maxAge.toString(); + + if (isSecure != null && isSecure) cookieValue += "; Secure"; + + if (sameSite != null) + cookieValue += "; SameSite=" + sameSite.toNativeValue(); + + cookieValue += ";"; + + if (webViewController != null) { + final javaScriptEnabled = + (await webViewController.getSettings())?.javaScriptEnabled ?? false; + if (javaScriptEnabled) { + await webViewController.evaluateJavascript( + source: 'document.cookie="$cookieValue"'); + return; + } + } + + final setCookieCompleter = Completer(); + final headlessWebView = + MacOSHeadlessInAppWebView(MacOSHeadlessInAppWebViewCreationParams( + initialUrlRequest: URLRequest(url: url), + onLoadStop: (controller, url) async { + await controller.evaluateJavascript( + source: 'document.cookie="$cookieValue"'); + setCookieCompleter.complete(); + })); + await headlessWebView.run(); + await setCookieCompleter.future; + await headlessWebView.dispose(); + } + + @override + Future> getCookies( + {required WebUri url, + @Deprecated("Use webViewController instead") + PlatformInAppWebViewController? iosBelow11WebViewController, + PlatformInAppWebViewController? webViewController}) async { + assert(url.toString().isNotEmpty); + + webViewController = webViewController ?? iosBelow11WebViewController; + + if (await _shouldUseJavascript()) { + return await _getCookiesWithJavaScript( + url: url, webViewController: webViewController); + } + + List cookies = []; + + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + List cookieListMap = + await channel?.invokeMethod('getCookies', args) ?? []; + cookieListMap = cookieListMap.cast>(); + + cookieListMap.forEach((cookieMap) { + cookies.add(Cookie( + name: cookieMap["name"], + value: cookieMap["value"], + expiresDate: cookieMap["expiresDate"], + isSessionOnly: cookieMap["isSessionOnly"], + domain: cookieMap["domain"], + sameSite: + HTTPCookieSameSitePolicy.fromNativeValue(cookieMap["sameSite"]), + isSecure: cookieMap["isSecure"], + isHttpOnly: cookieMap["isHttpOnly"], + path: cookieMap["path"])); + }); + return cookies; + } + + Future> _getCookiesWithJavaScript( + {required WebUri url, + PlatformInAppWebViewController? webViewController}) async { + assert(url.toString().isNotEmpty); + + List cookies = []; + + if (webViewController != null) { + final javaScriptEnabled = + (await webViewController.getSettings())?.javaScriptEnabled ?? false; + if (javaScriptEnabled) { + List documentCookies = (await webViewController + .evaluateJavascript(source: 'document.cookie') as String) + .split(';') + .map((documentCookie) => documentCookie.trim()) + .toList(); + documentCookies.forEach((documentCookie) { + List cookie = documentCookie.split('='); + if (cookie.length > 1) { + cookies.add(Cookie( + name: cookie[0], + value: cookie[1], + )); + } + }); + return cookies; + } + } + + final pageLoaded = Completer(); + final headlessWebView = + MacOSHeadlessInAppWebView(MacOSHeadlessInAppWebViewCreationParams( + initialUrlRequest: URLRequest(url: url), + onLoadStop: (controller, url) async { + pageLoaded.complete(); + }, + )); + await headlessWebView.run(); + await pageLoaded.future; + + List documentCookies = (await headlessWebView.webViewController! + .evaluateJavascript(source: 'document.cookie') as String) + .split(';') + .map((documentCookie) => documentCookie.trim()) + .toList(); + documentCookies.forEach((documentCookie) { + List cookie = documentCookie.split('='); + if (cookie.length > 1) { + cookies.add(Cookie( + name: cookie[0], + value: cookie[1], + )); + } + }); + await headlessWebView.dispose(); + return cookies; + } + + @override + Future getCookie( + {required WebUri url, + required String name, + @Deprecated("Use webViewController instead") + PlatformInAppWebViewController? iosBelow11WebViewController, + PlatformInAppWebViewController? webViewController}) async { + assert(url.toString().isNotEmpty); + assert(name.isNotEmpty); + + webViewController = webViewController ?? iosBelow11WebViewController; + + if (await _shouldUseJavascript()) { + List cookies = await _getCookiesWithJavaScript( + url: url, webViewController: webViewController); + return cookies + .cast() + .firstWhere((cookie) => cookie!.name == name, orElse: () => null); + } + + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + List cookies = + await channel?.invokeMethod('getCookies', args) ?? []; + cookies = cookies.cast>(); + for (var i = 0; i < cookies.length; i++) { + cookies[i] = cookies[i].cast(); + if (cookies[i]["name"] == name) + return Cookie( + name: cookies[i]["name"], + value: cookies[i]["value"], + expiresDate: cookies[i]["expiresDate"], + isSessionOnly: cookies[i]["isSessionOnly"], + domain: cookies[i]["domain"], + sameSite: HTTPCookieSameSitePolicy.fromNativeValue( + cookies[i]["sameSite"]), + isSecure: cookies[i]["isSecure"], + isHttpOnly: cookies[i]["isHttpOnly"], + path: cookies[i]["path"]); + } + return null; + } + + @override + Future deleteCookie( + {required WebUri url, + required String name, + String path = "/", + String? domain, + @Deprecated("Use webViewController instead") + PlatformInAppWebViewController? iosBelow11WebViewController, + PlatformInAppWebViewController? webViewController}) async { + assert(url.toString().isNotEmpty); + assert(name.isNotEmpty); + + webViewController = webViewController ?? iosBelow11WebViewController; + + if (await _shouldUseJavascript()) { + await _setCookieWithJavaScript( + url: url, + name: name, + value: "", + path: path, + domain: domain, + maxAge: -1, + webViewController: webViewController); + return true; + } + + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + args.putIfAbsent('name', () => name); + args.putIfAbsent('domain', () => domain); + args.putIfAbsent('path', () => path); + return await channel?.invokeMethod('deleteCookie', args) ?? false; + } + + @override + Future deleteCookies( + {required WebUri url, + String path = "/", + String? domain, + @Deprecated("Use webViewController instead") + PlatformInAppWebViewController? iosBelow11WebViewController, + PlatformInAppWebViewController? webViewController}) async { + assert(url.toString().isNotEmpty); + + webViewController = webViewController ?? iosBelow11WebViewController; + + if (await _shouldUseJavascript()) { + List cookies = await _getCookiesWithJavaScript( + url: url, webViewController: webViewController); + for (var i = 0; i < cookies.length; i++) { + await _setCookieWithJavaScript( + url: url, + name: cookies[i].name, + value: "", + path: path, + domain: domain, + maxAge: -1, + webViewController: webViewController); + } + return true; + } + + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + args.putIfAbsent('domain', () => domain); + args.putIfAbsent('path', () => path); + return await channel?.invokeMethod('deleteCookies', args) ?? false; + } + + @override + Future deleteAllCookies() async { + Map args = {}; + return await channel?.invokeMethod('deleteAllCookies', args) ?? false; + } + + @override + Future> getAllCookies() async { + List cookies = []; + + Map args = {}; + List cookieListMap = + await channel?.invokeMethod('getAllCookies', args) ?? []; + cookieListMap = cookieListMap.cast>(); + + cookieListMap.forEach((cookieMap) { + cookies.add(Cookie( + name: cookieMap["name"], + value: cookieMap["value"], + expiresDate: cookieMap["expiresDate"], + isSessionOnly: cookieMap["isSessionOnly"], + domain: cookieMap["domain"], + sameSite: + HTTPCookieSameSitePolicy.fromNativeValue(cookieMap["sameSite"]), + isSecure: cookieMap["isSecure"], + isHttpOnly: cookieMap["isHttpOnly"], + path: cookieMap["path"])); + }); + return cookies; + } + + Future _getCookieExpirationDate(int expiresDate) async { + var platformUtil = PlatformUtil.instance(); + var dateTime = DateTime.fromMillisecondsSinceEpoch(expiresDate).toUtc(); + return !kIsWeb + ? await platformUtil.formatDate( + date: dateTime, + format: 'EEE, dd MMM yyyy hh:mm:ss z', + locale: 'en_US', + timezone: 'GMT') + : await platformUtil.getWebCookieExpirationDate(date: dateTime); + } + + Future _shouldUseJavascript() async { + final platformUtil = PlatformUtil.instance(); + final systemVersion = await platformUtil.getSystemVersion(); + return systemVersion.compareTo("11") == -1; + } + + @override + void dispose() { + // empty + } +} + +extension InternalCookieManager on MacOSCookieManager { + get handleMethod => _handleMethod; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/find_interaction/find_interaction_controller.dart b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/find_interaction/find_interaction_controller.dart new file mode 100644 index 00000000..ca291cbc --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/find_interaction/find_interaction_controller.dart @@ -0,0 +1,124 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [MacOSFindInteractionController]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformFindInteractionControllerCreationParams] for +/// more information. +@immutable +class MacOSFindInteractionControllerCreationParams + extends PlatformFindInteractionControllerCreationParams { + /// Creates a new [MacOSFindInteractionControllerCreationParams] instance. + const MacOSFindInteractionControllerCreationParams( + {super.onFindResultReceived}); + + /// Creates a [MacOSFindInteractionControllerCreationParams] instance based on [PlatformFindInteractionControllerCreationParams]. + factory MacOSFindInteractionControllerCreationParams.fromPlatformFindInteractionControllerCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformFindInteractionControllerCreationParams params) { + return MacOSFindInteractionControllerCreationParams( + onFindResultReceived: params.onFindResultReceived); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController} +class MacOSFindInteractionController extends PlatformFindInteractionController + with ChannelController { + /// Constructs a [MacOSFindInteractionController]. + MacOSFindInteractionController( + PlatformFindInteractionControllerCreationParams params) + : super.implementation( + params is MacOSFindInteractionControllerCreationParams + ? params + : MacOSFindInteractionControllerCreationParams + .fromPlatformFindInteractionControllerCreationParams(params), + ); + + _debugLog(String method, dynamic args) { + debugLog( + className: this.runtimeType.toString(), + debugLoggingSettings: + PlatformFindInteractionController.debugLoggingSettings, + method: method, + args: args); + } + + Future _handleMethod(MethodCall call) async { + _debugLog(call.method, call.arguments); + + switch (call.method) { + case "onFindResultReceived": + if (onFindResultReceived != null) { + int activeMatchOrdinal = call.arguments["activeMatchOrdinal"]; + int numberOfMatches = call.arguments["numberOfMatches"]; + bool isDoneCounting = call.arguments["isDoneCounting"]; + onFindResultReceived!( + this, activeMatchOrdinal, numberOfMatches, isDoneCounting); + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.findAll} + Future findAll({String? find}) async { + Map args = {}; + args.putIfAbsent('find', () => find); + await channel?.invokeMethod('findAll', args); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.findNext} + Future findNext({bool forward = true}) async { + Map args = {}; + args.putIfAbsent('forward', () => forward); + await channel?.invokeMethod('findNext', args); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.clearMatches} + Future clearMatches() async { + Map args = {}; + await channel?.invokeMethod('clearMatches', args); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.setSearchText} + Future setSearchText(String? searchText) async { + Map args = {}; + args.putIfAbsent('searchText', () => searchText); + await channel?.invokeMethod('setSearchText', args); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.getSearchText} + Future getSearchText() async { + Map args = {}; + return await channel?.invokeMethod('getSearchText', args); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.getActiveFindSession} + Future getActiveFindSession() async { + Map args = {}; + Map? result = + (await channel?.invokeMethod('getActiveFindSession', args)) + ?.cast(); + return FindSession.fromMap(result); + } + + ///{@macro flutter_inappwebview_platform_interface.PlatformFindInteractionController.dispose} + @override + void dispose({bool isKeepAlive = false}) { + disposeChannel(removeMethodCallHandler: !isKeepAlive); + } +} + +extension InternalFindInteractionController on MacOSFindInteractionController { + void init(dynamic id) { + channel = MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_find_interaction_$id'); + handler = _handleMethod; + initMethodCallHandler(); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/find_interaction/main.dart b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/find_interaction/main.dart new file mode 100644 index 00000000..a7adaacf --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/find_interaction/main.dart @@ -0,0 +1,2 @@ +export 'find_interaction_controller.dart' + hide InternalFindInteractionController; diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/http_auth_credentials_database.dart b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/http_auth_credentials_database.dart new file mode 100755 index 00000000..28c9126f --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/http_auth_credentials_database.dart @@ -0,0 +1,155 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [MacOSHttpAuthCredentialDatabase]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformHttpAuthCredentialDatabaseCreationParams] for +/// more information. +@immutable +class MacOSHttpAuthCredentialDatabaseCreationParams + extends PlatformHttpAuthCredentialDatabaseCreationParams { + /// Creates a new [MacOSHttpAuthCredentialDatabaseCreationParams] instance. + const MacOSHttpAuthCredentialDatabaseCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformHttpAuthCredentialDatabaseCreationParams params, + ) : super(); + + /// Creates a [MacOSHttpAuthCredentialDatabaseCreationParams] instance based on [PlatformHttpAuthCredentialDatabaseCreationParams]. + factory MacOSHttpAuthCredentialDatabaseCreationParams.fromPlatformHttpAuthCredentialDatabaseCreationParams( + PlatformHttpAuthCredentialDatabaseCreationParams params) { + return MacOSHttpAuthCredentialDatabaseCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformHttpAuthCredentialDatabase} +class MacOSHttpAuthCredentialDatabase extends PlatformHttpAuthCredentialDatabase + with ChannelController { + /// Creates a new [MacOSHttpAuthCredentialDatabase]. + MacOSHttpAuthCredentialDatabase( + PlatformHttpAuthCredentialDatabaseCreationParams params) + : super.implementation( + params is MacOSHttpAuthCredentialDatabaseCreationParams + ? params + : MacOSHttpAuthCredentialDatabaseCreationParams + .fromPlatformHttpAuthCredentialDatabaseCreationParams(params), + ) { + channel = const MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_credential_database'); + handler = handleMethod; + initMethodCallHandler(); + } + + static MacOSHttpAuthCredentialDatabase? _instance; + + ///Gets the database shared instance. + static MacOSHttpAuthCredentialDatabase instance() { + return (_instance != null) ? _instance! : _init(); + } + + static MacOSHttpAuthCredentialDatabase _init() { + _instance = MacOSHttpAuthCredentialDatabase( + MacOSHttpAuthCredentialDatabaseCreationParams( + const PlatformHttpAuthCredentialDatabaseCreationParams())); + return _instance!; + } + + Future _handleMethod(MethodCall call) async {} + + @override + Future> + getAllAuthCredentials() async { + Map args = {}; + List allCredentials = + await channel?.invokeMethod('getAllAuthCredentials', args) ?? []; + + List result = []; + + for (Map map in allCredentials) { + var element = URLProtectionSpaceHttpAuthCredentials.fromMap( + map.cast()); + if (element != null) { + result.add(element); + } + } + return result; + } + + @override + Future> getHttpAuthCredentials( + {required URLProtectionSpace protectionSpace}) async { + Map args = {}; + args.putIfAbsent("host", () => protectionSpace.host); + args.putIfAbsent("protocol", () => protectionSpace.protocol); + args.putIfAbsent("realm", () => protectionSpace.realm); + args.putIfAbsent("port", () => protectionSpace.port); + List credentialList = + await channel?.invokeMethod('getHttpAuthCredentials', args) ?? []; + List credentials = []; + for (Map map in credentialList) { + var credential = URLCredential.fromMap(map.cast()); + if (credential != null) { + credentials.add(credential); + } + } + return credentials; + } + + @override + Future setHttpAuthCredential( + {required URLProtectionSpace protectionSpace, + required URLCredential credential}) async { + Map args = {}; + args.putIfAbsent("host", () => protectionSpace.host); + args.putIfAbsent("protocol", () => protectionSpace.protocol); + args.putIfAbsent("realm", () => protectionSpace.realm); + args.putIfAbsent("port", () => protectionSpace.port); + args.putIfAbsent("username", () => credential.username); + args.putIfAbsent("password", () => credential.password); + await channel?.invokeMethod('setHttpAuthCredential', args); + } + + @override + Future removeHttpAuthCredential( + {required URLProtectionSpace protectionSpace, + required URLCredential credential}) async { + Map args = {}; + args.putIfAbsent("host", () => protectionSpace.host); + args.putIfAbsent("protocol", () => protectionSpace.protocol); + args.putIfAbsent("realm", () => protectionSpace.realm); + args.putIfAbsent("port", () => protectionSpace.port); + args.putIfAbsent("username", () => credential.username); + args.putIfAbsent("password", () => credential.password); + await channel?.invokeMethod('removeHttpAuthCredential', args); + } + + @override + Future removeHttpAuthCredentials( + {required URLProtectionSpace protectionSpace}) async { + Map args = {}; + args.putIfAbsent("host", () => protectionSpace.host); + args.putIfAbsent("protocol", () => protectionSpace.protocol); + args.putIfAbsent("realm", () => protectionSpace.realm); + args.putIfAbsent("port", () => protectionSpace.port); + await channel?.invokeMethod('removeHttpAuthCredentials', args); + } + + @override + Future clearAllAuthCredentials() async { + Map args = {}; + await channel?.invokeMethod('clearAllAuthCredentials', args); + } + + @override + void dispose() { + // empty + } +} + +extension InternalHttpAuthCredentialDatabase + on MacOSHttpAuthCredentialDatabase { + get handleMethod => _handleMethod; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/in_app_browser/in_app_browser.dart b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/in_app_browser/in_app_browser.dart new file mode 100755 index 00000000..8aa55e1f --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/in_app_browser/in_app_browser.dart @@ -0,0 +1,373 @@ +import 'dart:async'; +import 'dart:collection'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +import '../find_interaction/find_interaction_controller.dart'; +import '../in_app_webview/in_app_webview_controller.dart'; + +/// Object specifying creation parameters for creating a [MacOSInAppBrowser]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformInAppBrowserCreationParams] for +/// more information. +class MacOSInAppBrowserCreationParams + extends PlatformInAppBrowserCreationParams { + /// Creates a new [MacOSInAppBrowserCreationParams] instance. + MacOSInAppBrowserCreationParams( + {super.contextMenu, + super.pullToRefreshController, + this.findInteractionController, + super.initialUserScripts, + super.windowId}); + + /// Creates a [MacOSInAppBrowserCreationParams] instance based on [PlatformInAppBrowserCreationParams]. + factory MacOSInAppBrowserCreationParams.fromPlatformInAppBrowserCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformInAppBrowserCreationParams params) { + return MacOSInAppBrowserCreationParams( + contextMenu: params.contextMenu, + pullToRefreshController: params.pullToRefreshController, + findInteractionController: + params.findInteractionController as MacOSFindInteractionController?, + initialUserScripts: params.initialUserScripts, + windowId: params.windowId); + } + + @override + final MacOSFindInteractionController? findInteractionController; +} + +///{@macro flutter_inappwebview_platform_interface.PlatformInAppBrowser} +class MacOSInAppBrowser extends PlatformInAppBrowser with ChannelController { + @override + final String id = IdGenerator.generate(); + + /// Constructs a [MacOSInAppBrowser]. + MacOSInAppBrowser(PlatformInAppBrowserCreationParams params) + : super.implementation( + params is MacOSInAppBrowserCreationParams + ? params + : MacOSInAppBrowserCreationParams + .fromPlatformInAppBrowserCreationParams(params), + ) { + _contextMenu = params.contextMenu; + } + + static final MacOSInAppBrowser _staticValue = + MacOSInAppBrowser(MacOSInAppBrowserCreationParams()); + + /// Provide static access. + factory MacOSInAppBrowser.static() { + return _staticValue; + } + + MacOSInAppBrowserCreationParams get _macosParams => + params as MacOSInAppBrowserCreationParams; + + static const MethodChannel _staticChannel = + const MethodChannel('com.pichillilorenzo/flutter_inappbrowser'); + + ContextMenu? _contextMenu; + + @override + ContextMenu? get contextMenu => _contextMenu; + + Map _menuItems = HashMap(); + bool _isOpened = false; + MacOSInAppWebViewController? _webViewController; + + @override + MacOSInAppWebViewController? get webViewController { + return _isOpened ? _webViewController : null; + } + + _init() { + channel = MethodChannel('com.pichillilorenzo/flutter_inappbrowser_$id'); + handler = _handleMethod; + initMethodCallHandler(); + + _webViewController = MacOSInAppWebViewController.fromInAppBrowser( + MacOSInAppWebViewControllerCreationParams(id: id), + channel!, + this, + this.initialUserScripts); + _macosParams.findInteractionController?.init(id); + } + + _debugLog(String method, dynamic args) { + debugLog( + className: this.runtimeType.toString(), + id: id, + debugLoggingSettings: PlatformInAppBrowser.debugLoggingSettings, + method: method, + args: args); + } + + Future _handleMethod(MethodCall call) async { + switch (call.method) { + case "onBrowserCreated": + _debugLog(call.method, call.arguments); + eventHandler?.onBrowserCreated(); + break; + case "onMenuItemClicked": + _debugLog(call.method, call.arguments); + int id = call.arguments["id"].toInt(); + if (this._menuItems[id] != null) { + if (this._menuItems[id]?.onClick != null) { + this._menuItems[id]?.onClick!(); + } + } + break; + case "onMainWindowWillClose": + _debugLog(call.method, call.arguments); + eventHandler?.onMainWindowWillClose(); + break; + case "onExit": + _debugLog(call.method, call.arguments); + _isOpened = false; + final onExit = eventHandler?.onExit; + dispose(); + onExit?.call(); + break; + default: + return _webViewController?.handleMethod(call); + } + } + + Map _prepareOpenRequest( + {@Deprecated('Use settings instead') InAppBrowserClassOptions? options, + InAppBrowserClassSettings? settings}) { + assert(!_isOpened, 'The browser is already opened.'); + _isOpened = true; + _init(); + + var initialSettings = settings?.toMap() ?? + options?.toMap() ?? + InAppBrowserClassSettings().toMap(); + + Map pullToRefreshSettings = + pullToRefreshController?.settings.toMap() ?? + pullToRefreshController?.options.toMap() ?? + PullToRefreshSettings(enabled: false).toMap(); + + List> menuItemList = []; + _menuItems.forEach((key, value) { + menuItemList.add(value.toMap()); + }); + + Map args = {}; + args.putIfAbsent('id', () => id); + args.putIfAbsent('settings', () => initialSettings); + args.putIfAbsent('contextMenu', () => contextMenu?.toMap() ?? {}); + args.putIfAbsent('windowId', () => windowId); + args.putIfAbsent('initialUserScripts', + () => initialUserScripts?.map((e) => e.toMap()).toList() ?? []); + args.putIfAbsent('pullToRefreshSettings', () => pullToRefreshSettings); + args.putIfAbsent('menuItems', () => menuItemList); + return args; + } + + @override + Future openUrlRequest( + {required URLRequest urlRequest, + // ignore: deprecated_member_use_from_same_package + @Deprecated('Use settings instead') InAppBrowserClassOptions? options, + InAppBrowserClassSettings? settings}) async { + assert(urlRequest.url != null && urlRequest.url.toString().isNotEmpty); + + Map args = + _prepareOpenRequest(options: options, settings: settings); + args.putIfAbsent('urlRequest', () => urlRequest.toMap()); + await _staticChannel.invokeMethod('open', args); + } + + @override + Future openFile( + {required String assetFilePath, + // ignore: deprecated_member_use_from_same_package + @Deprecated('Use settings instead') InAppBrowserClassOptions? options, + InAppBrowserClassSettings? settings}) async { + assert(assetFilePath.isNotEmpty); + + Map args = + _prepareOpenRequest(options: options, settings: settings); + args.putIfAbsent('assetFilePath', () => assetFilePath); + await _staticChannel.invokeMethod('open', args); + } + + @override + Future openData( + {required String data, + String mimeType = "text/html", + String encoding = "utf8", + WebUri? baseUrl, + @Deprecated("Use historyUrl instead") Uri? androidHistoryUrl, + WebUri? historyUrl, + // ignore: deprecated_member_use_from_same_package + @Deprecated('Use settings instead') InAppBrowserClassOptions? options, + InAppBrowserClassSettings? settings}) async { + Map args = + _prepareOpenRequest(options: options, settings: settings); + args.putIfAbsent('data', () => data); + args.putIfAbsent('mimeType', () => mimeType); + args.putIfAbsent('encoding', () => encoding); + args.putIfAbsent('baseUrl', () => baseUrl?.toString() ?? "about:blank"); + args.putIfAbsent('historyUrl', + () => (historyUrl ?? androidHistoryUrl)?.toString() ?? "about:blank"); + await _staticChannel.invokeMethod('open', args); + } + + @override + Future openWithSystemBrowser({required WebUri url}) async { + assert(url.toString().isNotEmpty); + + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + return await _staticChannel.invokeMethod('openWithSystemBrowser', args); + } + + @override + void addMenuItem(InAppBrowserMenuItem menuItem) { + _menuItems[menuItem.id] = menuItem; + } + + @override + void addMenuItems(List menuItems) { + menuItems.forEach((menuItem) { + _menuItems[menuItem.id] = menuItem; + }); + } + + @override + bool removeMenuItem(InAppBrowserMenuItem menuItem) { + return _menuItems.remove(menuItem.id) != null; + } + + @override + void removeMenuItems(List menuItems) { + for (final menuItem in menuItems) { + removeMenuItem(menuItem); + } + } + + @override + void removeAllMenuItem() { + _menuItems.clear(); + } + + @override + bool hasMenuItem(InAppBrowserMenuItem menuItem) { + return _menuItems.containsKey(menuItem.id); + } + + @override + Future show() async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + await channel?.invokeMethod('show', args); + } + + @override + Future hide() async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + await channel?.invokeMethod('hide', args); + } + + @override + Future close() async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + await channel?.invokeMethod('close', args); + } + + @override + Future isHidden() async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + return await channel?.invokeMethod('isHidden', args) ?? false; + } + + @override + @Deprecated('Use setSettings instead') + Future setOptions({required InAppBrowserClassOptions options}) async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + args.putIfAbsent('settings', () => options.toMap()); + await channel?.invokeMethod('setSettings', args); + } + + @override + @Deprecated('Use getSettings instead') + Future getOptions() async { + assert(_isOpened, 'The browser is not opened.'); + Map args = {}; + + Map? options = + await channel?.invokeMethod('getSettings', args); + if (options != null) { + options = options.cast(); + return InAppBrowserClassOptions.fromMap(options as Map); + } + + return null; + } + + @override + Future setSettings( + {required InAppBrowserClassSettings settings}) async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + args.putIfAbsent('settings', () => settings.toMap()); + await channel?.invokeMethod('setSettings', args); + } + + @override + Future getSettings() async { + assert(_isOpened, 'The browser is not opened.'); + + Map args = {}; + + Map? settings = + await channel?.invokeMethod('getSettings', args); + if (settings != null) { + settings = settings.cast(); + return InAppBrowserClassSettings.fromMap( + settings as Map); + } + + return null; + } + + @override + bool isOpened() { + return this._isOpened; + } + + @override + @mustCallSuper + void dispose() { + super.dispose(); + disposeChannel(); + _webViewController?.dispose(); + _webViewController = null; + pullToRefreshController?.dispose(); + findInteractionController?.dispose(); + } +} + +extension InternalInAppBrowser on MacOSInAppBrowser { + void setContextMenu(ContextMenu? contextMenu) { + _contextMenu = contextMenu; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/in_app_browser/main.dart b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/in_app_browser/main.dart new file mode 100644 index 00000000..e11eb8b1 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/in_app_browser/main.dart @@ -0,0 +1 @@ +export 'in_app_browser.dart' hide InternalInAppBrowser; diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/in_app_webview/_static_channel.dart b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/in_app_webview/_static_channel.dart new file mode 100644 index 00000000..beb7de70 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/in_app_webview/_static_channel.dart @@ -0,0 +1,4 @@ +import 'package:flutter/services.dart'; + +const IN_APP_WEBVIEW_STATIC_CHANNEL = + MethodChannel('com.pichillilorenzo/flutter_inappwebview_manager'); diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/in_app_webview/headless_in_app_webview.dart b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/in_app_webview/headless_in_app_webview.dart new file mode 100644 index 00000000..23055add --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/in_app_webview/headless_in_app_webview.dart @@ -0,0 +1,433 @@ +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; +import '../find_interaction/find_interaction_controller.dart'; +import 'in_app_webview_controller.dart'; + +/// Object specifying creation parameters for creating a [MacOSHeadlessInAppWebView]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformHeadlessInAppWebViewCreationParams] for +/// more information. +@immutable +class MacOSHeadlessInAppWebViewCreationParams + extends PlatformHeadlessInAppWebViewCreationParams { + /// Creates a new [MacOSHeadlessInAppWebViewCreationParams] instance. + MacOSHeadlessInAppWebViewCreationParams( + {super.controllerFromPlatform, + super.initialSize, + super.windowId, + super.onWebViewCreated, + super.onLoadStart, + super.onLoadStop, + @Deprecated('Use onReceivedError instead') super.onLoadError, + super.onReceivedError, + @Deprecated("Use onReceivedHttpError instead") super.onLoadHttpError, + super.onReceivedHttpError, + super.onProgressChanged, + super.onConsoleMessage, + super.shouldOverrideUrlLoading, + super.onLoadResource, + super.onScrollChanged, + @Deprecated('Use onDownloadStartRequest instead') super.onDownloadStart, + super.onDownloadStartRequest, + @Deprecated('Use onLoadResourceWithCustomScheme instead') + super.onLoadResourceCustomScheme, + super.onLoadResourceWithCustomScheme, + super.onCreateWindow, + super.onCloseWindow, + super.onJsAlert, + super.onJsConfirm, + super.onJsPrompt, + super.onReceivedHttpAuthRequest, + super.onReceivedServerTrustAuthRequest, + super.onReceivedClientCertRequest, + @Deprecated('Use FindInteractionController.onFindResultReceived instead') + super.onFindResultReceived, + super.shouldInterceptAjaxRequest, + super.onAjaxReadyStateChange, + super.onAjaxProgress, + super.shouldInterceptFetchRequest, + super.onUpdateVisitedHistory, + @Deprecated("Use onPrintRequest instead") super.onPrint, + super.onPrintRequest, + super.onLongPressHitTestResult, + super.onEnterFullscreen, + super.onExitFullscreen, + super.onPageCommitVisible, + super.onTitleChanged, + super.onWindowFocus, + super.onWindowBlur, + super.onOverScrolled, + super.onZoomScaleChanged, + @Deprecated('Use onSafeBrowsingHit instead') + super.androidOnSafeBrowsingHit, + super.onSafeBrowsingHit, + @Deprecated('Use onPermissionRequest instead') + super.androidOnPermissionRequest, + super.onPermissionRequest, + @Deprecated('Use onGeolocationPermissionsShowPrompt instead') + super.androidOnGeolocationPermissionsShowPrompt, + super.onGeolocationPermissionsShowPrompt, + @Deprecated('Use onGeolocationPermissionsHidePrompt instead') + super.androidOnGeolocationPermissionsHidePrompt, + super.onGeolocationPermissionsHidePrompt, + @Deprecated('Use shouldInterceptRequest instead') + super.androidShouldInterceptRequest, + super.shouldInterceptRequest, + @Deprecated('Use onRenderProcessGone instead') + super.androidOnRenderProcessGone, + super.onRenderProcessGone, + @Deprecated('Use onRenderProcessResponsive instead') + super.androidOnRenderProcessResponsive, + super.onRenderProcessResponsive, + @Deprecated('Use onRenderProcessUnresponsive instead') + super.androidOnRenderProcessUnresponsive, + super.onRenderProcessUnresponsive, + @Deprecated('Use onFormResubmission instead') + super.androidOnFormResubmission, + super.onFormResubmission, + @Deprecated('Use onZoomScaleChanged instead') super.androidOnScaleChanged, + @Deprecated('Use onReceivedIcon instead') super.androidOnReceivedIcon, + super.onReceivedIcon, + @Deprecated('Use onReceivedTouchIconUrl instead') + super.androidOnReceivedTouchIconUrl, + super.onReceivedTouchIconUrl, + @Deprecated('Use onJsBeforeUnload instead') super.androidOnJsBeforeUnload, + super.onJsBeforeUnload, + @Deprecated('Use onReceivedLoginRequest instead') + super.androidOnReceivedLoginRequest, + super.onReceivedLoginRequest, + super.onPermissionRequestCanceled, + super.onRequestFocus, + @Deprecated('Use onWebContentProcessDidTerminate instead') + super.iosOnWebContentProcessDidTerminate, + super.onWebContentProcessDidTerminate, + @Deprecated( + 'Use onDidReceiveServerRedirectForProvisionalNavigation instead') + super.iosOnDidReceiveServerRedirectForProvisionalNavigation, + super.onDidReceiveServerRedirectForProvisionalNavigation, + @Deprecated('Use onNavigationResponse instead') + super.iosOnNavigationResponse, + super.onNavigationResponse, + @Deprecated('Use shouldAllowDeprecatedTLS instead') + super.iosShouldAllowDeprecatedTLS, + super.shouldAllowDeprecatedTLS, + super.onCameraCaptureStateChanged, + super.onMicrophoneCaptureStateChanged, + super.onContentSizeChanged, + super.initialUrlRequest, + super.initialFile, + super.initialData, + @Deprecated('Use initialSettings instead') super.initialOptions, + super.initialSettings, + super.contextMenu, + super.initialUserScripts, + super.pullToRefreshController, + this.findInteractionController}); + + /// Creates a [MacOSHeadlessInAppWebViewCreationParams] instance based on [PlatformHeadlessInAppWebViewCreationParams]. + MacOSHeadlessInAppWebViewCreationParams.fromPlatformHeadlessInAppWebViewCreationParams( + PlatformHeadlessInAppWebViewCreationParams params) + : this( + controllerFromPlatform: params.controllerFromPlatform, + initialSize: params.initialSize, + windowId: params.windowId, + onWebViewCreated: params.onWebViewCreated, + onLoadStart: params.onLoadStart, + onLoadStop: params.onLoadStop, + onLoadError: params.onLoadError, + onReceivedError: params.onReceivedError, + onLoadHttpError: params.onLoadHttpError, + onReceivedHttpError: params.onReceivedHttpError, + onProgressChanged: params.onProgressChanged, + onConsoleMessage: params.onConsoleMessage, + shouldOverrideUrlLoading: params.shouldOverrideUrlLoading, + onLoadResource: params.onLoadResource, + onScrollChanged: params.onScrollChanged, + onDownloadStart: params.onDownloadStart, + onDownloadStartRequest: params.onDownloadStartRequest, + onLoadResourceCustomScheme: params.onLoadResourceCustomScheme, + onLoadResourceWithCustomScheme: + params.onLoadResourceWithCustomScheme, + onCreateWindow: params.onCreateWindow, + onCloseWindow: params.onCloseWindow, + onJsAlert: params.onJsAlert, + onJsConfirm: params.onJsConfirm, + onJsPrompt: params.onJsPrompt, + onReceivedHttpAuthRequest: params.onReceivedHttpAuthRequest, + onReceivedServerTrustAuthRequest: + params.onReceivedServerTrustAuthRequest, + onReceivedClientCertRequest: params.onReceivedClientCertRequest, + onFindResultReceived: params.onFindResultReceived, + shouldInterceptAjaxRequest: params.shouldInterceptAjaxRequest, + onAjaxReadyStateChange: params.onAjaxReadyStateChange, + onAjaxProgress: params.onAjaxProgress, + shouldInterceptFetchRequest: params.shouldInterceptFetchRequest, + onUpdateVisitedHistory: params.onUpdateVisitedHistory, + onPrint: params.onPrint, + onPrintRequest: params.onPrintRequest, + onLongPressHitTestResult: params.onLongPressHitTestResult, + onEnterFullscreen: params.onEnterFullscreen, + onExitFullscreen: params.onExitFullscreen, + onPageCommitVisible: params.onPageCommitVisible, + onTitleChanged: params.onTitleChanged, + onWindowFocus: params.onWindowFocus, + onWindowBlur: params.onWindowBlur, + onOverScrolled: params.onOverScrolled, + onZoomScaleChanged: params.onZoomScaleChanged, + androidOnSafeBrowsingHit: params.androidOnSafeBrowsingHit, + onSafeBrowsingHit: params.onSafeBrowsingHit, + androidOnPermissionRequest: params.androidOnPermissionRequest, + onPermissionRequest: params.onPermissionRequest, + androidOnGeolocationPermissionsShowPrompt: + params.androidOnGeolocationPermissionsShowPrompt, + onGeolocationPermissionsShowPrompt: + params.onGeolocationPermissionsShowPrompt, + androidOnGeolocationPermissionsHidePrompt: + params.androidOnGeolocationPermissionsHidePrompt, + onGeolocationPermissionsHidePrompt: + params.onGeolocationPermissionsHidePrompt, + androidShouldInterceptRequest: params.androidShouldInterceptRequest, + shouldInterceptRequest: params.shouldInterceptRequest, + androidOnRenderProcessGone: params.androidOnRenderProcessGone, + onRenderProcessGone: params.onRenderProcessGone, + androidOnRenderProcessResponsive: + params.androidOnRenderProcessResponsive, + onRenderProcessResponsive: params.onRenderProcessResponsive, + androidOnRenderProcessUnresponsive: + params.androidOnRenderProcessUnresponsive, + onRenderProcessUnresponsive: params.onRenderProcessUnresponsive, + androidOnFormResubmission: params.androidOnFormResubmission, + onFormResubmission: params.onFormResubmission, + androidOnScaleChanged: params.androidOnScaleChanged, + androidOnReceivedIcon: params.androidOnReceivedIcon, + onReceivedIcon: params.onReceivedIcon, + androidOnReceivedTouchIconUrl: params.androidOnReceivedTouchIconUrl, + onReceivedTouchIconUrl: params.onReceivedTouchIconUrl, + androidOnJsBeforeUnload: params.androidOnJsBeforeUnload, + onJsBeforeUnload: params.onJsBeforeUnload, + androidOnReceivedLoginRequest: params.androidOnReceivedLoginRequest, + onReceivedLoginRequest: params.onReceivedLoginRequest, + onPermissionRequestCanceled: params.onPermissionRequestCanceled, + onRequestFocus: params.onRequestFocus, + iosOnWebContentProcessDidTerminate: + params.iosOnWebContentProcessDidTerminate, + onWebContentProcessDidTerminate: + params.onWebContentProcessDidTerminate, + iosOnDidReceiveServerRedirectForProvisionalNavigation: + params.iosOnDidReceiveServerRedirectForProvisionalNavigation, + onDidReceiveServerRedirectForProvisionalNavigation: + params.onDidReceiveServerRedirectForProvisionalNavigation, + iosOnNavigationResponse: params.iosOnNavigationResponse, + onNavigationResponse: params.onNavigationResponse, + iosShouldAllowDeprecatedTLS: params.iosShouldAllowDeprecatedTLS, + shouldAllowDeprecatedTLS: params.shouldAllowDeprecatedTLS, + onCameraCaptureStateChanged: params.onCameraCaptureStateChanged, + onMicrophoneCaptureStateChanged: + params.onMicrophoneCaptureStateChanged, + onContentSizeChanged: params.onContentSizeChanged, + initialUrlRequest: params.initialUrlRequest, + initialFile: params.initialFile, + initialData: params.initialData, + initialOptions: params.initialOptions, + initialSettings: params.initialSettings, + contextMenu: params.contextMenu, + initialUserScripts: params.initialUserScripts, + pullToRefreshController: params.pullToRefreshController, + findInteractionController: params.findInteractionController + as MacOSFindInteractionController?); + + @override + final MacOSFindInteractionController? findInteractionController; +} + +///{@macro flutter_inappwebview_platform_interface.PlatformHeadlessInAppWebView} +class MacOSHeadlessInAppWebView extends PlatformHeadlessInAppWebView + with ChannelController { + @override + late final String id; + + bool _started = false; + bool _running = false; + + static const MethodChannel _sharedChannel = + const MethodChannel('com.pichillilorenzo/flutter_headless_inappwebview'); + + MacOSInAppWebViewController? _webViewController; + + /// Constructs a [MacOSHeadlessInAppWebView]. + MacOSHeadlessInAppWebView(PlatformHeadlessInAppWebViewCreationParams params) + : super.implementation( + params is MacOSHeadlessInAppWebViewCreationParams + ? params + : MacOSHeadlessInAppWebViewCreationParams + .fromPlatformHeadlessInAppWebViewCreationParams(params), + ) { + id = IdGenerator.generate(); + } + + @override + MacOSInAppWebViewController? get webViewController => _webViewController; + + dynamic _controllerFromPlatform; + + MacOSHeadlessInAppWebViewCreationParams get _macosParams => + params as MacOSHeadlessInAppWebViewCreationParams; + + _init() { + _webViewController = MacOSInAppWebViewController( + MacOSInAppWebViewControllerCreationParams(id: id, webviewParams: params), + ); + _controllerFromPlatform = + params.controllerFromPlatform?.call(_webViewController!) ?? + _webViewController!; + _macosParams.findInteractionController?.init(id); + channel = + MethodChannel('com.pichillilorenzo/flutter_headless_inappwebview_$id'); + handler = _handleMethod; + initMethodCallHandler(); + } + + Future _handleMethod(MethodCall call) async { + switch (call.method) { + case "onWebViewCreated": + if (params.onWebViewCreated != null && _webViewController != null) { + params.onWebViewCreated!(_controllerFromPlatform); + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + Future run() async { + if (_started) { + return; + } + _started = true; + _init(); + + final initialSettings = params.initialSettings ?? InAppWebViewSettings(); + _inferInitialSettings(initialSettings); + + Map settingsMap = + (params.initialSettings != null ? initialSettings.toMap() : null) ?? + params.initialOptions?.toMap() ?? + initialSettings.toMap(); + + Map pullToRefreshSettings = + _macosParams.pullToRefreshController?.params.settings.toMap() ?? + _macosParams.pullToRefreshController?.params.options.toMap() ?? + PullToRefreshSettings(enabled: false).toMap(); + + Map args = {}; + args.putIfAbsent('id', () => id); + args.putIfAbsent( + 'params', + () => { + 'initialUrlRequest': params.initialUrlRequest?.toMap(), + 'initialFile': params.initialFile, + 'initialData': params.initialData?.toMap(), + 'initialSettings': settingsMap, + 'contextMenu': params.contextMenu?.toMap() ?? {}, + 'windowId': params.windowId, + 'initialUserScripts': + params.initialUserScripts?.map((e) => e.toMap()).toList() ?? + [], + 'pullToRefreshSettings': pullToRefreshSettings, + 'initialSize': params.initialSize.toMap() + }); + await _sharedChannel.invokeMethod('run', args); + _running = true; + } + + void _inferInitialSettings(InAppWebViewSettings settings) { + if (params.shouldOverrideUrlLoading != null && + settings.useShouldOverrideUrlLoading == null) { + settings.useShouldOverrideUrlLoading = true; + } + if (params.onLoadResource != null && settings.useOnLoadResource == null) { + settings.useOnLoadResource = true; + } + if (params.onDownloadStartRequest != null && + settings.useOnDownloadStart == null) { + settings.useOnDownloadStart = true; + } + if (params.shouldInterceptAjaxRequest != null && + settings.useShouldInterceptAjaxRequest == null) { + settings.useShouldInterceptAjaxRequest = true; + } + if (params.shouldInterceptFetchRequest != null && + settings.useShouldInterceptFetchRequest == null) { + settings.useShouldInterceptFetchRequest = true; + } + if (params.shouldInterceptRequest != null && + settings.useShouldInterceptRequest == null) { + settings.useShouldInterceptRequest = true; + } + if (params.onRenderProcessGone != null && + settings.useOnRenderProcessGone == null) { + settings.useOnRenderProcessGone = true; + } + if (params.onNavigationResponse != null && + settings.useOnNavigationResponse == null) { + settings.useOnNavigationResponse = true; + } + } + + @override + bool isRunning() { + return _running; + } + + @override + Future setSize(Size size) async { + if (!_running) { + return; + } + + Map args = {}; + args.putIfAbsent('size', () => size.toMap()); + await channel?.invokeMethod('setSize', args); + } + + @override + Future getSize() async { + if (!_running) { + return null; + } + + Map args = {}; + Map sizeMap = + (await channel?.invokeMethod('getSize', args))?.cast(); + return MapSize.fromMap(sizeMap); + } + + @override + Future dispose() async { + if (!_running) { + return; + } + Map args = {}; + await channel?.invokeMethod('dispose', args); + disposeChannel(); + _started = false; + _running = false; + _webViewController?.dispose(); + _webViewController = null; + _controllerFromPlatform = null; + _macosParams.pullToRefreshController?.dispose(); + _macosParams.findInteractionController?.dispose(); + } +} + +extension InternalHeadlessInAppWebView on MacOSHeadlessInAppWebView { + Future internalDispose() async { + _started = false; + _running = false; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/in_app_webview/in_app_webview.dart b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/in_app_webview/in_app_webview.dart new file mode 100755 index 00000000..f2800a94 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/in_app_webview/in_app_webview.dart @@ -0,0 +1,408 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; +import 'headless_in_app_webview.dart'; + +import '../find_interaction/find_interaction_controller.dart'; +import 'in_app_webview_controller.dart'; + +/// Object specifying creation parameters for creating a [PlatformInAppWebViewWidget]. +/// +/// Platform specific implementations can add additional fields by extending +/// this class. +class MacOSInAppWebViewWidgetCreationParams + extends PlatformInAppWebViewWidgetCreationParams { + MacOSInAppWebViewWidgetCreationParams( + {super.controllerFromPlatform, + super.key, + super.layoutDirection, + super.gestureRecognizers, + super.headlessWebView, + super.keepAlive, + super.preventGestureDelay, + super.windowId, + super.onWebViewCreated, + super.onLoadStart, + super.onLoadStop, + @Deprecated('Use onReceivedError instead') super.onLoadError, + super.onReceivedError, + @Deprecated("Use onReceivedHttpError instead") super.onLoadHttpError, + super.onReceivedHttpError, + super.onProgressChanged, + super.onConsoleMessage, + super.shouldOverrideUrlLoading, + super.onLoadResource, + super.onScrollChanged, + @Deprecated('Use onDownloadStartRequest instead') super.onDownloadStart, + super.onDownloadStartRequest, + @Deprecated('Use onLoadResourceWithCustomScheme instead') + super.onLoadResourceCustomScheme, + super.onLoadResourceWithCustomScheme, + super.onCreateWindow, + super.onCloseWindow, + super.onJsAlert, + super.onJsConfirm, + super.onJsPrompt, + super.onReceivedHttpAuthRequest, + super.onReceivedServerTrustAuthRequest, + super.onReceivedClientCertRequest, + @Deprecated('Use FindInteractionController.onFindResultReceived instead') + super.onFindResultReceived, + super.shouldInterceptAjaxRequest, + super.onAjaxReadyStateChange, + super.onAjaxProgress, + super.shouldInterceptFetchRequest, + super.onUpdateVisitedHistory, + @Deprecated("Use onPrintRequest instead") super.onPrint, + super.onPrintRequest, + super.onLongPressHitTestResult, + super.onEnterFullscreen, + super.onExitFullscreen, + super.onPageCommitVisible, + super.onTitleChanged, + super.onWindowFocus, + super.onWindowBlur, + super.onOverScrolled, + super.onZoomScaleChanged, + @Deprecated('Use onSafeBrowsingHit instead') + super.androidOnSafeBrowsingHit, + super.onSafeBrowsingHit, + @Deprecated('Use onPermissionRequest instead') + super.androidOnPermissionRequest, + super.onPermissionRequest, + @Deprecated('Use onGeolocationPermissionsShowPrompt instead') + super.androidOnGeolocationPermissionsShowPrompt, + super.onGeolocationPermissionsShowPrompt, + @Deprecated('Use onGeolocationPermissionsHidePrompt instead') + super.androidOnGeolocationPermissionsHidePrompt, + super.onGeolocationPermissionsHidePrompt, + @Deprecated('Use shouldInterceptRequest instead') + super.androidShouldInterceptRequest, + super.shouldInterceptRequest, + @Deprecated('Use onRenderProcessGone instead') + super.androidOnRenderProcessGone, + super.onRenderProcessGone, + @Deprecated('Use onRenderProcessResponsive instead') + super.androidOnRenderProcessResponsive, + super.onRenderProcessResponsive, + @Deprecated('Use onRenderProcessUnresponsive instead') + super.androidOnRenderProcessUnresponsive, + super.onRenderProcessUnresponsive, + @Deprecated('Use onFormResubmission instead') + super.androidOnFormResubmission, + super.onFormResubmission, + @Deprecated('Use onZoomScaleChanged instead') super.androidOnScaleChanged, + @Deprecated('Use onReceivedIcon instead') super.androidOnReceivedIcon, + super.onReceivedIcon, + @Deprecated('Use onReceivedTouchIconUrl instead') + super.androidOnReceivedTouchIconUrl, + super.onReceivedTouchIconUrl, + @Deprecated('Use onJsBeforeUnload instead') super.androidOnJsBeforeUnload, + super.onJsBeforeUnload, + @Deprecated('Use onReceivedLoginRequest instead') + super.androidOnReceivedLoginRequest, + super.onReceivedLoginRequest, + super.onPermissionRequestCanceled, + super.onRequestFocus, + @Deprecated('Use onWebContentProcessDidTerminate instead') + super.iosOnWebContentProcessDidTerminate, + super.onWebContentProcessDidTerminate, + @Deprecated( + 'Use onDidReceiveServerRedirectForProvisionalNavigation instead') + super.iosOnDidReceiveServerRedirectForProvisionalNavigation, + super.onDidReceiveServerRedirectForProvisionalNavigation, + @Deprecated('Use onNavigationResponse instead') + super.iosOnNavigationResponse, + super.onNavigationResponse, + @Deprecated('Use shouldAllowDeprecatedTLS instead') + super.iosShouldAllowDeprecatedTLS, + super.shouldAllowDeprecatedTLS, + super.onCameraCaptureStateChanged, + super.onMicrophoneCaptureStateChanged, + super.onContentSizeChanged, + super.initialUrlRequest, + super.initialFile, + super.initialData, + @Deprecated('Use initialSettings instead') super.initialOptions, + super.initialSettings, + super.contextMenu, + super.initialUserScripts, + super.pullToRefreshController, + this.findInteractionController}); + + /// Constructs a [MacOSInAppWebViewWidgetCreationParams] using a + /// [PlatformInAppWebViewWidgetCreationParams]. + MacOSInAppWebViewWidgetCreationParams.fromPlatformInAppWebViewWidgetCreationParams( + PlatformInAppWebViewWidgetCreationParams params) + : this( + controllerFromPlatform: params.controllerFromPlatform, + key: params.key, + layoutDirection: params.layoutDirection, + gestureRecognizers: params.gestureRecognizers, + headlessWebView: params.headlessWebView, + keepAlive: params.keepAlive, + preventGestureDelay: params.preventGestureDelay, + windowId: params.windowId, + onWebViewCreated: params.onWebViewCreated, + onLoadStart: params.onLoadStart, + onLoadStop: params.onLoadStop, + onLoadError: params.onLoadError, + onReceivedError: params.onReceivedError, + onLoadHttpError: params.onLoadHttpError, + onReceivedHttpError: params.onReceivedHttpError, + onProgressChanged: params.onProgressChanged, + onConsoleMessage: params.onConsoleMessage, + shouldOverrideUrlLoading: params.shouldOverrideUrlLoading, + onLoadResource: params.onLoadResource, + onScrollChanged: params.onScrollChanged, + onDownloadStart: params.onDownloadStart, + onDownloadStartRequest: params.onDownloadStartRequest, + onLoadResourceCustomScheme: params.onLoadResourceCustomScheme, + onLoadResourceWithCustomScheme: + params.onLoadResourceWithCustomScheme, + onCreateWindow: params.onCreateWindow, + onCloseWindow: params.onCloseWindow, + onJsAlert: params.onJsAlert, + onJsConfirm: params.onJsConfirm, + onJsPrompt: params.onJsPrompt, + onReceivedHttpAuthRequest: params.onReceivedHttpAuthRequest, + onReceivedServerTrustAuthRequest: + params.onReceivedServerTrustAuthRequest, + onReceivedClientCertRequest: params.onReceivedClientCertRequest, + onFindResultReceived: params.onFindResultReceived, + shouldInterceptAjaxRequest: params.shouldInterceptAjaxRequest, + onAjaxReadyStateChange: params.onAjaxReadyStateChange, + onAjaxProgress: params.onAjaxProgress, + shouldInterceptFetchRequest: params.shouldInterceptFetchRequest, + onUpdateVisitedHistory: params.onUpdateVisitedHistory, + onPrint: params.onPrint, + onPrintRequest: params.onPrintRequest, + onLongPressHitTestResult: params.onLongPressHitTestResult, + onEnterFullscreen: params.onEnterFullscreen, + onExitFullscreen: params.onExitFullscreen, + onPageCommitVisible: params.onPageCommitVisible, + onTitleChanged: params.onTitleChanged, + onWindowFocus: params.onWindowFocus, + onWindowBlur: params.onWindowBlur, + onOverScrolled: params.onOverScrolled, + onZoomScaleChanged: params.onZoomScaleChanged, + androidOnSafeBrowsingHit: params.androidOnSafeBrowsingHit, + onSafeBrowsingHit: params.onSafeBrowsingHit, + androidOnPermissionRequest: params.androidOnPermissionRequest, + onPermissionRequest: params.onPermissionRequest, + androidOnGeolocationPermissionsShowPrompt: + params.androidOnGeolocationPermissionsShowPrompt, + onGeolocationPermissionsShowPrompt: + params.onGeolocationPermissionsShowPrompt, + androidOnGeolocationPermissionsHidePrompt: + params.androidOnGeolocationPermissionsHidePrompt, + onGeolocationPermissionsHidePrompt: + params.onGeolocationPermissionsHidePrompt, + androidShouldInterceptRequest: params.androidShouldInterceptRequest, + shouldInterceptRequest: params.shouldInterceptRequest, + androidOnRenderProcessGone: params.androidOnRenderProcessGone, + onRenderProcessGone: params.onRenderProcessGone, + androidOnRenderProcessResponsive: + params.androidOnRenderProcessResponsive, + onRenderProcessResponsive: params.onRenderProcessResponsive, + androidOnRenderProcessUnresponsive: + params.androidOnRenderProcessUnresponsive, + onRenderProcessUnresponsive: params.onRenderProcessUnresponsive, + androidOnFormResubmission: params.androidOnFormResubmission, + onFormResubmission: params.onFormResubmission, + androidOnScaleChanged: params.androidOnScaleChanged, + androidOnReceivedIcon: params.androidOnReceivedIcon, + onReceivedIcon: params.onReceivedIcon, + androidOnReceivedTouchIconUrl: params.androidOnReceivedTouchIconUrl, + onReceivedTouchIconUrl: params.onReceivedTouchIconUrl, + androidOnJsBeforeUnload: params.androidOnJsBeforeUnload, + onJsBeforeUnload: params.onJsBeforeUnload, + androidOnReceivedLoginRequest: params.androidOnReceivedLoginRequest, + onReceivedLoginRequest: params.onReceivedLoginRequest, + onPermissionRequestCanceled: params.onPermissionRequestCanceled, + onRequestFocus: params.onRequestFocus, + iosOnWebContentProcessDidTerminate: + params.iosOnWebContentProcessDidTerminate, + onWebContentProcessDidTerminate: + params.onWebContentProcessDidTerminate, + iosOnDidReceiveServerRedirectForProvisionalNavigation: + params.iosOnDidReceiveServerRedirectForProvisionalNavigation, + onDidReceiveServerRedirectForProvisionalNavigation: + params.onDidReceiveServerRedirectForProvisionalNavigation, + iosOnNavigationResponse: params.iosOnNavigationResponse, + onNavigationResponse: params.onNavigationResponse, + iosShouldAllowDeprecatedTLS: params.iosShouldAllowDeprecatedTLS, + shouldAllowDeprecatedTLS: params.shouldAllowDeprecatedTLS, + onCameraCaptureStateChanged: params.onCameraCaptureStateChanged, + onMicrophoneCaptureStateChanged: + params.onMicrophoneCaptureStateChanged, + onContentSizeChanged: params.onContentSizeChanged, + initialUrlRequest: params.initialUrlRequest, + initialFile: params.initialFile, + initialData: params.initialData, + initialOptions: params.initialOptions, + initialSettings: params.initialSettings, + contextMenu: params.contextMenu, + initialUserScripts: params.initialUserScripts, + pullToRefreshController: params.pullToRefreshController, + findInteractionController: params.findInteractionController + as MacOSFindInteractionController?); + + @override + final MacOSFindInteractionController? findInteractionController; +} + +///{@macro flutter_inappwebview_platform_interface.PlatformInAppWebViewWidget} +class MacOSInAppWebViewWidget extends PlatformInAppWebViewWidget { + /// Constructs a [MacOSInAppWebViewWidget]. + /// + ///{@macro flutter_inappwebview_platform_interface.PlatformInAppWebViewWidget} + MacOSInAppWebViewWidget(PlatformInAppWebViewWidgetCreationParams params) + : super.implementation( + params is MacOSInAppWebViewWidgetCreationParams + ? params + : MacOSInAppWebViewWidgetCreationParams + .fromPlatformInAppWebViewWidgetCreationParams(params), + ); + + MacOSInAppWebViewWidgetCreationParams get _macosParams => + params as MacOSInAppWebViewWidgetCreationParams; + + MacOSInAppWebViewController? _controller; + + MacOSHeadlessInAppWebView? get _macosHeadlessInAppWebView => + params.headlessWebView as MacOSHeadlessInAppWebView?; + + @override + Widget build(BuildContext context) { + final initialSettings = params.initialSettings ?? InAppWebViewSettings(); + _inferInitialSettings(initialSettings); + + Map settingsMap = + (params.initialSettings != null ? initialSettings.toMap() : null) ?? + // ignore: deprecated_member_use_from_same_package + params.initialOptions?.toMap() ?? + initialSettings.toMap(); + + Map pullToRefreshSettings = + params.pullToRefreshController?.params.settings.toMap() ?? + // ignore: deprecated_member_use_from_same_package + params.pullToRefreshController?.params.options.toMap() ?? + PullToRefreshSettings(enabled: false).toMap(); + + if ((params.headlessWebView?.isRunning() ?? false) && + params.keepAlive != null) { + final headlessId = params.headlessWebView?.id; + if (headlessId != null) { + // force keep alive id to match headless webview id + params.keepAlive?.id = headlessId; + } + } + + return AppKitView( + viewType: 'com.pichillilorenzo/flutter_inappwebview', + onPlatformViewCreated: _onPlatformViewCreated, + gestureRecognizers: params.gestureRecognizers, + creationParams: { + 'initialUrlRequest': params.initialUrlRequest?.toMap(), + 'initialFile': params.initialFile, + 'initialData': params.initialData?.toMap(), + 'initialSettings': settingsMap, + 'contextMenu': params.contextMenu?.toMap() ?? {}, + 'windowId': params.windowId, + 'headlessWebViewId': params.headlessWebView?.isRunning() ?? false + ? params.headlessWebView?.id + : null, + 'initialUserScripts': + params.initialUserScripts?.map((e) => e.toMap()).toList() ?? [], + 'pullToRefreshSettings': pullToRefreshSettings, + 'keepAliveId': params.keepAlive?.id, + 'preventGestureDelay': params.preventGestureDelay + }, + creationParamsCodec: const StandardMessageCodec(), + ); + } + + void _onPlatformViewCreated(int id) { + dynamic viewId = id; + if (params.headlessWebView?.isRunning() ?? false) { + viewId = params.headlessWebView?.id; + } + viewId = params.keepAlive?.id ?? viewId ?? id; + _macosHeadlessInAppWebView?.internalDispose(); + _controller = MacOSInAppWebViewController( + PlatformInAppWebViewControllerCreationParams( + id: viewId, webviewParams: params)); + _macosParams.findInteractionController?.init(viewId); + debugLog( + className: runtimeType.toString(), + id: viewId?.toString(), + debugLoggingSettings: + PlatformInAppWebViewController.debugLoggingSettings, + method: "onWebViewCreated", + args: []); + if (params.onWebViewCreated != null) { + params.onWebViewCreated!( + params.controllerFromPlatform?.call(_controller!) ?? _controller!); + } + } + + void _inferInitialSettings(InAppWebViewSettings settings) { + if (params.shouldOverrideUrlLoading != null && + settings.useShouldOverrideUrlLoading == null) { + settings.useShouldOverrideUrlLoading = true; + } + if (params.onLoadResource != null && settings.useOnLoadResource == null) { + settings.useOnLoadResource = true; + } + if (params.onDownloadStartRequest != null && + settings.useOnDownloadStart == null) { + settings.useOnDownloadStart = true; + } + if ((params.shouldInterceptAjaxRequest != null || + params.onAjaxProgress != null || + params.onAjaxReadyStateChange != null) && + settings.useShouldInterceptAjaxRequest == null) { + settings.useShouldInterceptAjaxRequest = true; + } + if (params.shouldInterceptFetchRequest != null && + settings.useShouldInterceptFetchRequest == null) { + settings.useShouldInterceptFetchRequest = true; + } + if (params.shouldInterceptRequest != null && + settings.useShouldInterceptRequest == null) { + settings.useShouldInterceptRequest = true; + } + if (params.onRenderProcessGone != null && + settings.useOnRenderProcessGone == null) { + settings.useOnRenderProcessGone = true; + } + if (params.onNavigationResponse != null && + settings.useOnNavigationResponse == null) { + settings.useOnNavigationResponse = true; + } + } + + @override + void dispose() { + dynamic viewId = _controller?.getViewId(); + debugLog( + className: runtimeType.toString(), + id: viewId?.toString(), + debugLoggingSettings: + PlatformInAppWebViewController.debugLoggingSettings, + method: "dispose", + args: []); + final isKeepAlive = params.keepAlive != null; + _controller?.dispose(isKeepAlive: isKeepAlive); + _controller = null; + params.findInteractionController?.dispose(isKeepAlive: isKeepAlive); + } + + @override + T controllerFromPlatform(PlatformInAppWebViewController controller) { + // unused + throw UnimplementedError(); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/in_app_webview/in_app_webview_controller.dart b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/in_app_webview/in_app_webview_controller.dart new file mode 100644 index 00000000..38ab1bc7 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/in_app_webview/in_app_webview_controller.dart @@ -0,0 +1,2689 @@ +import 'dart:io'; +import 'dart:collection'; +import 'dart:convert'; +import 'dart:core'; +import 'dart:developer' as developer; +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +import '../web_message/main.dart'; + +import '../in_app_browser/in_app_browser.dart'; +import '../web_storage/web_storage.dart'; + +import 'headless_in_app_webview.dart'; +import '_static_channel.dart'; + +import '../print_job/main.dart'; + +///List of forbidden names for JavaScript handlers. +// ignore: non_constant_identifier_names +final _JAVASCRIPT_HANDLER_FORBIDDEN_NAMES = UnmodifiableListView([ + "onLoadResource", + "shouldInterceptAjaxRequest", + "onAjaxReadyStateChange", + "onAjaxProgress", + "shouldInterceptFetchRequest", + "onPrintRequest", + "onWindowFocus", + "onWindowBlur", + "callAsyncJavaScript", + "evaluateJavaScriptWithContentWorld" +]); + +/// Object specifying creation parameters for creating a [MacOSInAppWebViewController]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformInAppWebViewControllerCreationParams] for +/// more information. +@immutable +class MacOSInAppWebViewControllerCreationParams + extends PlatformInAppWebViewControllerCreationParams { + /// Creates a new [MacOSInAppWebViewControllerCreationParams] instance. + const MacOSInAppWebViewControllerCreationParams( + {required super.id, super.webviewParams}); + + /// Creates a [MacOSInAppWebViewControllerCreationParams] instance based on [PlatformInAppWebViewControllerCreationParams]. + factory MacOSInAppWebViewControllerCreationParams.fromPlatformInAppWebViewControllerCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformInAppWebViewControllerCreationParams params) { + return MacOSInAppWebViewControllerCreationParams( + id: params.id, webviewParams: params.webviewParams); + } +} + +///Controls a WebView, such as an [InAppWebView] widget instance, a [MacOSHeadlessInAppWebView] instance or [MacOSInAppBrowser] WebView instance. +/// +///If you are using the [InAppWebView] widget, an [InAppWebViewController] instance can be obtained by setting the [InAppWebView.onWebViewCreated] +///callback. Instead, if you are using an [MacOSInAppBrowser] instance, you can get it through the [MacOSInAppBrowser.webViewController] attribute. +class MacOSInAppWebViewController extends PlatformInAppWebViewController + with ChannelController { + static final MethodChannel _staticChannel = IN_APP_WEBVIEW_STATIC_CHANNEL; + + // List of properties to be saved and restored for keep alive feature + Map _javaScriptHandlersMap = + HashMap(); + Map> _userScripts = { + UserScriptInjectionTime.AT_DOCUMENT_START: [], + UserScriptInjectionTime.AT_DOCUMENT_END: [] + }; + Set _webMessageListenerObjNames = Set(); + Map _injectedScriptsFromURL = {}; + Set _webMessageChannels = Set(); + Set _webMessageListeners = Set(); + + // static map that contains the properties to be saved and restored for keep alive feature + static final Map + _keepAliveMap = {}; + + MacOSInAppBrowser? _inAppBrowser; + + PlatformInAppBrowserEvents? get _inAppBrowserEventHandler => + _inAppBrowser?.eventHandler; + + dynamic _controllerFromPlatform; + + @override + late MacOSWebStorage webStorage; + + MacOSInAppWebViewController( + PlatformInAppWebViewControllerCreationParams params) + : super.implementation(params is MacOSInAppWebViewControllerCreationParams + ? params + : MacOSInAppWebViewControllerCreationParams + .fromPlatformInAppWebViewControllerCreationParams(params)) { + channel = MethodChannel('com.pichillilorenzo/flutter_inappwebview_$id'); + handler = handleMethod; + initMethodCallHandler(); + + final initialUserScripts = webviewParams?.initialUserScripts; + if (initialUserScripts != null) { + for (final userScript in initialUserScripts) { + if (userScript.injectionTime == + UserScriptInjectionTime.AT_DOCUMENT_START) { + this + ._userScripts[UserScriptInjectionTime.AT_DOCUMENT_START] + ?.add(userScript); + } else { + this + ._userScripts[UserScriptInjectionTime.AT_DOCUMENT_END] + ?.add(userScript); + } + } + } + + this._init(params); + } + + static final MacOSInAppWebViewController _staticValue = + MacOSInAppWebViewController( + MacOSInAppWebViewControllerCreationParams(id: null)); + + factory MacOSInAppWebViewController.static() { + return _staticValue; + } + + MacOSInAppWebViewController.fromInAppBrowser( + PlatformInAppWebViewControllerCreationParams params, + MethodChannel channel, + MacOSInAppBrowser inAppBrowser, + UnmodifiableListView? initialUserScripts) + : super.implementation(params is MacOSInAppWebViewControllerCreationParams + ? params + : MacOSInAppWebViewControllerCreationParams + .fromPlatformInAppWebViewControllerCreationParams(params)) { + this.channel = channel; + this._inAppBrowser = inAppBrowser; + + if (initialUserScripts != null) { + for (final userScript in initialUserScripts) { + if (userScript.injectionTime == + UserScriptInjectionTime.AT_DOCUMENT_START) { + this + ._userScripts[UserScriptInjectionTime.AT_DOCUMENT_START] + ?.add(userScript); + } else { + this + ._userScripts[UserScriptInjectionTime.AT_DOCUMENT_END] + ?.add(userScript); + } + } + } + this._init(params); + } + + void _init(PlatformInAppWebViewControllerCreationParams params) { + _controllerFromPlatform = + params.webviewParams?.controllerFromPlatform?.call(this) ?? this; + + webStorage = MacOSWebStorage(MacOSWebStorageCreationParams( + localStorage: MacOSLocalStorage.defaultStorage(controller: this), + sessionStorage: MacOSSessionStorage.defaultStorage(controller: this))); + + if (params.webviewParams is PlatformInAppWebViewWidgetCreationParams) { + final keepAlive = + (params.webviewParams as PlatformInAppWebViewWidgetCreationParams) + .keepAlive; + if (keepAlive != null) { + InAppWebViewControllerKeepAliveProps? props = _keepAliveMap[keepAlive]; + if (props == null) { + // save controller properties to restore it later + _keepAliveMap[keepAlive] = InAppWebViewControllerKeepAliveProps( + injectedScriptsFromURL: _injectedScriptsFromURL, + javaScriptHandlersMap: _javaScriptHandlersMap, + userScripts: _userScripts, + webMessageListenerObjNames: _webMessageListenerObjNames, + webMessageChannels: _webMessageChannels, + webMessageListeners: _webMessageListeners); + } else { + // restore controller properties + _injectedScriptsFromURL = props.injectedScriptsFromURL; + _javaScriptHandlersMap = props.javaScriptHandlersMap; + _userScripts = props.userScripts; + _webMessageListenerObjNames = props.webMessageListenerObjNames; + _webMessageChannels = + props.webMessageChannels as Set; + _webMessageListeners = + props.webMessageListeners as Set; + } + } + } + } + + _debugLog(String method, dynamic args) { + debugLog( + className: this.runtimeType.toString(), + name: _inAppBrowser == null + ? "WebView" + : _inAppBrowser.runtimeType.toString(), + id: (getViewId() ?? _inAppBrowser?.id).toString(), + debugLoggingSettings: + PlatformInAppWebViewController.debugLoggingSettings, + method: method, + args: args); + } + + Future _handleMethod(MethodCall call) async { + if (PlatformInAppWebViewController.debugLoggingSettings.enabled && + call.method != "onCallJsHandler") { + _debugLog(call.method, call.arguments); + } + + switch (call.method) { + case "onLoadStart": + _injectedScriptsFromURL.clear(); + if ((webviewParams != null && webviewParams!.onLoadStart != null) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + WebUri? uri = url != null ? WebUri(url) : null; + if (webviewParams != null && webviewParams!.onLoadStart != null) + webviewParams!.onLoadStart!(_controllerFromPlatform, uri); + else + _inAppBrowserEventHandler!.onLoadStart(uri); + } + break; + case "onLoadStop": + if ((webviewParams != null && webviewParams!.onLoadStop != null) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + WebUri? uri = url != null ? WebUri(url) : null; + if (webviewParams != null && webviewParams!.onLoadStop != null) + webviewParams!.onLoadStop!(_controllerFromPlatform, uri); + else + _inAppBrowserEventHandler!.onLoadStop(uri); + } + break; + case "onReceivedError": + if ((webviewParams != null && + (webviewParams!.onReceivedError != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.onLoadError != null)) || + _inAppBrowserEventHandler != null) { + WebResourceRequest request = WebResourceRequest.fromMap( + call.arguments["request"].cast())!; + WebResourceError error = WebResourceError.fromMap( + call.arguments["error"].cast())!; + var isForMainFrame = request.isForMainFrame ?? false; + + if (webviewParams != null) { + if (webviewParams!.onReceivedError != null) + webviewParams!.onReceivedError!( + _controllerFromPlatform, request, error); + else if (isForMainFrame) { + // ignore: deprecated_member_use_from_same_package + webviewParams!.onLoadError!(_controllerFromPlatform, request.url, + error.type.toNativeValue() ?? -1, error.description); + } + } else { + if (isForMainFrame) { + _inAppBrowserEventHandler!.onLoadError(request.url, + error.type.toNativeValue() ?? -1, error.description); + } + _inAppBrowserEventHandler!.onReceivedError(request, error); + } + } + break; + case "onReceivedHttpError": + if ((webviewParams != null && + (webviewParams!.onReceivedHttpError != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.onLoadHttpError != null)) || + _inAppBrowserEventHandler != null) { + WebResourceRequest request = WebResourceRequest.fromMap( + call.arguments["request"].cast())!; + WebResourceResponse errorResponse = WebResourceResponse.fromMap( + call.arguments["errorResponse"].cast())!; + var isForMainFrame = request.isForMainFrame ?? false; + + if (webviewParams != null) { + if (webviewParams!.onReceivedHttpError != null) + webviewParams!.onReceivedHttpError!( + _controllerFromPlatform, request, errorResponse); + else if (isForMainFrame) { + // ignore: deprecated_member_use_from_same_package + webviewParams!.onLoadHttpError!( + _controllerFromPlatform, + request.url, + errorResponse.statusCode ?? -1, + errorResponse.reasonPhrase ?? ''); + } + } else { + if (isForMainFrame) { + _inAppBrowserEventHandler!.onLoadHttpError( + request.url, + errorResponse.statusCode ?? -1, + errorResponse.reasonPhrase ?? ''); + } + _inAppBrowserEventHandler! + .onReceivedHttpError(request, errorResponse); + } + } + break; + case "onProgressChanged": + if ((webviewParams != null && + webviewParams!.onProgressChanged != null) || + _inAppBrowserEventHandler != null) { + int progress = call.arguments["progress"]; + if (webviewParams != null && webviewParams!.onProgressChanged != null) + webviewParams!.onProgressChanged!( + _controllerFromPlatform, progress); + else + _inAppBrowserEventHandler!.onProgressChanged(progress); + } + break; + case "shouldOverrideUrlLoading": + if ((webviewParams != null && + webviewParams!.shouldOverrideUrlLoading != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + NavigationAction navigationAction = + NavigationAction.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.shouldOverrideUrlLoading != null) + return (await webviewParams!.shouldOverrideUrlLoading!( + _controllerFromPlatform, navigationAction)) + ?.toNativeValue(); + return (await _inAppBrowserEventHandler! + .shouldOverrideUrlLoading(navigationAction)) + ?.toNativeValue(); + } + break; + case "onConsoleMessage": + if ((webviewParams != null && + webviewParams!.onConsoleMessage != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + ConsoleMessage consoleMessage = ConsoleMessage.fromMap(arguments)!; + if (webviewParams != null && webviewParams!.onConsoleMessage != null) + webviewParams!.onConsoleMessage!( + _controllerFromPlatform, consoleMessage); + else + _inAppBrowserEventHandler!.onConsoleMessage(consoleMessage); + } + break; + case "onScrollChanged": + if ((webviewParams != null && webviewParams!.onScrollChanged != null) || + _inAppBrowserEventHandler != null) { + int x = call.arguments["x"]; + int y = call.arguments["y"]; + if (webviewParams != null && webviewParams!.onScrollChanged != null) + webviewParams!.onScrollChanged!(_controllerFromPlatform, x, y); + else + _inAppBrowserEventHandler!.onScrollChanged(x, y); + } + break; + case "onDownloadStartRequest": + if ((webviewParams != null && + // ignore: deprecated_member_use_from_same_package + (webviewParams!.onDownloadStart != null || + webviewParams!.onDownloadStartRequest != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + DownloadStartRequest downloadStartRequest = + DownloadStartRequest.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.onDownloadStartRequest != null) + webviewParams!.onDownloadStartRequest!( + _controllerFromPlatform, downloadStartRequest); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.onDownloadStart!( + _controllerFromPlatform, downloadStartRequest.url); + } + } else { + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler! + .onDownloadStart(downloadStartRequest.url); + _inAppBrowserEventHandler! + .onDownloadStartRequest(downloadStartRequest); + } + } + break; + case "onLoadResourceWithCustomScheme": + if ((webviewParams != null && + (webviewParams!.onLoadResourceWithCustomScheme != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.onLoadResourceCustomScheme != null)) || + _inAppBrowserEventHandler != null) { + Map requestMap = + call.arguments["request"].cast(); + WebResourceRequest request = WebResourceRequest.fromMap(requestMap)!; + + if (webviewParams != null) { + if (webviewParams!.onLoadResourceWithCustomScheme != null) + return (await webviewParams!.onLoadResourceWithCustomScheme!( + _controllerFromPlatform, request)) + ?.toMap(); + else { + return (await params + .webviewParams! + // ignore: deprecated_member_use_from_same_package + .onLoadResourceCustomScheme!( + _controllerFromPlatform, request.url)) + ?.toMap(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onLoadResourceWithCustomScheme(request)) ?? + (await _inAppBrowserEventHandler! + .onLoadResourceCustomScheme(request.url))) + ?.toMap(); + } + } + break; + case "onCreateWindow": + if ((webviewParams != null && webviewParams!.onCreateWindow != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + CreateWindowAction createWindowAction = + CreateWindowAction.fromMap(arguments)!; + + if (webviewParams != null && webviewParams!.onCreateWindow != null) + return await webviewParams!.onCreateWindow!( + _controllerFromPlatform, createWindowAction); + else + return await _inAppBrowserEventHandler! + .onCreateWindow(createWindowAction); + } + break; + case "onCloseWindow": + if (webviewParams != null && webviewParams!.onCloseWindow != null) + webviewParams!.onCloseWindow!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onCloseWindow(); + break; + case "onTitleChanged": + if ((webviewParams != null && webviewParams!.onTitleChanged != null) || + _inAppBrowserEventHandler != null) { + String? title = call.arguments["title"]; + if (webviewParams != null && webviewParams!.onTitleChanged != null) + webviewParams!.onTitleChanged!(_controllerFromPlatform, title); + else + _inAppBrowserEventHandler!.onTitleChanged(title); + } + break; + case "onGeolocationPermissionsShowPrompt": + if ((webviewParams != null && + (webviewParams!.onGeolocationPermissionsShowPrompt != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnGeolocationPermissionsShowPrompt != + null)) || + _inAppBrowserEventHandler != null) { + String origin = call.arguments["origin"]; + + if (webviewParams != null) { + if (webviewParams!.onGeolocationPermissionsShowPrompt != null) + return (await webviewParams!.onGeolocationPermissionsShowPrompt!( + _controllerFromPlatform, origin)) + ?.toMap(); + else { + return (await params + .webviewParams! + // ignore: deprecated_member_use_from_same_package + .androidOnGeolocationPermissionsShowPrompt!( + _controllerFromPlatform, origin)) + ?.toMap(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onGeolocationPermissionsShowPrompt(origin)) ?? + (await _inAppBrowserEventHandler! + .androidOnGeolocationPermissionsShowPrompt(origin))) + ?.toMap(); + } + } + break; + case "onGeolocationPermissionsHidePrompt": + if (webviewParams != null && + (webviewParams!.onGeolocationPermissionsHidePrompt != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnGeolocationPermissionsHidePrompt != + null)) { + if (webviewParams!.onGeolocationPermissionsHidePrompt != null) + webviewParams! + .onGeolocationPermissionsHidePrompt!(_controllerFromPlatform); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnGeolocationPermissionsHidePrompt!( + _controllerFromPlatform); + } + } else if (_inAppBrowserEventHandler != null) { + _inAppBrowserEventHandler!.onGeolocationPermissionsHidePrompt(); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler! + .androidOnGeolocationPermissionsHidePrompt(); + } + break; + case "shouldInterceptRequest": + if ((webviewParams != null && + (webviewParams!.shouldInterceptRequest != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidShouldInterceptRequest != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + WebResourceRequest request = WebResourceRequest.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.shouldInterceptRequest != null) + return (await webviewParams!.shouldInterceptRequest!( + _controllerFromPlatform, request)) + ?.toMap(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.androidShouldInterceptRequest!( + _controllerFromPlatform, request)) + ?.toMap(); + } + } else { + return ((await _inAppBrowserEventHandler! + .shouldInterceptRequest(request)) ?? + (await _inAppBrowserEventHandler! + .androidShouldInterceptRequest(request))) + ?.toMap(); + } + } + break; + case "onRenderProcessUnresponsive": + if ((webviewParams != null && + (webviewParams!.onRenderProcessUnresponsive != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnRenderProcessUnresponsive != + null)) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + WebUri? uri = url != null ? WebUri(url) : null; + + if (webviewParams != null) { + if (webviewParams!.onRenderProcessUnresponsive != null) + return (await webviewParams!.onRenderProcessUnresponsive!( + _controllerFromPlatform, uri)) + ?.toNativeValue(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.androidOnRenderProcessUnresponsive!( + _controllerFromPlatform, uri)) + ?.toNativeValue(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onRenderProcessUnresponsive(uri)) ?? + (await _inAppBrowserEventHandler! + .androidOnRenderProcessUnresponsive(uri))) + ?.toNativeValue(); + } + } + break; + case "onRenderProcessResponsive": + if ((webviewParams != null && + (webviewParams!.onRenderProcessResponsive != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnRenderProcessResponsive != null)) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + WebUri? uri = url != null ? WebUri(url) : null; + + if (webviewParams != null) { + if (webviewParams!.onRenderProcessResponsive != null) + return (await webviewParams!.onRenderProcessResponsive!( + _controllerFromPlatform, uri)) + ?.toNativeValue(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.androidOnRenderProcessResponsive!( + _controllerFromPlatform, uri)) + ?.toNativeValue(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onRenderProcessResponsive(uri)) ?? + (await _inAppBrowserEventHandler! + .androidOnRenderProcessResponsive(uri))) + ?.toNativeValue(); + } + } + break; + case "onRenderProcessGone": + if ((webviewParams != null && + (webviewParams!.onRenderProcessGone != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnRenderProcessGone != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + RenderProcessGoneDetail detail = + RenderProcessGoneDetail.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.onRenderProcessGone != null) + webviewParams!.onRenderProcessGone!( + _controllerFromPlatform, detail); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnRenderProcessGone!( + _controllerFromPlatform, detail); + } + } else if (_inAppBrowserEventHandler != null) { + _inAppBrowserEventHandler!.onRenderProcessGone(detail); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler!.androidOnRenderProcessGone(detail); + } + } + break; + case "onFormResubmission": + if ((webviewParams != null && + (webviewParams!.onFormResubmission != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnFormResubmission != null)) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + WebUri? uri = url != null ? WebUri(url) : null; + + if (webviewParams != null) { + if (webviewParams!.onFormResubmission != null) + return (await webviewParams!.onFormResubmission!( + _controllerFromPlatform, uri)) + ?.toNativeValue(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.androidOnFormResubmission!( + _controllerFromPlatform, uri)) + ?.toNativeValue(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onFormResubmission(uri)) ?? + // ignore: deprecated_member_use_from_same_package + (await _inAppBrowserEventHandler! + .androidOnFormResubmission(uri))) + ?.toNativeValue(); + } + } + break; + case "onZoomScaleChanged": + if ((webviewParams != null && + // ignore: deprecated_member_use_from_same_package + (webviewParams!.androidOnScaleChanged != null || + webviewParams!.onZoomScaleChanged != null)) || + _inAppBrowserEventHandler != null) { + double oldScale = call.arguments["oldScale"]; + double newScale = call.arguments["newScale"]; + + if (webviewParams != null) { + if (webviewParams!.onZoomScaleChanged != null) + webviewParams!.onZoomScaleChanged!( + _controllerFromPlatform, oldScale, newScale); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnScaleChanged!( + _controllerFromPlatform, oldScale, newScale); + } + } else { + _inAppBrowserEventHandler!.onZoomScaleChanged(oldScale, newScale); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler! + .androidOnScaleChanged(oldScale, newScale); + } + } + break; + case "onReceivedIcon": + if ((webviewParams != null && + (webviewParams!.onReceivedIcon != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnReceivedIcon != null)) || + _inAppBrowserEventHandler != null) { + Uint8List icon = + Uint8List.fromList(call.arguments["icon"].cast()); + + if (webviewParams != null) { + if (webviewParams!.onReceivedIcon != null) + webviewParams!.onReceivedIcon!(_controllerFromPlatform, icon); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnReceivedIcon!( + _controllerFromPlatform, icon); + } + } else { + _inAppBrowserEventHandler!.onReceivedIcon(icon); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler!.androidOnReceivedIcon(icon); + } + } + break; + case "onReceivedTouchIconUrl": + if ((webviewParams != null && + (webviewParams!.onReceivedTouchIconUrl != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnReceivedTouchIconUrl != null)) || + _inAppBrowserEventHandler != null) { + String url = call.arguments["url"]; + bool precomposed = call.arguments["precomposed"]; + WebUri uri = WebUri(url); + + if (webviewParams != null) { + if (webviewParams!.onReceivedTouchIconUrl != null) + webviewParams!.onReceivedTouchIconUrl!( + _controllerFromPlatform, uri, precomposed); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnReceivedTouchIconUrl!( + _controllerFromPlatform, uri, precomposed); + } + } else { + _inAppBrowserEventHandler!.onReceivedTouchIconUrl(uri, precomposed); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler! + .androidOnReceivedTouchIconUrl(uri, precomposed); + } + } + break; + case "onJsAlert": + if ((webviewParams != null && webviewParams!.onJsAlert != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + JsAlertRequest jsAlertRequest = JsAlertRequest.fromMap(arguments)!; + + if (webviewParams != null && webviewParams!.onJsAlert != null) + return (await webviewParams!.onJsAlert!( + _controllerFromPlatform, jsAlertRequest)) + ?.toMap(); + else + return (await _inAppBrowserEventHandler!.onJsAlert(jsAlertRequest)) + ?.toMap(); + } + break; + case "onJsConfirm": + if ((webviewParams != null && webviewParams!.onJsConfirm != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + JsConfirmRequest jsConfirmRequest = + JsConfirmRequest.fromMap(arguments)!; + + if (webviewParams != null && webviewParams!.onJsConfirm != null) + return (await webviewParams!.onJsConfirm!( + _controllerFromPlatform, jsConfirmRequest)) + ?.toMap(); + else + return (await _inAppBrowserEventHandler! + .onJsConfirm(jsConfirmRequest)) + ?.toMap(); + } + break; + case "onJsPrompt": + if ((webviewParams != null && webviewParams!.onJsPrompt != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + JsPromptRequest jsPromptRequest = JsPromptRequest.fromMap(arguments)!; + + if (webviewParams != null && webviewParams!.onJsPrompt != null) + return (await webviewParams!.onJsPrompt!( + _controllerFromPlatform, jsPromptRequest)) + ?.toMap(); + else + return (await _inAppBrowserEventHandler! + .onJsPrompt(jsPromptRequest)) + ?.toMap(); + } + break; + case "onJsBeforeUnload": + if ((webviewParams != null && + (webviewParams!.onJsBeforeUnload != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnJsBeforeUnload != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + JsBeforeUnloadRequest jsBeforeUnloadRequest = + JsBeforeUnloadRequest.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.onJsBeforeUnload != null) + return (await webviewParams!.onJsBeforeUnload!( + _controllerFromPlatform, jsBeforeUnloadRequest)) + ?.toMap(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.androidOnJsBeforeUnload!( + _controllerFromPlatform, jsBeforeUnloadRequest)) + ?.toMap(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onJsBeforeUnload(jsBeforeUnloadRequest)) ?? + (await _inAppBrowserEventHandler! + .androidOnJsBeforeUnload(jsBeforeUnloadRequest))) + ?.toMap(); + } + } + break; + case "onSafeBrowsingHit": + if ((webviewParams != null && + (webviewParams!.onSafeBrowsingHit != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnSafeBrowsingHit != null)) || + _inAppBrowserEventHandler != null) { + String url = call.arguments["url"]; + SafeBrowsingThreat? threatType = + SafeBrowsingThreat.fromNativeValue(call.arguments["threatType"]); + WebUri uri = WebUri(url); + + if (webviewParams != null) { + if (webviewParams!.onSafeBrowsingHit != null) + return (await webviewParams!.onSafeBrowsingHit!( + _controllerFromPlatform, uri, threatType)) + ?.toMap(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.androidOnSafeBrowsingHit!( + _controllerFromPlatform, uri, threatType)) + ?.toMap(); + } + } else { + return ((await _inAppBrowserEventHandler! + .onSafeBrowsingHit(uri, threatType)) ?? + (await _inAppBrowserEventHandler! + .androidOnSafeBrowsingHit(uri, threatType))) + ?.toMap(); + } + } + break; + case "onReceivedLoginRequest": + if ((webviewParams != null && + (webviewParams!.onReceivedLoginRequest != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnReceivedLoginRequest != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + LoginRequest loginRequest = LoginRequest.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.onReceivedLoginRequest != null) + webviewParams!.onReceivedLoginRequest!( + _controllerFromPlatform, loginRequest); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnReceivedLoginRequest!( + _controllerFromPlatform, loginRequest); + } + } else { + _inAppBrowserEventHandler!.onReceivedLoginRequest(loginRequest); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler! + .androidOnReceivedLoginRequest(loginRequest); + } + } + break; + case "onPermissionRequestCanceled": + if ((webviewParams != null && + webviewParams!.onPermissionRequestCanceled != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + PermissionRequest permissionRequest = + PermissionRequest.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onPermissionRequestCanceled != null) + webviewParams!.onPermissionRequestCanceled!( + _controllerFromPlatform, permissionRequest); + else + _inAppBrowserEventHandler! + .onPermissionRequestCanceled(permissionRequest); + } + break; + case "onRequestFocus": + if ((webviewParams != null && webviewParams!.onRequestFocus != null) || + _inAppBrowserEventHandler != null) { + if (webviewParams != null && webviewParams!.onRequestFocus != null) + webviewParams!.onRequestFocus!(_controllerFromPlatform); + else + _inAppBrowserEventHandler!.onRequestFocus(); + } + break; + case "onReceivedHttpAuthRequest": + if ((webviewParams != null && + webviewParams!.onReceivedHttpAuthRequest != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + HttpAuthenticationChallenge challenge = + HttpAuthenticationChallenge.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onReceivedHttpAuthRequest != null) + return (await webviewParams!.onReceivedHttpAuthRequest!( + _controllerFromPlatform, challenge)) + ?.toMap(); + else + return (await _inAppBrowserEventHandler! + .onReceivedHttpAuthRequest(challenge)) + ?.toMap(); + } + break; + case "onReceivedServerTrustAuthRequest": + if ((webviewParams != null && + webviewParams!.onReceivedServerTrustAuthRequest != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + ServerTrustChallenge challenge = + ServerTrustChallenge.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onReceivedServerTrustAuthRequest != null) + return (await webviewParams!.onReceivedServerTrustAuthRequest!( + _controllerFromPlatform, challenge)) + ?.toMap(); + else + return (await _inAppBrowserEventHandler! + .onReceivedServerTrustAuthRequest(challenge)) + ?.toMap(); + } + break; + case "onReceivedClientCertRequest": + if ((webviewParams != null && + webviewParams!.onReceivedClientCertRequest != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + ClientCertChallenge challenge = + ClientCertChallenge.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onReceivedClientCertRequest != null) + return (await webviewParams!.onReceivedClientCertRequest!( + _controllerFromPlatform, challenge)) + ?.toMap(); + else + return (await _inAppBrowserEventHandler! + .onReceivedClientCertRequest(challenge)) + ?.toMap(); + } + break; + case "onFindResultReceived": + if ((webviewParams != null && + (webviewParams!.onFindResultReceived != null || + (webviewParams!.findInteractionController != null && + webviewParams!.findInteractionController!.params + .onFindResultReceived != + null))) || + _inAppBrowserEventHandler != null) { + int activeMatchOrdinal = call.arguments["activeMatchOrdinal"]; + int numberOfMatches = call.arguments["numberOfMatches"]; + bool isDoneCounting = call.arguments["isDoneCounting"]; + if (webviewParams != null) { + if (webviewParams!.findInteractionController != null && + webviewParams!.findInteractionController!.params + .onFindResultReceived != + null) + webviewParams! + .findInteractionController!.params.onFindResultReceived!( + webviewParams!.findInteractionController!, + activeMatchOrdinal, + numberOfMatches, + isDoneCounting); + else + webviewParams!.onFindResultReceived!(_controllerFromPlatform, + activeMatchOrdinal, numberOfMatches, isDoneCounting); + } else { + if (_inAppBrowser!.findInteractionController != null && + _inAppBrowser! + .findInteractionController!.onFindResultReceived != + null) + _inAppBrowser!.findInteractionController!.onFindResultReceived!( + webviewParams!.findInteractionController!, + activeMatchOrdinal, + numberOfMatches, + isDoneCounting); + else + _inAppBrowserEventHandler!.onFindResultReceived( + activeMatchOrdinal, numberOfMatches, isDoneCounting); + } + } + break; + case "onPermissionRequest": + if ((webviewParams != null && + (webviewParams!.onPermissionRequest != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.androidOnPermissionRequest != null)) || + _inAppBrowserEventHandler != null) { + String origin = call.arguments["origin"]; + List resources = call.arguments["resources"].cast(); + + Map arguments = + call.arguments.cast(); + PermissionRequest permissionRequest = + PermissionRequest.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.onPermissionRequest != null) + return (await webviewParams!.onPermissionRequest!( + _controllerFromPlatform, permissionRequest)) + ?.toMap(); + else { + return (await webviewParams!.androidOnPermissionRequest!( + _controllerFromPlatform, origin, resources)) + ?.toMap(); + } + } else { + return (await _inAppBrowserEventHandler! + .onPermissionRequest(permissionRequest)) + ?.toMap() ?? + (await _inAppBrowserEventHandler! + .androidOnPermissionRequest(origin, resources)) + ?.toMap(); + } + } + break; + case "onUpdateVisitedHistory": + if ((webviewParams != null && + webviewParams!.onUpdateVisitedHistory != null) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + bool? isReload = call.arguments["isReload"]; + WebUri? uri = url != null ? WebUri(url) : null; + if (webviewParams != null && + webviewParams!.onUpdateVisitedHistory != null) + webviewParams!.onUpdateVisitedHistory!( + _controllerFromPlatform, uri, isReload); + else + _inAppBrowserEventHandler!.onUpdateVisitedHistory(uri, isReload); + } + break; + case "onWebContentProcessDidTerminate": + if (webviewParams != null && + (webviewParams!.onWebContentProcessDidTerminate != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.iosOnWebContentProcessDidTerminate != null)) { + if (webviewParams!.onWebContentProcessDidTerminate != null) + webviewParams! + .onWebContentProcessDidTerminate!(_controllerFromPlatform); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams! + .iosOnWebContentProcessDidTerminate!(_controllerFromPlatform); + } + } else if (_inAppBrowserEventHandler != null) { + _inAppBrowserEventHandler!.onWebContentProcessDidTerminate(); + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler!.iosOnWebContentProcessDidTerminate(); + } + break; + case "onPageCommitVisible": + if ((webviewParams != null && + webviewParams!.onPageCommitVisible != null) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + WebUri? uri = url != null ? WebUri(url) : null; + if (webviewParams != null && + webviewParams!.onPageCommitVisible != null) + webviewParams!.onPageCommitVisible!(_controllerFromPlatform, uri); + else + _inAppBrowserEventHandler!.onPageCommitVisible(uri); + } + break; + case "onDidReceiveServerRedirectForProvisionalNavigation": + if (webviewParams != null && + (webviewParams! + .onDidReceiveServerRedirectForProvisionalNavigation != + null || + params + .webviewParams! + // ignore: deprecated_member_use_from_same_package + .iosOnDidReceiveServerRedirectForProvisionalNavigation != + null)) { + if (webviewParams! + .onDidReceiveServerRedirectForProvisionalNavigation != + null) + webviewParams!.onDidReceiveServerRedirectForProvisionalNavigation!( + _controllerFromPlatform); + else { + params + .webviewParams! + // ignore: deprecated_member_use_from_same_package + .iosOnDidReceiveServerRedirectForProvisionalNavigation!( + _controllerFromPlatform); + } + } else if (_inAppBrowserEventHandler != null) { + _inAppBrowserEventHandler! + .onDidReceiveServerRedirectForProvisionalNavigation(); + _inAppBrowserEventHandler! + .iosOnDidReceiveServerRedirectForProvisionalNavigation(); + } + break; + case "onNavigationResponse": + if ((webviewParams != null && + (webviewParams!.onNavigationResponse != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.iosOnNavigationResponse != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + // ignore: deprecated_member_use_from_same_package + IOSWKNavigationResponse iosOnNavigationResponse = + // ignore: deprecated_member_use_from_same_package + IOSWKNavigationResponse.fromMap(arguments)!; + + NavigationResponse navigationResponse = + NavigationResponse.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.onNavigationResponse != null) + return (await webviewParams!.onNavigationResponse!( + _controllerFromPlatform, navigationResponse)) + ?.toNativeValue(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.iosOnNavigationResponse!( + _controllerFromPlatform, iosOnNavigationResponse)) + ?.toNativeValue(); + } + } else { + return (await _inAppBrowserEventHandler! + .onNavigationResponse(navigationResponse)) + ?.toNativeValue() ?? + (await _inAppBrowserEventHandler! + .iosOnNavigationResponse(iosOnNavigationResponse)) + ?.toNativeValue(); + } + } + break; + case "shouldAllowDeprecatedTLS": + if ((webviewParams != null && + (webviewParams!.shouldAllowDeprecatedTLS != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.iosShouldAllowDeprecatedTLS != null)) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + URLAuthenticationChallenge challenge = + URLAuthenticationChallenge.fromMap(arguments)!; + + if (webviewParams != null) { + if (webviewParams!.shouldAllowDeprecatedTLS != null) + return (await webviewParams!.shouldAllowDeprecatedTLS!( + _controllerFromPlatform, challenge)) + ?.toNativeValue(); + else { + // ignore: deprecated_member_use_from_same_package + return (await webviewParams!.iosShouldAllowDeprecatedTLS!( + _controllerFromPlatform, challenge)) + ?.toNativeValue(); + } + } else { + return (await _inAppBrowserEventHandler! + .shouldAllowDeprecatedTLS(challenge)) + ?.toNativeValue() ?? + // ignore: deprecated_member_use_from_same_package + (await _inAppBrowserEventHandler! + .iosShouldAllowDeprecatedTLS(challenge)) + ?.toNativeValue(); + } + } + break; + case "onLongPressHitTestResult": + if ((webviewParams != null && + webviewParams!.onLongPressHitTestResult != null) || + _inAppBrowserEventHandler != null) { + Map arguments = + call.arguments.cast(); + InAppWebViewHitTestResult hitTestResult = + InAppWebViewHitTestResult.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onLongPressHitTestResult != null) + webviewParams!.onLongPressHitTestResult!( + _controllerFromPlatform, hitTestResult); + else + _inAppBrowserEventHandler!.onLongPressHitTestResult(hitTestResult); + } + break; + case "onCreateContextMenu": + ContextMenu? contextMenu; + if (webviewParams != null && webviewParams!.contextMenu != null) { + contextMenu = webviewParams!.contextMenu; + } else if (_inAppBrowserEventHandler != null && + _inAppBrowser!.contextMenu != null) { + contextMenu = _inAppBrowser!.contextMenu; + } + + if (contextMenu != null && contextMenu.onCreateContextMenu != null) { + Map arguments = + call.arguments.cast(); + InAppWebViewHitTestResult hitTestResult = + InAppWebViewHitTestResult.fromMap(arguments)!; + + contextMenu.onCreateContextMenu!(hitTestResult); + } + break; + case "onHideContextMenu": + ContextMenu? contextMenu; + if (webviewParams != null && webviewParams!.contextMenu != null) { + contextMenu = webviewParams!.contextMenu; + } else if (_inAppBrowserEventHandler != null && + _inAppBrowser!.contextMenu != null) { + contextMenu = _inAppBrowser!.contextMenu; + } + + if (contextMenu != null && contextMenu.onHideContextMenu != null) { + contextMenu.onHideContextMenu!(); + } + break; + case "onContextMenuActionItemClicked": + ContextMenu? contextMenu; + if (webviewParams != null && webviewParams!.contextMenu != null) { + contextMenu = webviewParams!.contextMenu; + } else if (_inAppBrowserEventHandler != null && + _inAppBrowser!.contextMenu != null) { + contextMenu = _inAppBrowser!.contextMenu; + } + + if (contextMenu != null) { + int? androidId = call.arguments["androidId"]; + String? iosId = call.arguments["iosId"]; + dynamic id = call.arguments["id"]; + String title = call.arguments["title"]; + + ContextMenuItem menuItemClicked = ContextMenuItem( + id: id, + // ignore: deprecated_member_use_from_same_package + androidId: androidId, + // ignore: deprecated_member_use_from_same_package + iosId: iosId, + title: title, + action: null); + + for (var menuItem in contextMenu.menuItems) { + if (menuItem.id == id) { + menuItemClicked = menuItem; + if (menuItem.action != null) { + menuItem.action!(); + } + break; + } + } + + if (contextMenu.onContextMenuActionItemClicked != null) { + contextMenu.onContextMenuActionItemClicked!(menuItemClicked); + } + } + break; + case "onEnterFullscreen": + if (webviewParams != null && webviewParams!.onEnterFullscreen != null) + webviewParams!.onEnterFullscreen!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onEnterFullscreen(); + break; + case "onExitFullscreen": + if (webviewParams != null && webviewParams!.onExitFullscreen != null) + webviewParams!.onExitFullscreen!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onExitFullscreen(); + break; + case "onOverScrolled": + if ((webviewParams != null && webviewParams!.onOverScrolled != null) || + _inAppBrowserEventHandler != null) { + int x = call.arguments["x"]; + int y = call.arguments["y"]; + bool clampedX = call.arguments["clampedX"]; + bool clampedY = call.arguments["clampedY"]; + + if (webviewParams != null && webviewParams!.onOverScrolled != null) + webviewParams!.onOverScrolled!( + _controllerFromPlatform, x, y, clampedX, clampedY); + else + _inAppBrowserEventHandler!.onOverScrolled(x, y, clampedX, clampedY); + } + break; + case "onWindowFocus": + if (webviewParams != null && webviewParams!.onWindowFocus != null) + webviewParams!.onWindowFocus!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onWindowFocus(); + break; + case "onWindowBlur": + if (webviewParams != null && webviewParams!.onWindowBlur != null) + webviewParams!.onWindowBlur!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onWindowBlur(); + break; + case "onPrintRequest": + if ((webviewParams != null && + (webviewParams!.onPrintRequest != null || + // ignore: deprecated_member_use_from_same_package + webviewParams!.onPrint != null)) || + _inAppBrowserEventHandler != null) { + String? url = call.arguments["url"]; + String? printJobId = call.arguments["printJobId"]; + WebUri? uri = url != null ? WebUri(url) : null; + MacOSPrintJobController? printJob = printJobId != null + ? MacOSPrintJobController( + MacOSPrintJobControllerCreationParams(id: printJobId)) + : null; + + if (webviewParams != null) { + if (webviewParams!.onPrintRequest != null) + return await webviewParams!.onPrintRequest!( + _controllerFromPlatform, uri, printJob); + else { + // ignore: deprecated_member_use_from_same_package + webviewParams!.onPrint!(_controllerFromPlatform, uri); + return false; + } + } else { + // ignore: deprecated_member_use_from_same_package + _inAppBrowserEventHandler!.onPrint(uri); + return await _inAppBrowserEventHandler! + .onPrintRequest(uri, printJob); + } + } + break; + case "onInjectedScriptLoaded": + String id = call.arguments[0]; + var onLoadCallback = _injectedScriptsFromURL[id]?.onLoad; + if ((webviewParams != null || _inAppBrowserEventHandler != null) && + onLoadCallback != null) { + onLoadCallback(); + } + break; + case "onInjectedScriptError": + String id = call.arguments[0]; + var onErrorCallback = _injectedScriptsFromURL[id]?.onError; + if ((webviewParams != null || _inAppBrowserEventHandler != null) && + onErrorCallback != null) { + onErrorCallback(); + } + break; + case "onCameraCaptureStateChanged": + if ((webviewParams != null && + webviewParams!.onCameraCaptureStateChanged != null) || + _inAppBrowserEventHandler != null) { + var oldState = + MediaCaptureState.fromNativeValue(call.arguments["oldState"]); + var newState = + MediaCaptureState.fromNativeValue(call.arguments["newState"]); + + if (webviewParams != null && + webviewParams!.onCameraCaptureStateChanged != null) + webviewParams!.onCameraCaptureStateChanged!( + _controllerFromPlatform, oldState, newState); + else + _inAppBrowserEventHandler! + .onCameraCaptureStateChanged(oldState, newState); + } + break; + case "onMicrophoneCaptureStateChanged": + if ((webviewParams != null && + webviewParams!.onMicrophoneCaptureStateChanged != null) || + _inAppBrowserEventHandler != null) { + var oldState = + MediaCaptureState.fromNativeValue(call.arguments["oldState"]); + var newState = + MediaCaptureState.fromNativeValue(call.arguments["newState"]); + + if (webviewParams != null && + webviewParams!.onMicrophoneCaptureStateChanged != null) + webviewParams!.onMicrophoneCaptureStateChanged!( + _controllerFromPlatform, oldState, newState); + else + _inAppBrowserEventHandler! + .onMicrophoneCaptureStateChanged(oldState, newState); + } + break; + case "onContentSizeChanged": + if ((webviewParams != null && + webviewParams!.onContentSizeChanged != null) || + _inAppBrowserEventHandler != null) { + var oldContentSize = MapSize.fromMap( + call.arguments["oldContentSize"]?.cast())!; + var newContentSize = MapSize.fromMap( + call.arguments["newContentSize"]?.cast())!; + + if (webviewParams != null && + webviewParams!.onContentSizeChanged != null) + webviewParams!.onContentSizeChanged!( + _controllerFromPlatform, oldContentSize, newContentSize); + else + _inAppBrowserEventHandler! + .onContentSizeChanged(oldContentSize, newContentSize); + } + break; + case "onCallJsHandler": + String handlerName = call.arguments["handlerName"]; + // decode args to json + List args = jsonDecode(call.arguments["args"]); + + _debugLog(handlerName, args); + + switch (handlerName) { + case "onLoadResource": + if ((webviewParams != null && + webviewParams!.onLoadResource != null) || + _inAppBrowserEventHandler != null) { + Map arguments = args[0].cast(); + arguments["startTime"] = arguments["startTime"] is int + ? arguments["startTime"].toDouble() + : arguments["startTime"]; + arguments["duration"] = arguments["duration"] is int + ? arguments["duration"].toDouble() + : arguments["duration"]; + + var response = LoadedResource.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onLoadResource != null) + webviewParams!.onLoadResource!( + _controllerFromPlatform, response); + else + _inAppBrowserEventHandler!.onLoadResource(response); + } + return null; + case "shouldInterceptAjaxRequest": + if ((webviewParams != null && + webviewParams!.shouldInterceptAjaxRequest != null) || + _inAppBrowserEventHandler != null) { + Map arguments = args[0].cast(); + AjaxRequest request = AjaxRequest.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.shouldInterceptAjaxRequest != null) + return jsonEncode( + await params.webviewParams!.shouldInterceptAjaxRequest!( + _controllerFromPlatform, request)); + else + return jsonEncode(await _inAppBrowserEventHandler! + .shouldInterceptAjaxRequest(request)); + } + return null; + case "onAjaxReadyStateChange": + if ((webviewParams != null && + webviewParams!.onAjaxReadyStateChange != null) || + _inAppBrowserEventHandler != null) { + Map arguments = args[0].cast(); + AjaxRequest request = AjaxRequest.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onAjaxReadyStateChange != null) + return (await webviewParams!.onAjaxReadyStateChange!( + _controllerFromPlatform, request)) + ?.toNativeValue(); + else + return (await _inAppBrowserEventHandler! + .onAjaxReadyStateChange(request)) + ?.toNativeValue(); + } + return null; + case "onAjaxProgress": + if ((webviewParams != null && + webviewParams!.onAjaxProgress != null) || + _inAppBrowserEventHandler != null) { + Map arguments = args[0].cast(); + AjaxRequest request = AjaxRequest.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.onAjaxProgress != null) + return (await webviewParams!.onAjaxProgress!( + _controllerFromPlatform, request)) + ?.toNativeValue(); + else + return (await _inAppBrowserEventHandler! + .onAjaxProgress(request)) + ?.toNativeValue(); + } + return null; + case "shouldInterceptFetchRequest": + if ((webviewParams != null && + webviewParams!.shouldInterceptFetchRequest != null) || + _inAppBrowserEventHandler != null) { + Map arguments = args[0].cast(); + FetchRequest request = FetchRequest.fromMap(arguments)!; + + if (webviewParams != null && + webviewParams!.shouldInterceptFetchRequest != null) + return jsonEncode( + await webviewParams!.shouldInterceptFetchRequest!( + _controllerFromPlatform, request)); + else + return jsonEncode(await _inAppBrowserEventHandler! + .shouldInterceptFetchRequest(request)); + } + return null; + case "onWindowFocus": + if (webviewParams != null && webviewParams!.onWindowFocus != null) + webviewParams!.onWindowFocus!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onWindowFocus(); + return null; + case "onWindowBlur": + if (webviewParams != null && webviewParams!.onWindowBlur != null) + webviewParams!.onWindowBlur!(_controllerFromPlatform); + else if (_inAppBrowserEventHandler != null) + _inAppBrowserEventHandler!.onWindowBlur(); + return null; + case "onInjectedScriptLoaded": + String id = args[0]; + var onLoadCallback = _injectedScriptsFromURL[id]?.onLoad; + if ((webviewParams != null || _inAppBrowserEventHandler != null) && + onLoadCallback != null) { + onLoadCallback(); + } + return null; + case "onInjectedScriptError": + String id = args[0]; + var onErrorCallback = _injectedScriptsFromURL[id]?.onError; + if ((webviewParams != null || _inAppBrowserEventHandler != null) && + onErrorCallback != null) { + onErrorCallback(); + } + return null; + } + + if (_javaScriptHandlersMap.containsKey(handlerName)) { + // convert result to json + try { + return jsonEncode(await _javaScriptHandlersMap[handlerName]!(args)); + } catch (error, stacktrace) { + developer.log(error.toString() + '\n' + stacktrace.toString(), + name: 'JavaScript Handler "$handlerName"'); + throw Exception(error.toString().replaceFirst('Exception: ', '')); + } + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + @override + Future getUrl() async { + Map args = {}; + String? url = await channel?.invokeMethod('getUrl', args); + return url != null ? WebUri(url) : null; + } + + @override + Future getTitle() async { + Map args = {}; + return await channel?.invokeMethod('getTitle', args); + } + + @override + Future getProgress() async { + Map args = {}; + return await channel?.invokeMethod('getProgress', args); + } + + @override + Future getHtml() async { + String? html; + + InAppWebViewSettings? settings = await getSettings(); + if (settings != null && settings.javaScriptEnabled == true) { + html = await evaluateJavascript( + source: "window.document.getElementsByTagName('html')[0].outerHTML;"); + if (html != null && html.isNotEmpty) return html; + } + + var webviewUrl = await getUrl(); + if (webviewUrl == null) { + return html; + } + + if (webviewUrl.isScheme("file")) { + var assetPathSplit = webviewUrl.toString().split("/flutter_assets/"); + var assetPath = assetPathSplit[assetPathSplit.length - 1]; + try { + var bytes = await rootBundle.load(assetPath); + html = utf8.decode(bytes.buffer.asUint8List()); + } catch (e) {} + } else { + try { + HttpClient client = HttpClient(); + var htmlRequest = await client.getUrl(webviewUrl); + html = + await (await htmlRequest.close()).transform(Utf8Decoder()).join(); + } catch (e) { + developer.log(e.toString(), name: this.runtimeType.toString()); + } + } + + return html; + } + + @override + Future> getFavicons() async { + List favicons = []; + + var webviewUrl = await getUrl(); + + if (webviewUrl == null) { + return favicons; + } + + String? manifestUrl; + + var html = await getHtml(); + if (html == null || html.isEmpty) { + return favicons; + } + var assetPathBase; + + if (webviewUrl.isScheme("file")) { + var assetPathSplit = webviewUrl.toString().split("/flutter_assets/"); + assetPathBase = assetPathSplit[0] + "/flutter_assets/"; + } + + InAppWebViewSettings? settings = await getSettings(); + if (settings != null && settings.javaScriptEnabled == true) { + List> links = (await evaluateJavascript(source: """ +(function() { + var linkNodes = document.head.getElementsByTagName("link"); + var links = []; + for (var i = 0; i < linkNodes.length; i++) { + var linkNode = linkNodes[i]; + if (linkNode.rel === 'manifest') { + links.push( + { + rel: linkNode.rel, + href: linkNode.href, + sizes: null + } + ); + } else if (linkNode.rel != null && linkNode.rel.indexOf('icon') >= 0) { + links.push( + { + rel: linkNode.rel, + href: linkNode.href, + sizes: linkNode.sizes != null && linkNode.sizes.value != "" ? linkNode.sizes.value : null + } + ); + } + } + return links; +})(); +"""))?.cast>() ?? []; + for (var link in links) { + if (link["rel"] == "manifest") { + manifestUrl = link["href"]; + if (!_isUrlAbsolute(manifestUrl!)) { + if (manifestUrl.startsWith("/")) { + manifestUrl = manifestUrl.substring(1); + } + manifestUrl = ((assetPathBase == null) + ? webviewUrl.scheme + "://" + webviewUrl.host + "/" + : assetPathBase) + + manifestUrl; + } + continue; + } + favicons.addAll(_createFavicons(webviewUrl, assetPathBase, link["href"], + link["rel"], link["sizes"], false)); + } + } + + // try to get /favicon.ico + try { + HttpClient client = HttpClient(); + var faviconUrl = + webviewUrl.scheme + "://" + webviewUrl.host + "/favicon.ico"; + var faviconUri = WebUri(faviconUrl); + var headRequest = await client.headUrl(faviconUri); + var headResponse = await headRequest.close(); + if (headResponse.statusCode == 200) { + favicons.add(Favicon(url: faviconUri, rel: "shortcut icon")); + } + } catch (e) { + developer.log("/favicon.ico file not found: " + e.toString(), + name: runtimeType.toString()); + } + + // try to get the manifest file + HttpClientRequest? manifestRequest; + HttpClientResponse? manifestResponse; + bool manifestFound = false; + if (manifestUrl == null) { + manifestUrl = + webviewUrl.scheme + "://" + webviewUrl.host + "/manifest.json"; + } + try { + HttpClient client = HttpClient(); + manifestRequest = await client.getUrl(Uri.parse(manifestUrl)); + manifestResponse = await manifestRequest.close(); + manifestFound = manifestResponse.statusCode == 200 && + manifestResponse.headers.contentType?.mimeType == "application/json"; + } catch (e) { + developer.log("Manifest file not found: " + e.toString(), + name: this.runtimeType.toString()); + } + + if (manifestFound) { + try { + Map manifest = json + .decode(await manifestResponse!.transform(Utf8Decoder()).join()); + if (manifest.containsKey("icons")) { + for (Map icon in manifest["icons"]) { + favicons.addAll(_createFavicons(webviewUrl, assetPathBase, + icon["src"], icon["rel"], icon["sizes"], true)); + } + } + } catch (e) { + developer.log( + "Cannot get favicons from Manifest file. It might not have a valid format: " + + e.toString(), + error: e, + name: runtimeType.toString()); + } + } + + return favicons; + } + + bool _isUrlAbsolute(String url) { + return url.startsWith("http://") || url.startsWith("https://"); + } + + List _createFavicons(WebUri url, String? assetPathBase, + String urlIcon, String? rel, String? sizes, bool isManifest) { + List favicons = []; + + List urlSplit = urlIcon.split("/"); + if (!_isUrlAbsolute(urlIcon)) { + if (urlIcon.startsWith("/")) { + urlIcon = urlIcon.substring(1); + } + urlIcon = ((assetPathBase == null) + ? url.scheme + "://" + url.host + "/" + : assetPathBase) + + urlIcon; + } + if (isManifest) { + rel = (sizes != null) + ? urlSplit[urlSplit.length - 1] + .replaceFirst("-" + sizes, "") + .split(" ")[0] + .split(".")[0] + : null; + } + if (sizes != null && sizes.isNotEmpty && sizes != "any") { + List sizesSplit = sizes.split(" "); + for (String size in sizesSplit) { + int width = int.parse(size.split("x")[0]); + int height = int.parse(size.split("x")[1]); + favicons.add(Favicon( + url: WebUri(urlIcon), rel: rel, width: width, height: height)); + } + } else { + favicons.add( + Favicon(url: WebUri(urlIcon), rel: rel, width: null, height: null)); + } + + return favicons; + } + + @override + Future loadUrl( + {required URLRequest urlRequest, + @Deprecated('Use allowingReadAccessTo instead') + Uri? iosAllowingReadAccessTo, + WebUri? allowingReadAccessTo}) async { + assert(urlRequest.url != null && urlRequest.url.toString().isNotEmpty); + assert( + allowingReadAccessTo == null || allowingReadAccessTo.isScheme("file")); + assert(iosAllowingReadAccessTo == null || + iosAllowingReadAccessTo.isScheme("file")); + + Map args = {}; + args.putIfAbsent('urlRequest', () => urlRequest.toMap()); + args.putIfAbsent( + 'allowingReadAccessTo', + () => + allowingReadAccessTo?.toString() ?? + iosAllowingReadAccessTo?.toString()); + await channel?.invokeMethod('loadUrl', args); + } + + @override + Future postUrl( + {required WebUri url, required Uint8List postData}) async { + assert(url.toString().isNotEmpty); + Map args = {}; + args.putIfAbsent('url', () => url.toString()); + args.putIfAbsent('postData', () => postData); + await channel?.invokeMethod('postUrl', args); + } + + @override + Future loadData( + {required String data, + String mimeType = "text/html", + String encoding = "utf8", + WebUri? baseUrl, + @Deprecated('Use historyUrl instead') Uri? androidHistoryUrl, + WebUri? historyUrl, + @Deprecated('Use allowingReadAccessTo instead') + Uri? iosAllowingReadAccessTo, + WebUri? allowingReadAccessTo}) async { + assert( + allowingReadAccessTo == null || allowingReadAccessTo.isScheme("file")); + assert(iosAllowingReadAccessTo == null || + iosAllowingReadAccessTo.isScheme("file")); + + Map args = {}; + args.putIfAbsent('data', () => data); + args.putIfAbsent('mimeType', () => mimeType); + args.putIfAbsent('encoding', () => encoding); + args.putIfAbsent('baseUrl', () => baseUrl?.toString() ?? "about:blank"); + args.putIfAbsent( + 'historyUrl', + () => + historyUrl?.toString() ?? + androidHistoryUrl?.toString() ?? + "about:blank"); + args.putIfAbsent( + 'allowingReadAccessTo', + () => + allowingReadAccessTo?.toString() ?? + iosAllowingReadAccessTo?.toString()); + await channel?.invokeMethod('loadData', args); + } + + @override + Future loadFile({required String assetFilePath}) async { + assert(assetFilePath.isNotEmpty); + Map args = {}; + args.putIfAbsent('assetFilePath', () => assetFilePath); + await channel?.invokeMethod('loadFile', args); + } + + @override + Future reload() async { + Map args = {}; + await channel?.invokeMethod('reload', args); + } + + @override + Future goBack() async { + Map args = {}; + await channel?.invokeMethod('goBack', args); + } + + @override + Future canGoBack() async { + Map args = {}; + return await channel?.invokeMethod('canGoBack', args) ?? false; + } + + @override + Future goForward() async { + Map args = {}; + await channel?.invokeMethod('goForward', args); + } + + @override + Future canGoForward() async { + Map args = {}; + return await channel?.invokeMethod('canGoForward', args) ?? false; + } + + @override + Future goBackOrForward({required int steps}) async { + Map args = {}; + args.putIfAbsent('steps', () => steps); + await channel?.invokeMethod('goBackOrForward', args); + } + + @override + Future canGoBackOrForward({required int steps}) async { + Map args = {}; + args.putIfAbsent('steps', () => steps); + return await channel?.invokeMethod('canGoBackOrForward', args) ?? + false; + } + + @override + Future goTo({required WebHistoryItem historyItem}) async { + var steps = historyItem.offset; + if (steps != null) { + await goBackOrForward(steps: steps); + } + } + + @override + Future isLoading() async { + Map args = {}; + return await channel?.invokeMethod('isLoading', args) ?? false; + } + + @override + Future stopLoading() async { + Map args = {}; + await channel?.invokeMethod('stopLoading', args); + } + + @override + Future evaluateJavascript( + {required String source, ContentWorld? contentWorld}) async { + Map args = {}; + args.putIfAbsent('source', () => source); + args.putIfAbsent('contentWorld', () => contentWorld?.toMap()); + var data = await channel?.invokeMethod('evaluateJavascript', args); + return data; + } + + @override + Future injectJavascriptFileFromUrl( + {required WebUri urlFile, + ScriptHtmlTagAttributes? scriptHtmlTagAttributes}) async { + assert(urlFile.toString().isNotEmpty); + var id = scriptHtmlTagAttributes?.id; + if (scriptHtmlTagAttributes != null && id != null) { + _injectedScriptsFromURL[id] = scriptHtmlTagAttributes; + } + Map args = {}; + args.putIfAbsent('urlFile', () => urlFile.toString()); + args.putIfAbsent( + 'scriptHtmlTagAttributes', () => scriptHtmlTagAttributes?.toMap()); + await channel?.invokeMethod('injectJavascriptFileFromUrl', args); + } + + @override + Future injectJavascriptFileFromAsset( + {required String assetFilePath}) async { + String source = await rootBundle.loadString(assetFilePath); + return await evaluateJavascript(source: source); + } + + @override + Future injectCSSCode({required String source}) async { + Map args = {}; + args.putIfAbsent('source', () => source); + await channel?.invokeMethod('injectCSSCode', args); + } + + @override + Future injectCSSFileFromUrl( + {required WebUri urlFile, + CSSLinkHtmlTagAttributes? cssLinkHtmlTagAttributes}) async { + assert(urlFile.toString().isNotEmpty); + Map args = {}; + args.putIfAbsent('urlFile', () => urlFile.toString()); + args.putIfAbsent( + 'cssLinkHtmlTagAttributes', () => cssLinkHtmlTagAttributes?.toMap()); + await channel?.invokeMethod('injectCSSFileFromUrl', args); + } + + @override + Future injectCSSFileFromAsset({required String assetFilePath}) async { + String source = await rootBundle.loadString(assetFilePath); + await injectCSSCode(source: source); + } + + @override + void addJavaScriptHandler( + {required String handlerName, + required JavaScriptHandlerCallback callback}) { + assert(!_JAVASCRIPT_HANDLER_FORBIDDEN_NAMES.contains(handlerName), + '"$handlerName" is a forbidden name!'); + this._javaScriptHandlersMap[handlerName] = (callback); + } + + @override + JavaScriptHandlerCallback? removeJavaScriptHandler( + {required String handlerName}) { + return this._javaScriptHandlersMap.remove(handlerName); + } + + @override + bool hasJavaScriptHandler({required String handlerName}) { + return this._javaScriptHandlersMap.containsKey(handlerName); + } + + @override + Future takeScreenshot( + {ScreenshotConfiguration? screenshotConfiguration}) async { + Map args = {}; + args.putIfAbsent( + 'screenshotConfiguration', () => screenshotConfiguration?.toMap()); + return await channel?.invokeMethod('takeScreenshot', args); + } + + @override + @Deprecated('Use setSettings instead') + Future setOptions({required InAppWebViewGroupOptions options}) async { + InAppWebViewSettings settings = + InAppWebViewSettings.fromMap(options.toMap()) ?? InAppWebViewSettings(); + await setSettings(settings: settings); + } + + @override + @Deprecated('Use getSettings instead') + Future getOptions() async { + InAppWebViewSettings? settings = await getSettings(); + + Map? options = settings?.toMap(); + if (options != null) { + options = options.cast(); + return InAppWebViewGroupOptions.fromMap(options as Map); + } + + return null; + } + + @override + Future setSettings({required InAppWebViewSettings settings}) async { + Map args = {}; + + args.putIfAbsent('settings', () => settings.toMap()); + await channel?.invokeMethod('setSettings', args); + } + + @override + Future getSettings() async { + Map args = {}; + + Map? settings = + await channel?.invokeMethod('getSettings', args); + if (settings != null) { + settings = settings.cast(); + return InAppWebViewSettings.fromMap(settings as Map); + } + + return null; + } + + @override + Future getCopyBackForwardList() async { + Map args = {}; + Map? result = + (await channel?.invokeMethod('getCopyBackForwardList', args)) + ?.cast(); + return WebHistory.fromMap(result); + } + + @override + @Deprecated("Use InAppWebViewController.clearAllCache instead") + Future clearCache() async { + Map args = {}; + await channel?.invokeMethod('clearCache', args); + } + + @override + @Deprecated("Use FindInteractionController.findAll instead") + Future findAllAsync({required String find}) async { + Map args = {}; + args.putIfAbsent('find', () => find); + await channel?.invokeMethod('findAll', args); + } + + @override + @Deprecated("Use FindInteractionController.findNext instead") + Future findNext({required bool forward}) async { + Map args = {}; + args.putIfAbsent('forward', () => forward); + await channel?.invokeMethod('findNext', args); + } + + @override + @Deprecated("Use FindInteractionController.clearMatches instead") + Future clearMatches() async { + Map args = {}; + await channel?.invokeMethod('clearMatches', args); + } + + @override + @Deprecated("Use tRexRunnerHtml instead") + Future getTRexRunnerHtml() async { + return await tRexRunnerHtml; + } + + @override + @Deprecated("Use tRexRunnerCss instead") + Future getTRexRunnerCss() async { + return await tRexRunnerCss; + } + + @override + Future scrollTo( + {required int x, required int y, bool animated = false}) async { + Map args = {}; + args.putIfAbsent('x', () => x); + args.putIfAbsent('y', () => y); + args.putIfAbsent('animated', () => animated); + await channel?.invokeMethod('scrollTo', args); + } + + @override + Future scrollBy( + {required int x, required int y, bool animated = false}) async { + Map args = {}; + args.putIfAbsent('x', () => x); + args.putIfAbsent('y', () => y); + args.putIfAbsent('animated', () => animated); + await channel?.invokeMethod('scrollBy', args); + } + + @override + Future pauseTimers() async { + Map args = {}; + await channel?.invokeMethod('pauseTimers', args); + } + + @override + Future resumeTimers() async { + Map args = {}; + await channel?.invokeMethod('resumeTimers', args); + } + + @override + Future printCurrentPage( + {PrintJobSettings? settings}) async { + Map args = {}; + args.putIfAbsent("settings", () => settings?.toMap()); + String? jobId = + await channel?.invokeMethod('printCurrentPage', args); + if (jobId != null) { + return MacOSPrintJobController( + PlatformPrintJobControllerCreationParams(id: jobId)); + } + return null; + } + + @override + Future getContentHeight() async { + Map args = {}; + var height = await channel?.invokeMethod('getContentHeight', args); + if (height == null || height == 0) { + // try to use javascript + var scrollHeight = await evaluateJavascript( + source: "document.documentElement.scrollHeight;"); + if (scrollHeight != null && scrollHeight is num) { + height = scrollHeight.toInt(); + } + } + return height; + } + + @override + Future getContentWidth() async { + Map args = {}; + var height = await channel?.invokeMethod('getContentWidth', args); + if (height == null || height == 0) { + // try to use javascript + var scrollHeight = await evaluateJavascript( + source: "document.documentElement.scrollWidth;"); + if (scrollHeight != null && scrollHeight is num) { + height = scrollHeight.toInt(); + } + } + return height; + } + + @override + Future zoomBy( + {required double zoomFactor, + @Deprecated('Use animated instead') bool? iosAnimated, + bool animated = false}) async { + Map args = {}; + args.putIfAbsent('zoomFactor', () => zoomFactor); + args.putIfAbsent('animated', () => iosAnimated ?? animated); + return await channel?.invokeMethod('zoomBy', args); + } + + @override + Future getOriginalUrl() async { + Map args = {}; + String? url = await channel?.invokeMethod('getOriginalUrl', args); + return url != null ? WebUri(url) : null; + } + + @override + @Deprecated('Use getZoomScale instead') + Future getScale() async { + return await getZoomScale(); + } + + @override + Future getSelectedText() async { + Map args = {}; + return await channel?.invokeMethod('getSelectedText', args); + } + + @override + Future> getMetaTags() async { + List metaTags = []; + + List>? metaTagList = + (await evaluateJavascript(source: """ +(function() { + var metaTags = []; + var metaTagNodes = document.head.getElementsByTagName('meta'); + for (var i = 0; i < metaTagNodes.length; i++) { + var metaTagNode = metaTagNodes[i]; + + var otherAttributes = metaTagNode.getAttributeNames(); + var nameIndex = otherAttributes.indexOf("name"); + if (nameIndex !== -1) otherAttributes.splice(nameIndex, 1); + var contentIndex = otherAttributes.indexOf("content"); + if (contentIndex !== -1) otherAttributes.splice(contentIndex, 1); + + var attrs = []; + for (var j = 0; j < otherAttributes.length; j++) { + var otherAttribute = otherAttributes[j]; + attrs.push( + { + name: otherAttribute, + value: metaTagNode.getAttribute(otherAttribute) + } + ); + } + + metaTags.push( + { + name: metaTagNode.name, + content: metaTagNode.content, + attrs: attrs + } + ); + } + return metaTags; +})(); + """))?.cast>(); + + if (metaTagList == null) { + return metaTags; + } + + for (var metaTag in metaTagList) { + var attrs = []; + + for (var metaTagAttr in metaTag["attrs"]) { + attrs.add(MetaTagAttribute( + name: metaTagAttr["name"], value: metaTagAttr["value"])); + } + + metaTags.add(MetaTag( + name: metaTag["name"], content: metaTag["content"], attrs: attrs)); + } + + return metaTags; + } + + @override + Future getMetaThemeColor() async { + Color? themeColor; + + try { + Map args = {}; + themeColor = UtilColor.fromStringRepresentation( + await channel?.invokeMethod('getMetaThemeColor', args)); + return themeColor; + } catch (e) { + // not implemented + } + + // try using javascript + var metaTags = await getMetaTags(); + MetaTag? metaTagThemeColor; + + for (var metaTag in metaTags) { + if (metaTag.name == "theme-color") { + metaTagThemeColor = metaTag; + break; + } + } + + if (metaTagThemeColor == null) { + return null; + } + + var colorValue = metaTagThemeColor.content; + + themeColor = colorValue != null + ? UtilColor.fromStringRepresentation(colorValue) + : null; + + return themeColor; + } + + @override + Future getScrollX() async { + Map args = {}; + return await channel?.invokeMethod('getScrollX', args); + } + + @override + Future getScrollY() async { + Map args = {}; + return await channel?.invokeMethod('getScrollY', args); + } + + @override + Future getCertificate() async { + Map args = {}; + Map? sslCertificateMap = + (await channel?.invokeMethod('getCertificate', args)) + ?.cast(); + return SslCertificate.fromMap(sslCertificateMap); + } + + @override + Future addUserScript({required UserScript userScript}) async { + assert(webviewParams?.windowId == null); + + Map args = {}; + args.putIfAbsent('userScript', () => userScript.toMap()); + if (!(_userScripts[userScript.injectionTime]?.contains(userScript) ?? + false)) { + _userScripts[userScript.injectionTime]?.add(userScript); + await channel?.invokeMethod('addUserScript', args); + } + } + + @override + Future addUserScripts({required List userScripts}) async { + assert(webviewParams?.windowId == null); + + for (var i = 0; i < userScripts.length; i++) { + await addUserScript(userScript: userScripts[i]); + } + } + + @override + Future removeUserScript({required UserScript userScript}) async { + assert(webviewParams?.windowId == null); + + var index = _userScripts[userScript.injectionTime]?.indexOf(userScript); + if (index == null || index == -1) { + return false; + } + + _userScripts[userScript.injectionTime]?.remove(userScript); + Map args = {}; + args.putIfAbsent('userScript', () => userScript.toMap()); + args.putIfAbsent('index', () => index); + await channel?.invokeMethod('removeUserScript', args); + + return true; + } + + @override + Future removeUserScriptsByGroupName({required String groupName}) async { + assert(webviewParams?.windowId == null); + + final List userScriptsAtDocumentStart = List.from( + _userScripts[UserScriptInjectionTime.AT_DOCUMENT_START] ?? []); + for (final userScript in userScriptsAtDocumentStart) { + if (userScript.groupName == groupName) { + _userScripts[userScript.injectionTime]?.remove(userScript); + } + } + + final List userScriptsAtDocumentEnd = + List.from(_userScripts[UserScriptInjectionTime.AT_DOCUMENT_END] ?? []); + for (final userScript in userScriptsAtDocumentEnd) { + if (userScript.groupName == groupName) { + _userScripts[userScript.injectionTime]?.remove(userScript); + } + } + + Map args = {}; + args.putIfAbsent('groupName', () => groupName); + await channel?.invokeMethod('removeUserScriptsByGroupName', args); + } + + @override + Future removeUserScripts( + {required List userScripts}) async { + assert(webviewParams?.windowId == null); + + for (final userScript in userScripts) { + await removeUserScript(userScript: userScript); + } + } + + @override + Future removeAllUserScripts() async { + assert(webviewParams?.windowId == null); + + _userScripts[UserScriptInjectionTime.AT_DOCUMENT_START]?.clear(); + _userScripts[UserScriptInjectionTime.AT_DOCUMENT_END]?.clear(); + + Map args = {}; + await channel?.invokeMethod('removeAllUserScripts', args); + } + + @override + bool hasUserScript({required UserScript userScript}) { + return _userScripts[userScript.injectionTime]?.contains(userScript) ?? + false; + } + + @override + Future callAsyncJavaScript( + {required String functionBody, + Map arguments = const {}, + ContentWorld? contentWorld}) async { + Map args = {}; + args.putIfAbsent('functionBody', () => functionBody); + args.putIfAbsent('arguments', () => arguments); + args.putIfAbsent('contentWorld', () => contentWorld?.toMap()); + var data = await channel?.invokeMethod('callAsyncJavaScript', args); + if (data == null) { + return null; + } + return CallAsyncJavaScriptResult( + value: data["value"], error: data["error"]); + } + + @override + Future saveWebArchive( + {required String filePath, bool autoname = false}) async { + if (!autoname) { + assert( + filePath.endsWith("." + WebArchiveFormat.WEBARCHIVE.toNativeValue())); + } + + Map args = {}; + args.putIfAbsent("filePath", () => filePath); + args.putIfAbsent("autoname", () => autoname); + return await channel?.invokeMethod('saveWebArchive', args); + } + + @override + Future isSecureContext() async { + Map args = {}; + return await channel?.invokeMethod('isSecureContext', args) ?? false; + } + + @override + Future createWebMessageChannel() async { + Map args = {}; + Map? result = + (await channel?.invokeMethod('createWebMessageChannel', args)) + ?.cast(); + final webMessageChannel = MacOSWebMessageChannel.static().fromMap(result); + if (webMessageChannel != null) { + _webMessageChannels.add(webMessageChannel); + } + return webMessageChannel; + } + + @override + Future postWebMessage( + {required WebMessage message, WebUri? targetOrigin}) async { + if (targetOrigin == null) { + targetOrigin = WebUri(''); + } + Map args = {}; + args.putIfAbsent('message', () => message.toMap()); + args.putIfAbsent('targetOrigin', () => targetOrigin.toString()); + await channel?.invokeMethod('postWebMessage', args); + } + + @override + Future addWebMessageListener( + PlatformWebMessageListener webMessageListener) async { + assert(!_webMessageListeners.contains(webMessageListener), + "${webMessageListener} was already added."); + assert( + !_webMessageListenerObjNames + .contains(webMessageListener.params.jsObjectName), + "jsObjectName ${webMessageListener.params.jsObjectName} was already added."); + _webMessageListeners.add(webMessageListener as MacOSWebMessageListener); + _webMessageListenerObjNames.add(webMessageListener.params.jsObjectName); + + Map args = {}; + args.putIfAbsent('webMessageListener', () => webMessageListener.toMap()); + await channel?.invokeMethod('addWebMessageListener', args); + } + + @override + bool hasWebMessageListener(PlatformWebMessageListener webMessageListener) { + return _webMessageListeners.contains(webMessageListener) || + _webMessageListenerObjNames + .contains(webMessageListener.params.jsObjectName); + } + + @override + Future canScrollVertically() async { + Map args = {}; + return await channel?.invokeMethod('canScrollVertically', args) ?? + false; + } + + @override + Future canScrollHorizontally() async { + Map args = {}; + return await channel?.invokeMethod('canScrollHorizontally', args) ?? + false; + } + + @override + Future reloadFromOrigin() async { + Map args = {}; + await channel?.invokeMethod('reloadFromOrigin', args); + } + + @override + Future createPdf( + {@Deprecated("Use pdfConfiguration instead") + // ignore: deprecated_member_use_from_same_package + IOSWKPDFConfiguration? iosWKPdfConfiguration, + PDFConfiguration? pdfConfiguration}) async { + Map args = {}; + args.putIfAbsent('pdfConfiguration', + () => pdfConfiguration?.toMap() ?? iosWKPdfConfiguration?.toMap()); + return await channel?.invokeMethod('createPdf', args); + } + + @override + Future createWebArchiveData() async { + Map args = {}; + return await channel?.invokeMethod('createWebArchiveData', args); + } + + @override + Future hasOnlySecureContent() async { + Map args = {}; + return await channel?.invokeMethod('hasOnlySecureContent', args) ?? + false; + } + + @override + Future pauseAllMediaPlayback() async { + Map args = {}; + return await channel?.invokeMethod('pauseAllMediaPlayback', args); + } + + @override + Future setAllMediaPlaybackSuspended({required bool suspended}) async { + Map args = {}; + args.putIfAbsent("suspended", () => suspended); + return await channel?.invokeMethod('setAllMediaPlaybackSuspended', args); + } + + @override + Future closeAllMediaPresentations() async { + Map args = {}; + return await channel?.invokeMethod('closeAllMediaPresentations', args); + } + + @override + Future requestMediaPlaybackState() async { + Map args = {}; + return MediaPlaybackState.fromNativeValue( + await channel?.invokeMethod('requestMediaPlaybackState', args)); + } + + @override + Future isInFullscreen() async { + Map args = {}; + return await channel?.invokeMethod('isInFullscreen', args) ?? false; + } + + @override + Future getCameraCaptureState() async { + Map args = {}; + return MediaCaptureState.fromNativeValue( + await channel?.invokeMethod('getCameraCaptureState', args)); + } + + @override + Future setCameraCaptureState({required MediaCaptureState state}) async { + Map args = {}; + args.putIfAbsent('state', () => state.toNativeValue()); + await channel?.invokeMethod('setCameraCaptureState', args); + } + + @override + Future getMicrophoneCaptureState() async { + Map args = {}; + return MediaCaptureState.fromNativeValue( + await channel?.invokeMethod('getMicrophoneCaptureState', args)); + } + + @override + Future setMicrophoneCaptureState( + {required MediaCaptureState state}) async { + Map args = {}; + args.putIfAbsent('state', () => state.toNativeValue()); + await channel?.invokeMethod('setMicrophoneCaptureState', args); + } + + @override + Future loadSimulatedRequest( + {required URLRequest urlRequest, + required Uint8List data, + URLResponse? urlResponse}) async { + Map args = {}; + args.putIfAbsent('urlRequest', () => urlRequest.toMap()); + args.putIfAbsent('data', () => data); + args.putIfAbsent('urlResponse', () => urlResponse?.toMap()); + await channel?.invokeMethod('loadSimulatedRequest', args); + } + + @override + Future getDefaultUserAgent() async { + Map args = {}; + return await _staticChannel.invokeMethod( + 'getDefaultUserAgent', args) ?? + ''; + } + + @override + Future handlesURLScheme(String urlScheme) async { + Map args = {}; + args.putIfAbsent('urlScheme', () => urlScheme); + return await _staticChannel.invokeMethod('handlesURLScheme', args); + } + + @override + Future disposeKeepAlive(InAppWebViewKeepAlive keepAlive) async { + Map args = {}; + args.putIfAbsent('keepAliveId', () => keepAlive.id); + await _staticChannel.invokeMethod('disposeKeepAlive', args); + _keepAliveMap[keepAlive] = null; + } + + @override + Future clearAllCache({bool includeDiskFiles = true}) async { + Map args = {}; + args.putIfAbsent('includeDiskFiles', () => includeDiskFiles); + await _staticChannel.invokeMethod('clearAllCache', args); + } + + @override + Future get tRexRunnerHtml async => await rootBundle.loadString( + 'packages/flutter_inappwebview/assets/t_rex_runner/t-rex.html'); + + @override + Future get tRexRunnerCss async => await rootBundle.loadString( + 'packages/flutter_inappwebview/assets/t_rex_runner/t-rex.css'); + + @override + dynamic getViewId() { + return id; + } + + @override + void dispose({bool isKeepAlive = false}) { + disposeChannel(removeMethodCallHandler: !isKeepAlive); + _inAppBrowser = null; + webStorage.dispose(); + if (!isKeepAlive) { + _controllerFromPlatform = null; + _javaScriptHandlersMap.clear(); + _userScripts.clear(); + _webMessageListenerObjNames.clear(); + _injectedScriptsFromURL.clear(); + for (final webMessageChannel in _webMessageChannels) { + webMessageChannel.dispose(); + } + _webMessageChannels.clear(); + for (final webMessageListener in _webMessageListeners) { + webMessageListener.dispose(); + } + _webMessageListeners.clear(); + } + } +} + +extension InternalInAppWebViewController on MacOSInAppWebViewController { + get handleMethod => _handleMethod; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/in_app_webview/main.dart b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/in_app_webview/main.dart new file mode 100644 index 00000000..b83b0611 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/in_app_webview/main.dart @@ -0,0 +1,3 @@ +export 'in_app_webview_controller.dart' hide InternalInAppWebViewController; +export 'in_app_webview.dart'; +export 'headless_in_app_webview.dart' hide InternalHeadlessInAppWebView; diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/inappwebview_platform.dart b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/inappwebview_platform.dart new file mode 100644 index 00000000..a4acb28c --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/inappwebview_platform.dart @@ -0,0 +1,240 @@ +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +import 'cookie_manager.dart'; +import 'http_auth_credentials_database.dart'; +import 'find_interaction/main.dart'; +import 'in_app_browser/in_app_browser.dart'; +import 'in_app_webview/main.dart'; +import 'print_job/main.dart'; +import 'web_message/main.dart'; +import 'web_storage/main.dart'; +import 'web_authentication_session/main.dart'; + +/// Implementation of [InAppWebViewPlatform] using the WebKit API. +class MacOSInAppWebViewPlatform extends InAppWebViewPlatform { + /// Registers this class as the default instance of [InAppWebViewPlatform]. + static void registerWith() { + InAppWebViewPlatform.instance = MacOSInAppWebViewPlatform(); + } + + /// Creates a new [MacOSCookieManager]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [CookieManager] in `flutter_inappwebview` instead. + @override + MacOSCookieManager createPlatformCookieManager( + PlatformCookieManagerCreationParams params, + ) { + return MacOSCookieManager(params); + } + + /// Creates a new [MacOSInAppWebViewController]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [InAppWebViewController] in `flutter_inappwebview` instead. + @override + MacOSInAppWebViewController createPlatformInAppWebViewController( + PlatformInAppWebViewControllerCreationParams params, + ) { + return MacOSInAppWebViewController(params); + } + + /// Creates a new empty [MacOSInAppWebViewController] to access static methods. + /// + /// This function should only be called by the app-facing package. + /// Look at using [InAppWebViewController] in `flutter_inappwebview` instead. + @override + MacOSInAppWebViewController createPlatformInAppWebViewControllerStatic() { + return MacOSInAppWebViewController.static(); + } + + /// Creates a new [MacOSInAppWebViewWidget]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [InAppWebView] in `flutter_inappwebview` instead. + @override + MacOSInAppWebViewWidget createPlatformInAppWebViewWidget( + PlatformInAppWebViewWidgetCreationParams params, + ) { + return MacOSInAppWebViewWidget(params); + } + + /// Creates a new [MacOSFindInteractionController]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [FindInteractionController] in `flutter_inappwebview` instead. + @override + MacOSFindInteractionController createPlatformFindInteractionController( + PlatformFindInteractionControllerCreationParams params, + ) { + return MacOSFindInteractionController(params); + } + + /// Creates a new [MacOSPrintJobController]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [PrintJobController] in `flutter_inappwebview` instead. + @override + MacOSPrintJobController createPlatformPrintJobController( + PlatformPrintJobControllerCreationParams params, + ) { + return MacOSPrintJobController(params); + } + + /// Creates a new [MacOSWebMessageChannel]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebMessageChannel] in `flutter_inappwebview` instead. + @override + MacOSWebMessageChannel createPlatformWebMessageChannel( + PlatformWebMessageChannelCreationParams params, + ) { + return MacOSWebMessageChannel(params); + } + + /// Creates a new empty [MacOSWebMessageChannel] to access static methods. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebMessageChannel] in `flutter_inappwebview` instead. + @override + MacOSWebMessageChannel createPlatformWebMessageChannelStatic() { + return MacOSWebMessageChannel.static(); + } + + /// Creates a new [MacOSWebMessageListener]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebMessageListener] in `flutter_inappwebview` instead. + @override + MacOSWebMessageListener createPlatformWebMessageListener( + PlatformWebMessageListenerCreationParams params, + ) { + return MacOSWebMessageListener(params); + } + + /// Creates a new [MacOSJavaScriptReplyProxy]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [JavaScriptReplyProxy] in `flutter_inappwebview` instead. + @override + MacOSJavaScriptReplyProxy createPlatformJavaScriptReplyProxy( + PlatformJavaScriptReplyProxyCreationParams params, + ) { + return MacOSJavaScriptReplyProxy(params); + } + + /// Creates a new [MacOSWebMessagePort]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebMessagePort] in `flutter_inappwebview` instead. + @override + MacOSWebMessagePort createPlatformWebMessagePort( + PlatformWebMessagePortCreationParams params, + ) { + return MacOSWebMessagePort(params); + } + + /// Creates a new [MacOSWebStorage]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebStorage] in `flutter_inappwebview` instead. + @override + MacOSWebStorage createPlatformWebStorage( + PlatformWebStorageCreationParams params, + ) { + return MacOSWebStorage(params); + } + + /// Creates a new [MacOSLocalStorage]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [LocalStorage] in `flutter_inappwebview` instead. + @override + MacOSLocalStorage createPlatformLocalStorage( + PlatformLocalStorageCreationParams params, + ) { + return MacOSLocalStorage(params); + } + + /// Creates a new [MacOSSessionStorage]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [SessionStorage] in `flutter_inappwebview` instead. + @override + MacOSSessionStorage createPlatformSessionStorage( + PlatformSessionStorageCreationParams params, + ) { + return MacOSSessionStorage(params); + } + + /// Creates a new [MacOSHeadlessInAppWebView]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [HeadlessInAppWebView] in `flutter_inappwebview` instead. + @override + MacOSHeadlessInAppWebView createPlatformHeadlessInAppWebView( + PlatformHeadlessInAppWebViewCreationParams params, + ) { + return MacOSHeadlessInAppWebView(params); + } + + /// Creates a new [MacOSHttpAuthCredentialDatabase]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [HttpAuthCredentialDatabase] in `flutter_inappwebview` instead. + @override + MacOSHttpAuthCredentialDatabase createPlatformHttpAuthCredentialDatabase( + PlatformHttpAuthCredentialDatabaseCreationParams params, + ) { + return MacOSHttpAuthCredentialDatabase(params); + } + + /// Creates a new [MacOSInAppBrowser]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [InAppBrowser] in `flutter_inappwebview` instead. + @override + MacOSInAppBrowser createPlatformInAppBrowser( + PlatformInAppBrowserCreationParams params, + ) { + return MacOSInAppBrowser(params); + } + + /// Creates a new empty [MacOSInAppBrowser] to access static methods. + /// + /// This function should only be called by the app-facing package. + /// Look at using [InAppBrowser] in `flutter_inappwebview` instead. + @override + MacOSInAppBrowser createPlatformInAppBrowserStatic() { + return MacOSInAppBrowser.static(); + } + + /// Creates a new empty [MacOSWebStorageManager] to access static methods. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebStorageManager] in `flutter_inappwebview` instead. + @override + MacOSWebStorageManager createPlatformWebStorageManager( + PlatformWebStorageManagerCreationParams params) { + return MacOSWebStorageManager(params); + } + + /// Creates a new [MacOSWebAuthenticationSession]. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebAuthenticationSession] in `flutter_inappwebview` instead. + @override + MacOSWebAuthenticationSession createPlatformWebAuthenticationSession( + PlatformWebAuthenticationSessionCreationParams params) { + return MacOSWebAuthenticationSession(params); + } + + /// Creates a new empty [MacOSWebAuthenticationSession] to access static methods. + /// + /// This function should only be called by the app-facing package. + /// Look at using [WebAuthenticationSession] in `flutter_inappwebview` instead. + @override + MacOSWebAuthenticationSession createPlatformWebAuthenticationSessionStatic() { + return MacOSWebAuthenticationSession.static(); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/main.dart b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/main.dart new file mode 100644 index 00000000..e58414ae --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/main.dart @@ -0,0 +1,11 @@ +export 'inappwebview_platform.dart'; +export 'in_app_webview/main.dart'; +export 'in_app_browser/main.dart'; +export 'web_storage/main.dart'; +export 'cookie_manager.dart' hide InternalCookieManager; +export 'http_auth_credentials_database.dart' + hide InternalHttpAuthCredentialDatabase; +export 'web_message/main.dart'; +export 'print_job/main.dart'; +export 'find_interaction/main.dart'; +export 'web_authentication_session/main.dart'; diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/platform_util.dart b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/platform_util.dart new file mode 100644 index 00000000..27e96478 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/platform_util.dart @@ -0,0 +1,64 @@ +import 'package:flutter/services.dart'; + +///Platform native utilities +class PlatformUtil { + static PlatformUtil? _instance; + static const MethodChannel _channel = + MethodChannel('com.pichillilorenzo/flutter_inappwebview_platformutil'); + + PlatformUtil._(); + + ///Get [PlatformUtil] instance. + static PlatformUtil instance() { + return (_instance != null) ? _instance! : _init(); + } + + static PlatformUtil _init() { + _channel.setMethodCallHandler((call) async { + try { + return await _handleMethod(call); + } on Error catch (e) { + print(e); + print(e.stackTrace); + } + }); + _instance = PlatformUtil._(); + return _instance!; + } + + static Future _handleMethod(MethodCall call) async {} + + String? _cachedSystemVersion; + + ///Get current platform system version. + Future getSystemVersion() async { + if (_cachedSystemVersion != null) { + return _cachedSystemVersion!; + } + Map args = {}; + _cachedSystemVersion = + await _channel.invokeMethod('getSystemVersion', args); + return _cachedSystemVersion!; + } + + ///Format date. + Future formatDate( + {required DateTime date, + required String format, + String locale = "en_US", + String timezone = "UTC"}) async { + Map args = {}; + args.putIfAbsent('date', () => date.millisecondsSinceEpoch); + args.putIfAbsent('format', () => format); + args.putIfAbsent('locale', () => locale); + args.putIfAbsent('timezone', () => timezone); + return await _channel.invokeMethod('formatDate', args); + } + + ///Get cookie expiration date used by Web platform. + Future getWebCookieExpirationDate({required DateTime date}) async { + Map args = {}; + args.putIfAbsent('date', () => date.millisecondsSinceEpoch); + return await _channel.invokeMethod('getWebCookieExpirationDate', args); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/print_job/main.dart b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/print_job/main.dart new file mode 100644 index 00000000..4e70ad94 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/print_job/main.dart @@ -0,0 +1 @@ +export 'print_job_controller.dart'; diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/print_job/print_job_controller.dart b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/print_job/print_job_controller.dart new file mode 100644 index 00000000..be3136d6 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/print_job/print_job_controller.dart @@ -0,0 +1,73 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [MacOSPrintJobController]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformPrintJobControllerCreationParams] for +/// more information. +@immutable +class MacOSPrintJobControllerCreationParams + extends PlatformPrintJobControllerCreationParams { + /// Creates a new [MacOSPrintJobControllerCreationParams] instance. + const MacOSPrintJobControllerCreationParams( + {required super.id, super.onComplete}); + + /// Creates a [MacOSPrintJobControllerCreationParams] instance based on [PlatformPrintJobControllerCreationParams]. + factory MacOSPrintJobControllerCreationParams.fromPlatformPrintJobControllerCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformPrintJobControllerCreationParams params) { + return MacOSPrintJobControllerCreationParams( + id: params.id, onComplete: params.onComplete); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformPrintJobController} +class MacOSPrintJobController extends PlatformPrintJobController + with ChannelController { + /// Constructs a [MacOSPrintJobController]. + MacOSPrintJobController(PlatformPrintJobControllerCreationParams params) + : super.implementation( + params is MacOSPrintJobControllerCreationParams + ? params + : MacOSPrintJobControllerCreationParams + .fromPlatformPrintJobControllerCreationParams(params), + ) { + onComplete = params.onComplete; + channel = MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_printjobcontroller_${params.id}'); + handler = _handleMethod; + initMethodCallHandler(); + } + + Future _handleMethod(MethodCall call) async { + switch (call.method) { + case "onComplete": + bool completed = call.arguments["completed"]; + String? error = call.arguments["error"]; + if (onComplete != null) { + onComplete!(completed, error); + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + } + + @override + Future getInfo() async { + Map args = {}; + Map? infoMap = + (await channel?.invokeMethod('getInfo', args))?.cast(); + return PrintJobInfo.fromMap(infoMap); + } + + @override + Future dispose() async { + Map args = {}; + await channel?.invokeMethod('dispose', args); + disposeChannel(); + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_authentication_session/main.dart b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_authentication_session/main.dart new file mode 100644 index 00000000..37ca0c24 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_authentication_session/main.dart @@ -0,0 +1 @@ +export 'web_authenticate_session.dart'; diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_authentication_session/web_authenticate_session.dart b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_authentication_session/web_authenticate_session.dart new file mode 100755 index 00000000..b59f7cf5 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_authentication_session/web_authenticate_session.dart @@ -0,0 +1,164 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [MacOSWebAuthenticationSession]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebAuthenticationSessionCreationParams] for +/// more information. +class MacOSWebAuthenticationSessionCreationParams + extends PlatformWebAuthenticationSessionCreationParams { + /// Creates a new [MacOSWebAuthenticationSessionCreationParams] instance. + const MacOSWebAuthenticationSessionCreationParams(); + + /// Creates a [MacOSWebAuthenticationSessionCreationParams] instance based on [PlatformWebAuthenticationSessionCreationParams]. + factory MacOSWebAuthenticationSessionCreationParams.fromPlatformWebAuthenticationSessionCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformWebAuthenticationSessionCreationParams params) { + return MacOSWebAuthenticationSessionCreationParams(); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformWebAuthenticationSession} +class MacOSWebAuthenticationSession extends PlatformWebAuthenticationSession + with ChannelController { + /// Constructs a [MacOSWebAuthenticationSession]. + MacOSWebAuthenticationSession( + PlatformWebAuthenticationSessionCreationParams params) + : super.implementation( + params is MacOSWebAuthenticationSessionCreationParams + ? params + : MacOSWebAuthenticationSessionCreationParams + .fromPlatformWebAuthenticationSessionCreationParams(params), + ); + + static final MacOSWebAuthenticationSession _staticValue = + MacOSWebAuthenticationSession( + MacOSWebAuthenticationSessionCreationParams()); + + /// Provide static access. + factory MacOSWebAuthenticationSession.static() { + return _staticValue; + } + + @override + final String id = IdGenerator.generate(); + + @override + late final WebUri url; + + @override + late final String? callbackURLScheme; + + @override + late final WebAuthenticationSessionSettings? initialSettings; + + @override + late final WebAuthenticationSessionCompletionHandler onComplete; + + static const MethodChannel _staticChannel = const MethodChannel( + 'com.pichillilorenzo/flutter_webauthenticationsession'); + + @override + Future create( + {required WebUri url, + String? callbackURLScheme, + WebAuthenticationSessionCompletionHandler onComplete, + WebAuthenticationSessionSettings? initialSettings}) async { + var session = MacOSWebAuthenticationSession._create( + url: url, + callbackURLScheme: callbackURLScheme, + onComplete: onComplete, + initialSettings: initialSettings); + initialSettings = + session.initialSettings ?? WebAuthenticationSessionSettings(); + Map args = {}; + args.putIfAbsent("id", () => session.id); + args.putIfAbsent("url", () => session.url.toString()); + args.putIfAbsent("callbackURLScheme", () => session.callbackURLScheme); + args.putIfAbsent("initialSettings", () => initialSettings?.toMap()); + await _staticChannel.invokeMethod('create', args); + return session; + } + + MacOSWebAuthenticationSession._create( + {required this.url, + this.callbackURLScheme, + this.onComplete, + WebAuthenticationSessionSettings? initialSettings}) + : super.implementation(MacOSWebAuthenticationSessionCreationParams()) { + assert(url.toString().isNotEmpty); + assert(['http', 'https'].contains(url.scheme), + 'The specified URL has an unsupported scheme. Only HTTP and HTTPS URLs are supported on iOS.'); + + this.initialSettings = + initialSettings ?? WebAuthenticationSessionSettings(); + channel = MethodChannel( + 'com.pichillilorenzo/flutter_webauthenticationsession_$id'); + handler = _handleMethod; + initMethodCallHandler(); + } + + _debugLog(String method, dynamic args) { + debugLog( + className: this.runtimeType.toString(), + debugLoggingSettings: + PlatformWebAuthenticationSession.debugLoggingSettings, + id: id, + method: method, + args: args); + } + + Future _handleMethod(MethodCall call) async { + _debugLog(call.method, call.arguments); + + switch (call.method) { + case "onComplete": + String? url = call.arguments["url"]; + WebUri? uri = url != null ? WebUri(url) : null; + var error = WebAuthenticationSessionError.fromNativeValue( + call.arguments["errorCode"]); + if (onComplete != null) { + onComplete!(uri, error); + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + } + + @override + Future canStart() async { + Map args = {}; + return await channel?.invokeMethod('canStart', args) ?? false; + } + + @override + Future start() async { + Map args = {}; + return await channel?.invokeMethod('start', args) ?? false; + } + + @override + Future cancel() async { + Map args = {}; + await channel?.invokeMethod("cancel", args); + } + + @override + Future dispose() async { + Map args = {}; + await channel?.invokeMethod("dispose", args); + disposeChannel(); + } + + @override + Future isAvailable() async { + Map args = {}; + return await _staticChannel.invokeMethod("isAvailable", args) ?? + false; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_message/main.dart b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_message/main.dart new file mode 100644 index 00000000..d41e30c7 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_message/main.dart @@ -0,0 +1,3 @@ +export 'web_message_port.dart' hide InternalWebMessagePort; +export 'web_message_channel.dart' hide InternalWebMessageChannel; +export 'web_message_listener.dart'; diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_message/web_message_channel.dart b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_message/web_message_channel.dart new file mode 100644 index 00000000..aedbc5d1 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_message/web_message_channel.dart @@ -0,0 +1,120 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; +import 'web_message_port.dart'; + +/// Object specifying creation parameters for creating a [MacOSWebMessageChannel]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebMessageChannelCreationParams] for +/// more information. +@immutable +class MacOSWebMessageChannelCreationParams + extends PlatformWebMessageChannelCreationParams { + /// Creates a new [MacOSWebMessageChannelCreationParams] instance. + const MacOSWebMessageChannelCreationParams( + {required super.id, required super.port1, required super.port2}); + + /// Creates a [MacOSWebMessageChannelCreationParams] instance based on [PlatformWebMessageChannelCreationParams]. + factory MacOSWebMessageChannelCreationParams.fromPlatformWebMessageChannelCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformWebMessageChannelCreationParams params) { + return MacOSWebMessageChannelCreationParams( + id: params.id, port1: params.port1, port2: params.port2); + } + + @override + String toString() { + return 'MacOSWebMessageChannelCreationParams{id: $id, port1: $port1, port2: $port2}'; + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformWebMessageChannel} +class MacOSWebMessageChannel extends PlatformWebMessageChannel + with ChannelController { + /// Constructs a [MacOSWebMessageChannel]. + MacOSWebMessageChannel(PlatformWebMessageChannelCreationParams params) + : super.implementation( + params is MacOSWebMessageChannelCreationParams + ? params + : MacOSWebMessageChannelCreationParams + .fromPlatformWebMessageChannelCreationParams(params), + ) { + channel = MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_web_message_channel_${params.id}'); + handler = _handleMethod; + initMethodCallHandler(); + } + + static final MacOSWebMessageChannel _staticValue = MacOSWebMessageChannel( + MacOSWebMessageChannelCreationParams( + id: '', + port1: + MacOSWebMessagePort(MacOSWebMessagePortCreationParams(index: 0)), + port2: MacOSWebMessagePort( + MacOSWebMessagePortCreationParams(index: 1)))); + + /// Provide static access. + factory MacOSWebMessageChannel.static() { + return _staticValue; + } + + MacOSWebMessagePort get _macosPort1 => port1 as MacOSWebMessagePort; + + MacOSWebMessagePort get _macosPort2 => port2 as MacOSWebMessagePort; + + static MacOSWebMessageChannel? _fromMap(Map? map) { + if (map == null) { + return null; + } + var webMessageChannel = MacOSWebMessageChannel( + MacOSWebMessageChannelCreationParams( + id: map["id"], + port1: MacOSWebMessagePort( + MacOSWebMessagePortCreationParams(index: 0)), + port2: MacOSWebMessagePort( + MacOSWebMessagePortCreationParams(index: 1)))); + webMessageChannel._macosPort1.webMessageChannel = webMessageChannel; + webMessageChannel._macosPort2.webMessageChannel = webMessageChannel; + return webMessageChannel; + } + + Future _handleMethod(MethodCall call) async { + switch (call.method) { + case "onMessage": + int index = call.arguments["index"]; + var port = index == 0 ? _macosPort1 : _macosPort2; + if (port.onMessage != null) { + WebMessage? message = call.arguments["message"] != null + ? WebMessage.fromMap( + call.arguments["message"].cast()) + : null; + port.onMessage!(message); + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + @override + MacOSWebMessageChannel? fromMap(Map? map) { + return _fromMap(map); + } + + @override + void dispose() { + disposeChannel(); + } + + @override + String toString() { + return 'MacOSWebMessageChannel{id: $id, port1: $port1, port2: $port2}'; + } +} + +extension InternalWebMessageChannel on MacOSWebMessageChannel { + MethodChannel? get internalChannel => channel; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_message/web_message_listener.dart b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_message/web_message_listener.dart new file mode 100644 index 00000000..b2791eba --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_message/web_message_listener.dart @@ -0,0 +1,164 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [MacOSWebMessageListener]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebMessageListenerCreationParams] for +/// more information. +@immutable +class MacOSWebMessageListenerCreationParams + extends PlatformWebMessageListenerCreationParams { + /// Creates a new [MacOSWebMessageListenerCreationParams] instance. + const MacOSWebMessageListenerCreationParams( + {required this.allowedOriginRules, + required super.jsObjectName, + super.onPostMessage}); + + /// Creates a [MacOSWebMessageListenerCreationParams] instance based on [PlatformWebMessageListenerCreationParams]. + factory MacOSWebMessageListenerCreationParams.fromPlatformWebMessageListenerCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformWebMessageListenerCreationParams params) { + return MacOSWebMessageListenerCreationParams( + allowedOriginRules: params.allowedOriginRules ?? Set.from(["*"]), + jsObjectName: params.jsObjectName, + onPostMessage: params.onPostMessage); + } + + @override + final Set allowedOriginRules; + + @override + String toString() { + return 'MacOSWebMessageListenerCreationParams{jsObjectName: $jsObjectName, allowedOriginRules: $allowedOriginRules, onPostMessage: $onPostMessage}'; + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformWebMessageListener} +class MacOSWebMessageListener extends PlatformWebMessageListener + with ChannelController { + /// Constructs a [MacOSWebMessageListener]. + MacOSWebMessageListener(PlatformWebMessageListenerCreationParams params) + : super.implementation( + params is MacOSWebMessageListenerCreationParams + ? params + : MacOSWebMessageListenerCreationParams + .fromPlatformWebMessageListenerCreationParams(params), + ) { + assert(!this._macosParams.allowedOriginRules.contains(""), + "allowedOriginRules cannot contain empty strings"); + channel = MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_web_message_listener_${_id}_${params.jsObjectName}'); + handler = _handleMethod; + initMethodCallHandler(); + } + + ///Message Listener ID used internally. + final String _id = IdGenerator.generate(); + + MacOSJavaScriptReplyProxy? _replyProxy; + + MacOSWebMessageListenerCreationParams get _macosParams => + params as MacOSWebMessageListenerCreationParams; + + Future _handleMethod(MethodCall call) async { + switch (call.method) { + case "onPostMessage": + if (_replyProxy == null) { + _replyProxy = MacOSJavaScriptReplyProxy( + PlatformJavaScriptReplyProxyCreationParams( + webMessageListener: this)); + } + if (onPostMessage != null) { + WebMessage? message = call.arguments["message"] != null + ? WebMessage.fromMap( + call.arguments["message"].cast()) + : null; + WebUri? sourceOrigin = call.arguments["sourceOrigin"] != null + ? WebUri(call.arguments["sourceOrigin"]) + : null; + bool isMainFrame = call.arguments["isMainFrame"]; + onPostMessage!(message, sourceOrigin, isMainFrame, _replyProxy!); + } + break; + default: + throw UnimplementedError("Unimplemented ${call.method} method"); + } + return null; + } + + @override + void dispose() { + disposeChannel(); + } + + @override + Map toMap() { + return { + "id": _id, + "jsObjectName": params.jsObjectName, + "allowedOriginRules": _macosParams.allowedOriginRules.toList(), + }; + } + + @override + Map toJson() { + return this.toMap(); + } + + @override + String toString() { + return 'MacOSWebMessageListener{id: ${_id}, jsObjectName: ${params.jsObjectName}, allowedOriginRules: ${params.allowedOriginRules}, replyProxy: $_replyProxy}'; + } +} + +/// Object specifying creation parameters for creating a [MacOSJavaScriptReplyProxy]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformJavaScriptReplyProxyCreationParams] for +/// more information. +@immutable +class MacOSJavaScriptReplyProxyCreationParams + extends PlatformJavaScriptReplyProxyCreationParams { + /// Creates a new [MacOSJavaScriptReplyProxyCreationParams] instance. + const MacOSJavaScriptReplyProxyCreationParams( + {required super.webMessageListener}); + + /// Creates a [MacOSJavaScriptReplyProxyCreationParams] instance based on [PlatformJavaScriptReplyProxyCreationParams]. + factory MacOSJavaScriptReplyProxyCreationParams.fromPlatformJavaScriptReplyProxyCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformJavaScriptReplyProxyCreationParams params) { + return MacOSJavaScriptReplyProxyCreationParams( + webMessageListener: params.webMessageListener); + } +} + +///{@macro flutter_inappwebview_platform_interface.JavaScriptReplyProxy} +class MacOSJavaScriptReplyProxy extends PlatformJavaScriptReplyProxy { + /// Constructs a [MacOSWebMessageListener]. + MacOSJavaScriptReplyProxy(PlatformJavaScriptReplyProxyCreationParams params) + : super.implementation( + params is MacOSJavaScriptReplyProxyCreationParams + ? params + : MacOSJavaScriptReplyProxyCreationParams + .fromPlatformJavaScriptReplyProxyCreationParams(params), + ); + + MacOSWebMessageListener get _macosWebMessageListener => + params.webMessageListener as MacOSWebMessageListener; + + @override + Future postMessage(WebMessage message) async { + Map args = {}; + args.putIfAbsent('message', () => message.toMap()); + await _macosWebMessageListener.channel?.invokeMethod('postMessage', args); + } + + @override + String toString() { + return 'MacOSJavaScriptReplyProxy{}'; + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_message/web_message_port.dart b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_message/web_message_port.dart new file mode 100644 index 00000000..c0682298 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_message/web_message_port.dart @@ -0,0 +1,95 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +import 'web_message_channel.dart'; + +/// Object specifying creation parameters for creating a [MacOSWebMessagePort]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebMessagePortCreationParams] for +/// more information. +@immutable +class MacOSWebMessagePortCreationParams + extends PlatformWebMessagePortCreationParams { + /// Creates a new [MacOSWebMessagePortCreationParams] instance. + const MacOSWebMessagePortCreationParams({required super.index}); + + /// Creates a [MacOSWebMessagePortCreationParams] instance based on [PlatformWebMessagePortCreationParams]. + factory MacOSWebMessagePortCreationParams.fromPlatformWebMessagePortCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformWebMessagePortCreationParams params) { + return MacOSWebMessagePortCreationParams(index: params.index); + } + + @override + String toString() { + return 'MacOSWebMessagePortCreationParams{index: $index}'; + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformWebMessagePort} +class MacOSWebMessagePort extends PlatformWebMessagePort { + WebMessageCallback? _onMessage; + late MacOSWebMessageChannel _webMessageChannel; + + /// Constructs a [MacOSWebMessagePort]. + MacOSWebMessagePort(PlatformWebMessagePortCreationParams params) + : super.implementation( + params is MacOSWebMessagePortCreationParams + ? params + : MacOSWebMessagePortCreationParams + .fromPlatformWebMessagePortCreationParams(params), + ); + + @override + Future setWebMessageCallback(WebMessageCallback? onMessage) async { + Map args = {}; + args.putIfAbsent('index', () => params.index); + await _webMessageChannel.internalChannel + ?.invokeMethod('setWebMessageCallback', args); + this._onMessage = onMessage; + } + + @override + Future postMessage(WebMessage message) async { + Map args = {}; + args.putIfAbsent('index', () => params.index); + args.putIfAbsent('message', () => message.toMap()); + await _webMessageChannel.internalChannel?.invokeMethod('postMessage', args); + } + + @override + Future close() async { + Map args = {}; + args.putIfAbsent('index', () => params.index); + await _webMessageChannel.internalChannel?.invokeMethod('close', args); + } + + @override + Map toMap() { + return { + "index": params.index, + "webMessageChannelId": this._webMessageChannel.params.id + }; + } + + @override + Map toJson() { + return toMap(); + } + + @override + String toString() { + return 'MacOSWebMessagePort{index: ${params.index}}'; + } +} + +extension InternalWebMessagePort on MacOSWebMessagePort { + WebMessageCallback? get onMessage => _onMessage; + void set onMessage(WebMessageCallback? value) => _onMessage = value; + + MacOSWebMessageChannel get webMessageChannel => _webMessageChannel; + void set webMessageChannel(MacOSWebMessageChannel value) => + _webMessageChannel = value; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_storage/main.dart b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_storage/main.dart new file mode 100644 index 00000000..7265ae52 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_storage/main.dart @@ -0,0 +1,2 @@ +export 'web_storage.dart'; +export 'web_storage_manager.dart' hide InternalWebStorageManager; diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_storage/web_storage.dart b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_storage/web_storage.dart new file mode 100644 index 00000000..b8e65179 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_storage/web_storage.dart @@ -0,0 +1,257 @@ +import 'dart:convert'; + +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +import '../in_app_webview/in_app_webview_controller.dart'; + +/// Object specifying creation parameters for creating a [MacOSWebStorage]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebStorageCreationParams] for +/// more information. +class MacOSWebStorageCreationParams extends PlatformWebStorageCreationParams { + /// Creates a new [MacOSWebStorageCreationParams] instance. + MacOSWebStorageCreationParams( + {required super.localStorage, required super.sessionStorage}); + + /// Creates a [MacOSWebStorageCreationParams] instance based on [PlatformWebStorageCreationParams]. + factory MacOSWebStorageCreationParams.fromPlatformWebStorageCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformWebStorageCreationParams params) { + return MacOSWebStorageCreationParams( + localStorage: params.localStorage, + sessionStorage: params.sessionStorage); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformWebStorage} +class MacOSWebStorage extends PlatformWebStorage { + /// Constructs a [MacOSWebStorage]. + MacOSWebStorage(PlatformWebStorageCreationParams params) + : super.implementation( + params is MacOSWebStorageCreationParams + ? params + : MacOSWebStorageCreationParams + .fromPlatformWebStorageCreationParams(params), + ); + + @override + PlatformLocalStorage get localStorage => params.localStorage; + + @override + PlatformSessionStorage get sessionStorage => params.sessionStorage; + + @override + void dispose() { + localStorage.dispose(); + sessionStorage.dispose(); + } +} + +/// Object specifying creation parameters for creating a [MacOSStorage]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformStorageCreationParams] for +/// more information. +class MacOSStorageCreationParams extends PlatformStorageCreationParams { + /// Creates a new [MacOSStorageCreationParams] instance. + MacOSStorageCreationParams( + {required super.controller, required super.webStorageType}); + + /// Creates a [MacOSStorageCreationParams] instance based on [PlatformStorageCreationParams]. + factory MacOSStorageCreationParams.fromPlatformStorageCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformStorageCreationParams params) { + return MacOSStorageCreationParams( + controller: params.controller, webStorageType: params.webStorageType); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformStorage} +abstract mixin class MacOSStorage implements PlatformStorage { + @override + MacOSInAppWebViewController? controller; + + @override + Future length() async { + var result = await controller?.evaluateJavascript(source: """ + window.$webStorageType.length; + """); + return result != null ? int.parse(json.decode(result)) : null; + } + + @override + Future setItem({required String key, required dynamic value}) async { + var encodedValue = json.encode(value); + await controller?.evaluateJavascript(source: """ + window.$webStorageType.setItem("$key", ${value is String ? encodedValue : "JSON.stringify($encodedValue)"}); + """); + } + + @override + Future getItem({required String key}) async { + var itemValue = await controller?.evaluateJavascript(source: """ + window.$webStorageType.getItem("$key"); + """); + + if (itemValue == null) { + return null; + } + + try { + return json.decode(itemValue); + } catch (e) {} + + return itemValue; + } + + @override + Future removeItem({required String key}) async { + await controller?.evaluateJavascript(source: """ + window.$webStorageType.removeItem("$key"); + """); + } + + @override + Future> getItems() async { + var webStorageItems = []; + + List>? items = + (await controller?.evaluateJavascript(source: """ +(function() { + var webStorageItems = []; + for(var i = 0; i < window.$webStorageType.length; i++){ + var key = window.$webStorageType.key(i); + webStorageItems.push( + { + key: key, + value: window.$webStorageType.getItem(key) + } + ); + } + return webStorageItems; +})(); + """))?.cast>(); + + if (items == null) { + return webStorageItems; + } + + for (var item in items) { + webStorageItems + .add(WebStorageItem(key: item["key"], value: item["value"])); + } + + return webStorageItems; + } + + @override + Future clear() async { + await controller?.evaluateJavascript(source: """ + window.$webStorageType.clear(); + """); + } + + @override + Future key({required int index}) async { + var result = await controller?.evaluateJavascript(source: """ + window.$webStorageType.key($index); + """); + return result != null ? json.decode(result) : null; + } + + @override + void dispose() { + controller = null; + } +} + +/// Object specifying creation parameters for creating a [MacOSLocalStorage]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformLocalStorageCreationParams] for +/// more information. +class MacOSLocalStorageCreationParams + extends PlatformLocalStorageCreationParams { + /// Creates a new [MacOSLocalStorageCreationParams] instance. + MacOSLocalStorageCreationParams(super.params); + + /// Creates a [MacOSLocalStorageCreationParams] instance based on [PlatformLocalStorageCreationParams]. + factory MacOSLocalStorageCreationParams.fromPlatformLocalStorageCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformLocalStorageCreationParams params) { + return MacOSLocalStorageCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformLocalStorage} +class MacOSLocalStorage extends PlatformLocalStorage with MacOSStorage { + /// Constructs a [MacOSLocalStorage]. + MacOSLocalStorage(PlatformLocalStorageCreationParams params) + : super.implementation( + params is MacOSLocalStorageCreationParams + ? params + : MacOSLocalStorageCreationParams + .fromPlatformLocalStorageCreationParams(params), + ); + + /// Default storage + factory MacOSLocalStorage.defaultStorage( + {required PlatformInAppWebViewController? controller}) { + return MacOSLocalStorage(MacOSLocalStorageCreationParams( + PlatformLocalStorageCreationParams(PlatformStorageCreationParams( + controller: controller, + webStorageType: WebStorageType.LOCAL_STORAGE)))); + } + + @override + MacOSInAppWebViewController? get controller => + params.controller as MacOSInAppWebViewController?; +} + +/// Object specifying creation parameters for creating a [MacOSSessionStorage]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformSessionStorageCreationParams] for +/// more information. +class MacOSSessionStorageCreationParams + extends PlatformSessionStorageCreationParams { + /// Creates a new [MacOSSessionStorageCreationParams] instance. + MacOSSessionStorageCreationParams(super.params); + + /// Creates a [MacOSSessionStorageCreationParams] instance based on [PlatformSessionStorageCreationParams]. + factory MacOSSessionStorageCreationParams.fromPlatformSessionStorageCreationParams( + // Recommended placeholder to prevent being broken by platform interface. + // ignore: avoid_unused_constructor_parameters + PlatformSessionStorageCreationParams params) { + return MacOSSessionStorageCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformSessionStorage} +class MacOSSessionStorage extends PlatformSessionStorage with MacOSStorage { + /// Constructs a [MacOSSessionStorage]. + MacOSSessionStorage(PlatformSessionStorageCreationParams params) + : super.implementation( + params is MacOSSessionStorageCreationParams + ? params + : MacOSSessionStorageCreationParams + .fromPlatformSessionStorageCreationParams(params), + ); + + /// Default storage + factory MacOSSessionStorage.defaultStorage( + {required PlatformInAppWebViewController? controller}) { + return MacOSSessionStorage(MacOSSessionStorageCreationParams( + PlatformSessionStorageCreationParams(PlatformStorageCreationParams( + controller: controller, + webStorageType: WebStorageType.SESSION_STORAGE)))); + } + + @override + MacOSInAppWebViewController? get controller => + params.controller as MacOSInAppWebViewController?; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_storage/web_storage_manager.dart b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_storage/web_storage_manager.dart new file mode 100755 index 00000000..b281f0d7 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/lib/src/web_storage/web_storage_manager.dart @@ -0,0 +1,134 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_inappwebview_platform_interface/flutter_inappwebview_platform_interface.dart'; + +/// Object specifying creation parameters for creating a [MacOSWebStorageManager]. +/// +/// When adding additional fields make sure they can be null or have a default +/// value to avoid breaking changes. See [PlatformWebStorageManagerCreationParams] for +/// more information. +@immutable +class MacOSWebStorageManagerCreationParams + extends PlatformWebStorageManagerCreationParams { + /// Creates a new [MacOSWebStorageManagerCreationParams] instance. + const MacOSWebStorageManagerCreationParams( + // This parameter prevents breaking changes later. + // ignore: avoid_unused_constructor_parameters + PlatformWebStorageManagerCreationParams params, + ) : super(); + + /// Creates a [MacOSWebStorageManagerCreationParams] instance based on [PlatformWebStorageManagerCreationParams]. + factory MacOSWebStorageManagerCreationParams.fromPlatformWebStorageManagerCreationParams( + PlatformWebStorageManagerCreationParams params) { + return MacOSWebStorageManagerCreationParams(params); + } +} + +///{@macro flutter_inappwebview_platform_interface.PlatformWebStorageManager} +class MacOSWebStorageManager extends PlatformWebStorageManager + with ChannelController { + /// Creates a new [MacOSWebStorageManager]. + MacOSWebStorageManager(PlatformWebStorageManagerCreationParams params) + : super.implementation( + params is MacOSWebStorageManagerCreationParams + ? params + : MacOSWebStorageManagerCreationParams + .fromPlatformWebStorageManagerCreationParams(params), + ) { + channel = const MethodChannel( + 'com.pichillilorenzo/flutter_inappwebview_webstoragemanager'); + handler = handleMethod; + initMethodCallHandler(); + } + + static MacOSWebStorageManager? _instance; + + ///Gets the WebStorage manager shared instance. + static MacOSWebStorageManager instance() { + return (_instance != null) ? _instance! : _init(); + } + + static MacOSWebStorageManager _init() { + _instance = MacOSWebStorageManager(MacOSWebStorageManagerCreationParams( + const PlatformWebStorageManagerCreationParams())); + return _instance!; + } + + Future _handleMethod(MethodCall call) async {} + + @override + Future> fetchDataRecords( + {required Set dataTypes}) async { + List recordList = []; + List dataTypesList = []; + for (var dataType in dataTypes) { + dataTypesList.add(dataType.toNativeValue()); + } + Map args = {}; + args.putIfAbsent("dataTypes", () => dataTypesList); + List> records = + (await channel?.invokeMethod('fetchDataRecords', args)) + ?.cast>() ?? + []; + for (var record in records) { + List dataTypesString = record["dataTypes"].cast(); + Set dataTypes = Set(); + for (var dataTypeValue in dataTypesString) { + var dataType = WebsiteDataType.fromNativeValue(dataTypeValue); + if (dataType != null) { + dataTypes.add(dataType); + } + } + recordList.add(WebsiteDataRecord( + displayName: record["displayName"], dataTypes: dataTypes)); + } + return recordList; + } + + @override + Future removeDataFor( + {required Set dataTypes, + required List dataRecords}) async { + List dataTypesList = []; + for (var dataType in dataTypes) { + dataTypesList.add(dataType.toNativeValue()); + } + + List> recordList = []; + for (var record in dataRecords) { + recordList.add(record.toMap()); + } + + Map args = {}; + args.putIfAbsent("dataTypes", () => dataTypesList); + args.putIfAbsent("recordList", () => recordList); + await channel?.invokeMethod('removeDataFor', args); + } + + @override + Future removeDataModifiedSince( + {required Set dataTypes, required DateTime date}) async { + List dataTypesList = []; + for (var dataType in dataTypes) { + dataTypesList.add(dataType.toNativeValue()); + } + + var timestamp = date.millisecondsSinceEpoch; + + Map args = {}; + args.putIfAbsent("dataTypes", () => dataTypesList); + args.putIfAbsent("timestamp", () => timestamp); + await channel?.invokeMethod('removeDataModifiedSince', args); + } + + @override + void dispose() { + // empty + } +} + +extension InternalWebStorageManager on MacOSWebStorageManager { + get handleMethod => _handleMethod; +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/CredentialDatabase.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/CredentialDatabase.swift new file mode 100755 index 00000000..c11ac05b --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/CredentialDatabase.swift @@ -0,0 +1,172 @@ +// +// CredentialDatabase.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 29/10/2019. +// + +import Foundation +import FlutterMacOS + +public class CredentialDatabase: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_credential_database" + var plugin: InAppWebViewFlutterPlugin? + static var credentialStore = URLCredentialStorage.shared + + init(plugin: InAppWebViewFlutterPlugin) { + super.init(channel: FlutterMethodChannel(name: CredentialDatabase.METHOD_CHANNEL_NAME, binaryMessenger: plugin.registrar!.messenger)) + self.plugin = plugin + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + switch call.method { + case "getAllAuthCredentials": + var allCredentials: [[String: Any?]] = [] + + for (protectionSpace, credentials) in CredentialDatabase.credentialStore.allCredentials { + var crendentials: [[String: Any?]] = [] + for c in credentials { + let credential: [String: Any?] = c.value.toMap() + crendentials.append(credential) + } + if crendentials.count > 0 { + let dict: [String : Any] = [ + "protectionSpace": protectionSpace.toMap(), + "credentials": crendentials + ] + allCredentials.append(dict) + } + } + result(allCredentials) + break + case "getHttpAuthCredentials": + var crendentials: [[String: Any?]] = [] + + let host = arguments!["host"] as! String + let urlProtocol = arguments!["protocol"] as? String + let urlPort = arguments!["port"] as? Int ?? 0 + var realm = arguments!["realm"] as? String; + if let r = realm, r.isEmpty { + realm = nil + } + + for (protectionSpace, credentials) in CredentialDatabase.credentialStore.allCredentials { + if protectionSpace.host == host && protectionSpace.realm == realm && + protectionSpace.protocol == urlProtocol && protectionSpace.port == urlPort { + for c in credentials { + crendentials.append(c.value.toMap()) + } + break + } + } + result(crendentials) + break + case "setHttpAuthCredential": + let host = arguments!["host"] as! String + let urlProtocol = arguments!["protocol"] as? String + let urlPort = arguments!["port"] as? Int ?? 0 + var realm = arguments!["realm"] as? String; + if let r = realm, r.isEmpty { + realm = nil + } + let username = arguments!["username"] as! String + let password = arguments!["password"] as! String + let credential = URLCredential(user: username, password: password, persistence: .permanent) + CredentialDatabase.credentialStore.set(credential, + for: URLProtectionSpace(host: host, port: urlPort, protocol: urlProtocol, + realm: realm, authenticationMethod: NSURLAuthenticationMethodHTTPBasic)) + result(true) + break + case "removeHttpAuthCredential": + let host = arguments!["host"] as! String + let urlProtocol = arguments!["protocol"] as? String + let urlPort = arguments!["port"] as? Int ?? 0 + var realm = arguments!["realm"] as? String; + if let r = realm, r.isEmpty { + realm = nil + } + let username = arguments!["username"] as! String + let password = arguments!["password"] as! String + + var credential: URLCredential? = nil; + var protectionSpaceCredential: URLProtectionSpace? = nil + + for (protectionSpace, credentials) in CredentialDatabase.credentialStore.allCredentials { + if protectionSpace.host == host && protectionSpace.realm == realm && + protectionSpace.protocol == urlProtocol && protectionSpace.port == urlPort { + for c in credentials { + if c.value.user == username, c.value.password == password { + credential = c.value + protectionSpaceCredential = protectionSpace + break + } + } + } + if credential != nil { + break + } + } + + if let c = credential, let protectionSpace = protectionSpaceCredential { + CredentialDatabase.credentialStore.remove(c, for: protectionSpace) + } + + result(true) + break + case "removeHttpAuthCredentials": + let host = arguments!["host"] as! String + let urlProtocol = arguments!["protocol"] as? String + let urlPort = arguments!["port"] as? Int ?? 0 + var realm = arguments!["realm"] as? String; + if let r = realm, r.isEmpty { + realm = nil + } + + var credentialsToRemove: [URLCredential] = []; + var protectionSpaceCredential: URLProtectionSpace? = nil + + for (protectionSpace, credentials) in CredentialDatabase.credentialStore.allCredentials { + if protectionSpace.host == host && protectionSpace.realm == realm && + protectionSpace.protocol == urlProtocol && protectionSpace.port == urlPort { + protectionSpaceCredential = protectionSpace + for c in credentials { + if let _ = c.value.user, let _ = c.value.password { + credentialsToRemove.append(c.value) + } + } + break + } + } + + if let protectionSpace = protectionSpaceCredential { + for credential in credentialsToRemove { + CredentialDatabase.credentialStore.remove(credential, for: protectionSpace) + } + } + + result(true) + break + case "clearAllAuthCredentials": + for (protectionSpace, credentials) in CredentialDatabase.credentialStore.allCredentials { + for credential in credentials { + CredentialDatabase.credentialStore.remove(credential.value, for: protectionSpace) + } + } + result(true) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public override func dispose() { + super.dispose() + plugin = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/FindInteraction/FindInteractionChannelDelegate.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/FindInteraction/FindInteractionChannelDelegate.swift new file mode 100644 index 00000000..e471d286 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/FindInteraction/FindInteractionChannelDelegate.swift @@ -0,0 +1,112 @@ +// +// FindInteractionChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/10/22. +// + +import Foundation +import FlutterMacOS + +public class FindInteractionChannelDelegate: ChannelDelegate { + private weak var findInteractionController: FindInteractionController? + + public init(findInteractionController: FindInteractionController, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.findInteractionController = findInteractionController + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "findAll": + if let findInteractionController = findInteractionController { + let find = arguments!["find"] as! String + findInteractionController.findAll(find: find, completionHandler: {(value, error) in + if error != nil { + result(FlutterError(code: "FindInteractionChannelDelegate", message: error?.localizedDescription, details: nil)) + return + } + result(true) + }) + } else { + result(false) + } + break + case "findNext": + if let findInteractionController = findInteractionController { + let forward = arguments!["forward"] as! Bool + findInteractionController.findNext(forward: forward, completionHandler: {(value, error) in + if error != nil { + result(FlutterError(code: "FindInteractionChannelDelegate", message: error?.localizedDescription, details: nil)) + return + } + result(true) + }) + } else { + result(false) + } + break + case "clearMatches": + if let findInteractionController = findInteractionController { + findInteractionController.clearMatches(completionHandler: {(value, error) in + if error != nil { + result(FlutterError(code: "FindInteractionChannelDelegate", message: error?.localizedDescription, details: nil)) + return + } + result(true) + }) + } else { + result(false) + } + break + case "setSearchText": + if let findInteractionController = findInteractionController { + let searchText = arguments!["searchText"] as? String + findInteractionController.searchText = searchText + result(true) + } else { + result(false) + } + break + case "getSearchText": + result(findInteractionController?.searchText) + break + case "getActiveFindSession": + if let findInteractionController = findInteractionController { + result(findInteractionController.activeFindSession?.toMap()) + } else { + result(nil) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onFindResultReceived(activeMatchOrdinal: Int, numberOfMatches: Int, isDoneCounting: Bool) { + if isDoneCounting, let findInteractionController = findInteractionController { + findInteractionController.activeFindSession = FindSession(resultCount: numberOfMatches, + highlightedResultIndex: activeMatchOrdinal, + searchResultDisplayStyle: 2) // matches UIFindSession.SearchResultDisplayStyle.none + } + + let arguments: [String : Any?] = [ + "activeMatchOrdinal": activeMatchOrdinal, + "numberOfMatches": numberOfMatches, + "isDoneCounting": isDoneCounting + ] + channel?.invokeMethod("onFindResultReceived", arguments: arguments) + } + + public override func dispose() { + super.dispose() + findInteractionController = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/FindInteraction/FindInteractionController.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/FindInteraction/FindInteractionController.swift new file mode 100644 index 00000000..68bcae59 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/FindInteraction/FindInteractionController.swift @@ -0,0 +1,101 @@ +// +// FindInteractionController.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/10/22. +// + +import Foundation +import FlutterMacOS + +public class FindInteractionController: NSObject, Disposable { + + static var METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_find_interaction_"; + var plugin: InAppWebViewFlutterPlugin? + var webView: InAppWebView? + var channelDelegate: FindInteractionChannelDelegate? + var settings: FindInteractionSettings? + var shouldCallOnRefresh = false + var searchText: String? + var activeFindSession: FindSession? + + public init(plugin: InAppWebViewFlutterPlugin, id: Any, webView: InAppWebView, settings: FindInteractionSettings?) { + super.init() + self.plugin = plugin + self.webView = webView + self.settings = settings + if let registrar = plugin.registrar { + let channel = FlutterMethodChannel(name: FindInteractionController.METHOD_CHANNEL_NAME_PREFIX + String(describing: id), + binaryMessenger: registrar.messenger) + self.channelDelegate = FindInteractionChannelDelegate(findInteractionController: self, channel: channel) + } + } + + public func prepare() { +// if let settings = settings { +// +// } + } + + public func findAll(find: String?, completionHandler: ((Any?, Error?) -> Void)?) { + guard let webView else { + if let completionHandler = completionHandler { + completionHandler(nil, nil) + } + return + } + + var find = find + if find == nil { + find = searchText + } else { + // updated searchText + searchText = find + } + + guard let find else { + if let completionHandler = completionHandler { + completionHandler(nil, nil) + } + return + } + + if find != "" { + let startSearch = "window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsync('\(find)');" + webView.evaluateJavaScript(startSearch, completionHandler: completionHandler) + } + } + + public func findNext(forward: Bool, completionHandler: ((Any?, Error?) -> Void)?) { + guard let webView else { + if let completionHandler = completionHandler { + completionHandler(nil, nil) + } + return + } + webView.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._findNext(\(forward ? "true" : "false"));", completionHandler: completionHandler) + } + + public func clearMatches(completionHandler: ((Any?, Error?) -> Void)?) { + guard let webView else { + if let completionHandler = completionHandler { + completionHandler(nil, nil) + } + return + } + webView.evaluateJavaScript("window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatches();", completionHandler: completionHandler) + } + + public func dispose() { + channelDelegate?.dispose() + channelDelegate = nil + webView = nil + activeFindSession = nil + plugin = nil + } + + deinit { + debugPrint("FindInteractionControl - dealloc") + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/FindInteraction/FindInteractionSettings.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/FindInteraction/FindInteractionSettings.swift new file mode 100644 index 00000000..3c4ecb14 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/FindInteraction/FindInteractionSettings.swift @@ -0,0 +1,25 @@ +// +// FindInteractionSettings.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/10/22. +// + +import Foundation + +public class FindInteractionSettings: ISettings { + + override init(){ + super.init() + } + + override func parse(settings: [String: Any?]) -> FindInteractionSettings { + let _ = super.parse(settings: settings) + return self + } + + override func getRealSettings(obj: FindInteractionController?) -> [String: Any?] { + let realSettings: [String: Any?] = toMap() + return realSettings + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/GopeedProfiles.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/GopeedProfiles.swift new file mode 100644 index 00000000..fff25b8c --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/GopeedProfiles.swift @@ -0,0 +1,29 @@ +// Internal Gopeed host bridge. No extension-facing API. +import Foundation +import WebKit +import Network +import FlutterMacOS + +final class GopeedProfiles { + private static var stores: [String: WKWebsiteDataStore] = [:] + static func store(_ identifier: String) -> WKWebsiteDataStore? { stores[identifier] } + + static func prepare(arguments: NSDictionary?, result: @escaping FlutterResult) { + guard #available(macOS 14.0, iOS 17.0, *), + let identifier = arguments?["gopeedProfileId"] as? String, + let uuid = UUID(uuidString: identifier), + let proxyURL = arguments?["proxyUrl"] as? String, + let url = URL(string: proxyURL), url.scheme == "socks5", + let host = url.host, host == "127.0.0.1", + let port = url.port, port > 0 && port <= 65535 else { + result(FlutterError(code: "UNAVAILABLE", message: "Isolated WebView profiles and proxy require macOS 14 / iOS 17 or newer and valid host configuration", details: nil)) + return + } + let store = stores[identifier] ?? WKWebsiteDataStore(forIdentifier: uuid) + let endpoint = nw_endpoint_create_host(host, String(port)) + let proxy = nw_proxy_config_create_socksv5(endpoint) + store.__proxyConfigurations = [proxy] + stores[identifier] = store + result(true) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/HeadlessInAppWebView/HeadlessInAppWebView.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/HeadlessInAppWebView/HeadlessInAppWebView.swift new file mode 100644 index 00000000..b95222fb --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/HeadlessInAppWebView/HeadlessInAppWebView.swift @@ -0,0 +1,100 @@ +// +// HeadlessInAppWebView.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 26/03/21. +// + +import Foundation +import FlutterMacOS + +public class HeadlessInAppWebView: Disposable { + static let METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_headless_inappwebview_" + var id: String + var channelDelegate: HeadlessWebViewChannelDelegate? + var flutterWebView: FlutterWebViewController? + var plugin: InAppWebViewFlutterPlugin? + + public init(plugin: InAppWebViewFlutterPlugin, id: String, flutterWebView: FlutterWebViewController) { + self.id = id + self.flutterWebView = flutterWebView + self.plugin = plugin + let channel = FlutterMethodChannel(name: HeadlessInAppWebView.METHOD_CHANNEL_NAME_PREFIX + id, + binaryMessenger: plugin.registrar!.messenger) + self.channelDelegate = HeadlessWebViewChannelDelegate(headlessWebView: self, channel: channel) + } + + public func onWebViewCreated() { + channelDelegate?.onWebViewCreated(); + } + + public func prepare(params: NSDictionary) { + if let view = flutterWebView?.view() { + view.alphaValue = 0.01 + let initialSize = params["initialSize"] as? [String: Any?] + if let size = Size2D.fromMap(map: initialSize) { + setSize(size: size) + } else { + view.frame = CGRect(x: 0.0, y: 0.0, width: NSApplication.shared.mainWindow?.contentView?.bounds.width ?? 0.0, + height: NSApplication.shared.mainWindow?.contentView?.bounds.height ?? 0.0) + } + /// Note: The WKWebView behaves very unreliable when rendering offscreen + /// on a device. This is especially true with JavaScript, which simply + /// won't be executed sometimes. + /// So, add the headless WKWebView to the view hierarchy. + /// This way is also possible to take screenshots. + let wrapperView = NSView() // wrapper view with frame zero + wrapperView.addSubview(view, positioned: .below, relativeTo: nil) + NSApplication.shared.mainWindow?.contentView?.addSubview(wrapperView, positioned: .below, relativeTo: nil) + } + } + + public func setSize(size: Size2D) { + if let view = flutterWebView?.view() { + let width = size.width == -1.0 ? NSApplication.shared.mainWindow?.contentView?.bounds.width ?? 0.0 : CGFloat(size.width) + let height = size.height == -1.0 ? NSApplication.shared.mainWindow?.contentView?.bounds.height ?? 0.0 : CGFloat(size.height) + view.frame = CGRect(x: 0.0, y: 0.0, width: width, height: height) + } + } + + public func getSize() -> Size2D? { + if let view = flutterWebView?.view() { + return Size2D(width: Double(view.frame.width), height: Double(view.frame.height)) + } + return nil + } + + public func disposeAndGetFlutterWebView(withFrame frame: CGRect) -> FlutterWebViewController? { + let newFlutterWebView = flutterWebView + if let view = flutterWebView?.view() { + // restore WebView frame and alpha + view.frame = frame + view.alphaValue = 1.0 + // remove from parent + view.removeFromSuperview() + dispose(disposeWebView: false) + } + return newFlutterWebView + } + + + public func dispose(disposeWebView: Bool) { + channelDelegate?.dispose() + channelDelegate = nil + plugin?.headlessInAppWebViewManager?.webViews[id] = nil + if disposeWebView { + flutterWebView?.dispose(removeFromSuperview: true) + } + flutterWebView = nil + plugin = nil + } + + public func dispose() { + dispose(disposeWebView: true) + } + + deinit { + debugPrint("HeadlessInAppWebView - dealloc") + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/HeadlessInAppWebView/HeadlessInAppWebViewManager.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/HeadlessInAppWebView/HeadlessInAppWebViewManager.swift new file mode 100644 index 00000000..29bed518 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/HeadlessInAppWebView/HeadlessInAppWebViewManager.swift @@ -0,0 +1,71 @@ +// +// HeadlessInAppWebViewManager.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/05/2020. +// + +import Foundation + +import FlutterMacOS +import AppKit +import WebKit +import Foundation +import AVFoundation + +public class HeadlessInAppWebViewManager: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_headless_inappwebview" + var plugin: InAppWebViewFlutterPlugin? + var webViews: [String: HeadlessInAppWebView?] = [:] + + init(plugin: InAppWebViewFlutterPlugin) { + super.init(channel: FlutterMethodChannel(name: HeadlessInAppWebViewManager.METHOD_CHANNEL_NAME, binaryMessenger: plugin.registrar!.messenger)) + self.plugin = plugin + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + let id: String = arguments!["id"] as! String + + switch call.method { + case "run": + let params = arguments!["params"] as! [String: Any?] + run(id: id, params: params) + result(true) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func run(id: String, params: [String: Any?]) { + guard let plugin = plugin else { + return + } + let flutterWebView = FlutterWebViewController(plugin: plugin, + withFrame: CGRect.zero, + viewIdentifier: id, + params: params as NSDictionary) + let headlessInAppWebView = HeadlessInAppWebView(plugin: plugin, id: id, flutterWebView: flutterWebView) + webViews[id] = headlessInAppWebView + + headlessInAppWebView.prepare(params: params as NSDictionary) + headlessInAppWebView.onWebViewCreated() + flutterWebView.makeInitialLoad(params: params as NSDictionary) + } + + public override func dispose() { + super.dispose() + let headlessWebViews = webViews.values + headlessWebViews.forEach { (headlessWebView: HeadlessInAppWebView?) in + headlessWebView?.dispose() + } + webViews.removeAll() + plugin = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/HeadlessInAppWebView/HeadlessWebViewChannelDelegate.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/HeadlessInAppWebView/HeadlessWebViewChannelDelegate.swift new file mode 100644 index 00000000..9b4ee63e --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/HeadlessInAppWebView/HeadlessWebViewChannelDelegate.swift @@ -0,0 +1,64 @@ +// +// HeadlessWebViewChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 05/05/22. +// + +import Foundation +import FlutterMacOS + +public class HeadlessWebViewChannelDelegate: ChannelDelegate { + private weak var headlessWebView: HeadlessInAppWebView? + + public init(headlessWebView: HeadlessInAppWebView, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.headlessWebView = headlessWebView + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "dispose": + if let headlessWebView = headlessWebView { + headlessWebView.dispose() + result(true) + } else { + result(false) + } + break + case "setSize": + if let headlessWebView = headlessWebView { + let sizeMap = arguments!["size"] as? [String: Any?] + if let size = Size2D.fromMap(map: sizeMap) { + headlessWebView.setSize(size: size) + } + result(true) + } else { + result(false) + } + break + case "getSize": + result(headlessWebView?.getSize()?.toMap()) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onWebViewCreated() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onWebViewCreated", arguments: arguments) + } + + public override func dispose() { + super.dispose() + headlessWebView = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/ISettings.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/ISettings.swift new file mode 100755 index 00000000..1e21d4e8 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/ISettings.swift @@ -0,0 +1,45 @@ +// +// Options.swift +// flutter_inappwebview +// +// Created by Lorenzo on 26/09/18. +// + +import Foundation + +@objcMembers +public class ISettings: NSObject { + + override init(){ + super.init() + } + + func parse(settings: [String: Any?]) -> ISettings { + for (key, value) in settings { + if !(value is NSNull), value != nil { + if self.responds(to: Selector(key)) { + self.setValue(value, forKey: key) + } else if self.responds(to: Selector("_" + key)) { + self.setValue(value, forKey: "_" + key) + } + } + } + return self + } + + func toMap() -> [String: Any?] { + var settings: [String: Any?] = [:] + let mirrored_object = Mirror(reflecting: self) + for (_, attr) in mirrored_object.children.enumerated() { + if let property_name = attr.label as String? { + settings[property_name] = attr.value + } + } + return settings + } + + func getRealSettings(obj: T?) -> [String: Any?] { + let realSettings: [String: Any?] = toMap() + return realSettings + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppBrowser/InAppBrowserChannelDelegate.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppBrowser/InAppBrowserChannelDelegate.swift new file mode 100644 index 00000000..19dc499f --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppBrowser/InAppBrowserChannelDelegate.swift @@ -0,0 +1,41 @@ +// +// InAppBrowserChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 05/05/22. +// + +import Foundation +import FlutterMacOS + +public class InAppBrowserChannelDelegate: ChannelDelegate { + public override init(channel: FlutterMethodChannel) { + super.init(channel: channel) + } + + public func onBrowserCreated() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onBrowserCreated", arguments: arguments) + } + + public func onMenuItemClicked(menuItem: InAppBrowserMenuItem) { + let arguments: [String: Any?] = [ + "id": menuItem.id + ] + channel?.invokeMethod("onMenuItemClicked", arguments: arguments) + } + + public func onMainWindowWillClose() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onMainWindowWillClose", arguments: arguments) + } + + public func onExit() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onExit", arguments: arguments) + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppBrowser/InAppBrowserDelegate.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppBrowser/InAppBrowserDelegate.swift new file mode 100644 index 00000000..ba013c58 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppBrowser/InAppBrowserDelegate.swift @@ -0,0 +1,17 @@ +// +// InAppBrowserDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 14/02/21. +// + +import Foundation + +public protocol InAppBrowserDelegate { + func didChangeTitle(title: String?) + func didStartNavigation(url: URL?) + func didUpdateVisitedHistory(url: URL?) + func didFinishNavigation(url: URL?) + func didFailNavigation(url: URL?, error: Error) + func didChangeProgress(progress: Double) +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppBrowser/InAppBrowserManager.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppBrowser/InAppBrowserManager.swift new file mode 100755 index 00000000..9f3d5c53 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppBrowser/InAppBrowserManager.swift @@ -0,0 +1,122 @@ +// +// InAppBrowserManager.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 18/12/2019. +// + +import FlutterMacOS +import AppKit +import WebKit +import Foundation +import AVFoundation + +public class InAppBrowserManager: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappbrowser" + static let WEBVIEW_STORYBOARD = "WebView" + static let WEBVIEW_STORYBOARD_CONTROLLER_ID = "viewController" + static let NAV_STORYBOARD_CONTROLLER_ID = "navController" + var plugin: InAppWebViewFlutterPlugin? + + init(plugin: InAppWebViewFlutterPlugin) { + super.init(channel: FlutterMethodChannel(name: InAppBrowserManager.METHOD_CHANNEL_NAME, binaryMessenger: plugin.registrar!.messenger)) + self.plugin = plugin + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "open": + open(arguments: arguments!) + result(true) + break + case "openWithSystemBrowser": + let url = arguments!["url"] as! String + openWithSystemBrowser(url: url, result: result) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func open(arguments: NSDictionary) { + let id = arguments["id"] as! String + let urlRequest = arguments["urlRequest"] as? [String:Any?] + let assetFilePath = arguments["assetFilePath"] as? String + let data = arguments["data"] as? String + let mimeType = arguments["mimeType"] as? String + let encoding = arguments["encoding"] as? String + let baseUrl = arguments["baseUrl"] as? String + let settings = arguments["settings"] as! [String: Any?] + let windowId = arguments["windowId"] as? Int64 + let initialUserScripts = arguments["initialUserScripts"] as? [[String: Any]] + let menuItems = arguments["menuItems"] as! [[String: Any?]] + + let browserSettings = InAppBrowserSettings() + let _ = browserSettings.parse(settings: settings) + + let webViewSettings = InAppWebViewSettings() + let _ = webViewSettings.parse(settings: settings) + + let webViewController = InAppBrowserWebViewController() + webViewController.plugin = plugin + webViewController.browserSettings = browserSettings + webViewController.webViewSettings = webViewSettings + + webViewController.id = id + webViewController.initialUrlRequest = urlRequest != nil ? URLRequest.init(fromPluginMap: urlRequest!) : nil + webViewController.initialFile = assetFilePath + webViewController.initialData = data + webViewController.initialMimeType = mimeType + webViewController.initialEncoding = encoding + webViewController.initialBaseUrl = baseUrl + webViewController.windowId = windowId + webViewController.initialUserScripts = initialUserScripts ?? [] + webViewController.isHidden = browserSettings.hidden + + let window = InAppBrowserWindow(contentViewController: webViewController) + window.browserSettings = browserSettings + window.contentViewController = webViewController + for menuItem in menuItems { + window.menuItems.append(InAppBrowserMenuItem.fromMap(map: menuItem)!) + } + window.prepare() + + if #available(macOS 10.12, *), browserSettings.windowType == .tabbed { + NSApplication.shared.mainWindow?.addTabbedWindow(window, ordered: .above) + } else if browserSettings.windowType == .child { + NSApplication.shared.mainWindow?.addChildWindow(window, ordered: .above) + } else { + window.windowController?.showWindow(self) + } + + window.makeKeyAndOrderFront(self) + if browserSettings.hidden { + // https://github.com/pichillilorenzo/flutter_inappwebview/issues/1939 + // without calling first window.makeKeyAndOrderFront(self) + // window.hide() would deallocate and dispose the InAppBrowserWindow + window.hide() + NSApplication.shared.mainWindow?.makeKeyAndOrderFront(self) + } + } + + public func openWithSystemBrowser(url: String, result: @escaping FlutterResult) { + let absoluteUrl = URL(string: url)!.absoluteURL + if !NSWorkspace.shared.open(absoluteUrl) { + result(FlutterError(code: "InAppBrowserManager", message: url + " cannot be opened!", details: nil)) + return + } + result(true) + } + + public override func dispose() { + super.dispose() + plugin = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppBrowser/InAppBrowserSettings.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppBrowser/InAppBrowserSettings.swift new file mode 100755 index 00000000..6f01e97d --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppBrowser/InAppBrowserSettings.swift @@ -0,0 +1,91 @@ +// +// InAppBrowserOptions.swift +// flutter_inappwebview +// +// Created by Lorenzo on 17/09/18. +// + +import Foundation + +@objcMembers +public class InAppBrowserSettings: ISettings { + + var hidden = false + var hideToolbarTop = true + var toolbarTopBackgroundColor: String? + var hideUrlBar = false + var hideProgressBar = false + var toolbarTopFixedTitle: String? + var windowType = InAppBrowserWindowType.window + var windowAlphaValue = 1.0 + var _windowStyleMask: NSNumber? + var windowStyleMask: NSWindow.StyleMask? { + get { + return _windowStyleMask != nil ? + NSWindow.StyleMask.init(rawValue: _windowStyleMask!.uintValue) : + nil + } + set { + if let newValue = newValue { + _windowStyleMask = NSNumber.init(value: newValue.rawValue) + } else { + _windowStyleMask = nil + } + } + } + var _windowTitlebarSeparatorStyle: NSNumber? + @available(macOS 11.0, *) + var windowTitlebarSeparatorStyle: NSTitlebarSeparatorStyle? { + get { + return _windowTitlebarSeparatorStyle != nil ? + NSTitlebarSeparatorStyle.init(rawValue: _windowTitlebarSeparatorStyle!.intValue) : + nil + } + set { + if let newValue = newValue { + _windowTitlebarSeparatorStyle = NSNumber.init(value: newValue.rawValue) + } else { + _windowTitlebarSeparatorStyle = nil + } + } + } + var windowFrame: NSRect? + var hideDefaultMenuItems = false + + override init(){ + super.init() + } + + override func parse(settings: [String: Any?]) -> InAppBrowserSettings { + let _ = super.parse(settings: settings) + if let windowType = settings["windowType"] as? String { + self.windowType = InAppBrowserWindowType.init(rawValue: windowType) ?? InAppBrowserWindowType.child + } + if let windowFrame = settings["windowFrame"] as? [String:Double] { + self.windowFrame = NSRect(x: windowFrame["x"]!, + y: windowFrame["y"]!, + width: windowFrame["width"]!, + height: windowFrame["height"]!) + } + return self + } + + override func getRealSettings(obj: InAppBrowserWebViewController?) -> [String: Any?] { + var realOptions: [String: Any?] = toMap() + realOptions["windowType"] = windowType.rawValue + if let inAppBrowserWebViewController = obj { + realOptions["hidden"] = inAppBrowserWebViewController.isHidden + realOptions["hideUrlBar"] = inAppBrowserWebViewController.window?.searchBar?.isHidden + realOptions["hideProgressBar"] = inAppBrowserWebViewController.progressBar.isHidden + realOptions["hideToolbarTop"] = !(inAppBrowserWebViewController.window?.toolbar?.isVisible ?? true) + realOptions["toolbarTopBackgroundColor"] = inAppBrowserWebViewController.window?.backgroundColor.hexString + realOptions["windowAlphaValue"] = inAppBrowserWebViewController.window?.alphaValue + realOptions["windowStyleMask"] = inAppBrowserWebViewController.window?.styleMask.rawValue + if #available(macOS 11.0, *) { + realOptions["windowTitlebarSeparatorStyle"] = inAppBrowserWebViewController.window?.titlebarSeparatorStyle.rawValue + } + realOptions["windowFrame"] = inAppBrowserWebViewController.window?.frame.toMap() + } + return realOptions + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppBrowser/InAppBrowserWebViewController.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppBrowser/InAppBrowserWebViewController.swift new file mode 100755 index 00000000..48b16e93 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppBrowser/InAppBrowserWebViewController.swift @@ -0,0 +1,339 @@ +// +// InAppBrowserWebViewController.swift +// flutter_inappwebview +// +// Created by Lorenzo on 17/09/18. +// + +import FlutterMacOS +import AppKit +import WebKit +import Foundation + +public class InAppBrowserWebViewController: NSViewController, InAppBrowserDelegate, Disposable { + static var METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappbrowser_"; + + var progressBar: NSProgressIndicator! + + var window: InAppBrowserWindow? + var id: String = "" + var plugin: InAppWebViewFlutterPlugin? + var windowId: Int64? + var webView: InAppWebView? + var channelDelegate: InAppBrowserChannelDelegate? + var initialUrlRequest: URLRequest? + var initialFile: String? + var browserSettings: InAppBrowserSettings? + var webViewSettings: InAppWebViewSettings? + var initialData: String? + var initialMimeType: String? + var initialEncoding: String? + var initialBaseUrl: String? + var initialUserScripts: [[String: Any]] = [] + var isHidden = false + + public override func loadView() { + guard let plugin = plugin, let registrar = plugin.registrar else { + return + } + + let channel = FlutterMethodChannel(name: InAppBrowserWebViewController.METHOD_CHANNEL_NAME_PREFIX + id, binaryMessenger: registrar.messenger) + channelDelegate = InAppBrowserChannelDelegate(channel: channel) + + var userScripts: [UserScript] = [] + for initialUserScript in initialUserScripts { + userScripts.append(UserScript.fromMap(map: initialUserScript, windowId: windowId)!) + } + + let preWebviewConfiguration = InAppWebView.preWKWebViewConfiguration(settings: webViewSettings) + if let wId = windowId, let webViewTransport = plugin.inAppWebViewManager?.windowWebViews[wId] { + webView = webViewTransport.webView + webView!.initialUserScripts = userScripts + } else { + webView = InAppWebView(id: nil, + plugin: nil, + frame: .zero, + configuration: preWebviewConfiguration, + userScripts: userScripts) + } + + guard let webView = webView else { + return + } + + webView.inAppBrowserDelegate = self + webView.id = id + webView.plugin = plugin + webView.channelDelegate = WebViewChannelDelegate(webView: webView, channel: channel) + + let findInteractionController = FindInteractionController( + plugin: plugin, + id: id, webView: webView, settings: nil) + webView.findInteractionController = findInteractionController + findInteractionController.prepare() + + prepareWebView() + webView.windowCreated = true + + progressBar = NSProgressIndicator() + progressBar.style = .bar + progressBar.isIndeterminate = false + progressBar.startAnimation(self) + + view = NSView(frame: NSApplication.shared.mainWindow?.frame ?? .zero) + view.addSubview(webView) + view.addSubview(progressBar, positioned: .above, relativeTo: webView) + } + + public override func viewDidLoad() { + super.viewDidLoad() + + webView?.translatesAutoresizingMaskIntoConstraints = false + progressBar.translatesAutoresizingMaskIntoConstraints = false + + webView?.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0.0).isActive = true + webView?.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: 0.0).isActive = true + webView?.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0.0).isActive = true + webView?.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 0.0).isActive = true + + progressBar.topAnchor.constraint(equalTo: self.view.topAnchor, constant: -6.0).isActive = true + progressBar.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 0.0).isActive = true + progressBar.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: 0.0).isActive = true + + if let wId = windowId { + channelDelegate?.onBrowserCreated() + webView?.runWindowBeforeCreatedCallbacks() + } else { + if #available(macOS 10.13, *) { + if let contentBlockers = webView?.settings?.contentBlockers, contentBlockers.count > 0 { + do { + let jsonData = try JSONSerialization.data(withJSONObject: contentBlockers, options: []) + let blockRules = String(data: jsonData, encoding: .utf8) + WKContentRuleListStore.default().compileContentRuleList( + forIdentifier: "ContentBlockingRules", + encodedContentRuleList: blockRules) { (contentRuleList, error) in + + if let error = error { + print(error.localizedDescription) + return + } + + let configuration = self.webView!.configuration + configuration.userContentController.add(contentRuleList!) + + self.initLoad() + } + return + } catch { + print(error.localizedDescription) + } + } + } + + initLoad() + } + } + + public override func viewDidAppear() { + super.viewDidAppear() + window = view.window as? InAppBrowserWindow + } + + public func initLoad() { + if let initialFile = initialFile { + do { + try webView?.loadFile(assetFilePath: initialFile) + } + catch let error as NSError { + dump(error) + } + } + else if let initialData = initialData { + let baseUrl = URL(string: initialBaseUrl ?? "about:blank")! + var allowingReadAccessToURL: URL? = nil + if let allowingReadAccessTo = webView?.settings?.allowingReadAccessTo, baseUrl.scheme == "file" { + allowingReadAccessToURL = URL(string: allowingReadAccessTo) + if allowingReadAccessToURL?.scheme != "file" { + allowingReadAccessToURL = nil + } + } + webView?.loadData(data: initialData, mimeType: initialMimeType!, encoding: initialEncoding!, baseUrl: baseUrl, allowingReadAccessTo: allowingReadAccessToURL) + } + else if let initialUrlRequest = initialUrlRequest { + var allowingReadAccessToURL: URL? = nil + if let allowingReadAccessTo = webView?.settings?.allowingReadAccessTo, let url = initialUrlRequest.url, url.scheme == "file" { + allowingReadAccessToURL = URL(string: allowingReadAccessTo) + if allowingReadAccessToURL?.scheme != "file" { + allowingReadAccessToURL = nil + } + } + webView?.loadUrl(urlRequest: initialUrlRequest, allowingReadAccessTo: allowingReadAccessToURL) + } + + channelDelegate?.onBrowserCreated() + } + + public func prepareWebView() { + webView?.settings = webViewSettings + webView?.prepare() + + if let browserSettings = browserSettings { + if browserSettings.hideProgressBar { + progressBar.isHidden = true + } + } + } + + public func didChangeTitle(title: String?) { + guard let title = title else { + return + } + if let browserSettings = browserSettings, + let toolbarTopFixedTitle = browserSettings.toolbarTopFixedTitle { + window?.title = toolbarTopFixedTitle + } else { + window?.title = title + } + window?.update() + } + + public func didStartNavigation(url: URL?) { + window?.forwardButton?.isEnabled = webView?.canGoForward ?? false + window?.backButton?.isEnabled = webView?.canGoBack ?? false + progressBar.doubleValue = 0.0 + progressBar.isHidden = false + guard let url = url else { + return + } + window?.searchBar?.stringValue = url.absoluteString + } + + public func didUpdateVisitedHistory(url: URL?) { + window?.forwardButton?.isEnabled = webView?.canGoForward ?? false + window?.backButton?.isEnabled = webView?.canGoBack ?? false + guard let url = url else { + return + } + window?.searchBar?.stringValue = url.absoluteString + } + + public func didFinishNavigation(url: URL?) { + window?.forwardButton?.isEnabled = webView?.canGoForward ?? false + window?.backButton?.isEnabled = webView?.canGoBack ?? false + progressBar.doubleValue = 0.0 + progressBar.isHidden = true + guard let url = url else { + return + } + window?.searchBar?.stringValue = url.absoluteString + } + + public func didFailNavigation(url: URL?, error: Error) { + window?.forwardButton?.isEnabled = webView?.canGoForward ?? false + window?.backButton?.isEnabled = webView?.canGoBack ?? false + progressBar.doubleValue = 0.0 + progressBar.isHidden = true + } + + public func didChangeProgress(progress: Double) { + progressBar.isHidden = false + progressBar.doubleValue = progress * 100 + if progress == 100.0 { + progressBar.isHidden = true + } + } + + @objc public func reload() { + webView?.reload() + didUpdateVisitedHistory(url: webView?.url) + } + + @objc public func goBack() { + if let webView = webView, webView.canGoBack { + webView.goBack() + } + } + + @objc public func goForward() { + if let webView = webView, webView.canGoForward { + webView.goForward() + } + } + + @objc public func goBackOrForward(steps: Int) { + webView?.goBackOrForward(steps: steps) + } + + @objc public func onMenuItemClicked(sender: Any?) { + var identifier: String? + if let sender = sender as? NSMenuItem { + identifier = sender.identifier?.rawValue + } + if identifier == nil, let sender = sender as? NSButton { + identifier = sender.identifier?.rawValue + } + if let identifier = identifier, let window = window { + let menuItem = window.menuItems.first { item in + return item.id == Int64(identifier) + } + if let menuItem = menuItem { + channelDelegate?.onMenuItemClicked(menuItem: menuItem) + } + } + } + + public func setSettings(newSettings: InAppBrowserSettings, newSettingsMap: [String: Any]) { + window?.setSettings(newSettings: newSettings, newSettingsMap: newSettingsMap) + + let newInAppWebViewSettings = InAppWebViewSettings() + let _ = newInAppWebViewSettings.parse(settings: newSettingsMap) + webView?.setSettings(newSettings: newInAppWebViewSettings, newSettingsMap: newSettingsMap) + + if newSettingsMap["hideProgressBar"] != nil, browserSettings?.hideProgressBar != newSettings.hideProgressBar { + progressBar.isHidden = newSettings.hideProgressBar + } + + browserSettings = newSettings + webViewSettings = newInAppWebViewSettings + } + + public func getSettings() -> [String: Any?]? { + let webViewSettingsMap = webView?.getSettings() + if (self.browserSettings == nil || webViewSettingsMap == nil) { + return nil + } + var settingsMap = self.browserSettings!.getRealSettings(obj: self) + settingsMap.merge(webViewSettingsMap!, uniquingKeysWith: { (current, _) in current }) + return settingsMap + } + + public func hide() { + isHidden = true + window?.hide() + } + + public func show() { + isHidden = false + window?.show() + } + + public func close() { + window?.close() + } + + public func dispose() { + channelDelegate?.onExit() + channelDelegate?.dispose() + channelDelegate = nil + webView?.dispose() + webView?.removeFromSuperview() + webView = nil + window = nil + plugin = nil + } + + deinit { + debugPrint("InAppBrowserWebViewController - dealloc") + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppBrowser/InAppBrowserWindow.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppBrowser/InAppBrowserWindow.swift new file mode 100644 index 00000000..72aaff20 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppBrowser/InAppBrowserWindow.swift @@ -0,0 +1,387 @@ +// +// InAppBrowserNavigationController.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 14/02/21. +// + +import Foundation + +struct ToolbarIdentifiers { + static let searchBar = NSToolbarItem.Identifier(rawValue: "SearchBar") + static let backButton = NSToolbarItem.Identifier(rawValue: "BackButton") + static let forwardButton = NSToolbarItem.Identifier(rawValue: "ForwardButton") + static let reloadButton = NSToolbarItem.Identifier(rawValue: "ReloadButton") + static let menuButton = NSToolbarItem.Identifier(rawValue: "MenuButton") +} + +public class InAppBrowserWindow: NSWindow, NSWindowDelegate, NSToolbarDelegate, NSSearchFieldDelegate { + var searchItem: NSToolbarItem? + var backItem: NSToolbarItem? + var forwardItem: NSToolbarItem? + var reloadItem: NSToolbarItem? + var menuItem: NSToolbarItem? + var actionItems: [NSToolbarItem] = [] + + var reloadButton: NSButton? { + get { + return reloadItem?.view as? NSButton + } + } + var backButton: NSButton? { + get { + return backItem?.view as? NSButton + } + } + var forwardButton: NSButton? { + get { + return forwardItem?.view as? NSButton + } + } + var searchBar: NSSearchField? { + get { + if #available(macOS 11.0, *), let searchItem = searchItem as? NSSearchToolbarItem { + return searchItem.searchField + } else { + return searchItem?.view as? NSSearchField + } + } + } + var menuButton: NSPopUpButton? { + get { + return menuItem?.view as? NSPopUpButton + } + } + + var browserSettings: InAppBrowserSettings? + var menuItems: [InAppBrowserMenuItem] = [] + + public func prepare() { + title = "" + collectionBehavior = .fullScreenPrimary + delegate = self + + NotificationCenter.default.addObserver(self, + selector: #selector(onMainWindowWillClose(_:)), + name: NSWindow.willCloseNotification, + object: NSApplication.shared.mainWindow) + + if #available(macOS 10.13, *) { + let windowToolbar = NSToolbar() + windowToolbar.delegate = self + if #available(macOS 11.0, *) { + searchItem = NSSearchToolbarItem(itemIdentifier: ToolbarIdentifiers.searchBar) + (searchItem as! NSSearchToolbarItem).searchField.delegate = self + toolbarStyle = .expanded + } else { + searchItem = NSToolbarItem(itemIdentifier: ToolbarIdentifiers.searchBar) + let textField = NSSearchField() + textField.usesSingleLineMode = true + textField.delegate = self + searchItem?.view = textField + } + searchItem?.label = "" + windowToolbar.displayMode = .default + + backItem = NSToolbarItem(itemIdentifier: ToolbarIdentifiers.backButton) + backItem?.label = "" + if let webViewController = contentViewController as? InAppBrowserWebViewController { + if #available(macOS 11.0, *) { + backItem?.view = NSButton(image: NSImage(systemSymbolName: "chevron.left", + accessibilityDescription: "Go Back")!, + target: webViewController, + action: #selector(InAppBrowserWebViewController.goBack)) + } else { + backItem?.view = NSButton(title: "\u{2039}", + target: webViewController, + action: #selector(InAppBrowserWebViewController.goBack)) + } + } + + forwardItem = NSToolbarItem(itemIdentifier: ToolbarIdentifiers.forwardButton) + forwardItem?.label = "" + if let webViewController = contentViewController as? InAppBrowserWebViewController { + if #available(macOS 11.0, *) { + forwardItem?.view = NSButton(image: NSImage(systemSymbolName: "chevron.right", + accessibilityDescription: "Go Forward")!, + target: webViewController, + action: #selector(InAppBrowserWebViewController.goForward)) + } else { + forwardItem?.view = NSButton(title: "\u{203A}", + target: webViewController, + action: #selector(InAppBrowserWebViewController.goForward)) + } + } + + reloadItem = NSToolbarItem(itemIdentifier: ToolbarIdentifiers.reloadButton) + reloadItem?.label = "" + if let webViewController = contentViewController as? InAppBrowserWebViewController { + if #available(macOS 11.0, *) { + reloadItem?.view = NSButton(image: NSImage(systemSymbolName: "arrow.counterclockwise", + accessibilityDescription: "Reload")!, + target: webViewController, + action: #selector(InAppBrowserWebViewController.reload)) + } else { + reloadItem?.view = NSButton(title: "Reload", + target: webViewController, + action: #selector(InAppBrowserWebViewController.reload)) + } + } + + if #available(macOS 10.15, *), !menuItems.isEmpty { + menuItem = NSMenuToolbarItem(itemIdentifier: ToolbarIdentifiers.menuButton) + if let menuItem = menuItem as? NSMenuToolbarItem { + menuItem.label = "" + if #available(macOS 11.0, *) { + menuItem.image = NSImage(systemSymbolName: "ellipsis.circle", + accessibilityDescription: "Options") + menuItem.showsIndicator = true + menuItem.isBordered = true + } else { + menuItem.title = "Options" + } + let menu = NSMenu() + menuItems = menuItems.sorted(by: {$0.order ?? 0 < $1.order ?? 0}) + for item in menuItems { + if !item.showAsAction { + let nsItem = NSMenuItem(title: item.title, action: #selector(InAppBrowserWebViewController.onMenuItemClicked), keyEquivalent: "") + nsItem.identifier = NSUserInterfaceItemIdentifier.init(String(item.id)) + nsItem.image = item.icon + menu.addItem(nsItem) + } else { + let actionItem = NSMenuToolbarItem(itemIdentifier: NSToolbarItem.Identifier(rawValue: String(item.id))) + actionItem.label = "" + if let webViewController = contentViewController as? InAppBrowserWebViewController { + let actionButton = NSButton(title: item.title, + target: webViewController, + action: #selector(InAppBrowserWebViewController.onMenuItemClicked)) + actionButton.identifier = NSUserInterfaceItemIdentifier.init(String(item.id)) + actionButton.image = item.icon + actionItem.view = actionButton + } + actionItems.append(actionItem) + } + } + menuItem.menu = menu + } + } + + + if #available(macOS 10.14, *) { + windowToolbar.centeredItemIdentifier = ToolbarIdentifiers.searchBar + } + toolbar = windowToolbar + } + + forwardButton?.isEnabled = false + backButton?.isEnabled = false + + if let browserSettings = browserSettings { + if let toolbarTopFixedTitle = browserSettings.toolbarTopFixedTitle { + title = toolbarTopFixedTitle + } + if !browserSettings.hideToolbarTop { + toolbar?.isVisible = true + if browserSettings.hideUrlBar { + if #available(macOS 11.0, *) { + (searchItem as! NSSearchToolbarItem).searchField.isHidden = true + } else { + searchItem?.view?.isHidden = true + } + } + if let bgColor = browserSettings.toolbarTopBackgroundColor, !bgColor.isEmpty { + backgroundColor = NSColor(hexString: bgColor) + } + } + else { + toolbar?.isVisible = false + } + if #available(macOS 11.0, *), let windowTitlebarSeparatorStyle = browserSettings.windowTitlebarSeparatorStyle { + titlebarSeparatorStyle = windowTitlebarSeparatorStyle + } + alphaValue = browserSettings.windowAlphaValue + if let windowStyleMask = browserSettings.windowStyleMask { + styleMask = windowStyleMask + } + if let windowFrame = browserSettings.windowFrame { + setFrame(windowFrame, display: true) + } + reloadItem?.view?.isHidden = browserSettings.hideDefaultMenuItems + backItem?.view?.isHidden = browserSettings.hideDefaultMenuItems + forwardItem?.view?.isHidden = browserSettings.hideDefaultMenuItems + } + } + + public func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return [[ ToolbarIdentifiers.menuButton, + ToolbarIdentifiers.searchBar, + ToolbarIdentifiers.backButton, + ToolbarIdentifiers.forwardButton, + ToolbarIdentifiers.reloadButton, + .flexibleSpace ], actionItems.compactMap({ item in + return item.itemIdentifier + })].flatMap { $0 } + } + + public func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return [[.flexibleSpace, + ToolbarIdentifiers.searchBar, + .flexibleSpace, + ToolbarIdentifiers.reloadButton, + ToolbarIdentifiers.backButton, + ToolbarIdentifiers.forwardButton], + actionItems.compactMap({ item in + return item.itemIdentifier + }), + [ToolbarIdentifiers.menuButton]].flatMap { $0 } + } + + public func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { + switch(itemIdentifier) { + case ToolbarIdentifiers.searchBar: + return searchItem + case ToolbarIdentifiers.backButton: + return backItem + case ToolbarIdentifiers.forwardButton: + return forwardItem + case ToolbarIdentifiers.reloadButton: + return reloadItem + case ToolbarIdentifiers.menuButton: + return menuItem + default: + let actionItem = actionItems.first { item in + return item.itemIdentifier == itemIdentifier + } + return actionItem + } + } + + public func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { + if (commandSelector == #selector(NSResponder.insertNewline(_:))) { + // ENTER key + var searchField: NSSearchField? = nil + if #available(macOS 11.0, *), let searchBar = searchItem as? NSSearchToolbarItem { + searchField = searchBar.searchField + } else if let searchBar = searchItem { + searchField = searchBar.view as? NSSearchField + } + + guard let searchField, + let urlEncoded = searchField.stringValue.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), + let url = URL(string: urlEncoded) else { + return false + } + + let request = URLRequest(url: url) + (contentViewController as? InAppBrowserWebViewController)?.webView?.load(request) + + return true + } + + return false + } + + public func hide() { + orderOut(self) + } + + public func show() { + let mainWindow = parent ?? NSApplication.shared.mainWindow + if #available(macOS 10.12, *), + !(mainWindow?.tabbedWindows?.contains(self) ?? false), + browserSettings?.windowType == .tabbed { + mainWindow?.addTabbedWindow(self, ordered: .above) + } else if !(mainWindow?.childWindows?.contains(self) ?? false) { + mainWindow?.addChildWindow(self, ordered: .above) + } + makeKeyAndOrderFront(self) + NSApplication.shared.activate(ignoringOtherApps: true) + } + + public func setSettings(newSettings: InAppBrowserSettings, newSettingsMap: [String: Any]) { + if newSettingsMap["hidden"] != nil, browserSettings?.hidden != newSettings.hidden { + if newSettings.hidden { + hide() + } + else { + show() + } + } + + if newSettingsMap["hideUrlBar"] != nil, browserSettings?.hideUrlBar != newSettings.hideUrlBar { + searchBar?.isHidden = newSettings.hideUrlBar + } + + if newSettingsMap["hideToolbarTop"] != nil, browserSettings?.hideToolbarTop != newSettings.hideToolbarTop { + toolbar?.isVisible = !newSettings.hideToolbarTop + } + + if newSettingsMap["toolbarTopBackgroundColor"] != nil, browserSettings?.toolbarTopBackgroundColor != newSettings.toolbarTopBackgroundColor { + if let bgColor = newSettings.toolbarTopBackgroundColor, !bgColor.isEmpty { + backgroundColor = NSColor(hexString: bgColor) + } else { + backgroundColor = nil + } + } + if #available(macOS 11.0, *), newSettingsMap["windowTitlebarSeparatorStyle"] != nil, + browserSettings?.windowTitlebarSeparatorStyle != newSettings.windowTitlebarSeparatorStyle { + titlebarSeparatorStyle = newSettings.windowTitlebarSeparatorStyle! + } + if newSettingsMap["windowAlphaValue"] != nil, browserSettings?.windowAlphaValue != newSettings.windowAlphaValue { + alphaValue = newSettings.windowAlphaValue + } + if newSettingsMap["windowStyleMask"] != nil, browserSettings?.windowStyleMask != newSettings.windowStyleMask { + styleMask = newSettings.windowStyleMask! + } + if newSettingsMap["windowFrame"] != nil, browserSettings?.windowFrame != newSettings.windowFrame { + setFrame(newSettings.windowFrame!, display: true) + } + if newSettingsMap["hideDefaultMenuItems"] != nil, browserSettings?.hideDefaultMenuItems != newSettings.hideDefaultMenuItems { + reloadItem?.view?.isHidden = newSettings.hideDefaultMenuItems + backItem?.view?.isHidden = newSettings.hideDefaultMenuItems + forwardItem?.view?.isHidden = newSettings.hideDefaultMenuItems + } + browserSettings = newSettings + } + + public func windowWillClose(_ notification: Notification) { + dispose() + } + + @objc func onMainWindowWillClose(_ notification: Notification) { + if let webViewController = contentViewController as? InAppBrowserWebViewController { + webViewController.channelDelegate?.onMainWindowWillClose() + } + } + + public func dispose() { + delegate = nil + NotificationCenter.default.removeObserver(self, + name: NSWindow.willCloseNotification, + object: NSApplication.shared.mainWindow) + if let webViewController = contentViewController as? InAppBrowserWebViewController { + webViewController.dispose() + } + if #available(macOS 11.0, *) { + (searchItem as? NSSearchToolbarItem)?.searchField.delegate = nil + } else { + (searchItem?.view as? NSTextField)?.delegate = nil + searchItem?.view = nil + } + searchItem = nil + (backItem?.view as? NSButton)?.target = nil + backItem?.view = nil + backItem = nil + (forwardItem?.view as? NSButton)?.target = nil + forwardItem?.view = nil + forwardItem = nil + (reloadItem?.view as? NSButton)?.target = nil + reloadItem?.view = nil + reloadItem = nil + } + + deinit { + debugPrint("InAppBrowserWindow - dealloc") + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/ContextMenuSettings.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/ContextMenuSettings.swift new file mode 100644 index 00000000..a1df58d9 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/ContextMenuSettings.swift @@ -0,0 +1,17 @@ +// +// ContextMenuOptions.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 30/05/2020. +// + +import Foundation + +public class ContextMenuSettings: ISettings { + + var hideDefaultSystemContextMenuItems = false; + + override init(){ + super.init() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/CustomSchemeHandler.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/CustomSchemeHandler.swift new file mode 100755 index 00000000..d4119f34 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/CustomSchemeHandler.swift @@ -0,0 +1,45 @@ +// +// CustomSchemeHandler.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 25/10/2019. +// + +import FlutterMacOS +import Foundation +import WebKit + +@available(macOS 10.13, *) +public class CustomSchemeHandler: NSObject, WKURLSchemeHandler { + var schemeHandlers: [Int: WKURLSchemeTask] = [:] + + public func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { + schemeHandlers[urlSchemeTask.hash] = urlSchemeTask + let inAppWebView = webView as! InAppWebView + let request = WebResourceRequest.init(fromURLRequest: urlSchemeTask.request) + let callback = WebViewChannelDelegate.LoadResourceWithCustomSchemeCallback() + callback.nonNullSuccess = { (response: CustomSchemeResponse) in + if (self.schemeHandlers[urlSchemeTask.hash] != nil) { + let urlResponse = URLResponse(url: request.url, mimeType: response.contentType, expectedContentLength: -1, textEncodingName: response.contentEncoding) + urlSchemeTask.didReceive(urlResponse) + urlSchemeTask.didReceive(response.data) + urlSchemeTask.didFinish() + self.schemeHandlers.removeValue(forKey: urlSchemeTask.hash) + } + return false + } + callback.error = { (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + } + + if let channelDelegate = inAppWebView.channelDelegate { + channelDelegate.onLoadResourceWithCustomScheme(request: request, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + public func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) { + schemeHandlers.removeValue(forKey: urlSchemeTask.hash) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/FlutterWebViewController.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/FlutterWebViewController.swift new file mode 100755 index 00000000..2caa19c6 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/FlutterWebViewController.swift @@ -0,0 +1,202 @@ +// +// FlutterWebViewController.swift +// flutter_inappwebview +// +// Created by Lorenzo on 13/11/18. +// + +import Foundation +import WebKit +import FlutterMacOS + +public class FlutterWebViewController: NSView, Disposable { + + var keepAliveId: String? + + init(plugin: InAppWebViewFlutterPlugin, withFrame frame: CGRect, viewIdentifier viewId: Any, params: NSDictionary) { + super.init(frame: frame) + + keepAliveId = params["keepAliveId"] as? String + + let initialSettings = params["initialSettings"] as! [String: Any?] + let windowId = params["windowId"] as? Int64 + let initialUserScripts = params["initialUserScripts"] as? [[String: Any]] + + var userScripts: [UserScript] = [] + if let initialUserScripts = initialUserScripts { + for initialUserScript in initialUserScripts { + userScripts.append(UserScript.fromMap(map: initialUserScript, windowId: windowId)!) + } + } + + let settings = InAppWebViewSettings() + let _ = settings.parse(settings: initialSettings) + let preWebviewConfiguration = InAppWebView.preWKWebViewConfiguration(settings: settings) + + var webView: InAppWebView? + + if let wId = windowId, let webViewTransport = plugin.inAppWebViewManager?.windowWebViews[wId] { + webView = webViewTransport.webView + webView!.id = viewId + webView!.plugin = plugin + if let registrar = plugin.registrar { + let channel = FlutterMethodChannel(name: InAppWebView.METHOD_CHANNEL_NAME_PREFIX + String(describing: viewId), + binaryMessenger: registrar.messenger) + webView!.channelDelegate = WebViewChannelDelegate(webView: webView!, channel: channel) + } + webView!.frame = self.bounds + webView!.initialUserScripts = userScripts + } else { + webView = InAppWebView(id: viewId, + plugin: plugin, + frame: self.bounds, + configuration: preWebviewConfiguration, + userScripts: userScripts) + } + + let findInteractionController = FindInteractionController( + plugin: plugin, + id: viewId, webView: webView!, settings: nil) + webView!.findInteractionController = findInteractionController + findInteractionController.prepare() + + webView!.autoresizingMask = [.width, .height] + self.autoresizesSubviews = true + self.autoresizingMask = [.width, .height] + self.addSubview(webView!) + + webView!.settings = settings + webView!.prepare() + webView!.windowCreated = true + } + + required init?(coder nsCoder: NSCoder) { + super.init(coder: nsCoder) + } + + public func webView() -> InAppWebView? { + for subview in self.subviews + { + if let item = subview as? InAppWebView + { + return item + } + } + return nil + } + + public func view() -> NSView { + return self + } + + public func makeInitialLoad(params: NSDictionary) { + guard let webView = webView() else { + return + } + + let windowId = params["windowId"] as? Int64 + let initialUrlRequest = params["initialUrlRequest"] as? [String: Any?] + let initialFile = params["initialFile"] as? String + let initialData = params["initialData"] as? [String: String?] + + if windowId == nil { + if #available(macOS 10.13, *) { + webView.configuration.userContentController.removeAllContentRuleLists() + if let contentBlockers = webView.settings?.contentBlockers, contentBlockers.count > 0 { + do { + let jsonData = try JSONSerialization.data(withJSONObject: contentBlockers, options: []) + let blockRules = String(data: jsonData, encoding: .utf8) + WKContentRuleListStore.default().compileContentRuleList( + forIdentifier: "ContentBlockingRules", + encodedContentRuleList: blockRules) { (contentRuleList, error) in + + if let error = error { + print(error.localizedDescription) + return + } + + let configuration = webView.configuration + configuration.userContentController.add(contentRuleList!) + + self.load(initialUrlRequest: initialUrlRequest, initialFile: initialFile, initialData: initialData) + } + return + } catch { + print(error.localizedDescription) + } + } + } + load(initialUrlRequest: initialUrlRequest, initialFile: initialFile, initialData: initialData) + } + else if let wId = windowId { + webView.runWindowBeforeCreatedCallbacks() + } + } + + func load(initialUrlRequest: [String:Any?]?, initialFile: String?, initialData: [String: String?]?) { + guard let webView = webView() else { + return + } + + if let initialFile = initialFile { + do { + try webView.loadFile(assetFilePath: initialFile) + } + catch let error as NSError { + dump(error) + } + } + else if let initialData = initialData, let data = initialData["data"]!, + let mimeType = initialData["mimeType"]!, let encoding = initialData["encoding"]!, + let baseUrl = URL(string: initialData["baseUrl"]! ?? "about:blank") { + var allowingReadAccessToURL: URL? = nil + if let allowingReadAccessTo = webView.settings?.allowingReadAccessTo, baseUrl.scheme == "file" { + allowingReadAccessToURL = URL(string: allowingReadAccessTo) + if allowingReadAccessToURL?.scheme != "file" { + allowingReadAccessToURL = nil + } + } + webView.loadData(data: data, + mimeType: mimeType, + encoding: encoding, + baseUrl: baseUrl, + allowingReadAccessTo: allowingReadAccessToURL) + } + else if let initialUrlRequest = initialUrlRequest { + let urlRequest = URLRequest.init(fromPluginMap: initialUrlRequest) + var allowingReadAccessToURL: URL? = nil + if let allowingReadAccessTo = webView.settings?.allowingReadAccessTo, let url = urlRequest.url, url.scheme == "file" { + allowingReadAccessToURL = URL(string: allowingReadAccessTo) + if allowingReadAccessToURL?.scheme != "file" { + allowingReadAccessToURL = nil + } + } + webView.loadUrl(urlRequest: urlRequest, allowingReadAccessTo: allowingReadAccessToURL) + } + } + + // method added to fix: + // https://github.com/pichillilorenzo/flutter_inappwebview/issues/1837 + public func dispose(removeFromSuperview: Bool) { + if keepAliveId == nil { + if let webView = webView() { + webView.dispose() + if removeFromSuperview { + webView.removeFromSuperview() + } + } + if removeFromSuperview { + self.removeFromSuperview() + } + } + } + + public func dispose() { + dispose(removeFromSuperview: false) + } + + deinit { + debugPrint("FlutterWebViewController - dealloc") + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/FlutterWebViewFactory.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/FlutterWebViewFactory.swift new file mode 100755 index 00000000..d659dc1a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/FlutterWebViewFactory.swift @@ -0,0 +1,72 @@ +// +// FlutterWebViewFactory.swift +// flutter_inappwebview +// +// Created by Lorenzo on 13/11/18. +// + +import SwiftUI +import Cocoa +import FlutterMacOS +import Foundation + +public class FlutterWebViewFactory: NSObject, FlutterPlatformViewFactory { + static let VIEW_TYPE_ID = "com.pichillilorenzo/flutter_inappwebview" + private var plugin: InAppWebViewFlutterPlugin + + init(plugin: InAppWebViewFlutterPlugin) { + self.plugin = plugin + super.init() + } + + public func createArgsCodec() -> (FlutterMessageCodec & NSObjectProtocol)? { + return FlutterStandardMessageCodec.sharedInstance() + } + + public func create(withViewIdentifier viewId: Int64, arguments args: Any?) -> NSView { + let arguments = args as? NSDictionary + var flutterWebView: FlutterWebViewController? + var id: Any = viewId + + let keepAliveId = arguments?["keepAliveId"] as? String + let headlessWebViewId = arguments?["headlessWebViewId"] as? String + + if let headlessWebViewId = headlessWebViewId, + let headlessWebView = plugin.headlessInAppWebViewManager?.webViews[headlessWebViewId], + let platformView = headlessWebView?.disposeAndGetFlutterWebView(withFrame: .zero) { + flutterWebView = platformView + flutterWebView?.keepAliveId = keepAliveId + } + + if let keepAliveId = keepAliveId, + flutterWebView == nil, + let keepAliveWebView = plugin.inAppWebViewManager?.keepAliveWebViews[keepAliveId] { + flutterWebView = keepAliveWebView + if let view = flutterWebView?.view() { + // remove from parent + view.removeFromSuperview() + } + } + + let shouldMakeInitialLoad = flutterWebView == nil + if flutterWebView == nil { + if let keepAliveId = keepAliveId { + id = keepAliveId + } + flutterWebView = FlutterWebViewController(plugin: plugin, + withFrame: .zero, + viewIdentifier: id, + params: arguments!) + } + + if let keepAliveId = keepAliveId { + plugin.inAppWebViewManager?.keepAliveWebViews[keepAliveId] = flutterWebView! + } + + if shouldMakeInitialLoad { + flutterWebView?.makeInitialLoad(params: arguments!) + } + + return flutterWebView! + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/InAppWebView.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/InAppWebView.swift new file mode 100755 index 00000000..849fd870 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/InAppWebView.swift @@ -0,0 +1,2661 @@ +// +// InAppWebView.swift +// flutter_inappwebview +// +// Created by Lorenzo on 21/10/18. +// + +import FlutterMacOS +import Foundation +import WebKit + +public class InAppWebView: WKWebView, WKUIDelegate, + WKNavigationDelegate, WKScriptMessageHandler, + WKDownloadDelegate, + Disposable { + static var METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_" + + var id: Any? // viewId + var plugin: InAppWebViewFlutterPlugin? + var windowId: Int64? + var windowCreated = false + var windowBeforeCreatedCallbacks: [() -> ()] = [] + var inAppBrowserDelegate: InAppBrowserDelegate? + var channelDelegate: WebViewChannelDelegate? + var settings: InAppWebViewSettings? + var findInteractionController: FindInteractionController? + var webMessageChannels: [String:WebMessageChannel] = [:] + var webMessageListeners: [WebMessageListener] = [] + var currentOriginalUrl: URL? + var inFullscreen = false + private var printJobCompletionHandler: PrintJobController.CompletionHandler? + + static var sslCertificatesMap: [String: SslCertificate] = [:] // [URL host name : SslCertificate] + static var credentialsProposed: [URLCredential] = [] + + var lastScrollX: CGFloat = 0 + var lastScrollY: CGFloat = 0 + + // Used to manage pauseTimers() and resumeTimers() + var isPausedTimers = false + var isPausedTimersCompletionHandler: (() -> Void)? + + var initialUserScripts: [UserScript] = [] + + var customIMPs: [IMP] = [] + + var callAsyncJavaScriptBelowMacOS11Results: [String:((Any?) -> Void)] = [:] + + var currentOpenPanel: NSOpenPanel? + + fileprivate var interceptOnlyAsyncAjaxRequestsPluginScript: PluginScript? + + init(id: Any?, plugin: InAppWebViewFlutterPlugin?, frame: CGRect, configuration: WKWebViewConfiguration, + userScripts: [UserScript] = []) { + super.init(frame: frame, configuration: configuration) + self.id = id + self.plugin = plugin + if let id = id, let registrar = plugin?.registrar { + let channel = FlutterMethodChannel(name: InAppWebView.METHOD_CHANNEL_NAME_PREFIX + String(describing: id), + binaryMessenger: registrar.messenger) + self.channelDelegate = WebViewChannelDelegate(webView: self, channel: channel) + } + self.initialUserScripts = userScripts + uiDelegate = self + navigationDelegate = self + } + + required public init(coder aDecoder: NSCoder) { + super.init(coder: aDecoder)! + } + + public func prepare() { + addObserver(self, + forKeyPath: #keyPath(WKWebView.estimatedProgress), + options: .new, + context: nil) + + addObserver(self, + forKeyPath: #keyPath(WKWebView.url), + options: [.new, .old], + context: nil) + + addObserver(self, + forKeyPath: #keyPath(WKWebView.title), + options: [.new, .old], + context: nil) + + if #available(macOS 12.0, *) { + addObserver(self, + forKeyPath: #keyPath(WKWebView.cameraCaptureState), + options: [.new, .old], + context: nil) + + addObserver(self, + forKeyPath: #keyPath(WKWebView.microphoneCaptureState), + options: [.new, .old], + context: nil) + } + + // TODO: Still not working on iOS 16.0! +// if #available(iOS 16.0, *) { +// addObserver(self, +// forKeyPath: #keyPath(WKWebView.fullscreenState), +// options: .new, +// context: nil) +// } else { + // listen for videos playing in fullscreen + NotificationCenter.default.addObserver(self, + selector: #selector(onEnterFullscreen(_:)), + name: NSWindow.didEnterFullScreenNotification, + object: window) + + // listen for videos stopping to play in fullscreen + NotificationCenter.default.addObserver(self, + selector: #selector(onExitFullscreen(_:)), + name: NSWindow.didExitFullScreenNotification, + object: window) +// } + + if let settings = settings { + + if #available(macOS 12.0, *), settings.transparentBackground { + underPageBackgroundColor = .clear + } + + allowsBackForwardNavigationGestures = settings.allowsBackForwardNavigationGestures + allowsLinkPreview = settings.allowsLinkPreview + if !settings.userAgent.isEmpty { + customUserAgent = settings.userAgent + } + + if #available(macOS 11.0, *) { + mediaType = settings.mediaType + pageZoom = CGFloat(settings.pageZoom) + } + + if #available(macOS 12.0, *) { + if let underPageBackgroundColor = settings.underPageBackgroundColor, !underPageBackgroundColor.isEmpty { + self.underPageBackgroundColor = NSColor(hexString: underPageBackgroundColor) + } + } + + if #available(macOS 13.3, *) { + isInspectable = settings.isInspectable + } + + if settings.clearCache { + clearCache() + } + } + + prepareAndAddUserScripts() + + if windowId != nil { + // The new created window webview has the same WKWebViewConfiguration variable reference. + // So, we cannot set another WKWebViewConfiguration for it unfortunately! + // This is a limitation of the official WebKit API. + return + } + + configuration.preferences = WKPreferences() + if let settings = settings { + configuration.allowsAirPlayForMediaPlayback = settings.allowsAirPlayForMediaPlayback + configuration.preferences.javaScriptCanOpenWindowsAutomatically = settings.javaScriptCanOpenWindowsAutomatically + configuration.preferences.minimumFontSize = CGFloat(settings.minimumFontSize) + + if #available(macOS 10.15, *) { + configuration.preferences.isFraudulentWebsiteWarningEnabled = settings.isFraudulentWebsiteWarningEnabled + configuration.defaultWebpagePreferences.preferredContentMode = WKWebpagePreferences.ContentMode(rawValue: settings.preferredContentMode)! + } + + configuration.preferences.javaScriptEnabled = settings.javaScriptEnabled + if #available(macOS 11.0, *) { + configuration.defaultWebpagePreferences.allowsContentJavaScript = settings.javaScriptEnabled + } + if #available(macOS 11.3, *) { + configuration.preferences.isTextInteractionEnabled = settings.isTextInteractionEnabled + } + if #available(macOS 12.3, *) { + configuration.preferences.isSiteSpecificQuirksModeEnabled = settings.isSiteSpecificQuirksModeEnabled + configuration.preferences.isElementFullscreenEnabled = settings.isElementFullscreenEnabled + } + if #available(macOS 13.3, *) { + configuration.preferences.shouldPrintBackgrounds = settings.shouldPrintBackgrounds + } + } + } + + public func prepareAndAddUserScripts() -> Void { + if windowId != nil { + // The new created window webview has the same WKWebViewConfiguration variable reference. + // So, we cannot set another WKWebViewConfiguration for it unfortunately! + // This is a limitation of the official WebKit API. + return + } + configuration.userContentController = WKUserContentController() + configuration.userContentController.initialize() + + if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled { + return + } + + configuration.userContentController.addPluginScript(PROMISE_POLYFILL_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(CONSOLE_LOG_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(PRINT_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(FIND_ELEMENTS_AT_POINT_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(FIND_TEXT_HIGHLIGHT_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_PLUGIN_SCRIPT) + configuration.userContentController.addPluginScript(ON_SCROLL_CHANGED_EVENT_JS_PLUGIN_SCRIPT) + if let settings = settings { + interceptOnlyAsyncAjaxRequestsPluginScript = createInterceptOnlyAsyncAjaxRequestsPluginScript(onlyAsync: settings.interceptOnlyAsyncAjaxRequests) + if settings.useShouldInterceptAjaxRequest { + if let interceptOnlyAsyncAjaxRequestsPluginScript = interceptOnlyAsyncAjaxRequestsPluginScript { + configuration.userContentController.addPluginScript(interceptOnlyAsyncAjaxRequestsPluginScript) + } + configuration.userContentController.addPluginScript(INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT) + } + if settings.useShouldInterceptFetchRequest { + configuration.userContentController.addPluginScript(INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT) + } + if settings.useOnLoadResource { + configuration.userContentController.addPluginScript(ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT) + } + if !settings.supportZoom { + configuration.userContentController.addPluginScript(NOT_SUPPORT_ZOOM_JS_PLUGIN_SCRIPT) + } else if settings.enableViewportScale { + configuration.userContentController.addPluginScript(ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT) + } + } + configuration.userContentController.removeScriptMessageHandler(forName: "onCallAsyncJavaScriptResultBelowIOS14Received") + configuration.userContentController.add(self, name: "onCallAsyncJavaScriptResultBelowIOS14Received") + configuration.userContentController.removeScriptMessageHandler(forName: "onWebMessagePortMessageReceived") + configuration.userContentController.add(self, name: "onWebMessagePortMessageReceived") + configuration.userContentController.removeScriptMessageHandler(forName: "onWebMessageListenerPostMessageReceived") + configuration.userContentController.add(self, name: "onWebMessageListenerPostMessageReceived") + configuration.userContentController.addUserOnlyScripts(initialUserScripts) + configuration.userContentController.sync(scriptMessageHandler: self) + } + + public static func preWKWebViewConfiguration(settings: InAppWebViewSettings?) -> WKWebViewConfiguration { + let configuration = WKWebViewConfiguration() + + configuration.processPool = WKProcessPoolManager.sharedProcessPool + + if let settings = settings { + configuration.suppressesIncrementalRendering = settings.suppressesIncrementalRendering + + if settings.allowUniversalAccessFromFileURLs { + configuration.setValue(settings.allowUniversalAccessFromFileURLs, forKey: "allowUniversalAccessFromFileURLs") + } + + if settings.allowFileAccessFromFileURLs { + configuration.preferences.setValue(settings.allowFileAccessFromFileURLs, forKey: "allowFileAccessFromFileURLs") + } + + if settings.incognito { + configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent() + } else if settings.cacheEnabled { + configuration.websiteDataStore = GopeedProfiles.store(settings.gopeedProfileId) ?? WKWebsiteDataStore.default() + } + if !settings.applicationNameForUserAgent.isEmpty { + if let applicationNameForUserAgent = configuration.applicationNameForUserAgent { + configuration.applicationNameForUserAgent = applicationNameForUserAgent + " " + settings.applicationNameForUserAgent + } + } + + if #available(macOS 10.12, *) { + configuration.mediaTypesRequiringUserActionForPlayback = settings.mediaPlaybackRequiresUserGesture ? .all : [] + } + + if #available(macOS 10.13, *) { + for scheme in settings.resourceCustomSchemes { + configuration.setURLSchemeHandler(CustomSchemeHandler(), forURLScheme: scheme) + } + if settings.sharedCookiesEnabled { + // More info to sending cookies with WKWebView + // https://stackoverflow.com/questions/26573137/can-i-set-the-cookies-to-be-used-by-a-wkwebview/26577303#26577303 + // Set Cookies in iOS 11 and above, initialize websiteDataStore before setting cookies + // See also https://forums.developer.apple.com/thread/97194 + // check if websiteDataStore has not been initialized before + if(!settings.incognito && !settings.cacheEnabled) { + configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent() + } + for cookie in HTTPCookieStorage.shared.cookies ?? [] { + configuration.websiteDataStore.httpCookieStore.setCookie(cookie, completionHandler: nil) + } + } + } + + if #available(macOS 11.0, *) { + configuration.limitsNavigationsToAppBoundDomains = settings.limitsNavigationsToAppBoundDomains + } + + if #available(macOS 11.3, *) { + configuration.upgradeKnownHostsToHTTPS = settings.upgradeKnownHostsToHTTPS + } + } + + return configuration + } + + override public func observeValue(forKeyPath keyPath: String?, of object: Any?, + change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == #keyPath(WKWebView.estimatedProgress) { + initializeWindowIdJS() + let progress = Int(estimatedProgress * 100) + channelDelegate?.onProgressChanged(progress: progress) + inAppBrowserDelegate?.didChangeProgress(progress: estimatedProgress) + } else if keyPath == #keyPath(WKWebView.url) && change?[.newKey] is URL { + initializeWindowIdJS() + let newUrl = change?[NSKeyValueChangeKey.newKey] as? URL + channelDelegate?.onUpdateVisitedHistory(url: newUrl?.absoluteString, isReload: nil) + inAppBrowserDelegate?.didUpdateVisitedHistory(url: newUrl) + } else if keyPath == #keyPath(WKWebView.title) && change?[.newKey] is String { + let newTitle = change?[.newKey] as? String + channelDelegate?.onTitleChanged(title: newTitle) + inAppBrowserDelegate?.didChangeTitle(title: newTitle) + } + else if #available(macOS 12.0, *) { + if keyPath == #keyPath(WKWebView.cameraCaptureState) || keyPath == #keyPath(WKWebView.microphoneCaptureState) { + var oldState: WKMediaCaptureState? = nil + if let oldValue = change?[.oldKey] as? Int { + oldState = WKMediaCaptureState.init(rawValue: oldValue) + } + var newState: WKMediaCaptureState? = nil + if let newValue = change?[.newKey] as? Int { + newState = WKMediaCaptureState.init(rawValue: newValue) + } + if oldState != newState { + if keyPath == #keyPath(WKWebView.cameraCaptureState) { + channelDelegate?.onCameraCaptureStateChanged(oldState: oldState, newState: newState) + } else { + channelDelegate?.onMicrophoneCaptureStateChanged(oldState: oldState, newState: newState) + } + } + } + } else if #available(iOS 16.0, *) { + // TODO: Still not working on iOS 16.0! +// if keyPath == #keyPath(WKWebView.fullscreenState) { +// if fullscreenState == .enteringFullscreen { +// channelDelegate?.onEnterFullscreen() +// } else if fullscreenState == .exitingFullscreen { +// channelDelegate?.onExitFullscreen() +// } +// } + } + } + + public func initializeWindowIdJS() { + if let windowId = windowId { + if #available(macOS 11.0, *) { + let contentWorlds = configuration.userContentController.getContentWorlds(with: windowId) + for contentWorld in contentWorlds { + let source = WINDOW_ID_INITIALIZE_JS_SOURCE.replacingOccurrences(of: PluginScriptsUtil.VAR_PLACEHOLDER_VALUE, with: String(windowId)) + evaluateJavascript(source: source, contentWorld: contentWorld) + } + } else { + let source = WINDOW_ID_INITIALIZE_JS_SOURCE.replacingOccurrences(of: PluginScriptsUtil.VAR_PLACEHOLDER_VALUE, with: String(windowId)) + evaluateJavascript(source: source) + } + } + } + + public func goBackOrForward(steps: Int) { + if canGoBackOrForward(steps: steps) { + if (steps > 0) { + let index = steps - 1 + go(to: self.backForwardList.forwardList[index]) + } + else if (steps < 0){ + let backListLength = self.backForwardList.backList.count + let index = backListLength + steps + go(to: self.backForwardList.backList[index]) + } + } + } + + public func canGoBackOrForward(steps: Int) -> Bool { + let currentIndex = self.backForwardList.backList.count + return (steps >= 0) + ? steps <= self.backForwardList.forwardList.count + : currentIndex + steps >= 0 + } + + @available(macOS 10.13, *) + public func takeScreenshot (with: [String: Any?]?, completionHandler: @escaping (_ screenshot: Data?) -> Void) { + var snapshotConfiguration: WKSnapshotConfiguration? = nil + if let with = with { + snapshotConfiguration = WKSnapshotConfiguration() + if let rect = with["rect"] as? [String: Double] { + snapshotConfiguration!.rect = CGRect.fromMap(map: rect) + } + if let snapshotWidth = with["snapshotWidth"] as? Double { + snapshotConfiguration!.snapshotWidth = NSNumber(value: snapshotWidth) + } + if #available(macOS 10.15, *), let afterScreenUpdates = with["afterScreenUpdates"] as? Bool { + snapshotConfiguration!.afterScreenUpdates = afterScreenUpdates + } + } + takeSnapshot(with: snapshotConfiguration, completionHandler: {(image, error) -> Void in + var imageData: Data? = nil + if let screenshot = image, let cgImage = screenshot.cgImage(forProposedRect: nil, context: nil, hints: nil) { + let newRep = NSBitmapImageRep(cgImage: cgImage) + if let with = with { + switch with["compressFormat"] as! String { + case "JPEG": + let quality = Float(with["quality"] as! Int) / 100 + imageData = newRep.representation(using: .jpeg, properties: [ + NSBitmapImageRep.PropertyKey.compressionFactor:quality]) + break + case "PNG": + imageData = newRep.representation(using: .png, properties: [:]) + break + default: + imageData = newRep.representation(using: .png, properties: [:]) + } + } + else { + imageData = newRep.representation(using: .png, properties: [:]) + } + } + completionHandler(imageData) + }) + } + + @available(macOS 11.0, *) + public func createPdf (configuration: [String: Any?]?, completionHandler: @escaping (_ pdf: Data?) -> Void) { + let pdfConfiguration: WKPDFConfiguration = .init() + if let configuration = configuration { + if let rect = configuration["rect"] as? [String: Double] { + pdfConfiguration.rect = CGRect.fromMap(map: rect) + } + } + createPDF(configuration: pdfConfiguration) { (result) in + switch (result) { + case .success(let data): + completionHandler(data) + return + case .failure(let error): + print(error.localizedDescription) + completionHandler(nil) + return + } + } + } + + @available(macOS 11.0, *) + public func createWebArchiveData (dataCompletionHandler: @escaping (_ webArchiveData: Data?) -> Void) { + createWebArchiveData(completionHandler: { (result) in + switch (result) { + case .success(let data): + dataCompletionHandler(data) + return + case .failure(let error): + print(error.localizedDescription) + dataCompletionHandler(nil) + return + } + }) + } + + @available(macOS 11.0, *) + public func saveWebArchive (filePath: String, autoname: Bool, completionHandler: @escaping (_ path: String?) -> Void) { + createWebArchiveData(dataCompletionHandler: { (webArchiveData) in + if let webArchiveData = webArchiveData { + var localUrl = URL(fileURLWithPath: filePath) + if autoname { + if let url = self.url { + // tries to mimic Android saveWebArchive method + let invalidCharacters = CharacterSet(charactersIn: "\\/:*?\"<>|") + .union(.newlines) + .union(.illegalCharacters) + .union(.controlCharacters) + + let currentPageUrlFileName = url.path + .components(separatedBy: invalidCharacters) + .joined(separator: "") + + let fullPath = filePath + "/" + currentPageUrlFileName + ".webarchive" + localUrl = URL(fileURLWithPath: fullPath) + } else { + completionHandler(nil) + return + } + } + do { + try webArchiveData.write(to: localUrl) + completionHandler(localUrl.path) + } catch { + // Catch any errors + print(error.localizedDescription) + completionHandler(nil) + } + } else { + completionHandler(nil) + } + }) + } + + public func loadUrl(urlRequest: URLRequest, allowingReadAccessTo: URL?) { + let url = urlRequest.url! + + if let allowingReadAccessTo = allowingReadAccessTo, url.scheme == "file", allowingReadAccessTo.scheme == "file" { + loadFileURL(url, allowingReadAccessTo: allowingReadAccessTo) + } else { + load(urlRequest) + } + } + + public func postUrl(url: URL, postData: Data) { + var request = URLRequest(url: url) + + request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + request.httpMethod = "POST" + request.httpBody = postData + load(request) + } + + public func loadData(data: String, mimeType: String, encoding: String, baseUrl: URL, allowingReadAccessTo: URL?) { + if let allowingReadAccessTo = allowingReadAccessTo, baseUrl.scheme == "file", allowingReadAccessTo.scheme == "file" { + loadFileURL(baseUrl, allowingReadAccessTo: allowingReadAccessTo) + } + load(data.data(using: .utf8)!, mimeType: mimeType, characterEncodingName: encoding, baseURL: baseUrl) + } + + public func loadFile(assetFilePath: String) throws { + let assetURL = try Util.getUrlAsset(assetFilePath: assetFilePath) + let urlRequest = URLRequest(url: assetURL) + loadUrl(urlRequest: urlRequest, allowingReadAccessTo: nil) + } + + func setSettings(newSettings: InAppWebViewSettings, newSettingsMap: [String: Any]) { + + // MUST be the first! In this way, all the settings that uses evaluateJavaScript can be applied/blocked! + if newSettingsMap["applePayAPIEnabled"] != nil && settings?.applePayAPIEnabled != newSettings.applePayAPIEnabled { + if let settings = settings { + settings.applePayAPIEnabled = newSettings.applePayAPIEnabled + } + if !newSettings.applePayAPIEnabled { + // re-add WKUserScripts for the next page load + prepareAndAddUserScripts() + } else { + configuration.userContentController.removeAllUserScripts() + } + } + + if (newSettingsMap["incognito"] != nil && settings?.incognito != newSettings.incognito && newSettings.incognito) { + configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent() + } else if (newSettingsMap["cacheEnabled"] != nil && settings?.cacheEnabled != newSettings.cacheEnabled && newSettings.cacheEnabled) { + configuration.websiteDataStore = GopeedProfiles.store(settings?.gopeedProfileId ?? "") ?? WKWebsiteDataStore.default() + } + + if #available(macOS 10.13, *) { + if (newSettingsMap["sharedCookiesEnabled"] != nil && settings?.sharedCookiesEnabled != newSettings.sharedCookiesEnabled && newSettings.sharedCookiesEnabled) { + if(!newSettings.incognito && !newSettings.cacheEnabled) { + configuration.websiteDataStore = WKWebsiteDataStore.nonPersistent() + } + for cookie in HTTPCookieStorage.shared.cookies ?? [] { + configuration.websiteDataStore.httpCookieStore.setCookie(cookie, completionHandler: nil) + } + } + } + + if newSettingsMap["enableViewportScale"] != nil && settings?.enableViewportScale != newSettings.enableViewportScale { + if !newSettings.enableViewportScale { + if configuration.userContentController.userScripts.contains(ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT) { + configuration.userContentController.removePluginScript(ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT) + evaluateJavaScript(NOT_ENABLE_VIEWPORT_SCALE_JS_SOURCE) + } + } else { + evaluateJavaScript(ENABLE_VIEWPORT_SCALE_JS_SOURCE) + configuration.userContentController.addUserScript(ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT) + } + } + + if newSettingsMap["supportZoom"] != nil && settings?.supportZoom != newSettings.supportZoom { + if newSettings.supportZoom { + if configuration.userContentController.userScripts.contains(NOT_SUPPORT_ZOOM_JS_PLUGIN_SCRIPT) { + configuration.userContentController.removePluginScript(NOT_SUPPORT_ZOOM_JS_PLUGIN_SCRIPT) + evaluateJavaScript(SUPPORT_ZOOM_JS_SOURCE) + } + } else { + evaluateJavaScript(NOT_SUPPORT_ZOOM_JS_SOURCE) + configuration.userContentController.addUserScript(NOT_SUPPORT_ZOOM_JS_PLUGIN_SCRIPT) + } + } + + if newSettingsMap["useOnLoadResource"] != nil && settings?.useOnLoadResource != newSettings.useOnLoadResource { + if let applePayAPIEnabled = settings?.applePayAPIEnabled, !applePayAPIEnabled { + enablePluginScriptAtRuntime(flagVariable: FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE, + enable: newSettings.useOnLoadResource, + pluginScript: ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT) + } else { + newSettings.useOnLoadResource = false + } + } + + if newSettingsMap["useShouldInterceptAjaxRequest"] != nil && settings?.useShouldInterceptAjaxRequest != newSettings.useShouldInterceptAjaxRequest { + if let applePayAPIEnabled = settings?.applePayAPIEnabled, !applePayAPIEnabled { + enablePluginScriptAtRuntime(flagVariable: FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE, + enable: newSettings.useShouldInterceptAjaxRequest, + pluginScript: INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT) + } else { + newSettings.useShouldInterceptAjaxRequest = false + } + } + + if newSettingsMap["interceptOnlyAsyncAjaxRequests"] != nil && settings?.interceptOnlyAsyncAjaxRequests != newSettings.interceptOnlyAsyncAjaxRequests { + if let applePayAPIEnabled = settings?.applePayAPIEnabled, !applePayAPIEnabled, + let interceptOnlyAsyncAjaxRequestsPluginScript = interceptOnlyAsyncAjaxRequestsPluginScript { + enablePluginScriptAtRuntime(flagVariable: FLAG_VARIABLE_FOR_INTERCEPT_ONLY_ASYNC_AJAX_REQUESTS_JS_SOURCE, + enable: newSettings.interceptOnlyAsyncAjaxRequests, + pluginScript: interceptOnlyAsyncAjaxRequestsPluginScript) + } + } + + if newSettingsMap["useShouldInterceptFetchRequest"] != nil && settings?.useShouldInterceptFetchRequest != newSettings.useShouldInterceptFetchRequest { + if let applePayAPIEnabled = settings?.applePayAPIEnabled, !applePayAPIEnabled { + enablePluginScriptAtRuntime(flagVariable: FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE, + enable: newSettings.useShouldInterceptFetchRequest, + pluginScript: INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT) + } else { + newSettings.useShouldInterceptFetchRequest = false + } + } + + if newSettingsMap["mediaPlaybackRequiresUserGesture"] != nil && settings?.mediaPlaybackRequiresUserGesture != newSettings.mediaPlaybackRequiresUserGesture { + if #available(macOS 10.12, *) { + configuration.mediaTypesRequiringUserActionForPlayback = (newSettings.mediaPlaybackRequiresUserGesture) ? .all : [] + } + } + + if newSettingsMap["suppressesIncrementalRendering"] != nil && settings?.suppressesIncrementalRendering != newSettings.suppressesIncrementalRendering { + configuration.suppressesIncrementalRendering = newSettings.suppressesIncrementalRendering + } + + if newSettingsMap["allowsBackForwardNavigationGestures"] != nil && settings?.allowsBackForwardNavigationGestures != newSettings.allowsBackForwardNavigationGestures { + allowsBackForwardNavigationGestures = newSettings.allowsBackForwardNavigationGestures + } + + if newSettingsMap["javaScriptCanOpenWindowsAutomatically"] != nil && settings?.javaScriptCanOpenWindowsAutomatically != newSettings.javaScriptCanOpenWindowsAutomatically { + configuration.preferences.javaScriptCanOpenWindowsAutomatically = newSettings.javaScriptCanOpenWindowsAutomatically + } + + if newSettingsMap["minimumFontSize"] != nil && settings?.minimumFontSize != newSettings.minimumFontSize { + configuration.preferences.minimumFontSize = CGFloat(newSettings.minimumFontSize) + } + + if #available(macOS 10.15, *) { + if newSettingsMap["isFraudulentWebsiteWarningEnabled"] != nil && settings?.isFraudulentWebsiteWarningEnabled != newSettings.isFraudulentWebsiteWarningEnabled { + configuration.preferences.isFraudulentWebsiteWarningEnabled = newSettings.isFraudulentWebsiteWarningEnabled + } + if newSettingsMap["preferredContentMode"] != nil && settings?.preferredContentMode != newSettings.preferredContentMode { + configuration.defaultWebpagePreferences.preferredContentMode = WKWebpagePreferences.ContentMode(rawValue: newSettings.preferredContentMode)! + } + } + + if newSettingsMap["allowsLinkPreview"] != nil && settings?.allowsLinkPreview != newSettings.allowsLinkPreview { + allowsLinkPreview = newSettings.allowsLinkPreview + } + if newSettingsMap["allowsAirPlayForMediaPlayback"] != nil && settings?.allowsAirPlayForMediaPlayback != newSettings.allowsAirPlayForMediaPlayback { + configuration.allowsAirPlayForMediaPlayback = newSettings.allowsAirPlayForMediaPlayback + } + if newSettingsMap["applicationNameForUserAgent"] != nil && settings?.applicationNameForUserAgent != newSettings.applicationNameForUserAgent && newSettings.applicationNameForUserAgent != "" { + configuration.applicationNameForUserAgent = newSettings.applicationNameForUserAgent + } + if newSettingsMap["userAgent"] != nil && settings?.userAgent != newSettings.userAgent && newSettings.userAgent != "" { + customUserAgent = newSettings.userAgent + } + + if newSettingsMap["allowUniversalAccessFromFileURLs"] != nil && settings?.allowUniversalAccessFromFileURLs != newSettings.allowUniversalAccessFromFileURLs { + configuration.setValue(newSettings.allowUniversalAccessFromFileURLs, forKey: "allowUniversalAccessFromFileURLs") + } + + if newSettingsMap["allowFileAccessFromFileURLs"] != nil && settings?.allowFileAccessFromFileURLs != newSettings.allowFileAccessFromFileURLs { + configuration.preferences.setValue(newSettings.allowFileAccessFromFileURLs, forKey: "allowFileAccessFromFileURLs") + } + + if newSettingsMap["clearCache"] != nil && newSettings.clearCache { + clearCache() + } + + if newSettingsMap["javaScriptEnabled"] != nil && settings?.javaScriptEnabled != newSettings.javaScriptEnabled { + configuration.preferences.javaScriptEnabled = newSettings.javaScriptEnabled + } + + if #available(macOS 11.0, *) { + if settings?.mediaType != newSettings.mediaType { + mediaType = newSettings.mediaType + } + + if newSettingsMap["pageZoom"] != nil && settings?.pageZoom != newSettings.pageZoom { + pageZoom = CGFloat(newSettings.pageZoom) + } + + if newSettingsMap["limitsNavigationsToAppBoundDomains"] != nil && settings?.limitsNavigationsToAppBoundDomains != newSettings.limitsNavigationsToAppBoundDomains { + configuration.limitsNavigationsToAppBoundDomains = newSettings.limitsNavigationsToAppBoundDomains + } + + if newSettingsMap["javaScriptEnabled"] != nil && settings?.javaScriptEnabled != newSettings.javaScriptEnabled { + configuration.defaultWebpagePreferences.allowsContentJavaScript = newSettings.javaScriptEnabled + } + } + + if #available(macOS 10.13, *), newSettingsMap["contentBlockers"] != nil { + configuration.userContentController.removeAllContentRuleLists() + let contentBlockers = newSettings.contentBlockers + if contentBlockers.count > 0 { + do { + let jsonData = try JSONSerialization.data(withJSONObject: contentBlockers, options: []) + let blockRules = String(data: jsonData, encoding: .utf8) + WKContentRuleListStore.default().compileContentRuleList( + forIdentifier: "ContentBlockingRules", + encodedContentRuleList: blockRules) { (contentRuleList, error) in + if let error = error { + print(error.localizedDescription) + return + } + self.configuration.userContentController.add(contentRuleList!) + } + } catch { + print(error.localizedDescription) + } + } + } + + if #available(macOS 11.3, *) { + if newSettingsMap["upgradeKnownHostsToHTTPS"] != nil && settings?.upgradeKnownHostsToHTTPS != newSettings.upgradeKnownHostsToHTTPS { + configuration.upgradeKnownHostsToHTTPS = newSettings.upgradeKnownHostsToHTTPS + } + if newSettingsMap["isTextInteractionEnabled"] != nil && settings?.isTextInteractionEnabled != newSettings.isTextInteractionEnabled { + configuration.preferences.isTextInteractionEnabled = newSettings.isTextInteractionEnabled + } + } + + if #available(macOS 12.0, *) { + if newSettingsMap["underPageBackgroundColor"] != nil, settings?.underPageBackgroundColor != newSettings.underPageBackgroundColor, + let underPageBackgroundColor = newSettings.underPageBackgroundColor, !underPageBackgroundColor.isEmpty { + self.underPageBackgroundColor = NSColor(hexString: underPageBackgroundColor) + } + } + if #available(macOS 12.3, *) { + if newSettingsMap["isSiteSpecificQuirksModeEnabled"] != nil, settings?.isSiteSpecificQuirksModeEnabled != newSettings.isSiteSpecificQuirksModeEnabled { + configuration.preferences.isSiteSpecificQuirksModeEnabled = newSettings.isSiteSpecificQuirksModeEnabled + } + } + if #available(macOS 13.3, *) { + if newSettingsMap["isInspectable"] != nil, settings?.isInspectable != newSettings.isInspectable { + isInspectable = newSettings.isInspectable + } + if newSettingsMap["shouldPrintBackgrounds"] != nil, settings?.shouldPrintBackgrounds != newSettings.shouldPrintBackgrounds { + configuration.preferences.shouldPrintBackgrounds = newSettings.shouldPrintBackgrounds + } + } + + self.settings = newSettings + } + + func getSettings() -> [String: Any?]? { + if (self.settings == nil) { + return nil + } + return self.settings!.getRealSettings(obj: self) + } + + public func enablePluginScriptAtRuntime(flagVariable: String, enable: Bool, pluginScript: PluginScript) { + evaluateJavascript(source: flagVariable) { (alreadyLoaded) in + if let alreadyLoaded = alreadyLoaded as? Bool, alreadyLoaded { + let enableSource = "\(flagVariable) = \(enable);" + if #available(macOS 11.0, *), pluginScript.requiredInAllContentWorlds { + for contentWorld in self.configuration.userContentController.contentWorlds { + self.evaluateJavaScript(enableSource, frame: nil, contentWorld: contentWorld, completionHandler: nil) + } + } else { + self.evaluateJavaScript(enableSource, completionHandler: nil) + } + if !enable { + self.configuration.userContentController.removePluginScripts(with: pluginScript.groupName!) + } + } + else if enable { + if #available(macOS 11.0, *), pluginScript.requiredInAllContentWorlds { + for contentWorld in self.configuration.userContentController.contentWorlds { + self.evaluateJavaScript(pluginScript.source, frame: nil, contentWorld: contentWorld, completionHandler: nil) + self.configuration.userContentController.addPluginScript(pluginScript) + } + } else { + self.evaluateJavaScript(pluginScript.source, completionHandler: nil) + self.configuration.userContentController.addPluginScript(pluginScript) + } + self.configuration.userContentController.sync(scriptMessageHandler: self) + } + } + } + + @available(*, deprecated, message: "Use InAppWebViewManager.clearAllCache instead.") + public func clearCache() { + let date = NSDate(timeIntervalSince1970: 0) + WKWebsiteDataStore.default().removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), modifiedSince: date as Date, completionHandler:{ }) + } + + public func injectDeferredObject(source: String, withWrapper jsWrapper: String?, completionHandler: ((Any?) -> Void)? = nil) { + var jsToInject = source + if let wrapper = jsWrapper { + let jsonData: Data? = try? JSONSerialization.data(withJSONObject: [source], options: []) + let sourceArrayString = String(data: jsonData!, encoding: .utf8) + let sourceString: String? = (sourceArrayString! as NSString).substring(with: NSRange(location: 1, length: (sourceArrayString?.count ?? 0) - 2)) + jsToInject = String(format: wrapper, sourceString!) + } + + evaluateJavaScript(jsToInject) { (value, error) in + guard let completionHandler = completionHandler else { + return + } + + if let error = error { + let userInfo = (error as NSError).userInfo + let errorMessage = userInfo["WKJavaScriptExceptionMessage"] ?? + userInfo["NSLocalizedDescription"] as? String ?? + error.localizedDescription + self.channelDelegate?.onConsoleMessage(message: String(describing: errorMessage), messageLevel: 3) + } + + if value == nil { + completionHandler(nil) + return + } + + completionHandler(value) + } + } + + @available(macOS 11.0, *) + public func injectDeferredObject(source: String, contentWorld: WKContentWorld, withWrapper jsWrapper: String?, completionHandler: ((Any?) -> Void)? = nil) { + var jsToInject = source + if let wrapper = jsWrapper { + let jsonData: Data? = try? JSONSerialization.data(withJSONObject: [source], options: []) + let sourceArrayString = String(data: jsonData!, encoding: .utf8) + let sourceString: String? = (sourceArrayString! as NSString).substring(with: NSRange(location: 1, length: (sourceArrayString?.count ?? 0) - 2)) + jsToInject = String(format: wrapper, sourceString!) + } + + jsToInject = configuration.userContentController.generateCodeForScriptEvaluation(scriptMessageHandler: self, source: jsToInject, contentWorld: contentWorld) + + evaluateJavaScript(jsToInject, frame: nil, contentWorld: contentWorld) { (evalResult) in + guard let completionHandler = completionHandler else { + return + } + + switch (evalResult) { + case .success(let value): + completionHandler(value) + return + case .failure(let error): + let userInfo = (error as NSError).userInfo + let errorMessage = userInfo["WKJavaScriptExceptionMessage"] ?? + userInfo["NSLocalizedDescription"] as? String ?? + error.localizedDescription + self.channelDelegate?.onConsoleMessage(message: String(describing: errorMessage), messageLevel: 3) + break + } + + completionHandler(nil) + } + } + +#if compiler(>=6.0) + public override func evaluateJavaScript(_ javaScriptString: String, completionHandler: (@MainActor @Sendable (Any?, (any Error)?) -> Void)? = nil) { + if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled { + if let completionHandler = completionHandler { + completionHandler(nil, nil) + } + return + } + super.evaluateJavaScript(javaScriptString, completionHandler: completionHandler) + } +#else + public override func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil) { + if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled { + if let completionHandler = completionHandler { + completionHandler(nil, nil) + } + return + } + super.evaluateJavaScript(javaScriptString, completionHandler: completionHandler) + } +#endif + + @available(macOS 11.0, *) + public func evaluateJavaScript(_ javaScript: String, frame: WKFrameInfo? = nil, contentWorld: WKContentWorld, completionHandler: ((Result) -> Void)? = nil) { + if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled { + return + } + super.evaluateJavaScript(javaScript, in: frame, in: contentWorld, completionHandler: completionHandler) + } + + public func evaluateJavascript(source: String, completionHandler: ((Any?) -> Void)? = nil) { + injectDeferredObject(source: source, withWrapper: nil, completionHandler: completionHandler) + } + + @available(macOS 11.0, *) + public func evaluateJavascript(source: String, contentWorld: WKContentWorld, completionHandler: ((Any?) -> Void)? = nil) { + injectDeferredObject(source: source, contentWorld: contentWorld, withWrapper: nil, completionHandler: completionHandler) + } + + @available(macOS 11.0, *) + public func callAsyncJavaScript(_ functionBody: String, arguments: [String : Any] = [:], frame: WKFrameInfo? = nil, contentWorld: WKContentWorld, completionHandler: ((Result) -> Void)? = nil) { + if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled { + return + } + super.callAsyncJavaScript(functionBody, arguments: arguments, in: frame, in: contentWorld, completionHandler: completionHandler) + } + + @available(macOS 11.0, *) + public func callAsyncJavaScript(functionBody: String, arguments: [String:Any], contentWorld: WKContentWorld, completionHandler: ((Any?) -> Void)? = nil) { + let jsToInject = configuration.userContentController.generateCodeForScriptEvaluation(scriptMessageHandler: self, source: functionBody, contentWorld: contentWorld) + + callAsyncJavaScript(jsToInject, arguments: arguments, frame: nil, contentWorld: contentWorld) { (evalResult) in + guard let completionHandler = completionHandler else { + return + } + + var body: [String: Any?] = [ + "value": nil, + "error": nil + ] + + switch (evalResult) { + case .success(let value): + body["value"] = value + break + case .failure(let error): + let userInfo = (error as NSError).userInfo + body["error"] = userInfo["WKJavaScriptExceptionMessage"] ?? + userInfo["NSLocalizedDescription"] as? String ?? + error.localizedDescription + self.channelDelegate?.onConsoleMessage(message: String(describing: body["error"]), messageLevel: 3) + break + } + + completionHandler(body) + } + } + + public func callAsyncJavaScript(functionBody: String, arguments: [String:Any], completionHandler: ((Any?) -> Void)? = nil) { + if let applePayAPIEnabled = settings?.applePayAPIEnabled, applePayAPIEnabled { + completionHandler?(nil) + } + + var jsToInject = functionBody + + let resultUuid = NSUUID().uuidString + if let completionHandler = completionHandler { + callAsyncJavaScriptBelowMacOS11Results[resultUuid] = completionHandler + } + + var functionArgumentNamesList: [String] = [] + var functionArgumentValuesList: [String] = [] + let keys = arguments.keys + keys.forEach { (key) in + functionArgumentNamesList.append(key) + functionArgumentValuesList.append("obj.\(key)") + } + + let functionArgumentNames = functionArgumentNamesList.joined(separator: ", ") + let functionArgumentValues = functionArgumentValuesList.joined(separator: ", ") + + jsToInject = CALL_ASYNC_JAVASCRIPT_BELOW_IOS_14_WRAPPER_JS + .replacingOccurrences(of: PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_NAMES, with: functionArgumentNames) + .replacingOccurrences(of: PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_VALUES, with: functionArgumentValues) + .replacingOccurrences(of: PluginScriptsUtil.VAR_FUNCTION_ARGUMENTS_OBJ, with: Util.JSONStringify(value: arguments)) + .replacingOccurrences(of: PluginScriptsUtil.VAR_FUNCTION_BODY, with: jsToInject) + .replacingOccurrences(of: PluginScriptsUtil.VAR_RESULT_UUID, with: resultUuid) + + evaluateJavaScript(jsToInject) { (value, error) in + if let error = error { + let userInfo = (error as NSError).userInfo + let errorMessage = userInfo["WKJavaScriptExceptionMessage"] ?? + userInfo["NSLocalizedDescription"] as? String ?? + error.localizedDescription + self.channelDelegate?.onConsoleMessage(message: String(describing: errorMessage), messageLevel: 3) + completionHandler?(nil) + self.callAsyncJavaScriptBelowMacOS11Results.removeValue(forKey: resultUuid) + } + } + } + + public func injectJavascriptFileFromUrl(urlFile: String, scriptHtmlTagAttributes: [String:Any?]?) { + var scriptAttributes = "" + if let scriptHtmlTagAttributes = scriptHtmlTagAttributes { + if let typeAttr = scriptHtmlTagAttributes["type"] as? String { + scriptAttributes += " script.type = '\(typeAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let idAttr = scriptHtmlTagAttributes["id"] as? String { + let scriptIdEscaped = idAttr.replacingOccurrences(of: "\'", with: "\\'") + scriptAttributes += " script.id = '\(scriptIdEscaped)'; " + scriptAttributes += """ + script.onload = function() { + if (window.\(JAVASCRIPT_BRIDGE_NAME) != null) { + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onInjectedScriptLoaded', '\(scriptIdEscaped)'); + } + }; + """ + scriptAttributes += """ + script.onerror = function() { + if (window.\(JAVASCRIPT_BRIDGE_NAME) != null) { + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onInjectedScriptError', '\(scriptIdEscaped)'); + } + }; + """ + } + if let asyncAttr = scriptHtmlTagAttributes["async"] as? Bool, asyncAttr { + scriptAttributes += " script.async = true; " + } + if let deferAttr = scriptHtmlTagAttributes["defer"] as? Bool, deferAttr { + scriptAttributes += " script.defer = true; " + } + if let crossOriginAttr = scriptHtmlTagAttributes["crossOrigin"] as? String { + scriptAttributes += " script.crossOrigin = '\(crossOriginAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let integrityAttr = scriptHtmlTagAttributes["integrity"] as? String { + scriptAttributes += " script.integrity = '\(integrityAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let noModuleAttr = scriptHtmlTagAttributes["noModule"] as? Bool, noModuleAttr { + scriptAttributes += " script.noModule = true; " + } + if let nonceAttr = scriptHtmlTagAttributes["nonce"] as? String { + scriptAttributes += " script.nonce = '\(nonceAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let referrerPolicyAttr = scriptHtmlTagAttributes["referrerPolicy"] as? String { + scriptAttributes += " script.referrerPolicy = '\(referrerPolicyAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + } + let jsWrapper = "(function(d) { var script = d.createElement('script'); \(scriptAttributes) script.src = %@; d.body.appendChild(script); })(document);" + injectDeferredObject(source: urlFile, withWrapper: jsWrapper, completionHandler: nil) + } + + public func injectCSSCode(source: String) { + let jsWrapper = "(function(d) { var style = d.createElement('style'); style.innerHTML = %@; d.head.appendChild(style); })(document);" + injectDeferredObject(source: source, withWrapper: jsWrapper, completionHandler: nil) + } + + public func injectCSSFileFromUrl(urlFile: String, cssLinkHtmlTagAttributes: [String:Any?]?) { + var cssLinkAttributes = "" + var alternateStylesheet = "" + if let cssLinkHtmlTagAttributes = cssLinkHtmlTagAttributes { + if let idAttr = cssLinkHtmlTagAttributes["id"] as? String { + cssLinkAttributes += " link.id = '\(idAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let mediaAttr = cssLinkHtmlTagAttributes["media"] as? String { + cssLinkAttributes += " link.media = '\(mediaAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let crossOriginAttr = cssLinkHtmlTagAttributes["crossOrigin"] as? String { + cssLinkAttributes += " link.crossOrigin = '\(crossOriginAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let integrityAttr = cssLinkHtmlTagAttributes["integrity"] as? String { + cssLinkAttributes += " link.integrity = '\(integrityAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let referrerPolicyAttr = cssLinkHtmlTagAttributes["referrerPolicy"] as? String { + cssLinkAttributes += " link.referrerPolicy = '\(referrerPolicyAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + if let disabledAttr = cssLinkHtmlTagAttributes["disabled"] as? Bool, disabledAttr { + cssLinkAttributes += " link.disabled = true; " + } + if let alternateAttr = cssLinkHtmlTagAttributes["alternate"] as? Bool, alternateAttr { + alternateStylesheet = "alternate " + } + if let titleAttr = cssLinkHtmlTagAttributes["title"] as? String { + cssLinkAttributes += " link.title = '\(titleAttr.replacingOccurrences(of: "\'", with: "\\'"))'; " + } + } + let jsWrapper = "(function(d) { var link = d.createElement('link'); link.rel='\(alternateStylesheet)stylesheet', link.type='text/css'; \(cssLinkAttributes) link.href = %@; d.head.appendChild(link); })(document);" + injectDeferredObject(source: urlFile, withWrapper: jsWrapper, completionHandler: nil) + } + + public func getCopyBackForwardList() -> [String: Any] { + let currentList = backForwardList + let currentIndex = currentList.backList.count + var completeList = currentList.backList + if currentList.currentItem != nil { + completeList.append(currentList.currentItem!) + } + completeList.append(contentsOf: currentList.forwardList) + + var history: [[String: String]] = [] + + for historyItem in completeList { + var historyItemMap: [String: String] = [:] + historyItemMap["originalUrl"] = historyItem.initialURL.absoluteString + historyItemMap["title"] = historyItem.title + historyItemMap["url"] = historyItem.url.absoluteString + history.append(historyItemMap) + } + + var result: [String: Any] = [:] + result["list"] = history + result["currentIndex"] = currentIndex + + return result; + } + + @available(macOS 12.0, *) + public func webView(_ webView: WKWebView, + requestMediaCapturePermissionFor origin: WKSecurityOrigin, + initiatedByFrame frame: WKFrameInfo, + type: WKMediaCaptureType, + decisionHandler: @escaping (WKPermissionDecision) -> Void) { + let origin = "\(origin.protocol)://\(origin.host)\(origin.port != 0 ? ":" + String(origin.port) : "")" + let permissionRequest = PermissionRequest(origin: origin, resources: [type.rawValue], frame: frame) + + var decisionHandlerCalled = false + let callback = WebViewChannelDelegate.PermissionRequestCallback() + callback.nonNullSuccess = { (response: PermissionResponse) in + if let action = response.action { + decisionHandlerCalled = true + switch action { + case 1: + decisionHandler(.grant) + break + case 2: + decisionHandler(.prompt) + break + default: + decisionHandler(.deny) + } + return false + } + return true + } + callback.defaultBehaviour = { (response: PermissionResponse?) in + if !decisionHandlerCalled { + decisionHandlerCalled = true + decisionHandler(.deny) + } + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + if let channelDelegate = channelDelegate { + channelDelegate.onPermissionRequest(request: permissionRequest, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + @available(macOS 10.15, *) + public func webView(_ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + preferences: WKWebpagePreferences, + decisionHandler: @escaping (WKNavigationActionPolicy, WKWebpagePreferences) -> Void) { + self.webView(webView, decidePolicyFor: navigationAction, decisionHandler: {(navigationActionPolicy) -> Void in + decisionHandler(navigationActionPolicy, preferences) + }) + } + + @available(macOS 11.3, *) + public func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String, completionHandler: @escaping (URL?) -> Void) { + if let url = response.url, let useOnDownloadStart = settings?.useOnDownloadStart, useOnDownloadStart { + let downloadStartRequest = DownloadStartRequest(url: url.absoluteString, + userAgent: nil, + contentDisposition: nil, + mimeType: response.mimeType, + contentLength: response.expectedContentLength, + suggestedFilename: suggestedFilename, + textEncodingName: response.textEncodingName) + channelDelegate?.onDownloadStartRequest(request: downloadStartRequest) + } + download.delegate = nil + // cancel the download + completionHandler(nil) + } + + @available(macOS 11.3, *) + public func webView(_ webView: WKWebView, navigationResponse: WKNavigationResponse, didBecome download: WKDownload) { + let response = navigationResponse.response + if let url = response.url, let useOnDownloadStart = settings?.useOnDownloadStart, useOnDownloadStart { + let downloadStartRequest = DownloadStartRequest(url: url.absoluteString, + userAgent: nil, + contentDisposition: nil, + mimeType: response.mimeType, + contentLength: response.expectedContentLength, + suggestedFilename: response.suggestedFilename, + textEncodingName: response.textEncodingName) + channelDelegate?.onDownloadStartRequest(request: downloadStartRequest) + } + download.delegate = nil + } + + public func webView(_ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + var decisionHandlerCalled = false + let callback = WebViewChannelDelegate.ShouldOverrideUrlLoadingCallback() + callback.nonNullSuccess = { (response: WKNavigationActionPolicy) in + decisionHandlerCalled = true + decisionHandler(response) + return false + } + callback.defaultBehaviour = { (response: WKNavigationActionPolicy?) in + if !decisionHandlerCalled { + decisionHandlerCalled = true + decisionHandler(.allow) + } + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + let runCallback = { + if let useShouldOverrideUrlLoading = self.settings?.useShouldOverrideUrlLoading, useShouldOverrideUrlLoading, let channelDelegate = self.channelDelegate { + channelDelegate.shouldOverrideUrlLoading(navigationAction: navigationAction, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + if windowId != nil, !windowCreated { + windowBeforeCreatedCallbacks.append(runCallback) + } else { + runCallback() + } + } + + public func webView(_ webView: WKWebView, + decidePolicyFor navigationResponse: WKNavigationResponse, + decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { + if let response = navigationResponse.response as? HTTPURLResponse, response.statusCode >= 400 { + let request = WebResourceRequest.init(fromWKNavigationResponse: navigationResponse) + let errorResponse = WebResourceResponse.init(fromWKNavigationResponse: navigationResponse) + channelDelegate?.onReceivedHttpError(request: request, errorResponse: errorResponse) + } + + let useOnNavigationResponse = settings?.useOnNavigationResponse + + if useOnNavigationResponse != nil, useOnNavigationResponse! { + var decisionHandlerCalled = false + let callback = WebViewChannelDelegate.NavigationResponseCallback() + callback.nonNullSuccess = { (response: WKNavigationResponsePolicy) in + decisionHandlerCalled = true + decisionHandler(response) + return false + } + callback.defaultBehaviour = { (response: WKNavigationResponsePolicy?) in + if !decisionHandlerCalled { + decisionHandlerCalled = true + decisionHandler(.allow) + } + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + if let channelDelegate = channelDelegate { + channelDelegate.onNavigationResponse(navigationResponse: navigationResponse, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + if let useOnDownloadStart = settings?.useOnDownloadStart, useOnDownloadStart { + if #available(macOS 11.3, *), !navigationResponse.canShowMIMEType, useOnNavigationResponse == nil || !useOnNavigationResponse! { + decisionHandler(.download) + return + } else { + let mimeType = navigationResponse.response.mimeType + if let url = navigationResponse.response.url, navigationResponse.isForMainFrame { + if url.scheme != "file", mimeType != nil, !mimeType!.starts(with: "text/") { + let downloadStartRequest = DownloadStartRequest(url: url.absoluteString, + userAgent: nil, + contentDisposition: nil, + mimeType: mimeType, + contentLength: navigationResponse.response.expectedContentLength, + suggestedFilename: navigationResponse.response.suggestedFilename, + textEncodingName: navigationResponse.response.textEncodingName) + channelDelegate?.onDownloadStartRequest(request: downloadStartRequest) + if useOnNavigationResponse == nil || !useOnNavigationResponse! { + decisionHandler(.cancel) + } + return + } + } + } + } + + if useOnNavigationResponse == nil || !useOnNavigationResponse! { + decisionHandler(.allow) + } + } + + public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + currentOriginalUrl = url + + disposeWebMessageChannels() + initializeWindowIdJS() + + if #available(macOS 11.0, *) { + configuration.userContentController.resetContentWorlds(windowId: windowId) + } + + channelDelegate?.onLoadStart(url: url?.absoluteString) + + inAppBrowserDelegate?.didStartNavigation(url: url) + } + + public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + initializeWindowIdJS() + + InAppWebView.credentialsProposed = [] + evaluateJavaScript(PLATFORM_READY_JS_SOURCE, completionHandler: nil) + + channelDelegate?.onLoadStop(url: url?.absoluteString) + + inAppBrowserDelegate?.didFinishNavigation(url: url) + } + + public func webView(_ view: WKWebView, + didFailProvisionalNavigation navigation: WKNavigation!, + withError error: Error) { + webView(view, didFail: navigation, withError: error) + } + + public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + InAppWebView.credentialsProposed = [] + + var urlError: URL = url ?? URL(string: "about:blank")! + var errorCode = -1 + var errorDescription = "domain=\(error._domain), code=\(error._code), \(error.localizedDescription)" + + if let info = error as? URLError { + if let failingURL = info.failingURL { + urlError = failingURL + } + errorCode = info.code.rawValue + errorDescription = info.localizedDescription + } + else if let info = error._userInfo as? [String: Any] { + if let failingUrl = info[NSURLErrorFailingURLErrorKey] as? URL { + urlError = failingUrl + } + if let failingUrlString = info[NSURLErrorFailingURLStringErrorKey] as? String, + let failingUrl = URL(string: failingUrlString) { + urlError = failingUrl + } + } + + let webResourceRequest = WebResourceRequest(url: urlError, headers: nil) + let webResourceError = WebResourceError(type: errorCode, errorDescription: errorDescription) + + channelDelegate?.onReceivedError(request: webResourceRequest, error: webResourceError) + + inAppBrowserDelegate?.didFailNavigation(url: url, error: error) + } + + public func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + var completionHandlerCalled = false + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic || + challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodDefault || + challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPDigest || + challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodNegotiate || + challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodNTLM { + let host = challenge.protectionSpace.host + let prot = challenge.protectionSpace.protocol + let realm = challenge.protectionSpace.realm + let port = challenge.protectionSpace.port + + let callback = WebViewChannelDelegate.ReceivedHttpAuthRequestCallback() + callback.nonNullSuccess = { (response: HttpAuthResponse) in + if let action = response.action { + completionHandlerCalled = true + switch action { + case 0: + InAppWebView.credentialsProposed = [] + // used .performDefaultHandling to maintain consistency with Android + // because .cancelAuthenticationChallenge will call webView(_:didFail:withError:) + completionHandler(.performDefaultHandling, nil) + //completionHandler(.cancelAuthenticationChallenge, nil) + break + case 1: + let username = response.username + let password = response.password + let permanentPersistence = response.permanentPersistence + let persistence = (permanentPersistence) ? URLCredential.Persistence.permanent : URLCredential.Persistence.forSession + let credential = URLCredential(user: username, password: password, persistence: persistence) + completionHandler(.useCredential, credential) + break + case 2: + if InAppWebView.credentialsProposed.count == 0 { + for (protectionSpace, credentials) in CredentialDatabase.credentialStore.allCredentials { + if protectionSpace.host == host && protectionSpace.realm == realm && + protectionSpace.protocol == prot && protectionSpace.port == port { + for credential in credentials { + InAppWebView.credentialsProposed.append(credential.value) + } + break + } + } + } + if InAppWebView.credentialsProposed.count == 0, let credential = challenge.proposedCredential { + InAppWebView.credentialsProposed.append(credential) + } + + if let credential = InAppWebView.credentialsProposed.popLast() { + completionHandler(.useCredential, credential) + } + else { + completionHandler(.performDefaultHandling, nil) + } + break + default: + InAppWebView.credentialsProposed = [] + completionHandler(.performDefaultHandling, nil) + } + return false + } + return true + } + callback.defaultBehaviour = { (response: HttpAuthResponse?) in + if !completionHandlerCalled { + completionHandlerCalled = true + completionHandler(.performDefaultHandling, nil) + } + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + let runCallback = { + if let channelDelegate = self.channelDelegate { + channelDelegate.onReceivedHttpAuthRequest(challenge: HttpAuthenticationChallenge(fromChallenge: challenge), callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + if windowId != nil, !windowCreated { + windowBeforeCreatedCallbacks.append(runCallback) + } else { + runCallback() + } + } + else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { + guard let serverTrust = challenge.protectionSpace.serverTrust else { + completionHandler(.performDefaultHandling, nil) + return + } + + if let scheme = challenge.protectionSpace.protocol, scheme == "https" { + // workaround for ProtectionSpace SSL Certificate + // https://github.com/pichillilorenzo/flutter_inappwebview/issues/1678 + DispatchQueue.global(qos: .background).async { + if let sslCertificate = challenge.protectionSpace.sslCertificate { + DispatchQueue.main.async { + InAppWebView.sslCertificatesMap[challenge.protectionSpace.host] = sslCertificate + } + } + } + } + + let callback = WebViewChannelDelegate.ReceivedServerTrustAuthRequestCallback() + callback.nonNullSuccess = { (response: ServerTrustAuthResponse) in + if let action = response.action { + completionHandlerCalled = true + switch action { + case 0: + InAppWebView.credentialsProposed = [] + completionHandler(.cancelAuthenticationChallenge, nil) + break + case 1: + // workaround for https://github.com/pichillilorenzo/flutter_inappwebview/issues/1924 + DispatchQueue.global(qos: .background).async { + let exceptions = SecTrustCopyExceptions(serverTrust) + SecTrustSetExceptions(serverTrust, exceptions) + let credential = URLCredential(trust: serverTrust) + completionHandler(.useCredential, credential) + } + break + default: + InAppWebView.credentialsProposed = [] + completionHandler(.performDefaultHandling, nil) + } + return false + } + return true + } + callback.defaultBehaviour = { (response: ServerTrustAuthResponse?) in + if !completionHandlerCalled { + completionHandlerCalled = true + completionHandler(.performDefaultHandling, nil) + } + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + let runCallback = { + if let channelDelegate = self.channelDelegate { + channelDelegate.onReceivedServerTrustAuthRequest(challenge: ServerTrustChallenge(fromChallenge: challenge), callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + if windowId != nil, !windowCreated { + windowBeforeCreatedCallbacks.append(runCallback) + } else { + runCallback() + } + } + else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate { + let callback = WebViewChannelDelegate.ReceivedClientCertRequestCallback() + callback.nonNullSuccess = { (response: ClientCertResponse) in + if let action = response.action { + completionHandlerCalled = true + switch action { + case 0: + completionHandler(.cancelAuthenticationChallenge, nil) + break + case 1: + let certificatePath = response.certificatePath + let certificatePassword = response.certificatePassword ?? ""; + + var path: String = certificatePath + do { + path = try Util.getAbsPathAsset(assetFilePath: certificatePath) + } catch {} + + if let PKCS12Data = NSData(contentsOfFile: path), + let identityAndTrust: IdentityAndTrust = self.extractIdentity(PKCS12Data: PKCS12Data, password: certificatePassword) { + let urlCredential: URLCredential = URLCredential( + identity: identityAndTrust.identityRef, + certificates: identityAndTrust.certArray as? [AnyObject], + persistence: URLCredential.Persistence.forSession); + completionHandler(.useCredential, urlCredential) + } else { + completionHandler(.performDefaultHandling, nil) + } + + break + case 2: + completionHandler(.cancelAuthenticationChallenge, nil) + break + default: + completionHandler(.performDefaultHandling, nil) + } + return false + } + return true + } + callback.defaultBehaviour = { (response: ClientCertResponse?) in + if !completionHandlerCalled { + completionHandlerCalled = true + completionHandler(.performDefaultHandling, nil) + } + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + let runCallback = { + if let channelDelegate = self.channelDelegate { + channelDelegate.onReceivedClientCertRequest(challenge: ClientCertChallenge(fromChallenge: challenge), callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + if windowId != nil, !windowCreated { + windowBeforeCreatedCallbacks.append(runCallback) + } else { + runCallback() + } + } + else { + completionHandler(.performDefaultHandling, nil) + } + } + + struct IdentityAndTrust { + var identityRef: SecIdentity + var trust: SecTrust + var certArray: AnyObject + } + + func extractIdentity(PKCS12Data: NSData, password: String) -> IdentityAndTrust? { + var identityAndTrust: IdentityAndTrust? + var securityError: OSStatus = errSecSuccess + + var importResult: CFArray? = nil + securityError = SecPKCS12Import( + PKCS12Data as NSData, + [kSecImportExportPassphrase as String: password] as NSDictionary, + &importResult + ) + + if securityError == errSecSuccess { + let certItems: CFArray = importResult! as CFArray; + let certItemsArray: Array = certItems as Array + let dict: AnyObject? = certItemsArray.first; + if let certEntry: Dictionary = dict as? Dictionary { + // grab the identity + let identityPointer: AnyObject? = certEntry["identity"] + let secIdentityRef:SecIdentity = (identityPointer as! SecIdentity?)! + // grab the trust + let trustPointer: AnyObject? = certEntry["trust"] + let trustRef:SecTrust = trustPointer as! SecTrust + // grab the cert + let chainPointer: AnyObject? = certEntry["chain"] + identityAndTrust = IdentityAndTrust(identityRef: secIdentityRef, trust: trustRef, certArray: chainPointer!) + } + } else { + print("Security Error: " + securityError.description) + print(SecCopyErrorMessageString(securityError,nil) ?? "") + } + return identityAndTrust; + } + + @available(macOS 10.12, *) + public func webView( + _ webView: WKWebView, + runOpenPanelWith parameters: WKOpenPanelParameters, + initiatedByFrame frame: WKFrameInfo, + completionHandler: @escaping ([URL]?) -> Void + ) { + let openPanel = NSOpenPanel() + currentOpenPanel = openPanel + openPanel.canChooseFiles = true + if #available(macOS 10.13.4, *) { + openPanel.canChooseDirectories = parameters.allowsDirectories + } + openPanel.allowsMultipleSelection = parameters.allowsMultipleSelection + openPanel.begin { (result) in + if result == .OK { + completionHandler(openPanel.urls) + } else { + completionHandler([]) + } + self.currentOpenPanel = nil + } + } + + func createAlertDialog(message: String?, responseMessage: String?, confirmButtonTitle: String?, completionHandler: @escaping () -> Void) { + let title = responseMessage != nil && !responseMessage!.isEmpty ? responseMessage : message + let okButton = confirmButtonTitle != nil && !confirmButtonTitle!.isEmpty ? confirmButtonTitle : NSLocalizedString("Ok", comment: "") + + let alert = NSAlert() + alert.messageText = title ?? "" + alert.alertStyle = .informational + alert.addButton(withTitle: okButton ?? "") + alert.runModal() + completionHandler() + } + + public func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, + initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) { + + if (isPausedTimers) { + isPausedTimersCompletionHandler = completionHandler + return + } + + var completionHandlerCalled = false + + let callback = WebViewChannelDelegate.JsAlertCallback() + callback.nonNullSuccess = { (response: JsAlertResponse) in + if response.handledByClient { + completionHandlerCalled = true + let action = response.action ?? 1 + switch action { + case 0: + completionHandler() + break + default: + completionHandler() + } + return false + } + return true + } + callback.defaultBehaviour = { [weak self] (response: JsAlertResponse?) in + if !completionHandlerCalled { + completionHandlerCalled = true + let responseMessage = response?.message + let confirmButtonTitle = response?.confirmButtonTitle + self?.createAlertDialog(message: message, responseMessage: responseMessage, + confirmButtonTitle: confirmButtonTitle, completionHandler: completionHandler) + } + } + callback.error = { (code: String, message: String?, details: Any?) in + if !completionHandlerCalled { + completionHandlerCalled = true + print(code + ", " + (message ?? "")) + completionHandler() + } + } + + if let channelDelegate = channelDelegate { + channelDelegate.onJsAlert(url: frame.request.url, message: message, isMainFrame: frame.isMainFrame, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + func createConfirmDialog(message: String?, responseMessage: String?, confirmButtonTitle: String?, cancelButtonTitle: String?, completionHandler: @escaping (Bool) -> Void) { + let dialogMessage = responseMessage != nil && !responseMessage!.isEmpty ? responseMessage : message + let okButton = confirmButtonTitle != nil && !confirmButtonTitle!.isEmpty ? confirmButtonTitle : NSLocalizedString("Ok", comment: "") + let cancelButton = cancelButtonTitle != nil && !cancelButtonTitle!.isEmpty ? cancelButtonTitle : NSLocalizedString("Cancel", comment: "") + + let alert = NSAlert() + alert.messageText = dialogMessage ?? "" + alert.alertStyle = .informational + alert.addButton(withTitle: okButton ?? "") + alert.addButton(withTitle: cancelButton ?? "") + let res = alert.runModal() + completionHandler(res == .alertFirstButtonReturn) + } + + public func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, + completionHandler: @escaping (Bool) -> Void) { + var completionHandlerCalled = false + + let callback = WebViewChannelDelegate.JsConfirmCallback() + callback.nonNullSuccess = { (response: JsConfirmResponse) in + if response.handledByClient { + completionHandlerCalled = true + let action = response.action ?? 1 + switch action { + case 0: + completionHandler(true) + break + case 1: + completionHandler(false) + break + default: + completionHandler(false) + } + return false + } + return true + } + callback.defaultBehaviour = { [weak self] (response: JsConfirmResponse?) in + if !completionHandlerCalled { + completionHandlerCalled = true + let responseMessage = response?.message + let confirmButtonTitle = response?.confirmButtonTitle + let cancelButtonTitle = response?.cancelButtonTitle + self?.createConfirmDialog(message: message, responseMessage: responseMessage, confirmButtonTitle: confirmButtonTitle, cancelButtonTitle: cancelButtonTitle, completionHandler: completionHandler) + } + } + callback.error = { (code: String, message: String?, details: Any?) in + if !completionHandlerCalled { + completionHandlerCalled = true + print(code + ", " + (message ?? "")) + completionHandler(false) + } + } + + if let channelDelegate = channelDelegate { + channelDelegate.onJsConfirm(url: frame.request.url, message: message, isMainFrame: frame.isMainFrame, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + func createPromptDialog(message: String, defaultValue: String?, responseMessage: String?, confirmButtonTitle: String?, cancelButtonTitle: String?, value: String?, completionHandler: @escaping (String?) -> Void) { + let dialogMessage = responseMessage != nil && !responseMessage!.isEmpty ? responseMessage : message + let okButton = confirmButtonTitle != nil && !confirmButtonTitle!.isEmpty ? confirmButtonTitle : NSLocalizedString("Ok", comment: "") + let cancelButton = cancelButtonTitle != nil && !cancelButtonTitle!.isEmpty ? cancelButtonTitle : NSLocalizedString("Cancel", comment: "") + + let alert = NSAlert() + alert.messageText = dialogMessage ?? "" + alert.alertStyle = .informational + alert.addButton(withTitle: okButton ?? "") + alert.addButton(withTitle: cancelButton ?? "") + let txt = NSTextField(frame: NSRect(x: 0, y: 0, width: 200, height: 24)) + txt.stringValue = defaultValue ?? "" + alert.accessoryView = txt + let res = alert.runModal() + + completionHandler(value != nil ? value : (res == .alertFirstButtonReturn ? txt.stringValue : nil)) + } + + public func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt message: String, defaultText defaultValue: String?, initiatedByFrame frame: WKFrameInfo, + completionHandler: @escaping (String?) -> Void) { + + var completionHandlerCalled = false + + let callback = WebViewChannelDelegate.JsPromptCallback() + callback.nonNullSuccess = { (response: JsPromptResponse) in + if response.handledByClient { + completionHandlerCalled = true + let action = response.action ?? 1 + switch action { + case 0: + completionHandler(response.value) + break + case 1: + completionHandler(nil) + break + default: + completionHandler(nil) + } + return false + } + return true + } + callback.defaultBehaviour = { [weak self] (response: JsPromptResponse?) in + if !completionHandlerCalled { + completionHandlerCalled = true + let responseMessage = response?.message + let confirmButtonTitle = response?.confirmButtonTitle + let cancelButtonTitle = response?.cancelButtonTitle + let value = response?.value + self?.createPromptDialog(message: message, defaultValue: defaultValue, responseMessage: responseMessage, confirmButtonTitle: confirmButtonTitle, + cancelButtonTitle: cancelButtonTitle, value: value, completionHandler: completionHandler) + } + } + callback.error = { (code: String, message: String?, details: Any?) in + if !completionHandlerCalled { + completionHandlerCalled = true + print(code + ", " + (message ?? "")) + completionHandler(nil) + } + } + + if let channelDelegate = channelDelegate { + channelDelegate.onJsPrompt(url: frame.request.url, message: message, defaultValue: defaultValue, isMainFrame: frame.isMainFrame, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + public func webView(_ webView: WKWebView, + createWebViewWith configuration: WKWebViewConfiguration, + for navigationAction: WKNavigationAction, + windowFeatures: WKWindowFeatures) -> WKWebView? { + var windowId: Int64 = 0 + let inAppWebViewManager = plugin?.inAppWebViewManager + if let inAppWebViewManager = inAppWebViewManager { + inAppWebViewManager.windowAutoincrementId += 1 + windowId = inAppWebViewManager.windowAutoincrementId + } + + let windowWebView = InAppWebView(id: nil, plugin: nil, frame: CGRect.zero, configuration: configuration) + windowWebView.windowId = windowId + + let webViewTransport = WebViewTransport( + webView: windowWebView, + request: navigationAction.request + ) + + inAppWebViewManager?.windowWebViews[windowId] = webViewTransport + + let createWindowAction = CreateWindowAction(navigationAction: navigationAction, windowId: windowId, windowFeatures: windowFeatures, isDialog: nil) + + let callback = WebViewChannelDelegate.CreateWindowCallback() + callback.nonNullSuccess = { (handledByClient: Bool) in + return !handledByClient + } + callback.defaultBehaviour = { [weak self] (handledByClient: Bool?) in + if inAppWebViewManager?.windowWebViews[windowId] != nil { + inAppWebViewManager?.windowWebViews.removeValue(forKey: windowId) + } + self?.loadUrl(urlRequest: navigationAction.request, allowingReadAccessTo: nil) + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + if let channelDelegate = channelDelegate { + channelDelegate.onCreateWindow(createWindowAction: createWindowAction, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + + return windowWebView + } + + public func webView(_ webView: WKWebView, + authenticationChallenge challenge: URLAuthenticationChallenge, + shouldAllowDeprecatedTLS decisionHandler: @escaping (Bool) -> Void) { + var decisionHandlerCalled = false + let callback = WebViewChannelDelegate.ShouldAllowDeprecatedTLSCallback() + callback.nonNullSuccess = { (action: Bool) in + decisionHandlerCalled = true + decisionHandler(action) + return false + } + callback.defaultBehaviour = { (action: Bool?) in + if !decisionHandlerCalled { + decisionHandlerCalled = true + decisionHandler(false) + } + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + + let runCallback = { + if let channelDelegate = self.channelDelegate { + channelDelegate.shouldAllowDeprecatedTLS(challenge: challenge, callback: callback) + } else { + callback.defaultBehaviour(nil) + } + } + + if windowId != nil, !windowCreated { + windowBeforeCreatedCallbacks.append(runCallback) + } else { + runCallback() + } + } + + public func webViewDidClose(_ webView: WKWebView) { + channelDelegate?.onCloseWindow() + } + + public func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { + channelDelegate?.onWebContentProcessDidTerminate() + } + + public func webView(_ webView: WKWebView, + didCommit navigation: WKNavigation!) { + channelDelegate?.onPageCommitVisible(url: url?.absoluteString) + } + + public func webView(_ webView: WKWebView, + didReceiveServerRedirectForProvisionalNavigation navigation: WKNavigation!) { + channelDelegate?.onDidReceiveServerRedirectForProvisionalNavigation() + } + +// @available(iOS 13.0, *) +// public func webView(_ webView: WKWebView, +// contextMenuConfigurationForElement elementInfo: WKContextMenuElementInfo, +// completionHandler: @escaping (UIContextMenuConfiguration?) -> Void) { +// print("contextMenuConfigurationForElement") +// let actionProvider: UIContextMenuActionProvider = { _ in +// let editMenu = UIMenu(title: "Edit...", children: [ +// UIAction(title: "Copy") { action in +// +// }, +// UIAction(title: "Duplicate") { action in +// +// } +// ]) +// return UIMenu(title: "Title", children: [ +// UIAction(title: "Share") { action in +// +// }, +// editMenu +// ]) +// } +// let contextMenuConfiguration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: actionProvider) +// //completionHandler(contextMenuConfiguration) +// completionHandler(nil) +//// onContextMenuConfigurationForElement(linkURL: elementInfo.linkURL?.absoluteString, result: nil/*{(result) -> Void in +//// if result is FlutterError { +//// print((result as! FlutterError).message ?? "") +//// } +//// else if (result as? NSObject) == FlutterMethodNotImplemented { +//// completionHandler(nil) +//// } +//// else { +//// var response: [String: Any] +//// if let r = result { +//// response = r as! [String: Any] +//// var action = response["action"] as? Int +//// action = action != nil ? action : 0; +//// switch action { +//// case 0: +//// break +//// case 1: +//// break +//// default: +//// completionHandler(nil) +//// } +//// return; +//// } +//// completionHandler(nil) +//// } +//// }*/) +// } +//// +// @available(iOS 13.0, *) +// public func webView(_ webView: WKWebView, +// contextMenuDidEndForElement elementInfo: WKContextMenuElementInfo) { +// print("contextMenuDidEndForElement") +// print(elementInfo) +// //onContextMenuDidEndForElement(linkURL: elementInfo.linkURL?.absoluteString) +// } +// +// @available(iOS 13.0, *) +// public func webView(_ webView: WKWebView, +// contextMenuForElement elementInfo: WKContextMenuElementInfo, +// willCommitWithAnimator animator: UIContextMenuInteractionCommitAnimating) { +// print("willCommitWithAnimator") +// print(elementInfo) +//// onWillCommitWithAnimator(linkURL: elementInfo.linkURL?.absoluteString, result: nil/*{(result) -> Void in +//// if result is FlutterError { +//// print((result as! FlutterError).message ?? "") +//// } +//// else if (result as? NSObject) == FlutterMethodNotImplemented { +//// +//// } +//// else { +//// var response: [String: Any] +//// if let r = result { +//// response = r as! [String: Any] +//// var action = response["action"] as? Int +//// action = action != nil ? action : 0; +////// switch action { +////// case 0: +////// break +////// case 1: +////// break +////// default: +////// +////// } +//// return; +//// } +//// +//// } +//// }*/) +// } +// +// @available(iOS 13.0, *) +// public func webView(_ webView: WKWebView, +// contextMenuWillPresentForElement elementInfo: WKContextMenuElementInfo) { +// print("contextMenuWillPresentForElement") +// print(elementInfo.linkURL) +// //onContextMenuWillPresentForElement(linkURL: elementInfo.linkURL?.absoluteString) +// } + + + // https://stackoverflow.com/a/42840541/4637638 + public func isVideoPlayerWindow(_ notificationObject: AnyObject?) -> Bool { + if let obj = notificationObject, let clazz = NSClassFromString("WebCoreFullScreenWindow") { + return obj.isKind(of: clazz) + } + return false + } + + @objc func onEnterFullscreen(_ notification: Notification) { + // TODO: Still not working on iOS 16.0! +// if #available(iOS 16.0, *) { +// channelDelegate?.onEnterFullscreen() +// inFullscreen = true +// } +// else + if (isVideoPlayerWindow(notification.object as AnyObject?)) { + channelDelegate?.onEnterFullscreen() + inFullscreen = true + } + } + + @objc func onExitFullscreen(_ notification: Notification) { + // TODO: Still not working on iOS 16.0! +// if #available(iOS 16.0, *) { +// channelDelegate?.onExitFullscreen() +// inFullscreen = false +// } +// else + if (isVideoPlayerWindow(notification.object as AnyObject?)) { + channelDelegate?.onExitFullscreen() + inFullscreen = false + } + } + +// public func onContextMenuConfigurationForElement(linkURL: String?, result: FlutterResult?) { +// let arguments: [String: Any?] = ["linkURL": linkURL] +// channel?.invokeMethod("onContextMenuConfigurationForElement", arguments: arguments, result: result) +// } +// +// public func onContextMenuDidEndForElement(linkURL: String?) { +// let arguments: [String: Any?] = ["linkURL": linkURL] +// channel?.invokeMethod("onContextMenuDidEndForElement", arguments: arguments) +// } +// +// public func onWillCommitWithAnimator(linkURL: String?, result: FlutterResult?) { +// let arguments: [String: Any?] = ["linkURL": linkURL] +// channel?.invokeMethod("onWillCommitWithAnimator", arguments: arguments, result: result) +// } +// +// public func onContextMenuWillPresentForElement(linkURL: String?) { +// let arguments: [String: Any?] = ["linkURL": linkURL] +// channel?.invokeMethod("onContextMenuWillPresentForElement", arguments: arguments) +// } + + public func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + guard let body = message.body as? [String: Any?] else { + return + } + + if ["consoleLog", "consoleDebug", "consoleError", "consoleInfo", "consoleWarn"].contains(message.name) { + var messageLevel = 1 + switch (message.name) { + case "consoleLog": + messageLevel = 1 + break; + case "consoleDebug": + // on Android, console.debug is TIP + messageLevel = 0 + break; + case "consoleError": + messageLevel = 3 + break; + case "consoleInfo": + // on Android, console.info is LOG + messageLevel = 1 + break; + case "consoleWarn": + messageLevel = 2 + break; + default: + messageLevel = 1 + break; + } + let consoleMessage = body["message"] as? String ?? "" + + let _windowId = body["_windowId"] as? Int64 + var webView = self + if let wId = _windowId, let webViewTransport = plugin?.inAppWebViewManager?.windowWebViews[wId] { + webView = webViewTransport.webView + } + webView.channelDelegate?.onConsoleMessage(message: consoleMessage, messageLevel: messageLevel) + } else if message.name == "callHandler", let handlerName = body["handlerName"] as? String { + if handlerName == "onPrintRequest" { + let settings = PrintJobSettings() + settings.handledByClient = true + if let printJobId = printCurrentPage(settings: settings) { + let callback = WebViewChannelDelegate.PrintRequestCallback() + callback.nonNullSuccess = { (handledByClient: Bool) in + return !handledByClient + } + callback.defaultBehaviour = { [weak self] (handledByClient: Bool?) in + if let printJob = self?.plugin?.printJobManager?.jobs[printJobId] { + printJob?.disposeNoDismiss() + } + } + callback.error = { [weak callback] (code: String, message: String?, details: Any?) in + print(code + ", " + (message ?? "")) + callback?.defaultBehaviour(nil) + } + channelDelegate?.onPrintRequest(url: url, printJobId: printJobId, callback: callback) + } + return + } + + let _callHandlerID = body["_callHandlerID"] as? Int64 ?? 0 + let args = body["args"] as? String ?? "" + + let _windowId = body["_windowId"] as? Int64 + var webView = self + if let wId = _windowId, let webViewTransport = plugin?.inAppWebViewManager?.windowWebViews[wId] { + webView = webViewTransport.webView + } + + let callback = WebViewChannelDelegate.CallJsHandlerCallback() + callback.defaultBehaviour = { [weak self] (response: Any?) in + var json = "null" + if let r = response as? String { + json = r + } + + self?.evaluateJavaScript(""" +if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { + window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)].resolve(\(json)); + delete window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)]; +} +""", completionHandler: nil) + } + callback.error = { [weak self] (code: String, message: String?, details: Any?) in + let errorMessage = code + (message != nil ? ", " + (message ?? "") : "") + print(errorMessage) + + self?.evaluateJavaScript(""" +if(window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)] != null) { + window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)].reject(new Error('\(errorMessage.replacingOccurrences(of: "\'", with: "\\'"))')); + delete window.\(JAVASCRIPT_BRIDGE_NAME)[\(_callHandlerID)]; +} +""", completionHandler: nil) + } + + if let channelDelegate = webView.channelDelegate { + channelDelegate.onCallJsHandler(handlerName: handlerName, args: args, callback: callback) + } + } else if message.name == "onFindResultReceived", + let findResult = body["findResult"] as? [String: Any], + let activeMatchOrdinal = findResult["activeMatchOrdinal"] as? Int, + let numberOfMatches = findResult["numberOfMatches"] as? Int, + let isDoneCounting = findResult["isDoneCounting"] as? Bool { + + let _windowId = body["_windowId"] as? Int64 + var webView = self + if let wId = _windowId, let webViewTransport = plugin?.inAppWebViewManager?.windowWebViews[wId] { + webView = webViewTransport.webView + } + webView.findInteractionController?.channelDelegate?.onFindResultReceived(activeMatchOrdinal: activeMatchOrdinal, numberOfMatches: numberOfMatches, isDoneCounting: isDoneCounting) + webView.channelDelegate?.onFindResultReceived(activeMatchOrdinal: activeMatchOrdinal, numberOfMatches: numberOfMatches, isDoneCounting: isDoneCounting) + } else if message.name == "onScrollChanged", + let x = body["x"] as? Int, + let y = body["y"] as? Int { + let _windowId = body["_windowId"] as? Int64 + var webView = self + if let wId = _windowId, let webViewTransport = plugin?.inAppWebViewManager?.windowWebViews[wId] { + webView = webViewTransport.webView + } + webView.channelDelegate?.onScrollChanged(x: x, y: y) + } else if message.name == "onCallAsyncJavaScriptResultBelowIOS14Received", + let resultUuid = body["resultUuid"] as? String, + let result = callAsyncJavaScriptBelowMacOS11Results[resultUuid] { + result([ + "value": body["value"], + "error": body["error"] + ]) + callAsyncJavaScriptBelowMacOS11Results.removeValue(forKey: resultUuid) + } else if message.name == "onWebMessagePortMessageReceived", + let webMessageChannelId = body["webMessageChannelId"] as? String, + let index = body["index"] as? Int64 { + var webMessage: WebMessage? = nil + if let webMessageMap = body["message"] as? [String : Any?] { + webMessage = WebMessage.fromMap(map: webMessageMap) + } + + if let webMessageChannel = webMessageChannels[webMessageChannelId] { + webMessageChannel.channelDelegate?.onMessage(index: index, message: webMessage) + } + } else if message.name == "onWebMessageListenerPostMessageReceived", let jsObjectName = body["jsObjectName"] as? String { + var webMessage: WebMessage? = nil + if let webMessageMap = body["message"] as? [String : Any?] { + webMessage = WebMessage.fromMap(map: webMessageMap) + } + + if let webMessageListener = webMessageListeners.first(where: ({($0.jsObjectName == jsObjectName)})) { + let isMainFrame = message.frameInfo.isMainFrame + + let securityOrigin = message.frameInfo.securityOrigin + let scheme = securityOrigin.protocol + let host = securityOrigin.host + let port = securityOrigin.port + + if !webMessageListener.isOriginAllowed(scheme: scheme, host: host, port: port) { + return + } + + var sourceOrigin: URL? = nil + if !scheme.isEmpty, !host.isEmpty { + sourceOrigin = URL(string: "\(scheme)://\(host)\(port != 0 ? ":" + String(port) : "")") + } + webMessageListener.channelDelegate?.onPostMessage(message: webMessage, sourceOrigin: sourceOrigin, isMainFrame: isMainFrame) + } + } + } + + public func scrollTo(x: Int, y: Int, animated: Bool) { + evaluateJavaScript("window.scrollTo({left: \(x), top: \(y), behavior: \(animated ? "'smooth'" : "'auto'")})") + } + + public func scrollBy(x: Int, y: Int, animated: Bool) { + evaluateJavaScript("window.scrollBy({left: \(x), top: \(y), behavior: \(animated ? "'smooth'" : "'auto'")})") + } + + + public func pauseTimers() { + if !isPausedTimers { + isPausedTimers = true + let script = "alert();"; + self.evaluateJavaScript(script, completionHandler: nil) + } + } + + public func resumeTimers() { + if isPausedTimers { + if let completionHandler = isPausedTimersCompletionHandler { + self.isPausedTimersCompletionHandler = nil + completionHandler() + } + isPausedTimers = false + } + } + + public func printCurrentPage(settings: PrintJobSettings? = nil, + completionHandler: PrintJobController.CompletionHandler? = nil) -> String? { + if #available(macOS 11.0, *) { + var printJobId: String? = nil + if let settings = settings, settings.handledByClient { + printJobId = NSUUID().uuidString + } + + var printInfoDictionary: [NSPrintInfo.AttributeKey : Any] = [:] + if let settings = settings { + if let jobSavingURL = settings.jobSavingURL, let url = URL(string: jobSavingURL) { + printInfoDictionary[.jobSavingURL] = url + } + printInfoDictionary[.copies] = settings.copies + if let firstPage = settings.firstPage { + printInfoDictionary[.firstPage] = firstPage + } + if let lastPage = settings.lastPage { + printInfoDictionary[.lastPage] = lastPage + } + printInfoDictionary[.detailedErrorReporting] = settings.detailedErrorReporting + printInfoDictionary[.faxNumber] = settings.faxNumber ?? "" + printInfoDictionary[.headerAndFooter] = settings.headerAndFooter + if let mustCollate = settings.mustCollate { + printInfoDictionary[.mustCollate] = mustCollate + } + if let pagesAcross = settings.pagesAcross { + printInfoDictionary[.pagesAcross] = pagesAcross + } + if let pagesDown = settings.pagesDown { + printInfoDictionary[.pagesDown] = pagesDown + } + if let time = settings.time { + printInfoDictionary[.time] = Date(timeIntervalSince1970: TimeInterval(Double(time)/1000)) + } + } + + let printInfo = NSPrintInfo(dictionary: printInfoDictionary) + + if let settings = settings { + if let orientationValue = settings.orientation, + let orientation = NSPrintInfo.PaperOrientation.init(rawValue: orientationValue) { + printInfo.orientation = orientation + } + if let margins = settings.margins { + printInfo.topMargin = margins.top + printInfo.rightMargin = margins.right + printInfo.bottomMargin = margins.bottom + printInfo.leftMargin = margins.left + } + if let numberOfPages = settings.numberOfPages { + printInfo.printSettings["com_apple_print_PrintSettings_PMLastPage"] = numberOfPages + } + if let colorMode = settings.colorMode { + printInfo.printSettings["ColorModel"] = colorMode + } + if let scalingFactor = settings.scalingFactor { + printInfo.scalingFactor = scalingFactor + } + if let jobDisposition = settings.jobDisposition { + printInfo.jobDisposition = Util.getNSPrintInfoJobDisposition(name: jobDisposition) + } + if let paperName = settings.paperName { + printInfo.paperName = NSPrinter.PaperName.init(rawValue: paperName) + } + if let horizontalPagination = settings.horizontalPagination, + let pagination = NSPrintInfo.PaginationMode.init(rawValue: horizontalPagination) { + printInfo.horizontalPagination = pagination + } + if let verticalPagination = settings.verticalPagination, + let pagination = NSPrintInfo.PaginationMode.init(rawValue: verticalPagination) { + printInfo.verticalPagination = pagination + } + printInfo.isHorizontallyCentered = settings.isHorizontallyCentered + printInfo.isVerticallyCentered = settings.isVerticallyCentered + } + let printOperation = printOperation(with: printInfo) + printOperation.jobTitle = settings?.jobName ?? (title ?? url?.absoluteString ?? "") + " Document" + printOperation.view?.frame = bounds + + if let settings = settings { + if let pageOrder = settings.pageOrder, let order = NSPrintOperation.PageOrder.init(rawValue: pageOrder) { + printOperation.pageOrder = order + } + printOperation.canSpawnSeparateThread = settings.canSpawnSeparateThread + printOperation.showsPrintPanel = settings.showsPrintPanel + printOperation.showsProgressPanel = settings.showsProgressPanel + if settings.showsPaperOrientation { + printOperation.printPanel.options.insert(.showsOrientation) + } else { + printOperation.printPanel.options.remove(.showsOrientation) + } + if settings.showsNumberOfCopies { + printOperation.printPanel.options.insert(.showsCopies) + } else { + printOperation.printPanel.options.remove(.showsCopies) + } + if settings.showsPaperSize { + printOperation.printPanel.options.insert(.showsPaperSize) + } else { + printOperation.printPanel.options.remove(.showsPaperSize) + } + if settings.showsScaling { + printOperation.printPanel.options.insert(.showsScaling) + } else { + printOperation.printPanel.options.remove(.showsScaling) + } + if settings.showsPageRange { + printOperation.printPanel.options.insert(.showsPageRange) + } else { + printOperation.printPanel.options.remove(.showsPageRange) + } + if settings.showsPageSetupAccessory { + printOperation.printPanel.options.insert(.showsPageSetupAccessory) + } else { + printOperation.printPanel.options.remove(.showsPageSetupAccessory) + } + if settings.showsPreview { + printOperation.printPanel.options.insert(.showsPreview) + } else { + printOperation.printPanel.options.remove(.showsPreview) + } + if settings.showsPrintSelection { + printOperation.printPanel.options.insert(.showsPrintSelection) + } else { + printOperation.printPanel.options.remove(.showsPrintSelection) + } + } + + if let id = printJobId, let plugin = plugin { + let printJob = PrintJobController(plugin: plugin, id: id, job: printOperation, settings: settings) + plugin.printJobManager?.jobs[id] = printJob + printJob.present(parentWindow: window, completionHandler: completionHandler) + } else if let window = window { + printJobCompletionHandler = completionHandler + printOperation.runModal(for: window, delegate: self, didRun: #selector(printOperationDidRun), contextInfo: nil) + } else { + printView(self) + } + + return printJobId + } else { + printView(self) + } + return nil + } + + @objc func printOperationDidRun(printOperation: NSPrintOperation, + success: Bool, + contextInfo: UnsafeMutableRawPointer?) { + if let completionHandler = printJobCompletionHandler { + completionHandler(printOperation, success, contextInfo) + printJobCompletionHandler = nil + } + } + + public func getContentHeight(completionHandler: @escaping ((Int64?, Error?) -> Void)) { + evaluateJavaScript("document.body.scrollHeight") { scrollHeight, error in + if let error = error { + completionHandler(nil, error) + } else { + completionHandler(Int64(scrollHeight as? Double ?? 0.0), nil) + } + } + } + + public func getContentWidth(completionHandler: @escaping ((Int64?, Error?) -> Void)) { + evaluateJavaScript("document.body.scrollWidth") { scrollWidth, error in + if let error = error { + completionHandler(nil, error) + } else { + completionHandler(Int64(scrollWidth as? Double ?? 0.0), nil) + } + } + } + + public func getOriginalUrl() -> URL? { + return currentOriginalUrl + } + + public func getSelectedText(completionHandler: @escaping (Any?, Error?) -> Void) { + if configuration.preferences.javaScriptEnabled { + evaluateJavaScript(PluginScriptsUtil.GET_SELECTED_TEXT_JS_SOURCE, completionHandler: completionHandler) + } else { + completionHandler(nil, nil) + } + } + + public func getCertificate() -> SslCertificate? { + guard let scheme = url?.scheme, + scheme == "https", + let host = url?.host, + let sslCertificate = InAppWebView.sslCertificatesMap[host] else { + return nil + } + return sslCertificate + } + + public func isSecureContext(completionHandler: @escaping (_ isSecureContext: Bool) -> Void) { + evaluateJavascript(source: "window.isSecureContext") { (isSecureContext) in + if let isSecureContext = isSecureContext { + completionHandler(isSecureContext as? Bool ?? false) + return + } + completionHandler(false) + } + } + + public func canScrollVertically(completionHandler: @escaping ((Bool, Error?) -> Void)) { + getContentHeight { contentHeight, error in + if let error = error { + completionHandler(false, error) + } else { + completionHandler(CGFloat(contentHeight ?? 0) > self.frame.height, nil) + } + } + } + + public func canScrollHorizontally(completionHandler: @escaping ((Bool, Error?) -> Void)) { + getContentWidth { contentWidth, error in + if let error = error { + completionHandler(false, error) + } else { + completionHandler(CGFloat(contentWidth ?? 0) > self.frame.width, nil) + } + } + } + + public func createWebMessageChannel(completionHandler: ((WebMessageChannel?) -> Void)? = nil) -> WebMessageChannel? { + guard let plugin = plugin else { + completionHandler?(nil) + return nil + } + let id = NSUUID().uuidString + let webMessageChannel = WebMessageChannel(plugin: plugin, id: id) + webMessageChannel.initJsInstance(webView: self, completionHandler: completionHandler) + webMessageChannels[id] = webMessageChannel + + return webMessageChannel + } + + public func postWebMessage(message: WebMessage, targetOrigin: String, completionHandler: ((Any?) -> Void)? = nil) throws { + var portsString = "null" + if let ports = message.ports { + var portArrayString: [String] = [] + for port in ports { + if port.isStarted { + throw NSError(domain: "Port is already started", code: 0) + } + if port.isClosed || port.isTransferred { + throw NSError(domain: "Port is already closed or transferred", code: 0) + } + port.isTransferred = true + portArrayString.append("\(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)['\(port.webMessageChannel!.id)'].\(port.name)") + } + portsString = "[" + portArrayString.joined(separator: ", ") + "]" + } + + let url = URL(string: targetOrigin)?.absoluteString ?? "*" + let source = """ + (function() { + window.postMessage(\(message.jsData), '\(url)', \(portsString)); + })(); + """ + evaluateJavascript(source: source, completionHandler: completionHandler) + message.dispose() + } + + public func addWebMessageListener(webMessageListener: WebMessageListener) throws { + if webMessageListeners.map({ ($0.jsObjectName) }).contains(webMessageListener.jsObjectName) { + throw NSError(domain: "jsObjectName \(webMessageListener.jsObjectName) was already added.", code: 0) + } + try webMessageListener.assertOriginRulesValid() + webMessageListener.initJsInstance(webView: self) + webMessageListeners.append(webMessageListener) + } + + public func disposeWebMessageChannels() { + for webMessageChannel in webMessageChannels.values { + webMessageChannel.dispose() + } + webMessageChannels.removeAll() + } + + public func getScrollX(completionHandler: @escaping ((Int64?, Error?) -> Void)) { + evaluateJavaScript("window.scrollX") { scrollX, error in + if let error = error { + completionHandler(nil, error) + } else { + completionHandler(Int64(scrollX as? Double ?? 0.0), nil) + } + } + } + + public func getScrollY(completionHandler: @escaping ((Int64?, Error?) -> Void)) { + evaluateJavaScript("window.scrollY") { scrollY, error in + if let error = error { + completionHandler(nil, error) + } else { + completionHandler(Int64(scrollY as? Double ?? 0.0), nil) + } + } + } + + public func runWindowBeforeCreatedCallbacks() { + let callbacks = windowBeforeCreatedCallbacks + callbacks.forEach { (callback) in + callback() + } + windowBeforeCreatedCallbacks.removeAll() + } + + public func dispose() { + channelDelegate?.dispose() + channelDelegate = nil + runWindowBeforeCreatedCallbacks() + currentOpenPanel?.cancel(self) + currentOpenPanel?.close() + currentOpenPanel = nil + printJobCompletionHandler = nil + removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress)) + removeObserver(self, forKeyPath: #keyPath(WKWebView.url)) + removeObserver(self, forKeyPath: #keyPath(WKWebView.title)) + if #available(macOS 12.0, *) { + removeObserver(self, forKeyPath: #keyPath(WKWebView.cameraCaptureState)) + removeObserver(self, forKeyPath: #keyPath(WKWebView.microphoneCaptureState)) + } + // TODO: Still not working on iOS 16.0! +// if #available(iOS 16.0, *) { +// removeObserver(self, forKeyPath: #keyPath(WKWebView.fullscreenState)) +// } + resumeTimers() + stopLoading() + disposeWebMessageChannels() + for webMessageListener in webMessageListeners { + webMessageListener.dispose() + } + webMessageListeners.removeAll() + interceptOnlyAsyncAjaxRequestsPluginScript = nil + if windowId == nil { + configuration.userContentController.removeAllPluginScriptMessageHandlers() + configuration.userContentController.removeScriptMessageHandler(forName: "onCallAsyncJavaScriptResultBelowIOS14Received") + configuration.userContentController.removeScriptMessageHandler(forName: "onWebMessagePortMessageReceived") + configuration.userContentController.removeScriptMessageHandler(forName: "onWebMessageListenerPostMessageReceived") + configuration.userContentController.removeAllUserScripts() + if #available(macOS 10.13, *) { + configuration.userContentController.removeAllContentRuleLists() + } + } else if let wId = windowId, plugin?.inAppWebViewManager?.windowWebViews[wId] != nil { + plugin?.inAppWebViewManager?.windowWebViews.removeValue(forKey: wId) + } + configuration.userContentController.dispose(windowId: windowId) + NotificationCenter.default.removeObserver(self) + for imp in customIMPs { + imp_removeBlock(imp) + } + findInteractionController?.dispose() + findInteractionController = nil + uiDelegate = nil + navigationDelegate = nil + isPausedTimersCompletionHandler = nil + callAsyncJavaScriptBelowMacOS11Results.removeAll() + plugin = nil + } + + deinit { + debugPrint("InAppWebView - dealloc") + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/InAppWebViewManager.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/InAppWebViewManager.swift new file mode 100755 index 00000000..1afc4fe8 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/InAppWebViewManager.swift @@ -0,0 +1,124 @@ +// +// InAppWebViewManager.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/12/2019. +// + +import Foundation +import WebKit +import FlutterMacOS + +public class InAppWebViewManager: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_manager" + var plugin: InAppWebViewFlutterPlugin? + var webViewForUserAgent: WKWebView? + var defaultUserAgent: String? + + var keepAliveWebViews: [String:FlutterWebViewController?] = [:] + var windowWebViews: [Int64:WebViewTransport] = [:] + var windowAutoincrementId: Int64 = 0 + + init(plugin: InAppWebViewFlutterPlugin) { + super.init(channel: FlutterMethodChannel(name: InAppWebViewManager.METHOD_CHANNEL_NAME, binaryMessenger: plugin.registrar!.messenger)) + self.plugin = plugin + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "getDefaultUserAgent": + getDefaultUserAgent(completionHandler: { (value) in + result(value) + }) + break + case "handlesURLScheme": + let urlScheme = arguments!["urlScheme"] as! String + if #available(macOS 10.13, *) { + result(WKWebView.handlesURLScheme(urlScheme)) + } else { + result(false) + } + break + case "disposeKeepAlive": + let keepAliveId = arguments!["keepAliveId"] as! String + disposeKeepAlive(keepAliveId: keepAliveId) + result(true) + break + case "clearAllCache": + let includeDiskFiles = arguments!["includeDiskFiles"] as! Bool + clearAllCache(includeDiskFiles: includeDiskFiles, completionHandler: { + result(true) + }) + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func getDefaultUserAgent(completionHandler: @escaping (_ value: String?) -> Void) { + if defaultUserAgent == nil { + webViewForUserAgent = WKWebView() + webViewForUserAgent?.evaluateJavaScript("navigator.userAgent") { (value, error) in + + if error != nil { + print("Error occurred to get userAgent") + self.webViewForUserAgent = nil + completionHandler(nil) + return + } + + if let unwrappedUserAgent = value as? String { + self.defaultUserAgent = unwrappedUserAgent + completionHandler(self.defaultUserAgent) + } else { + print("Failed to get userAgent") + } + self.webViewForUserAgent = nil + } + } else { + completionHandler(defaultUserAgent) + } + } + + public func disposeKeepAlive(keepAliveId: String) { + if let flutterWebView = keepAliveWebViews[keepAliveId] as? FlutterWebViewController { + flutterWebView.keepAliveId = nil + flutterWebView.dispose(removeFromSuperview: true) + keepAliveWebViews[keepAliveId] = nil + } + } + + public func clearAllCache(includeDiskFiles: Bool, completionHandler: @escaping () -> Void) { + var websiteDataTypes = Set([WKWebsiteDataTypeMemoryCache]) + if includeDiskFiles { + websiteDataTypes.insert(WKWebsiteDataTypeDiskCache) + if #available(macOS 10.13.4, *) { + websiteDataTypes.insert(WKWebsiteDataTypeFetchCache) + } + websiteDataTypes.insert(WKWebsiteDataTypeOfflineWebApplicationCache) + } + let date = NSDate(timeIntervalSince1970: 0) + WKWebsiteDataStore.default().removeData(ofTypes: websiteDataTypes, modifiedSince: date as Date, completionHandler: completionHandler) + } + + public override func dispose() { + super.dispose() + let keepAliveWebViewValues = keepAliveWebViews.values + keepAliveWebViewValues.forEach {(keepAliveWebView: FlutterWebViewController?) in + if let keepAliveId = keepAliveWebView?.keepAliveId { + disposeKeepAlive(keepAliveId: keepAliveId) + } + } + keepAliveWebViews.removeAll() + windowWebViews.removeAll() + webViewForUserAgent = nil + defaultUserAgent = nil + plugin = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/InAppWebViewSettings.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/InAppWebViewSettings.swift new file mode 100755 index 00000000..fbf0d804 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/InAppWebViewSettings.swift @@ -0,0 +1,118 @@ +// +// InAppWebViewSettings.swift +// flutter_inappwebview +// +// Created by Lorenzo on 21/10/18. +// + +import Foundation +import WebKit + +@objcMembers +public class InAppWebViewSettings: ISettings { + + var gopeedProfileId = "" + var useShouldOverrideUrlLoading = false + var useOnLoadResource = false + var useOnDownloadStart = false + @available(*, deprecated, message: "Use InAppWebViewManager.clearAllCache instead.") + var clearCache = false + var userAgent = "" + var applicationNameForUserAgent = "" + var javaScriptEnabled = true + var javaScriptCanOpenWindowsAutomatically = false + var mediaPlaybackRequiresUserGesture = true + var resourceCustomSchemes: [String] = [] + var contentBlockers: [[String: [String : Any]]] = [] + var minimumFontSize = 0 + var useShouldInterceptAjaxRequest = false + var interceptOnlyAsyncAjaxRequests = true + var useShouldInterceptFetchRequest = false + var incognito = false + var cacheEnabled = true + var transparentBackground = false + var supportZoom = true + var allowUniversalAccessFromFileURLs = false + var allowFileAccessFromFileURLs = false + + var enableViewportScale = false + var suppressesIncrementalRendering = false + var allowsAirPlayForMediaPlayback = true + var allowsBackForwardNavigationGestures = true + var allowsLinkPreview = true + var isFraudulentWebsiteWarningEnabled = true + var preferredContentMode = 0 + var sharedCookiesEnabled = false + var mediaType: String? = nil + var pageZoom = 1.0 + var limitsNavigationsToAppBoundDomains = false + var useOnNavigationResponse = false + var applePayAPIEnabled = false + var allowingReadAccessTo: String? = nil + var underPageBackgroundColor: String? + var isTextInteractionEnabled = true + var isSiteSpecificQuirksModeEnabled = true + var upgradeKnownHostsToHTTPS = true + var isElementFullscreenEnabled = true + var isInspectable = false + var shouldPrintBackgrounds = false + + override init(){ + super.init() + } + + override func parse(settings: [String: Any?]) -> InAppWebViewSettings { + let _ = super.parse(settings: settings) + if #available(macOS 10.15, *) {} else { + applePayAPIEnabled = false + } + return self + } + + override func getRealSettings(obj: InAppWebView?) -> [String: Any?] { + var realSettings: [String: Any?] = toMap() + if let webView = obj { + let configuration = webView.configuration + realSettings["userAgent"] = webView.customUserAgent + realSettings["applicationNameForUserAgent"] = configuration.applicationNameForUserAgent + realSettings["allowsAirPlayForMediaPlayback"] = configuration.allowsAirPlayForMediaPlayback + realSettings["allowsLinkPreview"] = webView.allowsLinkPreview + realSettings["javaScriptCanOpenWindowsAutomatically"] = configuration.preferences.javaScriptCanOpenWindowsAutomatically + if #available(macOS 10.12, *) { + realSettings["mediaPlaybackRequiresUserGesture"] = configuration.mediaTypesRequiringUserActionForPlayback == .all + } + realSettings["minimumFontSize"] = Int(configuration.preferences.minimumFontSize) + realSettings["suppressesIncrementalRendering"] = configuration.suppressesIncrementalRendering + realSettings["allowsBackForwardNavigationGestures"] = webView.allowsBackForwardNavigationGestures + if #available(macOS 10.15, *) { + realSettings["isFraudulentWebsiteWarningEnabled"] = configuration.preferences.isFraudulentWebsiteWarningEnabled + realSettings["preferredContentMode"] = configuration.defaultWebpagePreferences.preferredContentMode.rawValue + } + realSettings["allowUniversalAccessFromFileURLs"] = configuration.value(forKey: "allowUniversalAccessFromFileURLs") as? Bool + realSettings["allowFileAccessFromFileURLs"] = configuration.preferences.value(forKey: "allowFileAccessFromFileURLs") as? Bool + realSettings["javaScriptEnabled"] = configuration.preferences.javaScriptEnabled + if #available(macOS 11.0, *) { + realSettings["mediaType"] = webView.mediaType + realSettings["pageZoom"] = Float(webView.pageZoom) + realSettings["limitsNavigationsToAppBoundDomains"] = configuration.limitsNavigationsToAppBoundDomains + realSettings["javaScriptEnabled"] = configuration.defaultWebpagePreferences.allowsContentJavaScript + } + if #available(macOS 11.3, *) { + realSettings["isTextInteractionEnabled"] = configuration.preferences.isTextInteractionEnabled + realSettings["upgradeKnownHostsToHTTPS"] = configuration.upgradeKnownHostsToHTTPS + } + if #available(macOS 12.0, *) { + realSettings["underPageBackgroundColor"] = webView.underPageBackgroundColor.hexString + } + if #available(macOS 12.3, *) { + realSettings["isSiteSpecificQuirksModeEnabled"] = configuration.preferences.isSiteSpecificQuirksModeEnabled + realSettings["isElementFullscreenEnabled"] = configuration.preferences.isElementFullscreenEnabled + } + if #available(macOS 13.3, *) { + realSettings["isInspectable"] = webView.isInspectable + realSettings["shouldPrintBackgrounds"] = configuration.preferences.shouldPrintBackgrounds + } + } + return realSettings + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/WebMessage/WebMessageChannel.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/WebMessage/WebMessageChannel.swift new file mode 100644 index 00000000..c806e0de --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/WebMessage/WebMessageChannel.swift @@ -0,0 +1,80 @@ +// +// WebMessageChannel.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/03/21. +// + +import Foundation +import FlutterMacOS + +public class WebMessageChannel: FlutterMethodCallDelegate { + static var METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_web_message_channel_" + var id: String + var plugin: InAppWebViewFlutterPlugin? + var channelDelegate: WebMessageChannelChannelDelegate? + weak var webView: InAppWebView? + var ports: [WebMessagePort] = [] + + public init(plugin: InAppWebViewFlutterPlugin, id: String) { + self.id = id + self.plugin = plugin + super.init() + if let registrar = plugin.registrar { + let channel = FlutterMethodChannel(name: WebMessageChannel.METHOD_CHANNEL_NAME_PREFIX + id, + binaryMessenger: registrar.messenger) + self.channelDelegate = WebMessageChannelChannelDelegate(webMessageChannel: self, channel: channel) + } + self.ports = [ + WebMessagePort(name: "port1", index: 0, webMessageChannelId: self.id, webMessageChannel: self), + WebMessagePort(name: "port2", index: 1, webMessageChannelId: self.id, webMessageChannel: self) + ] + } + + public func initJsInstance(webView: InAppWebView, completionHandler: ((WebMessageChannel?) -> Void)? = nil) { + self.webView = webView + if let webView = self.webView { + webView.evaluateJavascript(source: """ + (function() { + \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(id)"] = new MessageChannel(); + })(); + """) { (_) in + completionHandler?(self) + } + } else { + completionHandler?(nil) + } + } + + public func toMap() -> [String:Any?] { + return [ + "id": id + ] + } + + public func dispose() { + channelDelegate?.dispose() + channelDelegate = nil + for port in ports { + port.dispose() + } + ports.removeAll() + webView?.evaluateJavascript(source: """ + (function() { + var webMessageChannel = \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(id)"]; + if (webMessageChannel != null) { + webMessageChannel.port1.close(); + webMessageChannel.port2.close(); + delete \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(id)"]; + } + })(); + """) + webView = nil + plugin = nil + } + + deinit { + debugPrint("WebMessageChannel - dealloc") + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/WebMessage/WebMessageChannelChannelDelegate.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/WebMessage/WebMessageChannelChannelDelegate.swift new file mode 100644 index 00000000..462141c5 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/WebMessage/WebMessageChannelChannelDelegate.swift @@ -0,0 +1,103 @@ +// +// WebMessageChannelChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation +import FlutterMacOS + +public class WebMessageChannelChannelDelegate: ChannelDelegate { + private weak var webMessageChannel: WebMessageChannel? + + public init(webMessageChannel: WebMessageChannel, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.webMessageChannel = webMessageChannel + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "setWebMessageCallback": + if let _ = webMessageChannel?.webView, let ports = webMessageChannel?.ports, ports.count > 0 { + let index = arguments!["index"] as! Int + let port = ports[index] + do { + try port.setWebMessageCallback { (_) in + result(true) + } + } catch let error as NSError { + result(FlutterError(code: "WebMessageChannel", message: error.domain, details: nil)) + } + + } else { + result(true) + } + break + case "postMessage": + if let webView = webMessageChannel?.webView, let ports = webMessageChannel?.ports, ports.count > 0 { + let index = arguments!["index"] as! Int + let port = ports[index] + var message = WebMessage.fromMap(map: arguments!["message"] as! [String: Any?]) + + var ports: [WebMessagePort] = [] + if let notConnectedPorts = message.ports { + for notConnectedPort in notConnectedPorts { + if let webMessageChannel = webView.webMessageChannels[notConnectedPort.webMessageChannelId] { + ports.append(webMessageChannel.ports[Int(notConnectedPort.index)]) + } + } + } + message.ports = ports + + do { + try port.postMessage(message: message) { (_) in + result(true) + } + } catch let error as NSError { + result(FlutterError(code: "WebMessageChannel", message: error.domain, details: nil)) + } + } else { + result(true) + } + break + case "close": + if let _ = webMessageChannel?.webView, let ports = webMessageChannel?.ports, ports.count > 0 { + let index = arguments!["index"] as! Int + let port = ports[index] + do { + try port.close { (_) in + result(true) + } + } catch let error as NSError { + result(FlutterError(code: "WebMessageChannel", message: error.domain, details: nil)) + } + } else { + result(true) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onMessage(index: Int64, message: WebMessage?) { + let arguments: [String:Any?] = [ + "index": index, + "message": message?.toMap() + ] + channel?.invokeMethod("onMessage", arguments: arguments) + } + + public override func dispose() { + super.dispose() + webMessageChannel = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/WebMessage/WebMessageListener.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/WebMessage/WebMessageListener.swift new file mode 100644 index 00000000..270f01d3 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/WebMessage/WebMessageListener.swift @@ -0,0 +1,193 @@ +// +// WebMessageListener.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/03/21. +// + +import Foundation +import WebKit +import FlutterMacOS + +public class WebMessageListener: FlutterMethodCallDelegate { + static var METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_web_message_listener_" + var id: String + var jsObjectName: String + var allowedOriginRules: Set + var channelDelegate: WebMessageListenerChannelDelegate? + weak var webView: InAppWebView? + var plugin: InAppWebViewFlutterPlugin? + + public init(plugin: InAppWebViewFlutterPlugin, id: String, jsObjectName: String, allowedOriginRules: Set) { + self.id = id + self.plugin = plugin + self.jsObjectName = jsObjectName + self.allowedOriginRules = allowedOriginRules + super.init() + if let registrar = plugin.registrar { + let channel = FlutterMethodChannel(name: WebMessageListener.METHOD_CHANNEL_NAME_PREFIX + self.id + "_" + self.jsObjectName, + binaryMessenger: registrar.messenger) + self.channelDelegate = WebMessageListenerChannelDelegate(webMessageListener: self, channel: channel) + } + } + + public func assertOriginRulesValid() throws { + for (index, originRule) in allowedOriginRules.enumerated() { + if originRule.isEmpty { + throw NSError(domain: "allowedOriginRules[\(index)] is empty", code: 0) + } + if originRule == "*" { + continue + } + if let url = URL(string: originRule) { + guard let scheme = url.scheme else { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + if scheme == "http" || scheme == "https", url.host == nil || url.host!.isEmpty { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + if scheme != "http", scheme != "https", url.host != nil || url.port != nil { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + if url.host == nil || url.host!.isEmpty, url.port != nil { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + if !url.path.isEmpty { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + if let hostname = url.host { + if let firstIndex = hostname.firstIndex(of: "*") { + let distance = hostname.distance(from: hostname.startIndex, to: firstIndex) + if distance != 0 || (distance == 0 && hostname.prefix(2) != "*.") { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + } + if hostname.hasPrefix("[") { + if !hostname.hasSuffix("]") { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + let fromIndex = hostname.index(hostname.startIndex, offsetBy: 1) + let toIndex = hostname.index(hostname.startIndex, offsetBy: hostname.count - 1) + let indexRange = Range(uncheckedBounds: (lower: fromIndex, upper: toIndex)) + let ipv6 = String(hostname[indexRange]) + if !Util.isIPv6(address: ipv6) { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + } + } + } else { + throw NSError(domain: "allowedOriginRules \(originRule) is invalid", code: 0) + } + } + } + + public func initJsInstance(webView: InAppWebView) { + self.webView = webView + if let webView = self.webView { + let jsObjectNameEscaped = jsObjectName.replacingOccurrences(of: "\'", with: "\\'") + let allowedOriginRulesString = allowedOriginRules.map { (allowedOriginRule) -> String in + if allowedOriginRule == "*" { + return "'*'" + } + let rule = URL(string: allowedOriginRule)! + let host = rule.host != nil ? "'" + rule.host!.replacingOccurrences(of: "\'", with: "\\'") + "'" : "null" + return """ + {scheme: '\(rule.scheme!)', host: \(host), port: \(rule.port != nil ? String(rule.port!) : "null")} + """ + }.joined(separator: ", ") + let source = """ + (function() { + var allowedOriginRules = [\(allowedOriginRulesString)]; + var isPageBlank = window.location.href === "about:blank"; + var scheme = !isPageBlank ? window.location.protocol.replace(":", "") : null; + var host = !isPageBlank ? window.location.hostname : null; + var port = !isPageBlank ? window.location.port : null; + if (window.\(JAVASCRIPT_BRIDGE_NAME)._isOriginAllowed(allowedOriginRules, scheme, host, port)) { + window['\(jsObjectNameEscaped)'] = new FlutterInAppWebViewWebMessageListener('\(jsObjectNameEscaped)'); + } + })(); + """ + webView.configuration.userContentController.addPluginScript(PluginScript( + groupName: "WebMessageListener-" + id + "-" + jsObjectName, + source: source, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: false, + messageHandlerNames: ["onWebMessageListenerPostMessageReceived"] + )) + webView.configuration.userContentController.sync(scriptMessageHandler: webView) + } + } + + public static func fromMap(plugin: InAppWebViewFlutterPlugin, map: [String:Any?]?) -> WebMessageListener? { + guard let map = map else { + return nil + } + return WebMessageListener( + plugin: plugin, + id: map["id"] as! String, + jsObjectName: map["jsObjectName"] as! String, + allowedOriginRules: Set(map["allowedOriginRules"] as! [String]) + ) + } + + public func isOriginAllowed(scheme: String?, host: String?, port: Int?) -> Bool { + for allowedOriginRule in allowedOriginRules { + if allowedOriginRule == "*" { + return true + } + if scheme == nil || scheme!.isEmpty { + continue + } + if scheme == nil || scheme!.isEmpty, host == nil || host!.isEmpty, port == nil || port == 0 { + continue + } + if let rule = URL(string: allowedOriginRule) { + let rulePort = rule.port == nil || rule.port == 0 ? (rule.scheme == "https" ? 443 : 80) : rule.port! + let currentPort = port == nil || port == 0 ? (scheme == "https" ? 443 : 80) : port! + var IPv6: String? = nil + if let hostname = rule.host, hostname.hasPrefix("[") { + let fromIndex = hostname.index(hostname.startIndex, offsetBy: 1) + let toIndex = hostname.index(hostname.startIndex, offsetBy: hostname.count - 1) + let indexRange = Range(uncheckedBounds: (lower: fromIndex, upper: toIndex)) + do { + IPv6 = try Util.normalizeIPv6(address: String(hostname[indexRange])) + } catch {} + } + var hostIPv6: String? = nil + if let host = host, Util.isIPv6(address: host) { + do { + hostIPv6 = try Util.normalizeIPv6(address: host) + } catch {} + } + + let schemeAllowed = scheme != nil && !scheme!.isEmpty && scheme == rule.scheme + + let hostAllowed = rule.host == nil || + rule.host!.isEmpty || + host == rule.host || + (rule.host!.hasPrefix("*") && host != nil && host!.hasSuffix(rule.host!.split(separator: "*", omittingEmptySubsequences: false)[1])) || + (hostIPv6 != nil && IPv6 != nil && hostIPv6 == IPv6) + + let portAllowed = rulePort == currentPort + + if schemeAllowed, hostAllowed, portAllowed { + return true + } + } + } + return false + } + + public func dispose() { + channelDelegate?.dispose() + channelDelegate = nil + webView = nil + plugin = nil + } + + deinit { + debugPrint("WebMessageListener - dealloc") + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/WebMessage/WebMessageListenerChannelDelegate.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/WebMessage/WebMessageListenerChannelDelegate.swift new file mode 100644 index 00000000..39304dae --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/WebMessage/WebMessageListenerChannelDelegate.swift @@ -0,0 +1,73 @@ +// +// WebMessageListenerChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation +import FlutterMacOS + +public class WebMessageListenerChannelDelegate: ChannelDelegate { + private weak var webMessageListener: WebMessageListener? + + public init(webMessageListener: WebMessageListener, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.webMessageListener = webMessageListener + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "postMessage": + if let webView = webMessageListener?.webView, let jsObjectName = webMessageListener?.jsObjectName { + let jsObjectNameEscaped = jsObjectName.replacingOccurrences(of: "\'", with: "\\'") + let message = WebMessage.fromMap(map: arguments!["message"] as! [String: Any?]) + + let source = """ + (function() { + var webMessageListener = window['\(jsObjectNameEscaped)']; + if (webMessageListener != null) { + var event = {data: \(message.jsData)}; + if (webMessageListener.onmessage != null) { + webMessageListener.onmessage(event); + } + for (var listener of webMessageListener.listeners) { + listener(event); + } + } + })(); + """ + webView.evaluateJavascript(source: source) { (_) in + result(true) + } + } else { + result(true) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onPostMessage(message: WebMessage?, sourceOrigin: URL?, isMainFrame: Bool) { + let arguments: [String:Any?] = [ + "message": message?.toMap(), + "sourceOrigin": sourceOrigin?.absoluteString, + "isMainFrame": isMainFrame + ] + channel?.invokeMethod("onPostMessage", arguments: arguments) + } + + public override func dispose() { + super.dispose() + webMessageListener = nil + } + + deinit { + dispose() + } +} + diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/WebViewChannelDelegate.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/WebViewChannelDelegate.swift new file mode 100644 index 00000000..9eb906e5 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/WebViewChannelDelegate.swift @@ -0,0 +1,1178 @@ +// +// WebViewChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 06/05/22. +// + +import Foundation +import WebKit +import FlutterMacOS + +public class WebViewChannelDelegate: ChannelDelegate { + private weak var webView: InAppWebView? + + public init(webView: InAppWebView, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.webView = webView + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + guard let method = WebViewChannelDelegateMethods.init(rawValue: call.method) else { + result(FlutterMethodNotImplemented) + return + } + + switch method { + case .getUrl: + result(webView?.url?.absoluteString) + break + case .getTitle: + result(webView?.title) + break + case .getProgress: + result( (webView != nil) ? Int(webView!.estimatedProgress * 100) : nil ) + break + case .loadUrl: + let urlRequest = arguments!["urlRequest"] as! [String:Any?] + let allowingReadAccessTo = arguments!["allowingReadAccessTo"] as? String + var allowingReadAccessToURL: URL? = nil + if let allowingReadAccessTo = allowingReadAccessTo { + allowingReadAccessToURL = URL(string: allowingReadAccessTo) + } + webView?.loadUrl(urlRequest: URLRequest.init(fromPluginMap: urlRequest), allowingReadAccessTo: allowingReadAccessToURL) + result(true) + break + case .postUrl: + if let webView = webView { + let url = arguments!["url"] as! String + let postData = arguments!["postData"] as! FlutterStandardTypedData + webView.postUrl(url: URL(string: url)!, postData: postData.data) + } + result(true) + break + case .loadData: + let data = arguments!["data"] as! String + let mimeType = arguments!["mimeType"] as! String + let encoding = arguments!["encoding"] as! String + let baseUrl = URL(string: arguments!["baseUrl"] as! String)! + let allowingReadAccessTo = arguments!["allowingReadAccessTo"] as? String + var allowingReadAccessToURL: URL? = nil + if let allowingReadAccessTo = allowingReadAccessTo { + allowingReadAccessToURL = URL(string: allowingReadAccessTo) + } + webView?.loadData(data: data, mimeType: mimeType, encoding: encoding, baseUrl: baseUrl, allowingReadAccessTo: allowingReadAccessToURL) + result(true) + break + case .loadFile: + let assetFilePath = arguments!["assetFilePath"] as! String + + do { + try webView?.loadFile(assetFilePath: assetFilePath) + } + catch let error as NSError { + result(FlutterError(code: "WebViewChannelDelegate", message: error.domain, details: nil)) + return + } + result(true) + break + case .evaluateJavascript: + if let webView = webView { + let source = arguments!["source"] as! String + let contentWorldMap = arguments!["contentWorld"] as? [String:Any?] + if #available(macOS 11.0, *), let contentWorldMap = contentWorldMap { + let contentWorld = WKContentWorld.fromMap(map: contentWorldMap, windowId: webView.windowId)! + webView.evaluateJavascript(source: source, contentWorld: contentWorld) { (value) in + result(value) + } + } else { + webView.evaluateJavascript(source: source) { (value) in + result(value) + } + } + } + else { + result(nil) + } + break + case .injectJavascriptFileFromUrl: + let urlFile = arguments!["urlFile"] as! String + let scriptHtmlTagAttributes = arguments!["scriptHtmlTagAttributes"] as? [String:Any?] + webView?.injectJavascriptFileFromUrl(urlFile: urlFile, scriptHtmlTagAttributes: scriptHtmlTagAttributes) + result(true) + break + case .injectCSSCode: + let source = arguments!["source"] as! String + webView?.injectCSSCode(source: source) + result(true) + break + case .injectCSSFileFromUrl: + let urlFile = arguments!["urlFile"] as! String + let cssLinkHtmlTagAttributes = arguments!["cssLinkHtmlTagAttributes"] as? [String:Any?] + webView?.injectCSSFileFromUrl(urlFile: urlFile, cssLinkHtmlTagAttributes: cssLinkHtmlTagAttributes) + result(true) + break + case .reload: + webView?.reload() + result(true) + break + case .goBack: + webView?.goBack() + result(true) + break + case .canGoBack: + result(webView?.canGoBack ?? false) + break + case .goForward: + webView?.goForward() + result(true) + break + case .canGoForward: + result(webView?.canGoForward ?? false) + break + case .goBackOrForward: + let steps = arguments!["steps"] as! Int + webView?.goBackOrForward(steps: steps) + result(true) + break + case .canGoBackOrForward: + let steps = arguments!["steps"] as! Int + result(webView?.canGoBackOrForward(steps: steps) ?? false) + break + case .stopLoading: + webView?.stopLoading() + result(true) + break + case .isLoading: + result(webView?.isLoading ?? false) + break + case .takeScreenshot: + if let webView = webView, #available(macOS 10.13, *) { + let screenshotConfiguration = arguments!["screenshotConfiguration"] as? [String: Any?] + webView.takeScreenshot(with: screenshotConfiguration, completionHandler: { (screenshot) -> Void in + result(screenshot) + }) + } + else { + result(nil) + } + break + case .setSettings: + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { + let inAppBrowserSettings = InAppBrowserSettings() + let inAppBrowserSettingsMap = arguments!["settings"] as! [String: Any] + let _ = inAppBrowserSettings.parse(settings: inAppBrowserSettingsMap) + iabController.setSettings(newSettings: inAppBrowserSettings, newSettingsMap: inAppBrowserSettingsMap) + } else { + let inAppWebViewSettings = InAppWebViewSettings() + let inAppWebViewSettingsMap = arguments!["settings"] as! [String: Any] + let _ = inAppWebViewSettings.parse(settings: inAppWebViewSettingsMap) + webView?.setSettings(newSettings: inAppWebViewSettings, newSettingsMap: inAppWebViewSettingsMap) + } + result(true) + break + case .getSettings: + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { + result(iabController.getSettings()) + } else { + result(webView?.getSettings()) + } + break + case .close: + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { + iabController.close() + result(true) + } else { + result(FlutterMethodNotImplemented) + } + break + case .show: + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { + iabController.show() + result(true) + } else { + result(FlutterMethodNotImplemented) + } + break + case .hide: + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { + iabController.hide() + result(true) + } else { + result(FlutterMethodNotImplemented) + } + break + case .isHidden: + if let iabController = webView?.inAppBrowserDelegate as? InAppBrowserWebViewController { + result(iabController.isHidden) + } else { + result(FlutterMethodNotImplemented) + } + break + case .getCopyBackForwardList: + result(webView?.getCopyBackForwardList()) + break + case .findAll: + if let webView = webView, let findInteractionController = webView.findInteractionController { + let find = arguments!["find"] as! String + findInteractionController.findAll(find: find, completionHandler: {(value, error) in + if error != nil { + result(FlutterError(code: "WebViewChannelDelegate", message: error?.localizedDescription, details: nil)) + return + } + result(true) + }) + } else { + result(false) + } + break + case .findNext: + if let webView = webView, let findInteractionController = webView.findInteractionController { + let forward = arguments!["forward"] as! Bool + findInteractionController.findNext(forward: forward, completionHandler: {(value, error) in + if error != nil { + result(FlutterError(code: "WebViewChannelDelegate", message: error?.localizedDescription, details: nil)) + return + } + result(true) + }) + } else { + result(false) + } + break + case .clearMatches: + if let webView = webView, let findInteractionController = webView.findInteractionController { + findInteractionController.clearMatches(completionHandler: {(value, error) in + if error != nil { + result(FlutterError(code: "WebViewChannelDelegate", message: error?.localizedDescription, details: nil)) + return + } + result(true) + }) + } else { + result(false) + } + break + case .clearCache: + webView?.clearCache() + result(true) + break + case .scrollTo: + let x = arguments!["x"] as! Int + let y = arguments!["y"] as! Int + let animated = arguments!["animated"] as! Bool + webView?.scrollTo(x: x, y: y, animated: animated) + result(true) + break + case .scrollBy: + let x = arguments!["x"] as! Int + let y = arguments!["y"] as! Int + let animated = arguments!["animated"] as! Bool + webView?.scrollBy(x: x, y: y, animated: animated) + result(true) + break + case .pauseTimers: + webView?.pauseTimers() + result(true) + break + case .resumeTimers: + webView?.resumeTimers() + result(true) + break + case .printCurrentPage: + if let webView = webView { + let settings = PrintJobSettings() + if let settingsMap = arguments!["settings"] as? [String: Any?] { + let _ = settings.parse(settings: settingsMap) + } + result(webView.printCurrentPage(settings: settings)) + } else { + result(nil) + } + break + case .getContentHeight: + webView?.getContentHeight { contentHeight, error in + if let error = error { + print(error) + result(nil) + return + } + result(contentHeight) + } + break + case .getContentWidth: + webView?.getContentWidth { contentWidth, error in + if let error = error { + print(error) + result(nil) + return + } + result(contentWidth) + } + break + case .reloadFromOrigin: + webView?.reloadFromOrigin() + result(true) + break + case .getOriginalUrl: + result(webView?.getOriginalUrl()?.absoluteString) + break + case .getZoomScale: + result(nil) + break + case .hasOnlySecureContent: + result(webView?.hasOnlySecureContent ?? false) + break + case .getSelectedText: + if let webView = webView { + webView.getSelectedText { (value, error) in + if let err = error { + print(err.localizedDescription) + result("") + return + } + result(value) + } + } + else { + result(nil) + } + break + case .getScrollX: + if let webView = webView { + webView.getScrollX { scrollX, error in + if let error = error { + print(error) + result(nil) + return + } + result(scrollX) + } + + } else { + result(nil) + } + break + case .getScrollY: + if let webView = webView { + webView.getScrollY { scrollY, error in + if let error = error { + print(error) + result(nil) + return + } + result(scrollY) + } + + } else { + result(nil) + } + break + case .getCertificate: + result(webView?.getCertificate()?.toMap()) + break + case .addUserScript: + if let webView = webView { + let userScriptMap = arguments!["userScript"] as! [String: Any?] + let userScript = UserScript.fromMap(map: userScriptMap, windowId: webView.windowId)! + webView.configuration.userContentController.addUserOnlyScript(userScript) + webView.configuration.userContentController.sync(scriptMessageHandler: webView) + } + result(true) + break + case .removeUserScript: + let index = arguments!["index"] as! Int + let userScriptMap = arguments!["userScript"] as! [String: Any?] + let userScript = UserScript.fromMap(map: userScriptMap, windowId: webView?.windowId)! + webView?.configuration.userContentController.removeUserOnlyScript(at: index, injectionTime: userScript.injectionTime) + result(true) + break + case .removeUserScriptsByGroupName: + let groupName = arguments!["groupName"] as! String + webView?.configuration.userContentController.removeUserOnlyScripts(with: groupName) + result(true) + break + case .removeAllUserScripts: + webView?.configuration.userContentController.removeAllUserOnlyScripts() + result(true) + break + case .callAsyncJavaScript: + if let webView = webView { + if #available(macOS 11.0, *) { // on iOS 14.0, for some reason, it crashes + let functionBody = arguments!["functionBody"] as! String + let functionArguments = arguments!["arguments"] as! [String:Any] + var contentWorld = WKContentWorld.page + if let contentWorldMap = arguments!["contentWorld"] as? [String:Any?] { + contentWorld = WKContentWorld.fromMap(map: contentWorldMap, windowId: webView.windowId)! + } + webView.callAsyncJavaScript(functionBody: functionBody, arguments: functionArguments, contentWorld: contentWorld) { (value) in + result(value) + } + } else { + let functionBody = arguments!["functionBody"] as! String + let functionArguments = arguments!["arguments"] as! [String:Any] + webView.callAsyncJavaScript(functionBody: functionBody, arguments: functionArguments) { (value) in + result(value) + } + } + } + else { + result(nil) + } + break + case .createPdf: + if let webView = webView, #available(macOS 11.0, *) { + let configuration = arguments!["pdfConfiguration"] as? [String: Any?] + webView.createPdf(configuration: configuration, completionHandler: { (pdf) -> Void in + result(pdf) + }) + } + else { + result(nil) + } + break + case .createWebArchiveData: + if let webView = webView, #available(macOS 11.0, *) { + webView.createWebArchiveData(dataCompletionHandler: { (webArchiveData) -> Void in + result(webArchiveData) + }) + } + else { + result(nil) + } + break + case .saveWebArchive: + if let webView = webView, #available(macOS 11.0, *) { + let filePath = arguments!["filePath"] as! String + let autoname = arguments!["autoname"] as! Bool + webView.saveWebArchive(filePath: filePath, autoname: autoname, completionHandler: { (path) -> Void in + result(path) + }) + } + else { + result(nil) + } + break + case .isSecureContext: + if let webView = webView { + webView.isSecureContext(completionHandler: { (isSecureContext) in + result(isSecureContext) + }) + } + else { + result(false) + } + break + case .createWebMessageChannel: + if let webView = webView { + let _ = webView.createWebMessageChannel { (webMessageChannel) in + guard let webMessageChannel = webMessageChannel else { + result(nil) + return + } + result(webMessageChannel.toMap()) + } + } else { + result(nil) + } + break + case .postWebMessage: + if let webView = webView { + var message = WebMessage.fromMap(map: arguments!["message"] as! [String: Any?]) + let targetOrigin = arguments!["targetOrigin"] as! String + + var ports: [WebMessagePort] = [] + if let notConnectedPorts = message.ports { + for notConnectedPort in notConnectedPorts { + if let webMessageChannel = webView.webMessageChannels[notConnectedPort.webMessageChannelId] { + ports.append(webMessageChannel.ports[Int(notConnectedPort.index)]) + } + } + } + message.ports = ports + + do { + try webView.postWebMessage(message: message, targetOrigin: targetOrigin) { (_) in + result(true) + } + } catch let error as NSError { + result(FlutterError(code: "WebViewChannelDelegate", message: error.domain, details: nil)) + } + } else { + result(false) + } + break + case .addWebMessageListener: + if let webView = webView, let plugin = webView.plugin { + let webMessageListenerMap = arguments!["webMessageListener"] as! [String: Any?] + let webMessageListener = WebMessageListener.fromMap(plugin: plugin, map: webMessageListenerMap)! + do { + try webView.addWebMessageListener(webMessageListener: webMessageListener) + result(false) + } catch let error as NSError { + result(FlutterError(code: "WebViewChannelDelegate", message: error.domain, details: nil)) + } + } else { + result(false) + } + break + case .canScrollVertically: + if let webView = webView { + webView.canScrollVertically { canScrollVertically, error in + if let error = error { + print(error) + result(false) + return + } + result(canScrollVertically) + } + } else { + result(false) + } + break + case .canScrollHorizontally: + if let webView = webView { + webView.canScrollHorizontally { canScrollHorizontally, error in + if let error = error { + print(error) + result(false) + return + } + result(canScrollHorizontally) + } + } else { + result(false) + } + break + case .pauseAllMediaPlayback: + if let webView = webView, #available(macOS 12.0 , *) { + webView.pauseAllMediaPlayback(completionHandler: { () -> Void in + result(true) + }) + } else { + result(false) + } + break + case .setAllMediaPlaybackSuspended: + if let webView = webView, #available(macOS 12.0 , *) { + let suspended = arguments!["suspended"] as! Bool + webView.setAllMediaPlaybackSuspended(suspended, completionHandler: { () -> Void in + result(true) + }) + } else { + result(false) + } + break + case .closeAllMediaPresentations: + if let webView = self.webView, #available(macOS 11.3, *) { + if #available(macOS 12.0, *) { + webView.closeAllMediaPresentations { + result(true) + } + } else { + webView.closeAllMediaPresentations() + result(true) + } + } else { + result(false) + } + break + case .requestMediaPlaybackState: + if let webView = webView, #available(macOS 12.0, *) { + webView.requestMediaPlaybackState(completionHandler: { (state) -> Void in + result(state.rawValue) + }) + } else { + result(nil) + } + break + case .getMetaThemeColor: + if let webView = webView, #available(macOS 12.0, *) { + result(webView.themeColor?.hexString) + } else { + result(nil) + } + break + case .isInFullscreen: + if let webView = webView { + if #available(macOS 13.0, *) { + result(webView.fullscreenState == .inFullscreen) + } else { + result(webView.inFullscreen) + } + } + else { + result(false) + } + break + case .getCameraCaptureState: + if let webView = webView, #available(macOS 12.0, *) { + result(webView.cameraCaptureState.rawValue) + } else { + result(nil) + } + break + case .setCameraCaptureState: + if let webView = webView, #available(macOS 12.0, *) { + let state = WKMediaCaptureState.init(rawValue: arguments!["state"] as! Int) ?? WKMediaCaptureState.none + webView.setCameraCaptureState(state) { + result(true) + } + } else { + result(false) + } + break + case .getMicrophoneCaptureState: + if let webView = webView, #available(macOS 12.0, *) { + result(webView.microphoneCaptureState.rawValue) + } else { + result(nil) + } + break + case .setMicrophoneCaptureState: + if let webView = webView, #available(macOS 12.0, *) { + let state = WKMediaCaptureState.init(rawValue: arguments!["state"] as! Int) ?? WKMediaCaptureState.none + webView.setMicrophoneCaptureState(state) { + result(true) + } + } else { + result(false) + } + break + case .loadSimulatedRequest: + if let webView = webView, #available(macOS 12.0, *) { + let request = URLRequest.init(fromPluginMap: arguments!["urlRequest"] as! [String:Any?]) + let data = arguments!["data"] as! FlutterStandardTypedData + var response: URLResponse? = nil + if let urlResponse = arguments!["urlResponse"] as? [String:Any?] { + response = URLResponse.init(fromPluginMap: urlResponse) + } + if let response = response { + webView.loadSimulatedRequest(request, response: response, responseData: data.data) + } else { + webView.loadSimulatedRequest(request, responseHTML: String(decoding: data.data, as: UTF8.self)) + } + result(true) + } else { + result(false) + } + } + } + + @available(*, deprecated, message: "Use FindInteractionChannelDelegate.onFindResultReceived instead.") + public func onFindResultReceived(activeMatchOrdinal: Int, numberOfMatches: Int, isDoneCounting: Bool) { + let arguments: [String : Any?] = [ + "activeMatchOrdinal": activeMatchOrdinal, + "numberOfMatches": numberOfMatches, + "isDoneCounting": isDoneCounting + ] + channel?.invokeMethod("onFindResultReceived", arguments: arguments) + } + + public func onLongPressHitTestResult(hitTestResult: HitTestResult) { + channel?.invokeMethod("onLongPressHitTestResult", arguments: hitTestResult.toMap()) + } + + public func onScrollChanged(x: Int, y: Int) { + let arguments: [String: Any?] = ["x": x, "y": y] + channel?.invokeMethod("onScrollChanged", arguments: arguments) + } + + public func onDownloadStartRequest(request: DownloadStartRequest) { + channel?.invokeMethod("onDownloadStartRequest", arguments: request.toMap()) + } + + public func onCreateContextMenu(hitTestResult: HitTestResult) { + channel?.invokeMethod("onCreateContextMenu", arguments: hitTestResult.toMap()) + } + + public func onOverScrolled(x: Int, y: Int, clampedX: Bool, clampedY: Bool) { + let arguments: [String: Any?] = ["x": x, "y": y, "clampedX": clampedX, "clampedY": clampedY] + channel?.invokeMethod("onOverScrolled", arguments: arguments) + } + + public func onContextMenuActionItemClicked(id: Any, title: String) { + let arguments: [String: Any?] = [ + "id": id, + "iosId": id is Int64 ? String(id as! Int64) : id as! String, + "androidId": nil, + "title": title + ] + channel?.invokeMethod("onContextMenuActionItemClicked", arguments: arguments) + } + + public func onHideContextMenu() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onHideContextMenu", arguments: arguments) + } + + public func onEnterFullscreen() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onEnterFullscreen", arguments: arguments) + } + + public func onExitFullscreen() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onExitFullscreen", arguments: arguments) + } + + public class JsAlertCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return JsAlertResponse.fromMap(map: obj as? [String:Any?]) + } + } + } + + public func onJsAlert(url: URL?, message: String, isMainFrame: Bool, callback: JsAlertCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + let arguments: [String: Any?] = [ + "url": url?.absoluteString, + "message": message, + "isMainFrame": isMainFrame + ] + channel?.invokeMethod("onJsAlert", arguments: arguments, callback: callback) + } + + public class JsConfirmCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return JsConfirmResponse.fromMap(map: obj as? [String:Any?]) + } + } + } + + public func onJsConfirm(url: URL?, message: String, isMainFrame: Bool, callback: JsConfirmCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + let arguments: [String: Any?] = [ + "url": url?.absoluteString, + "message": message, + "isMainFrame": isMainFrame + ] + channel?.invokeMethod("onJsConfirm", arguments: arguments, callback: callback) + } + + public class JsPromptCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return JsPromptResponse.fromMap(map: obj as? [String:Any?]) + } + } + } + + public func onJsPrompt(url: URL?, message: String, defaultValue: String?, isMainFrame: Bool, callback: JsPromptCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + let arguments: [String: Any?] = [ + "url": url?.absoluteString, + "message": message, + "defaultValue": defaultValue, + "isMainFrame": isMainFrame + ] + channel?.invokeMethod("onJsPrompt", arguments: arguments, callback: callback) + } + + public class CreateWindowCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return obj is Bool && obj as! Bool + } + } + } + + public func onCreateWindow(createWindowAction: CreateWindowAction, callback: CreateWindowCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + channel?.invokeMethod("onCreateWindow", arguments: createWindowAction.toMap(), callback: callback) + } + + public func onCloseWindow() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onCloseWindow", arguments: arguments) + } + + public func onConsoleMessage(message: String, messageLevel: Int) { + let arguments: [String: Any?] = [ + "message": message, + "messageLevel": messageLevel + ] + channel?.invokeMethod("onConsoleMessage", arguments: arguments) + } + + public func onProgressChanged(progress: Int) { + let arguments: [String: Any?] = [ + "progress": progress + ] + channel?.invokeMethod("onProgressChanged", arguments: arguments) + } + + public func onTitleChanged(title: String?) { + let arguments: [String: Any?] = [ + "title": title + ] + channel?.invokeMethod("onTitleChanged", arguments: arguments) + } + + public class PermissionRequestCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return PermissionResponse.fromMap(map: obj as? [String:Any?]) + } + } + + deinit { + self.defaultBehaviour(nil) + } + } + + public func onPermissionRequest(request: PermissionRequest, callback: PermissionRequestCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + channel?.invokeMethod("onPermissionRequest", arguments: request.toMap(), callback: callback) + } + + public class ShouldOverrideUrlLoadingCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + if let action = obj as? Int { + return WKNavigationActionPolicy.init(rawValue: action) ?? WKNavigationActionPolicy.cancel + } + return WKNavigationActionPolicy.cancel + } + } + + deinit { + self.defaultBehaviour(nil) + } + } + + public func shouldOverrideUrlLoading(navigationAction: WKNavigationAction, callback: ShouldOverrideUrlLoadingCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + channel?.invokeMethod("shouldOverrideUrlLoading", arguments: navigationAction.toMap(), callback: callback) + } + + public func onLoadStart(url: String?) { + let arguments: [String: Any?] = ["url": url] + channel?.invokeMethod("onLoadStart", arguments: arguments) + } + + public func onLoadStop(url: String?) { + let arguments: [String: Any?] = ["url": url] + channel?.invokeMethod("onLoadStop", arguments: arguments) + } + + public func onUpdateVisitedHistory(url: String?, isReload: Bool?) { + let arguments: [String: Any?] = [ + "url": url, + "isReload": nil + ] + channel?.invokeMethod("onUpdateVisitedHistory", arguments: arguments) + } + + public func onReceivedError(request: WebResourceRequest, error: WebResourceError) { + let arguments: [String: Any?] = [ + "request": request.toMap(), + "error": error.toMap() + ] + channel?.invokeMethod("onReceivedError", arguments: arguments) + } + + public func onReceivedHttpError(request: WebResourceRequest, errorResponse: WebResourceResponse) { + let arguments: [String: Any?] = [ + "request": request.toMap(), + "errorResponse": errorResponse.toMap() + ] + channel?.invokeMethod("onReceivedHttpError", arguments: arguments) + } + + public class ReceivedHttpAuthRequestCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return HttpAuthResponse.fromMap(map: obj as? [String:Any?]) + } + } + + deinit { + self.defaultBehaviour(nil) + } + } + + public func onReceivedHttpAuthRequest(challenge: HttpAuthenticationChallenge, callback: ReceivedHttpAuthRequestCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + // workaround for ProtectionSpace.toMap() SSL Certificate + // https://github.com/pichillilorenzo/flutter_inappwebview/issues/1678 + DispatchQueue.global(qos: .background).async { + let arguments = challenge.toMap() + DispatchQueue.main.async { [weak self] in + if self?.channel == nil { + callback.defaultBehaviour(nil) + return + } + self?.channel?.invokeMethod("onReceivedHttpAuthRequest", arguments: arguments, callback: callback) + } + } + } + + public class ReceivedServerTrustAuthRequestCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return ServerTrustAuthResponse.fromMap(map: obj as? [String:Any?]) + } + } + + deinit { + self.defaultBehaviour(nil) + } + } + + public func onReceivedServerTrustAuthRequest(challenge: ServerTrustChallenge, callback: ReceivedServerTrustAuthRequestCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + // workaround for ProtectionSpace.toMap() SSL Certificate + // https://github.com/pichillilorenzo/flutter_inappwebview/issues/1678 + DispatchQueue.global(qos: .background).async { + let arguments = challenge.toMap() + DispatchQueue.main.async { [weak self] in + if self?.channel == nil { + callback.defaultBehaviour(nil) + return + } + self?.channel?.invokeMethod("onReceivedServerTrustAuthRequest", arguments: arguments, callback: callback) + } + } + } + + public class ReceivedClientCertRequestCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return ClientCertResponse.fromMap(map: obj as? [String:Any?]) + } + } + + deinit { + self.defaultBehaviour(nil) + } + } + + public func onReceivedClientCertRequest(challenge: ClientCertChallenge, callback: ReceivedClientCertRequestCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + // workaround for ProtectionSpace.toMap() SSL Certificate + // https://github.com/pichillilorenzo/flutter_inappwebview/issues/1678 + DispatchQueue.global(qos: .background).async { + let arguments = challenge.toMap() + DispatchQueue.main.async { [weak self] in + if self?.channel == nil { + callback.defaultBehaviour(nil) + return + } + self?.channel?.invokeMethod("onReceivedClientCertRequest", arguments: arguments, callback: callback) + } + } + } + + public func onZoomScaleChanged(newScale: Float, oldScale: Float) { + let arguments: [String: Any?] = [ + "newScale": newScale, + "oldScale": oldScale + ] + channel?.invokeMethod("onZoomScaleChanged", arguments: arguments) + } + + public func onPageCommitVisible(url: String?) { + let arguments: [String: Any?] = [ + "url": url + ] + channel?.invokeMethod("onPageCommitVisible", arguments: arguments) + } + + public class LoadResourceWithCustomSchemeCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return CustomSchemeResponse.fromMap(map: obj as? [String:Any?]) + } + } + } + + public func onLoadResourceWithCustomScheme(request: WebResourceRequest, callback: LoadResourceWithCustomSchemeCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + let arguments: [String: Any?] = ["request": request.toMap()] + channel?.invokeMethod("onLoadResourceWithCustomScheme", arguments: arguments, callback: callback) + } + + public class CallJsHandlerCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return obj + } + } + } + + public func onCallJsHandler(handlerName: String, args: String, callback: CallJsHandlerCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + let arguments: [String: Any?] = [ + "handlerName": handlerName, + "args": args + ] + channel?.invokeMethod("onCallJsHandler", arguments: arguments, callback: callback) + } + + public class NavigationResponseCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + if let action = obj as? Int { + return WKNavigationResponsePolicy.init(rawValue: action) ?? WKNavigationResponsePolicy.cancel + } + return WKNavigationResponsePolicy.cancel + } + } + + deinit { + self.defaultBehaviour(nil) + } + } + + public func onNavigationResponse(navigationResponse: WKNavigationResponse, callback: NavigationResponseCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + channel?.invokeMethod("onNavigationResponse", arguments: navigationResponse.toMap(), callback: callback) + } + + public class ShouldAllowDeprecatedTLSCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + if let action = obj as? Int { + return action == 1 + } + return false + } + } + + deinit { + self.defaultBehaviour(nil) + } + } + + public func shouldAllowDeprecatedTLS(challenge: URLAuthenticationChallenge, callback: ShouldAllowDeprecatedTLSCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + // workaround for ProtectionSpace.toMap() SSL Certificate + // https://github.com/pichillilorenzo/flutter_inappwebview/issues/1678 + DispatchQueue.global(qos: .background).async { + let arguments = challenge.toMap() + DispatchQueue.main.async { [weak self] in + if self?.channel == nil { + callback.defaultBehaviour(nil) + return + } + self?.channel?.invokeMethod("shouldAllowDeprecatedTLS", arguments: arguments, callback: callback) + } + } + } + + public func onWebContentProcessDidTerminate() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onWebContentProcessDidTerminate", arguments: arguments) + } + + public func onDidReceiveServerRedirectForProvisionalNavigation() { + let arguments: [String: Any?] = [:] + channel?.invokeMethod("onDidReceiveServerRedirectForProvisionalNavigation", arguments: arguments) + } + + @available(macOS 12.0, *) + public func onCameraCaptureStateChanged(oldState: WKMediaCaptureState?, newState: WKMediaCaptureState?) { + let arguments = [ + "oldState": oldState?.rawValue, + "newState": newState?.rawValue + ] + channel?.invokeMethod("onCameraCaptureStateChanged", arguments: arguments) + } + + @available(macOS 12.0, *) + public func onMicrophoneCaptureStateChanged(oldState: WKMediaCaptureState?, newState: WKMediaCaptureState?) { + let arguments = [ + "oldState": oldState?.rawValue, + "newState": newState?.rawValue + ] + channel?.invokeMethod("onMicrophoneCaptureStateChanged", arguments: arguments) + } + + public class PrintRequestCallback: BaseCallbackResult { + override init() { + super.init() + self.decodeResult = { (obj: Any?) in + return obj is Bool && obj as! Bool + } + } + } + + public func onPrintRequest(url: URL?, printJobId: String?, callback: PrintRequestCallback) { + if channel == nil { + callback.defaultBehaviour(nil) + return + } + let arguments = [ + "url": url?.absoluteString, + "printJobId": printJobId, + ] + channel?.invokeMethod("onPrintRequest", arguments: arguments, callback: callback) + } + + public override func dispose() { + super.dispose() + webView = nil + } + + deinit { + debugPrint("WebViewChannelDelegate - dealloc") + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/WebViewChannelDelegateMethods.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/WebViewChannelDelegateMethods.swift new file mode 100644 index 00000000..4810bae3 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebView/WebViewChannelDelegateMethods.swift @@ -0,0 +1,87 @@ +// +// WebViewChannelDelegateMethods.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/10/22. +// + +import Foundation + +public enum WebViewChannelDelegateMethods: String { + case getUrl = "getUrl" + case getTitle = "getTitle" + case getProgress = "getProgress" + case loadUrl = "loadUrl" + case postUrl = "postUrl" + case loadData = "loadData" + case loadFile = "loadFile" + case evaluateJavascript = "evaluateJavascript" + case injectJavascriptFileFromUrl = "injectJavascriptFileFromUrl" + case injectCSSCode = "injectCSSCode" + case injectCSSFileFromUrl = "injectCSSFileFromUrl" + case reload = "reload" + case goBack = "goBack" + case canGoBack = "canGoBack" + case goForward = "goForward" + case canGoForward = "canGoForward" + case goBackOrForward = "goBackOrForward" + case canGoBackOrForward = "canGoBackOrForward" + case stopLoading = "stopLoading" + case isLoading = "isLoading" + case takeScreenshot = "takeScreenshot" + case setSettings = "setSettings" + case getSettings = "getSettings" + case close = "close" + case show = "show" + case hide = "hide" + case isHidden = "isHidden" + case getCopyBackForwardList = "getCopyBackForwardList" + @available(*, deprecated, message: "Use FindInteractionController.findAll instead.") + case findAll = "findAll" + @available(*, deprecated, message: "Use FindInteractionController.findNext instead.") + case findNext = "findNext" + @available(*, deprecated, message: "Use FindInteractionController.clearMatches instead.") + case clearMatches = "clearMatches" + @available(*, deprecated, message: "Use InAppWebViewManager.clearAllCache instead.") + case clearCache = "clearCache" + case scrollTo = "scrollTo" + case scrollBy = "scrollBy" + case pauseTimers = "pauseTimers" + case resumeTimers = "resumeTimers" + case printCurrentPage = "printCurrentPage" + case getContentHeight = "getContentHeight" + case getContentWidth = "getContentWidth" + case reloadFromOrigin = "reloadFromOrigin" + case getOriginalUrl = "getOriginalUrl" + case getZoomScale = "getZoomScale" + case hasOnlySecureContent = "hasOnlySecureContent" + case getSelectedText = "getSelectedText" + case getScrollX = "getScrollX" + case getScrollY = "getScrollY" + case getCertificate = "getCertificate" + case addUserScript = "addUserScript" + case removeUserScript = "removeUserScript" + case removeUserScriptsByGroupName = "removeUserScriptsByGroupName" + case removeAllUserScripts = "removeAllUserScripts" + case callAsyncJavaScript = "callAsyncJavaScript" + case createPdf = "createPdf" + case createWebArchiveData = "createWebArchiveData" + case saveWebArchive = "saveWebArchive" + case isSecureContext = "isSecureContext" + case createWebMessageChannel = "createWebMessageChannel" + case postWebMessage = "postWebMessage" + case addWebMessageListener = "addWebMessageListener" + case canScrollVertically = "canScrollVertically" + case canScrollHorizontally = "canScrollHorizontally" + case pauseAllMediaPlayback = "pauseAllMediaPlayback" + case setAllMediaPlaybackSuspended = "setAllMediaPlaybackSuspended" + case closeAllMediaPresentations = "closeAllMediaPresentations" + case requestMediaPlaybackState = "requestMediaPlaybackState" + case getMetaThemeColor = "getMetaThemeColor" + case isInFullscreen = "isInFullscreen" + case getCameraCaptureState = "getCameraCaptureState" + case setCameraCaptureState = "setCameraCaptureState" + case getMicrophoneCaptureState = "getMicrophoneCaptureState" + case setMicrophoneCaptureState = "setMicrophoneCaptureState" + case loadSimulatedRequest = "loadSimulatedRequest" +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebViewFlutterPlugin.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebViewFlutterPlugin.swift new file mode 100644 index 00000000..530efdd7 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/InAppWebViewFlutterPlugin.swift @@ -0,0 +1,86 @@ +/* + 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. + */ + +import Cocoa +import FlutterMacOS +import AppKit +import WebKit +import Foundation +import AVFoundation +import SafariServices + +public class InAppWebViewFlutterPlugin: NSObject, FlutterPlugin { + + var registrar: FlutterPluginRegistrar? + var platformUtil: PlatformUtil? + var inAppWebViewManager: InAppWebViewManager? + var myCookieManager: Any? + var myWebStorageManager: MyWebStorageManager? + var credentialDatabase: CredentialDatabase? + var inAppBrowserManager: InAppBrowserManager? + var headlessInAppWebViewManager: HeadlessInAppWebViewManager? + var webAuthenticationSessionManager: WebAuthenticationSessionManager? + var printJobManager: PrintJobManager? + + var webViewControllers: [String: InAppBrowserWebViewController?] = [:] + var safariViewControllers: [String: Any?] = [:] + + public init(with registrar: FlutterPluginRegistrar) { + super.init() + self.registrar = registrar + registrar.register(FlutterWebViewFactory(plugin: self) as FlutterPlatformViewFactory, withId: FlutterWebViewFactory.VIEW_TYPE_ID) + + platformUtil = PlatformUtil(plugin: self) + inAppBrowserManager = InAppBrowserManager(plugin: self) + headlessInAppWebViewManager = HeadlessInAppWebViewManager(plugin: self) + inAppWebViewManager = InAppWebViewManager(plugin: self) + credentialDatabase = CredentialDatabase(plugin: self) + if #available(macOS 10.13, *) { + myCookieManager = MyCookieManager(plugin: self) + } + myWebStorageManager = MyWebStorageManager(plugin: self) + webAuthenticationSessionManager = WebAuthenticationSessionManager(plugin: self) + printJobManager = PrintJobManager(plugin: self) + } + + public static func register(with registrar: FlutterPluginRegistrar) { + let _ = InAppWebViewFlutterPlugin(with: registrar) + } + + public func detachFromEngine(for registrar: FlutterPluginRegistrar) { + platformUtil?.dispose() + platformUtil = nil + inAppBrowserManager?.dispose() + inAppBrowserManager = nil + headlessInAppWebViewManager?.dispose() + headlessInAppWebViewManager = nil + inAppWebViewManager?.dispose() + inAppWebViewManager = nil + credentialDatabase?.dispose() + credentialDatabase = nil + if #available(macOS 10.13, *) { + (myCookieManager as! MyCookieManager?)?.dispose() + myCookieManager = nil + } + myWebStorageManager?.dispose() + myWebStorageManager = nil + webAuthenticationSessionManager?.dispose() + webAuthenticationSessionManager = nil + printJobManager?.dispose() + printJobManager = nil + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/LeakAvoider.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/LeakAvoider.swift new file mode 100755 index 00000000..9f447142 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/LeakAvoider.swift @@ -0,0 +1,26 @@ +// +// LeakAvoider.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/12/2019. +// + +import Foundation +import FlutterMacOS + +public class LeakAvoider: NSObject { + weak var delegate : FlutterMethodCallDelegate? + + init(delegate: FlutterMethodCallDelegate) { + super.init() + self.delegate = delegate + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + self.delegate?.handle(call, result: result) + } + + deinit { + debugPrint("LeakAvoider - dealloc") + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/MyCookieManager.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/MyCookieManager.swift new file mode 100755 index 00000000..763078ed --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/MyCookieManager.swift @@ -0,0 +1,305 @@ +// +// MyCookieManager.swift +// flutter_inappwebview +// +// Created by Lorenzo on 26/10/18. +// + +import Foundation +import WebKit +import FlutterMacOS + +@available(macOS 10.13, *) +public class MyCookieManager: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_cookiemanager" + var plugin: InAppWebViewFlutterPlugin? + static var httpCookieStore = WKWebsiteDataStore.default().httpCookieStore + + init(plugin: InAppWebViewFlutterPlugin) { + super.init(channel: FlutterMethodChannel(name: MyCookieManager.METHOD_CHANNEL_NAME, binaryMessenger: plugin.registrar!.messenger)) + self.plugin = plugin + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + if call.method == "gopeed.prepareProfile" { + GopeedProfiles.prepare(arguments: arguments, result: result) + return + } + let profileID = arguments?["gopeedProfileId"] as? String ?? "" + let selectedStore = GopeedProfiles.store(profileID) + if !profileID.isEmpty && selectedStore == nil { + result(FlutterError(code: "UNAVAILABLE", message: "WebView profile is not initialized", details: nil)) + return + } + let cookieStore = (selectedStore ?? WKWebsiteDataStore.default()).httpCookieStore + + switch call.method { + case "setCookie": + let url = arguments!["url"] as! String + let name = arguments!["name"] as! String + let value = arguments!["value"] as! String + let path = arguments!["path"] as! String + + var expiresDate: Int64? + if let expiresDateString = arguments!["expiresDate"] as? String { + expiresDate = Int64(expiresDateString) + } + + let maxAge = arguments!["maxAge"] as? Int64 + let isSecure = arguments!["isSecure"] as? Bool + let isHttpOnly = arguments!["isHttpOnly"] as? Bool + let sameSite = arguments!["sameSite"] as? String + let domain = arguments!["domain"] as? String + + MyCookieManager.setCookie(url: url, + name: name, + value: value, + path: path, + domain: domain, + expiresDate: expiresDate, + maxAge: maxAge, + isSecure: isSecure, + isHttpOnly: isHttpOnly, + sameSite: sameSite, + result: result, cookieStore: cookieStore) + break + case "getCookies": + let url = arguments!["url"] as! String + MyCookieManager.getCookies(url: url, result: result, cookieStore: cookieStore) + break + case "getAllCookies": + MyCookieManager.getAllCookies(result: result, cookieStore: cookieStore) + break + case "deleteCookie": + let url = arguments!["url"] as! String + let name = arguments!["name"] as! String + let path = arguments!["path"] as! String + let domain = arguments!["domain"] as? String + MyCookieManager.deleteCookie(url: url, name: name, path: path, domain: domain, result: result, cookieStore: cookieStore) + break; + case "deleteCookies": + let url = arguments!["url"] as! String + let path = arguments!["path"] as! String + let domain = arguments!["domain"] as? String + MyCookieManager.deleteCookies(url: url, path: path, domain: domain, result: result, cookieStore: cookieStore) + break; + case "deleteAllCookies": + MyCookieManager.deleteAllCookies(result: result, cookieStore: cookieStore) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public static func setCookie(url: String, + name: String, + value: String, + path: String, + domain: String?, + expiresDate: Int64?, + maxAge: Int64?, + isSecure: Bool?, + isHttpOnly: Bool?, + sameSite: String?, + result: @escaping FlutterResult, cookieStore: WKHTTPCookieStore = WKWebsiteDataStore.default().httpCookieStore) { + var properties: [HTTPCookiePropertyKey: Any] = [:] + properties[.originURL] = url + properties[.name] = name + properties[.value] = value + properties[.path] = path + + if domain != nil { + properties[.domain] = domain + } + + if expiresDate != nil { + // convert from milliseconds + properties[.expires] = Date(timeIntervalSince1970: TimeInterval(Double(expiresDate!)/1000)) + } + if maxAge != nil { + properties[.maximumAge] = String(maxAge!) + } + if isSecure != nil && isSecure! { + properties[.secure] = "TRUE" + } + if isHttpOnly != nil && isHttpOnly! { + properties[.init("HttpOnly")] = "YES" + } + if sameSite != nil { + if #available(macOS 10.15, *) { + var sameSiteValue = HTTPCookieStringPolicy(rawValue: "None") + switch sameSite { + case "Lax": + sameSiteValue = HTTPCookieStringPolicy.sameSiteLax + case "Strict": + sameSiteValue = HTTPCookieStringPolicy.sameSiteStrict + default: + break + } + properties[.sameSitePolicy] = sameSiteValue + } else { + properties[.init("SameSite")] = sameSite + } + } + + if let cookie = HTTPCookie(properties: properties) { + cookieStore.setCookie(cookie, completionHandler: {() in + result(true) + }) + } else { + result(false) + } + } + + public static func getCookies(url: String, result: @escaping FlutterResult, cookieStore: WKHTTPCookieStore = WKWebsiteDataStore.default().httpCookieStore) { + var cookieList: [[String: Any?]] = [] + + if let urlHost = URL(string: url)?.host { + cookieStore.getAllCookies { (cookies) in + for cookie in cookies { + if urlHost.hasSuffix(cookie.domain) || ".\(urlHost)".hasSuffix(cookie.domain) { + var sameSite: String? = nil + if #available(macOS 10.15, *) { + if let sameSiteValue = cookie.sameSitePolicy?.rawValue { + sameSite = sameSiteValue.prefix(1).capitalized + sameSiteValue.dropFirst() + } + } + + var expiresDateTimestamp: Int64 = -1 + if let expiresDate = cookie.expiresDate?.timeIntervalSince1970 { + // convert to milliseconds + expiresDateTimestamp = Int64(expiresDate * 1000) + } + + cookieList.append([ + "name": cookie.name, + "value": cookie.value, + "expiresDate": expiresDateTimestamp != -1 ? expiresDateTimestamp : nil, + "isSessionOnly": cookie.isSessionOnly, + "domain": cookie.domain, + "sameSite": sameSite, + "isSecure": cookie.isSecure, + "isHttpOnly": cookie.isHTTPOnly, + "path": cookie.path, + ]) + } + } + result(cookieList) + } + return + } else { + print("Cannot get WebView cookies. No HOST found for URL: \(url)") + } + + result(cookieList) + } + + public static func getAllCookies(result: @escaping FlutterResult, cookieStore: WKHTTPCookieStore = WKWebsiteDataStore.default().httpCookieStore) { + var cookieList: [[String: Any?]] = [] + + cookieStore.getAllCookies { (cookies) in + for cookie in cookies { + var sameSite: String? = nil + if #available(macOS 10.15, *) { + if let sameSiteValue = cookie.sameSitePolicy?.rawValue { + sameSite = sameSiteValue.prefix(1).capitalized + sameSiteValue.dropFirst() + } + } + + var expiresDateTimestamp: Int64 = -1 + if let expiresDate = cookie.expiresDate?.timeIntervalSince1970 { + // convert to milliseconds + expiresDateTimestamp = Int64(expiresDate * 1000) + } + + cookieList.append([ + "name": cookie.name, + "value": cookie.value, + "expiresDate": expiresDateTimestamp != -1 ? expiresDateTimestamp : nil, + "isSessionOnly": cookie.isSessionOnly, + "domain": cookie.domain, + "sameSite": sameSite, + "isSecure": cookie.isSecure, + "isHttpOnly": cookie.isHTTPOnly, + "path": cookie.path, + ]) + } + result(cookieList) + } + } + + public static func deleteCookie(url: String, name: String, path: String, domain: String?, result: @escaping FlutterResult, cookieStore: WKHTTPCookieStore = WKWebsiteDataStore.default().httpCookieStore) { + var domain = domain + cookieStore.getAllCookies { (cookies) in + for cookie in cookies { + var originURL = url + if cookie.properties![.originURL] is String { + originURL = cookie.properties![.originURL] as! String + } + else if cookie.properties![.originURL] is URL { + originURL = (cookie.properties![.originURL] as! URL).absoluteString + } + if domain == nil, let domainUrl = URL(string: originURL) { + domain = domainUrl.host + } + if let domain = domain, cookie.domain == domain, cookie.name == name, cookie.path == path { + cookieStore.delete(cookie, completionHandler: { + result(true) + }) + return + } + } + result(false) + } + } + + public static func deleteCookies(url: String, path: String, domain: String?, result: @escaping FlutterResult, cookieStore: WKHTTPCookieStore = WKWebsiteDataStore.default().httpCookieStore) { + var domain = domain + let dispatchGroup = DispatchGroup() + cookieStore.getAllCookies { (cookies) in + for cookie in cookies { + var originURL = url + if cookie.properties![.originURL] is String { + originURL = cookie.properties![.originURL] as! String + } + else if cookie.properties![.originURL] is URL { + originURL = (cookie.properties![.originURL] as! URL).absoluteString + } + if domain == nil, let domainUrl = URL(string: originURL) { + domain = domainUrl.host + } + if let domain = domain, cookie.domain == domain, cookie.path == path { + dispatchGroup.enter() + cookieStore.delete(cookie) { + dispatchGroup.leave() + } + } + } + dispatchGroup.notify(queue: .main) { + result(true) + } + } + } + + public static func deleteAllCookies(result: @escaping FlutterResult, cookieStore: WKHTTPCookieStore = WKWebsiteDataStore.default().httpCookieStore) { + cookieStore.getAllCookies { cookies in + let group = DispatchGroup() + for cookie in cookies { + group.enter() + cookieStore.delete(cookie) { group.leave() } + } + group.notify(queue: .main) { result(true) } + } + } + + public override func dispose() { + super.dispose() + plugin = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/MyWebStorageManager.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/MyWebStorageManager.swift new file mode 100755 index 00000000..a20b46e6 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/MyWebStorageManager.swift @@ -0,0 +1,95 @@ +// +// MyWebStorageManager.swift +// connectivity +// +// Created by Lorenzo Pichilli on 16/12/2019. +// + +import Foundation +import WebKit +import FlutterMacOS + +public class MyWebStorageManager: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_webstoragemanager" + var plugin: InAppWebViewFlutterPlugin? + static var websiteDataStore = WKWebsiteDataStore.default() + + init(plugin: InAppWebViewFlutterPlugin) { + super.init(channel: FlutterMethodChannel(name: MyWebStorageManager.METHOD_CHANNEL_NAME, binaryMessenger: plugin.registrar!.messenger)) + self.plugin = plugin + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + switch call.method { + case "fetchDataRecords": + let dataTypes = Set(arguments!["dataTypes"] as! [String]) + MyWebStorageManager.fetchDataRecords(dataTypes: dataTypes, result: result) + break + case "removeDataFor": + let dataTypes = Set(arguments!["dataTypes"] as! [String]) + let recordList = arguments!["recordList"] as! [[String: Any?]] + MyWebStorageManager.removeDataFor(dataTypes: dataTypes, recordList: recordList, result: result) + break + case "removeDataModifiedSince": + let dataTypes = Set(arguments!["dataTypes"] as! [String]) + let timestamp = arguments!["timestamp"] as! Int64 + MyWebStorageManager.removeDataModifiedSince(dataTypes: dataTypes, timestamp: timestamp, result: result) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public static func fetchDataRecords(dataTypes: Set, result: @escaping FlutterResult) { + var recordList: [[String: Any?]] = [] + + MyWebStorageManager.websiteDataStore.fetchDataRecords(ofTypes: dataTypes) { (data) in + for record in data { + recordList.append([ + "displayName": record.displayName, + "dataTypes": record.dataTypes.map({ (dataType) -> String in + return dataType + }) + ]) + } + result(recordList) + } + } + + public static func removeDataFor(dataTypes: Set, recordList: [[String: Any?]], result: @escaping FlutterResult) { + var records: [WKWebsiteDataRecord] = [] + + MyWebStorageManager.websiteDataStore.fetchDataRecords(ofTypes: dataTypes) { (data) in + for record in data { + for r in recordList { + let displayName = r["displayName"] as! String + if (record.displayName == displayName) { + records.append(record) + break + } + } + } + websiteDataStore.removeData(ofTypes: dataTypes, for: records) { + result(true) + } + } + } + + public static func removeDataModifiedSince(dataTypes: Set, timestamp: Int64, result: @escaping FlutterResult) { + let date = NSDate(timeIntervalSince1970: TimeInterval(timestamp)) + MyWebStorageManager.websiteDataStore.removeData(ofTypes: dataTypes, modifiedSince: date as Date) { + result(true) + } + } + + public override func dispose() { + super.dispose() + plugin = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PlatformUtil.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PlatformUtil.swift new file mode 100644 index 00000000..c9859d7d --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PlatformUtil.swift @@ -0,0 +1,67 @@ +// +// PlatformUtil.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 01/03/21. +// + +import Foundation +import FlutterMacOS + +public class PlatformUtil: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_platformutil" + var plugin: InAppWebViewFlutterPlugin? + + init(plugin: InAppWebViewFlutterPlugin) { + super.init(channel: FlutterMethodChannel(name: PlatformUtil.METHOD_CHANNEL_NAME, binaryMessenger: plugin.registrar!.messenger)) + self.plugin = plugin + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "getSystemVersion": + let version = ProcessInfo.processInfo.operatingSystemVersion + result("\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)") + break + case "formatDate": + let date = arguments!["date"] as! Int64 + let format = arguments!["format"] as! String + let locale = PlatformUtil.getLocaleFromString(locale: arguments!["locale"] as? String) + let timezone = TimeZone.init(abbreviation: arguments!["timezone"] as? String ?? "UTC")! + result(PlatformUtil.formatDate(date: date, format: format, locale: locale, timezone: timezone)) + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + static public func getLocaleFromString(locale: String?) -> Locale { + guard let locale = locale else { + return Locale.init(identifier: "en_US") + } + return Locale.init(identifier: locale) + } + + static public func getDateFromMilliseconds(date: Int64) -> Date { + return Date(timeIntervalSince1970: TimeInterval(Double(date)/1000)) + } + + static public func formatDate(date: Int64, format: String, locale: Locale, timezone: TimeZone) -> String { + let formatter = DateFormatter() + formatter.dateFormat = format + formatter.timeZone = timezone + return formatter.string(from: PlatformUtil.getDateFromMilliseconds(date: date)) + } + + public override func dispose() { + super.dispose() + plugin = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/CallAsyncJavaScriptBelowIOS14WrapperJS.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/CallAsyncJavaScriptBelowIOS14WrapperJS.swift new file mode 100644 index 00000000..5622691a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/CallAsyncJavaScriptBelowIOS14WrapperJS.swift @@ -0,0 +1,21 @@ +// +// CallAsyncJavaScriptBelowIOS14WrapperJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let CALL_ASYNC_JAVASCRIPT_BELOW_IOS_14_WRAPPER_JS = """ +(function(obj) { + (async function(\(PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_NAMES) { + \(PluginScriptsUtil.VAR_FUNCTION_BODY) + })(\(PluginScriptsUtil.VAR_FUNCTION_ARGUMENT_VALUES)).then(function(value) { + window.webkit.messageHandlers['onCallAsyncJavaScriptResultBelowIOS14Received'].postMessage({'value': value, 'error': null, 'resultUuid': '\(PluginScriptsUtil.VAR_RESULT_UUID)'}); + }).catch(function(error) { + window.webkit.messageHandlers['onCallAsyncJavaScriptResultBelowIOS14Received'].postMessage({'value': null, 'error': error + '', 'resultUuid': '\(PluginScriptsUtil.VAR_RESULT_UUID)'}); + }); + return null; +})(\(PluginScriptsUtil.VAR_FUNCTION_ARGUMENTS_OBJ)); +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/ConsoleLogJS.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/ConsoleLogJS.swift new file mode 100644 index 00000000..d244d899 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/ConsoleLogJS.swift @@ -0,0 +1,52 @@ +// +// ConsoleLogJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let CONSOLE_LOG_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_CONSOLE_LOG_JS_PLUGIN_SCRIPT" + +let CONSOLE_LOG_JS_PLUGIN_SCRIPT = PluginScript( + groupName: CONSOLE_LOG_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: CONSOLE_LOG_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: ["consoleLog", "consoleDebug", "consoleError", "consoleInfo", "consoleWarn"]) + +// the message needs to be concatenated with '' in order to have the same behavior like on Android +let CONSOLE_LOG_JS_SOURCE = """ +(function(console) { + + function _callHandler(oldLog, args) { + var message = ''; + for (var i in args) { + try { + message += message === '' ? args[i] : ' ' + args[i]; + } catch(ignored) {} + } + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + window.webkit.messageHandlers[oldLog].postMessage({'message': message, '_windowId': _windowId}); + } + + var oldLogs = { + 'consoleLog': console.log, + 'consoleDebug': console.debug, + 'consoleError': console.error, + 'consoleInfo': console.info, + 'consoleWarn': console.warn + }; + + for (var k in oldLogs) { + (function(oldLog) { + console[oldLog.replace('console', '').toLowerCase()] = function() { + oldLogs[oldLog].apply(null, arguments); + _callHandler(oldLog, arguments); + } + })(k); + } +})(window.console); +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/EnableViewportScaleJS.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/EnableViewportScaleJS.swift new file mode 100644 index 00000000..593ba00e --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/EnableViewportScaleJS.swift @@ -0,0 +1,36 @@ +// +// EnableViewportScaleJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT" + +let ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ENABLE_VIEWPORT_SCALE_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: ENABLE_VIEWPORT_SCALE_JS_SOURCE, + injectionTime: .atDocumentEnd, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let ENABLE_VIEWPORT_SCALE_JS_SOURCE = """ +(function() { + var meta = document.createElement('meta'); + meta.setAttribute('name', 'viewport'); + meta.setAttribute('content', 'width=device-width'); + document.getElementsByTagName('head')[0].appendChild(meta); +})() +""" + +let NOT_ENABLE_VIEWPORT_SCALE_JS_SOURCE = """ +(function() { + var meta = document.createElement('meta'); + meta.setAttribute('name', 'viewport'); + meta.setAttribute('content', window.\(JAVASCRIPT_BRIDGE_NAME)._originalViewPortMetaTagContent); + document.getElementsByTagName('head')[0].appendChild(meta); +})() +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/FindElementsAtPointJS.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/FindElementsAtPointJS.swift new file mode 100644 index 00000000..b3c22282 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/FindElementsAtPointJS.swift @@ -0,0 +1,73 @@ +// +// FindElementsAtPointJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let FIND_ELEMENTS_AT_POINT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_FIND_ELEMENTS_AT_POINT_JS_PLUGIN_SCRIPT" + +let FIND_ELEMENTS_AT_POINT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: FIND_ELEMENTS_AT_POINT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: FIND_ELEMENTS_AT_POINT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +/** + https://developer.android.com/reference/android/webkit/WebView.HitTestResult + */ +let FIND_ELEMENTS_AT_POINT_JS_SOURCE = """ +window.\(JAVASCRIPT_BRIDGE_NAME)._findElementsAtPoint = function(x, y) { + var hitTestResultType = { + UNKNOWN_TYPE: 0, + PHONE_TYPE: 2, + GEO_TYPE: 3, + EMAIL_TYPE: 4, + IMAGE_TYPE: 5, + SRC_ANCHOR_TYPE: 7, + SRC_IMAGE_ANCHOR_TYPE: 8, + EDIT_TEXT_TYPE: 9 + }; + var element = document.elementFromPoint(x, y); + var data = { + type: 0, + extra: null + }; + while (element) { + if (element.tagName === 'IMG' && element.src) { + if (element.parentNode && element.parentNode.tagName === 'A' && element.parentNode.href) { + data.type = hitTestResultType.SRC_IMAGE_ANCHOR_TYPE; + } else { + data.type = hitTestResultType.IMAGE_TYPE; + } + data.extra = element.src; + break; + } else if (element.tagName === 'A' && element.href) { + if (element.href.indexOf('mailto:') === 0) { + data.type = hitTestResultType.EMAIL_TYPE; + data.extra = element.href.replace('mailto:', ''); + } else if (element.href.indexOf('tel:') === 0) { + data.type = hitTestResultType.PHONE_TYPE; + data.extra = element.href.replace('tel:', ''); + } else if (element.href.indexOf('geo:') === 0) { + data.type = hitTestResultType.GEO_TYPE; + data.extra = element.href.replace('geo:', ''); + } else { + data.type = hitTestResultType.SRC_ANCHOR_TYPE; + data.extra = element.href; + } + break; + } else if ( + (element.tagName === 'INPUT' && ['text', 'email', 'password', 'number', 'search', 'tel', 'url'].indexOf(element.type) >= 0) || + element.tagName === 'TEXTAREA') { + data.type = hitTestResultType.EDIT_TEXT_TYPE + } + element = element.parentNode; + } + return data; +} +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/FindTextHighlightJS.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/FindTextHighlightJS.swift new file mode 100644 index 00000000..f0db6145 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/FindTextHighlightJS.swift @@ -0,0 +1,187 @@ +// +// FindTextHighlightJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let FIND_TEXT_HIGHLIGHT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_FIND_TEXT_HIGHLIGHT_JS_PLUGIN_SCRIPT" +let FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._searchResultCount" +let FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._currentHighlight" +let FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._isDoneCounting" + +let FIND_TEXT_HIGHLIGHT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: FIND_TEXT_HIGHLIGHT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: FIND_TEXT_HIGHLIGHT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: ["onFindResultReceived"]) + +let FIND_TEXT_HIGHLIGHT_JS_SOURCE = """ +\(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) = 0; +\(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE) = 0; +\(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) = false; +window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsyncForElement = function(element, keyword) { + if (element) { + if (element.nodeType == 3) { + // Text node + + var elementTmp = element; + while (true) { + var value = elementTmp.nodeValue; // Search for keyword in text node + var idx = value.toLowerCase().indexOf(keyword); + + if (idx < 0) break; + + var span = document.createElement("span"); + var text = document.createTextNode(value.substr(idx, keyword.length)); + span.appendChild(text); + + span.setAttribute( + "id", + "\(JAVASCRIPT_BRIDGE_NAME)_SEARCH_WORD_" + \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) + ); + span.setAttribute("class", "\(JAVASCRIPT_BRIDGE_NAME)_Highlight"); + var backgroundColor = \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) == 0 ? "#FF9732" : "#FFFF00"; + span.setAttribute("style", "color: #000 !important; background: " + backgroundColor + " !important; padding: 0px !important; margin: 0px !important; border: 0px !important;"); + + text = document.createTextNode(value.substr(idx + keyword.length)); + element.deleteData(idx, value.length - idx); + + var next = element.nextSibling; + element.parentNode.insertBefore(span, next); + element.parentNode.insertBefore(text, next); + element = text; + + \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE)++; + elementTmp = document.createTextNode( + value.substr(idx + keyword.length) + ); + + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + + window.webkit.messageHandlers["onFindResultReceived"].postMessage( + { + 'findResult': { + 'activeMatchOrdinal': \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE), + 'numberOfMatches': \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE), + 'isDoneCounting': \(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) + }, + '_windowId': _windowId + } + ); + } + } else if (element.nodeType == 1) { + // Element node + if ( + element.style.display != "none" && + element.nodeName.toLowerCase() != "select" + ) { + for (var i = element.childNodes.length - 1; i >= 0; i--) { + window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsyncForElement( + element.childNodes[element.childNodes.length - 1 - i], + keyword + ); + } + } + } + } +} + +// the main entry point to start the search +window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsync = function(keyword) { + window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatches(); + window.\(JAVASCRIPT_BRIDGE_NAME)._findAllAsyncForElement(document.body, keyword.toLowerCase()); + \(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) = true; + + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + + window.webkit.messageHandlers["onFindResultReceived"].postMessage( + { + 'findResult': { + 'activeMatchOrdinal': \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE), + 'numberOfMatches': \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE), + 'isDoneCounting': \(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) + }, + '_windowId': _windowId + } + ); +} + +// helper function, recursively removes the highlights in elements and their children +window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatchesForElement = function(element) { + if (element) { + if (element.nodeType == 1) { + if (element.getAttribute("class") == "\(JAVASCRIPT_BRIDGE_NAME)_Highlight") { + var text = element.removeChild(element.firstChild); + element.parentNode.insertBefore(text, element); + element.parentNode.removeChild(element); + return true; + } else { + var normalize = false; + for (var i = element.childNodes.length - 1; i >= 0; i--) { + if (window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatchesForElement(element.childNodes[i])) { + normalize = true; + } + } + if (normalize) { + element.normalize(); + } + } + } + } + return false; +} + +// the main entry point to remove the highlights +window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatches = function() { + \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) = 0; + \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE) = 0; + \(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) = false; + window.\(JAVASCRIPT_BRIDGE_NAME)._clearMatchesForElement(document.body); +} + +window.\(JAVASCRIPT_BRIDGE_NAME)._findNext = function(forward) { + if (\(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) <= 0) return; + + var idx = \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE) + (forward ? +1 : -1); + idx = + idx < 0 + ? \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) - 1 + : idx >= \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE) + ? 0 + : idx; + \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE) = idx; + + var scrollTo = document.getElementById("\(JAVASCRIPT_BRIDGE_NAME)_SEARCH_WORD_" + idx); + if (scrollTo) { + var highlights = document.getElementsByClassName("\(JAVASCRIPT_BRIDGE_NAME)_Highlight"); + for (var i = 0; i < highlights.length; i++) { + var span = highlights[i]; + span.style.backgroundColor = "#FFFF00"; + } + scrollTo.style.backgroundColor = "#FF9732"; + + scrollTo.scrollIntoView({ + behavior: "auto", + block: "center" + }); + + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + + window.webkit.messageHandlers["onFindResultReceived"].postMessage( + { + 'findResult': { + 'activeMatchOrdinal': \(FIND_TEXT_HIGHLIGHT_CURRENT_HIGHLIGHT_JS_SOURCE), + 'numberOfMatches': \(FIND_TEXT_HIGHLIGHT_SEARCH_RESULT_COUNT_JS_SOURCE), + 'isDoneCounting': \(FIND_TEXT_HIGHLIGHT_IS_DONE_COUNTING_JS_SOURCE) + }, + '_windowId': _windowId + } + ); + } +} +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/InterceptAjaxRequestJS.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/InterceptAjaxRequestJS.swift new file mode 100644 index 00000000..4a68e9b6 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/InterceptAjaxRequestJS.swift @@ -0,0 +1,268 @@ +// +// InterceptAjaxRequestsJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT" +let FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._useShouldInterceptAjaxRequest" + + +let FLAG_VARIABLE_FOR_INTERCEPT_ONLY_ASYNC_AJAX_REQUESTS_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._interceptOnlyAsyncAjaxRequests"; + +let INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT = PluginScript( + groupName: INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: INTERCEPT_AJAX_REQUEST_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: []) + +func createInterceptOnlyAsyncAjaxRequestsPluginScript(onlyAsync: Bool) -> PluginScript { + return PluginScript(groupName: INTERCEPT_AJAX_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: "\(FLAG_VARIABLE_FOR_INTERCEPT_ONLY_ASYNC_AJAX_REQUESTS_JS_SOURCE) = \(onlyAsync);", + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: [] + ); +} + +let INTERCEPT_AJAX_REQUEST_JS_SOURCE = """ +\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) = true; +(function(ajax) { + 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; + if (\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) == null || \(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.\(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 canBeIntercepted = self._flutter_inappwebview_isAsync || \(FLAG_VARIABLE_FOR_INTERCEPT_ONLY_ASYNC_AJAX_REQUESTS_JS_SOURCE) === false; + if (canBeIntercepted && (\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) == null || \(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() { + if (\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_AJAX_REQUEST_JS_SOURCE) == null || \(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.\(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); + \(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.\(JAVASCRIPT_BRIDGE_NAME).callHandler('shouldInterceptAjaxRequest', ajaxRequest).then(function(result) { + if (result != null) { + switch (result) { + case 0: + self.abort(); + return; + }; + if (result.data != null && !\(JAVASCRIPT_UTIL_VAR_NAME).isString(result.data) && result.data.length > 0) { + var bodyString = \(JAVASCRIPT_UTIL_VAR_NAME).arrayBufferToString(result.data); + if (\(JAVASCRIPT_UTIL_VAR_NAME).isBodyFormData(bodyString)) { + var formDataContentType = \(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 (\(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; + }; + if (result.headers != null) { + 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); +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/InterceptFetchRequestJS.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/InterceptFetchRequestJS.swift new file mode 100644 index 00000000..14539811 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/InterceptFetchRequestJS.swift @@ -0,0 +1,153 @@ +// +// InterceptFetchRequestsJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT" +let FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._useShouldInterceptFetchRequest" + +let INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT = PluginScript( + groupName: INTERCEPT_FETCH_REQUEST_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: INTERCEPT_FETCH_REQUEST_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: []) + +let INTERCEPT_FETCH_REQUEST_JS_SOURCE = """ +\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE) = true; +(function(fetch) { + if (fetch == null) { + return; + } + window.fetch = async function(resource, init) { + if (\(FLAG_VARIABLE_FOR_SHOULD_INTERCEPT_FETCH_REQUEST_JS_SOURCE) == null || \(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 = \(JAVASCRIPT_UTIL_VAR_NAME).convertHeadersToJson(fetchRequest.headers); + } + fetchRequest.credentials = \(JAVASCRIPT_UTIL_VAR_NAME).convertCredentialsToJson(fetchRequest.credentials); + return \(JAVASCRIPT_UTIL_VAR_NAME).convertBodyRequest(fetchRequest.body).then(function(body) { + fetchRequest.body = body; + return window.\(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 && !\(JAVASCRIPT_UTIL_VAR_NAME).isString(result.body) && result.body.length > 0) { + var bodyString = \(JAVASCRIPT_UTIL_VAR_NAME).arrayBufferToString(result.body); + if (\(JAVASCRIPT_UTIL_VAR_NAME).isBodyFormData(bodyString)) { + var formDataContentType = \(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 = \(JAVASCRIPT_UTIL_VAR_NAME).convertJsonToHeaders(result.headers); + } + if (\(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 = \(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); +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/JavaScriptBridgeJS.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/JavaScriptBridgeJS.swift new file mode 100644 index 00000000..1549c28b --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/JavaScriptBridgeJS.swift @@ -0,0 +1,243 @@ +// +// javaScriptBridgeJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let JAVASCRIPT_BRIDGE_NAME = "flutter_inappwebview" +let JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT" + +let JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT = PluginScript( + groupName: JAVASCRIPT_BRIDGE_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: JAVASCRIPT_BRIDGE_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: ["callHandler"]) + +let JAVASCRIPT_BRIDGE_JS_SOURCE = """ +window.\(JAVASCRIPT_BRIDGE_NAME) = {}; +\(WEB_MESSAGE_CHANNELS_VARIABLE_NAME) = {}; +window.\(JAVASCRIPT_BRIDGE_NAME).callHandler = function() { + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + var _callHandlerID = setTimeout(function(){}); + window.webkit.messageHandlers['callHandler'].postMessage( {'handlerName': arguments[0], '_callHandlerID': _callHandlerID, 'args': JSON.stringify(Array.prototype.slice.call(arguments, 1)), '_windowId': _windowId} ); + return new Promise(function(resolve, reject) { + window.\(JAVASCRIPT_BRIDGE_NAME)[_callHandlerID] = {resolve: resolve, reject: reject}; + }); +}; +\(WEB_MESSAGE_LISTENER_JS_SOURCE) +\(UTIL_JS_SOURCE) +""" + +let PLATFORM_READY_JS_SOURCE = "window.dispatchEvent(new Event('flutterInAppWebViewPlatformReady'));"; + +let JAVASCRIPT_UTIL_VAR_NAME = "window.\(JAVASCRIPT_BRIDGE_NAME)._Util" + +/* + https://github.com/github/fetch/blob/master/fetch.js + */ +let 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((resolve, reject) => resolve(null)); + } + if (\(JAVASCRIPT_UTIL_VAR_NAME).isString(body) || (\(JAVASCRIPT_UTIL_VAR_NAME).support.searchParams && body instanceof URLSearchParams)) { + return new Promise((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; + } +}; +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/OnLoadResourceJS.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/OnLoadResourceJS.swift new file mode 100644 index 00000000..828de9ef --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/OnLoadResourceJS.swift @@ -0,0 +1,39 @@ +// +// resourceObserverJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT" +let FLAG_VARIABLE_FOR_ON_LOAD_RESOURCE_JS_SOURCE = "window.\(JAVASCRIPT_BRIDGE_NAME)._useOnLoadResource" + +let ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ON_LOAD_RESOURCE_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: ON_LOAD_RESOURCE_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let ON_LOAD_RESOURCE_JS_SOURCE = """ +\(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.\(JAVASCRIPT_BRIDGE_NAME).callHandler("onLoadResource", resource); + } + }); + }); + observer.observe({entryTypes: ['resource']}); +})(); +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/OnScrollChangedJS.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/OnScrollChangedJS.swift new file mode 100644 index 00000000..4418c042 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/OnScrollChangedJS.swift @@ -0,0 +1,33 @@ +// +// OnScrollEvent.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/10/22. +// + +import Foundation + +let ON_SCROLL_CHANGED_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_SCROLL_CHANGED_EVENT_JS_PLUGIN_SCRIPT" + +let ON_SCROLL_CHANGED_EVENT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ON_SCROLL_CHANGED_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: ON_SCROLL_CHANGED_EVENT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: ["onScrollChanged"]) + +let ON_SCROLL_CHANGED_EVENT_JS_SOURCE = """ +(function(){ + document.addEventListener('scroll', function(e) { + var _windowId = \(WINDOW_ID_VARIABLE_JS_SOURCE); + window.webkit.messageHandlers["onScrollChanged"].postMessage( + { + x: window.scrollX, + y: window.scrollY, + _windowId: _windowId + } + ); + }); +})(); +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/OnWindowBlurEventJS.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/OnWindowBlurEventJS.swift new file mode 100644 index 00000000..345b5ece --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/OnWindowBlurEventJS.swift @@ -0,0 +1,26 @@ +// +// OnWindowBlurEventJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT" + +let ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ON_WINDOW_BLUR_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: ON_WINDOW_BLUR_EVENT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let ON_WINDOW_BLUR_EVENT_JS_SOURCE = """ +(function(){ + window.addEventListener('blur', function(e) { + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onWindowBlur'); + }); +})(); +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/OnWindowFocusEventJS.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/OnWindowFocusEventJS.swift new file mode 100644 index 00000000..9ebda3e9 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/OnWindowFocusEventJS.swift @@ -0,0 +1,26 @@ +// +// OnWindowFocusEventJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT" + +let ON_WINDOW_FOCUS_EVENT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ON_WINDOW_FOCUS_EVENT_JS_SOURCE, + source: ON_WINDOW_FOCUS_EVENT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let ON_WINDOW_FOCUS_EVENT_JS_SOURCE = """ +(function(){ + window.addEventListener('focus', function(e) { + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler('onWindowFocus'); + }); +})(); +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/OriginalViewPortMetaTagContentJS.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/OriginalViewPortMetaTagContentJS.swift new file mode 100644 index 00000000..a1c6637d --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/OriginalViewPortMetaTagContentJS.swift @@ -0,0 +1,31 @@ +// +// OriginalViewPortMetaTagContentJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_PLUGIN_SCRIPT" + +let ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_SOURCE, + injectionTime: .atDocumentEnd, + forMainFrameOnly: true, + requiredInAllContentWorlds: false, + messageHandlerNames: []) + +let ORIGINAL_VIEWPORT_METATAG_CONTENT_JS_SOURCE = """ +window.\(JAVASCRIPT_BRIDGE_NAME)._originalViewPortMetaTagContent = ""; +(function() { + var metaTagNodes = document.head.getElementsByTagName('meta'); + for (var i = 0; i < metaTagNodes.length; i++) { + var metaTagNode = metaTagNodes[i]; + if (metaTagNode.name === "viewport") { + window.\(JAVASCRIPT_BRIDGE_NAME)._originalViewPortMetaTagContent = metaTagNode.content; + } + } +})(); +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/PluginScriptsUtil.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/PluginScriptsUtil.swift new file mode 100644 index 00000000..51f9c867 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/PluginScriptsUtil.swift @@ -0,0 +1,31 @@ +// +// PluginScripts.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +public class PluginScriptsUtil { + public static let VAR_PLACEHOLDER_VALUE = "$IN_APP_WEBVIEW_PLACEHOLDER_VALUE" + public static let VAR_FUNCTION_ARGUMENT_NAMES = "$IN_APP_WEBVIEW_FUNCTION_ARGUMENT_NAMES" + public static let VAR_FUNCTION_ARGUMENT_VALUES = "$IN_APP_WEBVIEW_FUNCTION_ARGUMENT_VALUES" + public static let VAR_FUNCTION_ARGUMENTS_OBJ = "$IN_APP_WEBVIEW_FUNCTION_ARGUMENTS_OBJ" + public static let VAR_FUNCTION_BODY = "$IN_APP_WEBVIEW_FUNCTION_BODY" + public static let VAR_RESULT_UUID = "$IN_APP_WEBVIEW_RESULT_UUID" + + public static let 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; +})(); +""" +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/PrintJS.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/PrintJS.swift new file mode 100644 index 00000000..5ac12691 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/PrintJS.swift @@ -0,0 +1,24 @@ +// +// PrintJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let PRINT_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_PRINT_JS_PLUGIN_SCRIPT" + +let PRINT_JS_PLUGIN_SCRIPT = PluginScript( + groupName: PRINT_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: PRINT_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: []) + +let PRINT_JS_SOURCE = """ +window.print = function() { + window.\(JAVASCRIPT_BRIDGE_NAME).callHandler("onPrintRequest", window.location.href); +} +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/PromisePolyfillJS.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/PromisePolyfillJS.swift new file mode 100644 index 00000000..d2ef3e2a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/PromisePolyfillJS.swift @@ -0,0 +1,27 @@ +// +// PromisePolyfillJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation +import WebKit + +let PROMISE_POLYFILL_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_PROMISE_POLYFILL_JS_PLUGIN_SCRIPT" + +let PROMISE_POLYFILL_JS_PLUGIN_SCRIPT = PluginScript( + groupName: PROMISE_POLYFILL_JS_PLUGIN_SCRIPT_GROUP_NAME, + source: PROMISE_POLYFILL_JS_SOURCE, + injectionTime: .atDocumentStart, + forMainFrameOnly: false, + requiredInAllContentWorlds: true, + messageHandlerNames: []) + +// https://github.com/tildeio/rsvp.js +let 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;i2&&void 0!==arguments[2])||arguments[2],o=arguments[3];return function(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}(this,t.call(this,e,r,n,o))}return function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),e.prototype._init=function(t,e){this._result={},this._enumerate(e)},e.prototype._enumerate=function(t){var e=Object.keys(t),r=e.length,n=this.promise;this._remaining=r;for(var o=void 0,i=void 0,s=0;n._state===a&&s= 0) { + this.listeners.splice(index, 1); + } +}; + +window.\(JAVASCRIPT_BRIDGE_NAME)._normalizeIPv6 = function(ip_string) { + // replace ipv4 address if any + var ipv4 = ip_string.match(/(.*:)([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$)/); + if (ipv4) { + ip_string = ipv4[1]; + ipv4 = ipv4[2].match(/[0-9]+/g); + for (var i = 0;i < 4;i ++) { + var byte = parseInt(ipv4[i],10); + ipv4[i] = ("0" + byte.toString(16)).substr(-2); + } + ip_string += ipv4[0] + ipv4[1] + ':' + ipv4[2] + ipv4[3]; + } + + // take care of leading and trailing :: + ip_string = ip_string.replace(/^:|:$/g, ''); + + var ipv6 = ip_string.split(':'); + + for (var i = 0; i < ipv6.length; i ++) { + var hex = ipv6[i]; + if (hex != "") { + // normalize leading zeros + ipv6[i] = ("0000" + hex).substr(-4); + } + else { + // normalize grouped zeros :: + hex = []; + for (var j = ipv6.length; j <= 8; j ++) { + hex.push('0000'); + } + ipv6[i] = hex.join(':'); + } + } + + return ipv6.join(':'); +} + +window.\(JAVASCRIPT_BRIDGE_NAME)._isOriginAllowed = function(allowedOriginRules, scheme, host, port) { + for (var rule of allowedOriginRules) { + if (rule === "*") { + return true; + } + if (scheme == null || scheme === "") { + continue; + } + if ((scheme == null || scheme === "") && (host == null || host === "") && (port === 0 || port === "" || port == null)) { + continue; + } + var rulePort = rule.port == null || rule.port === 0 ? (rule.scheme == "https" ? 443 : 80) : rule.port; + var currentPort = port === 0 || port === "" || port == null ? (scheme == "https" ? 443 : 80) : port; + var IPv6 = null; + if (rule.host != null && rule.host[0] === "[") { + try { + IPv6 = window.\(JAVASCRIPT_BRIDGE_NAME)._normalizeIPv6(rule.host.substring(1, rule.host.length - 1)); + } catch {} + } + var hostIPv6 = null; + try { + hostIPv6 = window.\(JAVASCRIPT_BRIDGE_NAME)._normalizeIPv6(host); + } catch {} + + var schemeAllowed = scheme == rule.scheme; + + var hostAllowed = rule.host == null || + rule.host === "" || + host === rule.host || + (rule.host[0] === "*" && host != null && host.indexOf(rule.host.split("*")[1]) >= 0) || + (hostIPv6 != null && IPv6 != null && hostIPv6 === IPv6); + + var portAllowed = rulePort === currentPort; + + if (schemeAllowed && hostAllowed && portAllowed) { + return true; + } + } + return false; +} +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/WindowIdJS.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/WindowIdJS.swift new file mode 100644 index 00000000..8b3a1879 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PluginScriptsJS/WindowIdJS.swift @@ -0,0 +1,19 @@ +// +// WindowIdJS.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +let WINDOW_ID_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_WINDOW_ID_JS_PLUGIN_SCRIPT" + +let WINDOW_ID_VARIABLE_JS_SOURCE = "window._\(JAVASCRIPT_BRIDGE_NAME)_windowId" + +let WINDOW_ID_INITIALIZE_JS_SOURCE = """ +(function() { + \(WINDOW_ID_VARIABLE_JS_SOURCE) = \(PluginScriptsUtil.VAR_PLACEHOLDER_VALUE); + return \(WINDOW_ID_VARIABLE_JS_SOURCE); +})() +""" diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PrintJob/CustomUIPrintPageRenderer.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PrintJob/CustomUIPrintPageRenderer.swift new file mode 100644 index 00000000..214ad7de --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PrintJob/CustomUIPrintPageRenderer.swift @@ -0,0 +1,34 @@ +// +// CustomUIPrintPageRenderer.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/05/22. +// + +import Foundation +// +//public class CustomUIPrintPageRenderer: UIPrintPageRenderer { +// private var _numberOfPages: Int? +// private var forceRenderingQuality: Int? +// +// public init(numberOfPage: Int? = nil, forceRenderingQuality: Int? = nil) { +// super.init() +// self._numberOfPages = numberOfPage +// self.forceRenderingQuality = forceRenderingQuality +// } +// +// open override var numberOfPages: Int { +// get { +// return _numberOfPages ?? super.numberOfPages +// } +// } +// +// @available(iOS 14.5, *) +// open override func currentRenderingQuality(forRequested requestedRenderingQuality: UIPrintRenderingQuality) -> UIPrintRenderingQuality { +// if let forceRenderingQuality = forceRenderingQuality, +// let quality = UIPrintRenderingQuality.init(rawValue: forceRenderingQuality) { +// return quality +// } +// return super.currentRenderingQuality(forRequested: requestedRenderingQuality) +// } +//} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PrintJob/PrintAttributes.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PrintJob/PrintAttributes.swift new file mode 100644 index 00000000..50b3d69c --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PrintJob/PrintAttributes.swift @@ -0,0 +1,98 @@ +// +// PrintAttributes.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/05/22. +// + +import Foundation + +public class PrintAttributes: NSObject { + var orientation: NSPrintInfo.PaperOrientation? + var margins: NSEdgeInsets? + var paperRect: CGRect? + var colorMode: String? + var duplex: Int? + var paperName: String? + var localizedPaperName: String? + var horizontalPagination: UInt? + var verticalPagination: UInt? + var jobDisposition: String? + var printableRect: NSRect? + var isHorizontallyCentered: Bool? + var isVerticallyCentered: Bool? + var isSelectionOnly: Bool? + var scalingFactor: CGFloat? + var jobSavingURL: String? + var detailedErrorReporting: Bool? + var faxNumber: String? + var headerAndFooter: Bool? + var mustCollate: Bool? + var pagesAcross: Int? + var pagesDown: Int? + var time: Int? + + public init(fromPrintJobController: PrintJobController) { + super.init() + if let job = fromPrintJobController.job { + let printInfo = job.printInfo + let printInfoDictionary = printInfo.dictionary() + orientation = printInfo.orientation + margins = NSEdgeInsets(top: printInfo.topMargin, + left: printInfo.leftMargin, + bottom: printInfo.bottomMargin, + right: printInfo.rightMargin) + paperRect = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: printInfo.paperSize) + colorMode = printInfo.printSettings["ColorModel"] as? String + duplex = printInfo.printSettings["com_apple_print_PrintSettings_PMDuplexing"] as? Int + paperName = printInfo.paperName?.rawValue + localizedPaperName = printInfo.localizedPaperName + horizontalPagination = printInfo.horizontalPagination.rawValue + verticalPagination = printInfo.verticalPagination.rawValue + jobDisposition = printInfo.jobDisposition.rawValue + printableRect = printInfo.imageablePageBounds + isHorizontallyCentered = printInfo.isHorizontallyCentered + isVerticallyCentered = printInfo.isVerticallyCentered + isSelectionOnly = printInfo.isSelectionOnly + scalingFactor = printInfo.scalingFactor + jobSavingURL = (printInfoDictionary[NSPrintInfo.AttributeKey.jobSavingURL] as? URL)?.absoluteString + detailedErrorReporting = printInfoDictionary[NSPrintInfo.AttributeKey.detailedErrorReporting] as? Bool + faxNumber = printInfoDictionary[NSPrintInfo.AttributeKey.faxNumber] as? String + headerAndFooter = printInfoDictionary[NSPrintInfo.AttributeKey.headerAndFooter] as? Bool + mustCollate = printInfoDictionary[NSPrintInfo.AttributeKey.mustCollate] as? Bool + pagesAcross = printInfoDictionary[NSPrintInfo.AttributeKey.pagesAcross] as? Int + pagesDown = printInfoDictionary[NSPrintInfo.AttributeKey.pagesDown] as? Int + if let timestamp = (printInfoDictionary[NSPrintInfo.AttributeKey.time] as? Date)?.timeIntervalSince1970 { + time = Int(timestamp) + } + } + } + + public func toMap () -> [String:Any?] { + return [ + "paperRect": paperRect?.toMap(), + "margins": margins?.toMap(), + "orientation": orientation?.rawValue, + "colorMode": colorMode, + "duplex": duplex, + "paperName": paperName, + "localizedPaperName": localizedPaperName, + "horizontalPagination": horizontalPagination, + "verticalPagination": verticalPagination, + "jobDisposition": jobDisposition, + "printableRect": printableRect?.toMap(), + "isHorizontallyCentered": isHorizontallyCentered, + "isVerticallyCentered": isVerticallyCentered, + "isSelectionOnly": isSelectionOnly, + "scalingFactor": scalingFactor, + "jobSavingURL": jobSavingURL, + "detailedErrorReporting": detailedErrorReporting, + "faxNumber": faxNumber, + "headerAndFooter": headerAndFooter, + "mustCollate": mustCollate, + "pagesAcross": pagesAcross, + "pagesDown": pagesDown, + "time": time + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PrintJob/PrintJobChannelDelegate.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PrintJob/PrintJobChannelDelegate.swift new file mode 100644 index 00000000..7514dd4f --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PrintJob/PrintJobChannelDelegate.swift @@ -0,0 +1,60 @@ +// +// PrintJobChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 09/05/22. +// + +import Foundation +import FlutterMacOS + +public class PrintJobChannelDelegate: ChannelDelegate { + private weak var printJobController: PrintJobController? + + public init(printJobController: PrintJobController, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.printJobController = printJobController + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "getInfo": + if let printJobController = printJobController { + result(printJobController.getInfo()?.toMap()) + } else { + result(false) + } + break + case "dispose": + if let printJobController = printJobController { + printJobController.dispose() + result(true) + } else { + result(false) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onComplete(completed: Bool, error: Error?) { + let arguments: [String: Any?] = [ + "completed": completed, + "error": error?.localizedDescription + ] + channel?.invokeMethod("onComplete", arguments: arguments) + } + + public override func dispose() { + super.dispose() + printJobController = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PrintJob/PrintJobController.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PrintJob/PrintJobController.swift new file mode 100644 index 00000000..9b76a408 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PrintJob/PrintJobController.swift @@ -0,0 +1,93 @@ +// +// PrintJob.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 09/05/22. +// + +import Foundation +import FlutterMacOS + +public enum PrintJobState: Int { + case created = 1 + case started = 3 + case completed = 5 + case canceled = 7 +} + +public class PrintJobController: NSObject, Disposable { + static let METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_printjobcontroller_" + var id: String + var plugin: InAppWebViewFlutterPlugin? + var job: NSPrintOperation? + var settings: PrintJobSettings? + var channelDelegate: PrintJobChannelDelegate? + var state = PrintJobState.created + var creationTime = Int64(Date().timeIntervalSince1970 * 1000) + private var completionHandler: PrintJobController.CompletionHandler? + + public typealias CompletionHandler = (_ printOperation: NSPrintOperation, + _ success: Bool, + _ contextInfo: UnsafeMutableRawPointer?) -> Void + + public init(plugin: InAppWebViewFlutterPlugin, id: String, job: NSPrintOperation? = nil, settings: PrintJobSettings? = nil) { + self.id = id + self.plugin = plugin + super.init() + self.job = job + self.settings = settings + if let registrar = plugin.registrar { + let channel = FlutterMethodChannel(name: PrintJobController.METHOD_CHANNEL_NAME_PREFIX + id, + binaryMessenger: registrar.messenger) + self.channelDelegate = PrintJobChannelDelegate(printJobController: self, channel: channel) + } + } + + public func present(parentWindow: NSWindow? = nil, completionHandler: PrintJobController.CompletionHandler? = nil) { + guard let job = job else { + return + } + state = .started + self.completionHandler = completionHandler + if let mainWindow = parentWindow ?? NSApplication.shared.mainWindow { + job.runModal(for: mainWindow, delegate: self, didRun: #selector(printOperationDidRun), contextInfo: nil) + } + } + + @objc func printOperationDidRun(printOperation: NSPrintOperation, + success: Bool, + contextInfo: UnsafeMutableRawPointer?) { + state = success ? .completed : .canceled + channelDelegate?.onComplete(completed: success, error: nil) + if let completionHandler = completionHandler { + completionHandler(printOperation, success, contextInfo) + self.completionHandler = nil + } + } + + public func getInfo() -> PrintJobInfo? { + guard let _ = job else { + return nil + } + + return PrintJobInfo.init(fromPrintJobController: self) + } + + public func disposeNoDismiss() { + channelDelegate?.dispose() + channelDelegate = nil + completionHandler = nil + job = nil + plugin?.printJobManager?.jobs[id] = nil + plugin = nil + } + + public func dispose() { + channelDelegate?.dispose() + channelDelegate = nil + completionHandler = nil + job = nil + plugin?.printJobManager?.jobs[id] = nil + plugin = nil + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PrintJob/PrintJobInfo.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PrintJob/PrintJobInfo.swift new file mode 100644 index 00000000..eac04ac6 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PrintJob/PrintJobInfo.swift @@ -0,0 +1,80 @@ +// +// PrintJobInfo.swift +// flutter_downloader +// +// Created by Lorenzo Pichilli on 10/05/22. +// + +import Foundation + +public class PrintJobInfo: NSObject { + var state: PrintJobState + var attributes: PrintAttributes + var creationTime: Int64 + var numberOfPages: Int? + var copies: Int? + var label: String? + var printer: NSPrinter? + var pageOrder: Int? + var preferredRenderingQuality: Int? + var showsProgressPanel: Bool? + var showsPrintPanel: Bool? + var canSpawnSeparateThread: Bool? + var isCopyingOperation: Bool? + var currentPage: Int? + var firstPage: Int? + var lastPage: Int? + + public init(fromPrintJobController: PrintJobController) { + state = fromPrintJobController.state + creationTime = fromPrintJobController.creationTime + attributes = PrintAttributes.init(fromPrintJobController: fromPrintJobController) + super.init() + if let job = fromPrintJobController.job { + let printInfo = job.printInfo + let printInfoDictionary = printInfo.dictionary() + printer = printInfo.printer + copies = printInfo.printSettings["com_apple_print_PrintSettings_PMCopies"] as? Int + label = job.jobTitle + firstPage = printInfoDictionary[NSPrintInfo.AttributeKey.firstPage] as? Int + lastPage = printInfoDictionary[NSPrintInfo.AttributeKey.lastPage] as? Int + if let firstPage = firstPage, let lastPage = lastPage { + numberOfPages = lastPage - firstPage + 1 + } + if numberOfPages == nil { + numberOfPages = printInfo.printSettings["com_apple_print_PrintSettings_PMLastPage"] as? Int + if numberOfPages == nil || numberOfPages! > job.pageRange.length { + numberOfPages = job.pageRange.length + } + } + pageOrder = job.pageOrder.rawValue + preferredRenderingQuality = job.preferredRenderingQuality.rawValue + showsProgressPanel = job.showsProgressPanel + showsPrintPanel = job.showsPrintPanel + canSpawnSeparateThread = job.canSpawnSeparateThread + isCopyingOperation = job.isCopyingOperation + currentPage = job.currentPage + } + } + + public func toMap () -> [String:Any?] { + return [ + "state": state.rawValue, + "attributes": attributes.toMap(), + "numberOfPages": numberOfPages, + "copies": copies, + "creationTime": creationTime, + "label": label, + "printer": printer?.toMap(), + "pageOrder": pageOrder, + "preferredRenderingQuality": preferredRenderingQuality, + "showsProgressPanel": showsProgressPanel, + "showsPrintPanel": showsPrintPanel, + "canSpawnSeparateThread": canSpawnSeparateThread, + "isCopyingOperation": isCopyingOperation, + "currentPage": currentPage, + "firstPage": firstPage, + "lastPage": lastPage + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PrintJob/PrintJobManager.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PrintJob/PrintJobManager.swift new file mode 100644 index 00000000..cf86b1e4 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PrintJob/PrintJobManager.swift @@ -0,0 +1,31 @@ +// +// PrintJobManager.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 09/05/22. +// + +import Foundation + +public class PrintJobManager: NSObject, Disposable { + var plugin: InAppWebViewFlutterPlugin? + var jobs: [String: PrintJobController?] = [:] + + public init(plugin: InAppWebViewFlutterPlugin?) { + super.init() + self.plugin = plugin + } + + public func dispose() { + let jobValues = jobs.values + jobValues.forEach { (job: PrintJobController?) in + job?.dispose() + } + jobs.removeAll() + plugin = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PrintJob/PrintJobSettings.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PrintJob/PrintJobSettings.swift new file mode 100644 index 00000000..a3fe36b4 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/PrintJob/PrintJobSettings.swift @@ -0,0 +1,210 @@ +// +// PrintJobSettings.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 09/05/22. +// + +import Foundation + +@objcMembers +public class PrintJobSettings: ISettings { + + public var handledByClient = false + public var jobName: String? + public var _orientation: NSNumber? + public var orientation: Int? { + get { + return _orientation?.intValue + } + set { + if let newValue = newValue { + _orientation = NSNumber.init(value: newValue) + } else { + _orientation = nil + } + } + } + public var _numberOfPages: NSNumber? + public var numberOfPages: Int64? { + get { + return _numberOfPages?.int64Value + } + set { + if let newValue = newValue { + _numberOfPages = NSNumber.init(value: newValue) + } else { + _numberOfPages = nil + } + } + } + public var margins: NSEdgeInsets? + public var colorMode: String? + public var showsNumberOfCopies = true + public var showsPaperOrientation = true + public var showsPaperSize = true + public var showsScaling = true + public var showsPageRange = true + public var showsPageSetupAccessory = true + public var showsPreview = true + public var showsPrintSelection = true + public var showsPrintPanel = true + public var showsProgressPanel = true + public var _scalingFactor: NSNumber? + public var scalingFactor: Double? { + get { + return _scalingFactor?.doubleValue + } + set { + if let newValue = newValue { + _scalingFactor = NSNumber.init(value: newValue) + } else { + _scalingFactor = nil + } + } + } + public var jobDisposition: String? + public var jobSavingURL: String? + public var paperName: String? + public var _horizontalPagination: NSNumber? + public var horizontalPagination: UInt? { + get { + return _horizontalPagination?.uintValue + } + set { + if let newValue = newValue { + _horizontalPagination = NSNumber.init(value: newValue) + } else { + _horizontalPagination = nil + } + } + } + public var _verticalPagination: NSNumber? + public var verticalPagination: UInt? { + get { + return _verticalPagination?.uintValue + } + set { + if let newValue = newValue { + _verticalPagination = NSNumber.init(value: newValue) + } else { + _verticalPagination = nil + } + } + } + public var isHorizontallyCentered = true + public var isVerticallyCentered = true + public var _pageOrder: NSNumber? + public var pageOrder: Int? { + get { + return _pageOrder?.intValue + } + set { + if let newValue = newValue { + _pageOrder = NSNumber.init(value: newValue) + } else { + _pageOrder = nil + } + } + } + public var canSpawnSeparateThread = true + public var copies = 1 + public var _firstPage: NSNumber? + public var firstPage: Int64? { + get { + return _firstPage?.int64Value + } + set { + if let newValue = newValue { + _firstPage = NSNumber.init(value: newValue) + } else { + _firstPage = nil + } + } + } + public var _lastPage: NSNumber? + public var lastPage: Int64? { + get { + return _lastPage?.int64Value + } + set { + if let newValue = newValue { + _lastPage = NSNumber.init(value: newValue) + } else { + _lastPage = nil + } + } + } + public var detailedErrorReporting = false + public var faxNumber: String? + public var headerAndFooter = true + public var _mustCollate: NSNumber? + public var mustCollate: Bool? { + get { + return _mustCollate?.boolValue + } + set { + if let newValue = newValue { + _mustCollate = NSNumber.init(value: newValue) + } else { + _mustCollate = nil + } + } + } + public var _pagesAcross: NSNumber? + public var pagesAcross: Int64? { + get { + return _pagesAcross?.int64Value + } + set { + if let newValue = newValue { + _pagesAcross = NSNumber.init(value: newValue) + } else { + _pagesAcross = nil + } + } + } + public var _pagesDown: NSNumber? + public var pagesDown: Int64? { + get { + return _pagesDown?.int64Value + } + set { + if let newValue = newValue { + _pagesDown = NSNumber.init(value: newValue) + } else { + _pagesDown = nil + } + } + } + public var _time: NSNumber? + public var time: Int64? { + get { + return _time?.int64Value + } + set { + if let newValue = newValue { + _time = NSNumber.init(value: newValue) + } else { + _time = nil + } + } + } + + override init(){ + super.init() + } + + override func parse(settings: [String: Any?]) -> PrintJobSettings { + let _ = super.parse(settings: settings) + if let marginsMap = settings["margins"] as? [String : Double] { + margins = NSEdgeInsets.fromMap(map: marginsMap) + } + return self + } + + override func getRealSettings(obj: PrintJobController?) -> [String: Any?] { + var realOptions: [String: Any?] = toMap() + return realOptions + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/BaseCallbackResult.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/BaseCallbackResult.swift new file mode 100644 index 00000000..aa79e499 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/BaseCallbackResult.swift @@ -0,0 +1,32 @@ +// +// BaseCallbackResult.swift +// shared-apple +// +// Created by Lorenzo Pichilli on 17/10/22. +// + +import Foundation + +public class BaseCallbackResult: CallbackResult { + + override init() { + super.init() + + self.success = { [weak self] (obj: Any?) in + let result: T? = self?.decodeResult(obj) + var shouldRunDefaultBehaviour = false + if let result = result { + shouldRunDefaultBehaviour = self?.nonNullSuccess(result) ?? shouldRunDefaultBehaviour + } else { + shouldRunDefaultBehaviour = self?.nullSuccess() ?? shouldRunDefaultBehaviour + } + if shouldRunDefaultBehaviour { + self?.defaultBehaviour(result) + } + } + + self.notImplemented = { [weak self] in + self?.defaultBehaviour(nil) + } + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/CGRect.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/CGRect.swift new file mode 100644 index 00000000..61cb7627 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/CGRect.swift @@ -0,0 +1,23 @@ +// +// CGRect.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/05/22. +// + +import Foundation + +extension CGRect { + public static func fromMap(map: [String: Double]) -> CGRect { + return CGRect(x: map["x"]!, y: map["y"]!, width: map["width"]!, height: map["height"]!) + } + + public func toMap () -> [String:Any?] { + return [ + "x": minX, + "y": minY, + "width": width, + "height": height + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/CallbackResult.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/CallbackResult.swift new file mode 100644 index 00000000..083d6880 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/CallbackResult.swift @@ -0,0 +1,18 @@ +// +// CallbackResult.swift +// shared-apple +// +// Created by Lorenzo Pichilli on 17/10/22. +// + +import Foundation + +public class CallbackResult: MethodChannelResult { + public var notImplemented: () -> Void = {} + public var success: (Any?) -> Void = {_ in } + public var error: (String, String?, Any?) -> Void = {_,_,_ in } + public var nonNullSuccess: (T) -> Bool = {_ in true} + public var nullSuccess: () -> Bool = {true} + public var defaultBehaviour: (T?) -> Void = {_ in } + public var decodeResult: (Any?) -> T? = {_ in nil} +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/ChannelDelegate.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/ChannelDelegate.swift new file mode 100644 index 00000000..e56474f8 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/ChannelDelegate.swift @@ -0,0 +1,28 @@ +// +// ChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 04/05/22. +// + +import Foundation +import FlutterMacOS + +public class ChannelDelegate: FlutterMethodCallDelegate, Disposable { + var channel: FlutterMethodChannel? + + public init(channel: FlutterMethodChannel) { + super.init() + self.channel = channel + self.channel?.setMethodCallHandler(handle) + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + + } + + public func dispose() { + channel?.setMethodCallHandler(nil) + channel = nil + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/ClientCertChallenge.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/ClientCertChallenge.swift new file mode 100644 index 00000000..f2381ee4 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/ClientCertChallenge.swift @@ -0,0 +1,22 @@ +// +// ClientCertChallenge.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/02/21. +// + +import Foundation + +public class ClientCertChallenge: NSObject { + var protectionSpace: URLProtectionSpace! + + public init(fromChallenge: URLAuthenticationChallenge) { + protectionSpace = fromChallenge.protectionSpace + } + + public func toMap () -> [String:Any?] { + return [ + "protectionSpace": protectionSpace.toMap(), + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/ClientCertResponse.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/ClientCertResponse.swift new file mode 100644 index 00000000..6273db74 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/ClientCertResponse.swift @@ -0,0 +1,33 @@ +// +// ClientCertResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class ClientCertResponse: NSObject { + var certificatePath: String + var certificatePassword: String? + var keyStoreType: String? + var action: Int? + + public init(certificatePath: String, certificatePassword: String? = nil, keyStoreType: String? = nil, action: Int? = nil) { + self.certificatePath = certificatePath + self.certificatePassword = certificatePassword + self.keyStoreType = keyStoreType + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> ClientCertResponse? { + guard let map = map else { + return nil + } + let certificatePath = map["certificatePath"] as! String + let certificatePassword = map["certificatePassword"] as? String + let keyStoreType = map["keyStoreType"] as? String + let action = map["action"] as? Int + return ClientCertResponse(certificatePath: certificatePath, certificatePassword: certificatePassword, keyStoreType: keyStoreType, action: action) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/CreateWindowAction.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/CreateWindowAction.swift new file mode 100644 index 00000000..f3b4681f --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/CreateWindowAction.swift @@ -0,0 +1,31 @@ +// +// CreateWindowAction.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation +import WebKit + +public class CreateWindowAction: NSObject { + var navigationAction: WKNavigationAction + var windowId: Int64 + var windowFeatures: WKWindowFeatures + var isDialog: Bool? + + public init(navigationAction: WKNavigationAction, windowId: Int64, windowFeatures: WKWindowFeatures, isDialog: Bool? = nil) { + self.navigationAction = navigationAction + self.windowId = windowId + self.windowFeatures = windowFeatures + self.isDialog = isDialog + } + + public func toMap () -> [String:Any?] { + var map = navigationAction.toMap() + map["windowId"] = windowId + map["windowFeatures"] = windowFeatures.toMap() + map["isDialog"] = isDialog + return map + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/CustomSchemeResponse.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/CustomSchemeResponse.swift new file mode 100644 index 00000000..53836c94 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/CustomSchemeResponse.swift @@ -0,0 +1,31 @@ +// +// CustomSchemeResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation +import FlutterMacOS + +public class CustomSchemeResponse: NSObject { + var data: Data + var contentType: String + var contentEncoding: String + + public init(data: Data, contentType: String, contentEncoding: String) { + self.data = data + self.contentType = contentType + self.contentEncoding = contentEncoding + } + + public static func fromMap(map: [String:Any?]?) -> CustomSchemeResponse? { + guard let map = map else { + return nil + } + let data = map["data"] as! FlutterStandardTypedData + let contentType = map["contentType"] as! String + let contentEncoding = map["contentEncoding"] as! String + return CustomSchemeResponse(data: data.data, contentType: contentType, contentEncoding: contentEncoding) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/Disposable.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/Disposable.swift new file mode 100644 index 00000000..0a34a942 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/Disposable.swift @@ -0,0 +1,12 @@ +// +// Disposable.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 04/05/22. +// + +import Foundation + +public protocol Disposable { + func dispose() -> Void +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/DownloadStartRequest.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/DownloadStartRequest.swift new file mode 100644 index 00000000..808f9453 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/DownloadStartRequest.swift @@ -0,0 +1,42 @@ +// +// DownloadStartRequest.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 17/04/22. +// + +import Foundation + +public class DownloadStartRequest: NSObject { + var url: String + var userAgent: String? + var contentDisposition: String? + var mimeType: String? + var contentLength: Int64 + var suggestedFilename: String? + var textEncodingName: String? + + public init(url: String, userAgent: String?, contentDisposition: String?, + mimeType: String?, contentLength: Int64, + suggestedFilename: String?, textEncodingName: String?) { + self.url = url + self.userAgent = userAgent + self.contentDisposition = contentDisposition + self.mimeType = mimeType + self.contentLength = contentLength + self.suggestedFilename = suggestedFilename + self.textEncodingName = textEncodingName + } + + public func toMap () -> [String:Any?] { + return [ + "url": url, + "userAgent": userAgent, + "contentDisposition": contentDisposition, + "mimeType": mimeType, + "contentLength": contentLength, + "suggestedFilename": suggestedFilename, + "textEncodingName": textEncodingName + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/FindSession.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/FindSession.swift new file mode 100644 index 00000000..0c247004 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/FindSession.swift @@ -0,0 +1,28 @@ +// +// UIFindSession.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/10/22. +// + +import Foundation + +public class FindSession: NSObject { + var resultCount: Int + var highlightedResultIndex: Int + var searchResultDisplayStyle: Int + + public init(resultCount: Int, highlightedResultIndex: Int, searchResultDisplayStyle: Int) { + self.resultCount = resultCount + self.highlightedResultIndex = highlightedResultIndex + self.searchResultDisplayStyle = searchResultDisplayStyle + } + + public func toMap () -> [String:Any?] { + return [ + "resultCount": resultCount, + "highlightedResultIndex": highlightedResultIndex, + "searchResultDisplayStyle": searchResultDisplayStyle + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/FlutterMethodCallDelegate.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/FlutterMethodCallDelegate.swift new file mode 100755 index 00000000..0454d2f7 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/FlutterMethodCallDelegate.swift @@ -0,0 +1,19 @@ +// +// FlutterMethodCallDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/12/2019. +// + +import Foundation +import FlutterMacOS + +public class FlutterMethodCallDelegate: NSObject { + public override init() { + super.init() + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/FlutterMethodChannel.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/FlutterMethodChannel.swift new file mode 100644 index 00000000..e9055b2e --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/FlutterMethodChannel.swift @@ -0,0 +1,26 @@ +// +// FlutterMethodChannel.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 06/05/22. +// + +import Foundation +import FlutterMacOS + +extension FlutterMethodChannel { + public func invokeMethod(_ method: String, arguments: Any, callback: MethodChannelResult) { + invokeMethod(method, arguments: arguments) {(result) -> Void in + if result is FlutterError { + let error = result as! FlutterError + callback.error(error.code, error.message, error.details) + } + else if (result as? NSObject) == FlutterMethodNotImplemented { + callback.notImplemented() + } + else { + callback.success(result) + } + } + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/HitTestResult.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/HitTestResult.swift new file mode 100644 index 00000000..53307789 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/HitTestResult.swift @@ -0,0 +1,44 @@ +// +// HitTestResult.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +public enum HitTestResultType: Int { + case unknownType = 0 + case phoneType = 2 + case geoType = 3 + case emailType = 4 + case imageType = 5 + case srcAnchorType = 7 + case srcImageAnchorType = 8 + case editTextType = 9 +} + +public class HitTestResult: NSObject { + var type: HitTestResultType + var extra: String? + + public init(type: HitTestResultType, extra: String?) { + self.type = type + self.extra = extra + } + + public static func fromMap(map: [String:Any?]?) -> HitTestResult? { + guard let map = map else { + return nil + } + let type = HitTestResultType.init(rawValue: map["type"] as? Int ?? HitTestResultType.unknownType.rawValue) ?? HitTestResultType.unknownType + return HitTestResult(type: type, extra: map["extra"] as? String) + } + + public func toMap () -> [String:Any?] { + return [ + "type": type.rawValue, + "extra": extra, + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/HttpAuthResponse.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/HttpAuthResponse.swift new file mode 100644 index 00000000..f77d3ab1 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/HttpAuthResponse.swift @@ -0,0 +1,33 @@ +// +// HttpAuthResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class HttpAuthResponse: NSObject { + var username: String + var password: String + var permanentPersistence: Bool + var action: Int? + + public init(username: String, password: String, permanentPersistence: Bool, action: Int? = nil) { + self.username = username + self.password = password + self.permanentPersistence = permanentPersistence + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> HttpAuthResponse? { + guard let map = map else { + return nil + } + let username = map["username"] as! String + let password = map["password"] as! String + let permanentPersistence = map["permanentPersistence"] as! Bool + let action = map["action"] as? Int + return HttpAuthResponse(username: username, password: password, permanentPersistence: permanentPersistence, action: action) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/HttpAuthenticationChallenge.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/HttpAuthenticationChallenge.swift new file mode 100644 index 00000000..b22a0092 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/HttpAuthenticationChallenge.swift @@ -0,0 +1,34 @@ +// +// HttpAuthenticationChallenge.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/02/21. +// + +import Foundation + +public class HttpAuthenticationChallenge: NSObject { + var protectionSpace: URLProtectionSpace! + var previousFailureCount: Int = 0 + var failureResponse: URLResponse? + var error: Error? + var proposedCredential: URLCredential? + + public init(fromChallenge: URLAuthenticationChallenge) { + protectionSpace = fromChallenge.protectionSpace + previousFailureCount = fromChallenge.previousFailureCount + failureResponse = fromChallenge.failureResponse + error = fromChallenge.error + proposedCredential = fromChallenge.proposedCredential + } + + public func toMap () -> [String:Any?] { + return [ + "protectionSpace": protectionSpace.toMap(), + "previousFailureCount": previousFailureCount, + "failureResponse": failureResponse?.toMap(), + "error": error?.localizedDescription, + "proposedCredential": proposedCredential?.toMap() + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/InAppBrowserMenuItem.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/InAppBrowserMenuItem.swift new file mode 100644 index 00000000..beeee486 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/InAppBrowserMenuItem.swift @@ -0,0 +1,49 @@ +// +// InAppBrowserMenuItem.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 23/05/23. +// + +import Foundation +import FlutterMacOS + +public class InAppBrowserMenuItem: NSObject { + var id: Int64 + var title: String + var order: Int64? + var icon: NSImage? + var iconColor: NSColor? + var showAsAction = false + + public init(id: Int64, title: String, order: Int64?, icon: NSImage?, iconColor: NSColor?, showAsAction: Bool) { + self.id = id + self.title = title + self.order = order + self.icon = icon + self.iconColor = iconColor + self.showAsAction = showAsAction + if let icon = icon, let iconColor = iconColor { + self.icon = icon.tint(color: iconColor) + } + } + + public static func fromMap(map: [String:Any?]?) -> InAppBrowserMenuItem? { + guard let map = map else { + return nil + } + let id = map["id"] as! Int64 + let title = map["title"] as! String + let order = map["order"] as? Int64 + var icon = NSImage.fromMap(map: map["icon"] as? [String : Any?]) + if let data = map["icon"] as? FlutterStandardTypedData { + icon = NSImage(data: data.data) + } + var iconColor: NSColor? = nil + if let hexString = map["iconColor"] as? String { + iconColor = NSColor(hexString: hexString) + } + let showAsAction = map["showAsAction"] as! Bool + return InAppBrowserMenuItem(id: id, title: title, order: order, icon: icon, iconColor: iconColor, showAsAction: showAsAction) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/InAppBrowserWindowType.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/InAppBrowserWindowType.swift new file mode 100644 index 00000000..9650e2b9 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/InAppBrowserWindowType.swift @@ -0,0 +1,14 @@ +// +// InAppBrowserWindowType.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/10/22. +// + +import Foundation + +public enum InAppBrowserWindowType: String { + case window = "WINDOW" + case child = "CHILD" + case tabbed = "TABBED" +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/JsAlertResponse.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/JsAlertResponse.swift new file mode 100644 index 00000000..3e6b3e78 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/JsAlertResponse.swift @@ -0,0 +1,33 @@ +// +// JsAlertResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 06/05/22. +// + +import Foundation + +public class JsAlertResponse: NSObject { + var message: String + var confirmButtonTitle: String + var handledByClient: Bool + var action: Int? + + public init(message: String, confirmButtonTitle: String, handledByClient: Bool, action: Int? = nil) { + self.message = message + self.confirmButtonTitle = confirmButtonTitle + self.handledByClient = handledByClient + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> JsAlertResponse? { + guard let map = map else { + return nil + } + let message = map["message"] as! String + let confirmButtonTitle = map["confirmButtonTitle"] as! String + let handledByClient = map["handledByClient"] as! Bool + let action = map["action"] as? Int + return JsAlertResponse(message: message, confirmButtonTitle: confirmButtonTitle, handledByClient: handledByClient, action: action) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/JsConfirmResponse.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/JsConfirmResponse.swift new file mode 100644 index 00000000..76bfbe20 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/JsConfirmResponse.swift @@ -0,0 +1,36 @@ +// +// JsConfirmResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class JsConfirmResponse: NSObject { + var message: String + var confirmButtonTitle: String + var cancelButtonTitle: String + var handledByClient: Bool + var action: Int? + + public init(message: String, confirmButtonTitle: String, cancelButtonTitle: String, handledByClient: Bool, action: Int? = nil) { + self.message = message + self.confirmButtonTitle = confirmButtonTitle + self.cancelButtonTitle = cancelButtonTitle + self.handledByClient = handledByClient + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> JsConfirmResponse? { + guard let map = map else { + return nil + } + let message = map["message"] as! String + let confirmButtonTitle = map["confirmButtonTitle"] as! String + let cancelButtonTitle = map["cancelButtonTitle"] as! String + let handledByClient = map["handledByClient"] as! Bool + let action = map["action"] as? Int + return JsConfirmResponse(message: message, confirmButtonTitle: confirmButtonTitle, cancelButtonTitle: cancelButtonTitle, handledByClient: handledByClient, action: action) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/JsPromptResponse.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/JsPromptResponse.swift new file mode 100644 index 00000000..5c40b492 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/JsPromptResponse.swift @@ -0,0 +1,43 @@ +// +// JsPromptResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class JsPromptResponse: NSObject { + var message: String + var defaultValue: String + var confirmButtonTitle: String + var cancelButtonTitle: String + var handledByClient: Bool + var value: String? + var action: Int? + + public init(message: String, defaultValue: String, confirmButtonTitle: String, cancelButtonTitle: String, handledByClient: Bool, value: String? = nil, action: Int? = nil) { + self.message = message + self.defaultValue = defaultValue + self.confirmButtonTitle = confirmButtonTitle + self.cancelButtonTitle = cancelButtonTitle + self.handledByClient = handledByClient + self.value = value + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> JsPromptResponse? { + guard let map = map else { + return nil + } + let message = map["message"] as! String + let defaultValue = map["defaultValue"] as! String + let confirmButtonTitle = map["confirmButtonTitle"] as! String + let cancelButtonTitle = map["cancelButtonTitle"] as! String + let handledByClient = map["handledByClient"] as! Bool + let value = map["value"] as? String + let action = map["action"] as? Int + return JsPromptResponse(message: message, defaultValue: defaultValue, confirmButtonTitle: confirmButtonTitle, cancelButtonTitle: cancelButtonTitle, + handledByClient: handledByClient, value: value, action: action) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/MethodChannelResult.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/MethodChannelResult.swift new file mode 100644 index 00000000..cba7eb24 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/MethodChannelResult.swift @@ -0,0 +1,14 @@ +// +// MethodChannelResult.swift +// shared-apple +// +// Created by Lorenzo Pichilli on 17/10/22. +// + +import Foundation + +public protocol MethodChannelResult { + var success: (_ obj: Any?) -> Void { get set } + var error: (_ code: String, _ message: String?, _ details: Any?) -> Void { get set } + var notImplemented: () -> Void { get set } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/NSAttributedString.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/NSAttributedString.swift new file mode 100644 index 00000000..64380da6 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/NSAttributedString.swift @@ -0,0 +1,63 @@ +// +// NSAttributedString.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 05/03/21. +// + +import Foundation + +extension NSAttributedString { + public static func fromMap(map: [String:Any?]?) -> NSAttributedString? { + guard let map = map, let string = map["string"] as? String else { + return nil + } + + var attributes: [NSAttributedString.Key : Any] = [:] + + if let backgroundColor = map["backgroundColor"] as? String { + attributes[.backgroundColor] = NSColor(hexString: backgroundColor) + } + if let baselineOffset = map["baselineOffset"] as? Double { + attributes[.baselineOffset] = baselineOffset + } + if let expansion = map["expansion"] as? Double { + attributes[.expansion] = expansion + } + if let foregroundColor = map["foregroundColor"] as? String { + attributes[.foregroundColor] = NSColor(hexString: foregroundColor) + } + if let kern = map["kern"] as? Double { + attributes[.kern] = kern + } + if let ligature = map["ligature"] as? Int64 { + attributes[.ligature] = ligature + } + if let obliqueness = map["obliqueness"] as? Double { + attributes[.obliqueness] = obliqueness + } + if let strikethroughColor = map["strikethroughColor"] as? String { + attributes[.strikethroughColor] = NSColor(hexString: strikethroughColor) + } + if let strikethroughStyle = map["strikethroughStyle"] as? Int64 { + attributes[.strikethroughStyle] = strikethroughStyle + } + if let strokeColor = map["strokeColor"] as? String { + attributes[.strokeColor] = NSColor(hexString: strokeColor) + } + if let strokeWidth = map["strokeWidth"] as? Double { + attributes[.strokeWidth] = strokeWidth + } + if let textEffect = map["textEffect"] as? String { + attributes[.textEffect] = textEffect + } + if let underlineColor = map["underlineColor"] as? String { + attributes[.underlineColor] = NSColor(hexString: underlineColor) + } + if let underlineStyle = map["underlineStyle"] as? Int64 { + attributes[.underlineStyle] = underlineStyle + } + + return NSAttributedString(string: string, attributes: attributes) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/NSColor.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/NSColor.swift new file mode 100644 index 00000000..075e7059 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/NSColor.swift @@ -0,0 +1,56 @@ +// +// NSColor.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension NSColor { + convenience init(hexString: String) { + let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) + var int = UInt64() + Scanner(string: hex).scanHexInt64(&int) + let a, r, g, b: UInt64 + switch hex.count { + case 3: // RGB (12-bit) + (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) + case 6: // RGB (24-bit) + (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) + case 8: // ARGB (32-bit) + (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) + default: + (a, r, g, b) = (255, 0, 0, 0) + } + self.init(red: CGFloat(r) / 255, green: CGFloat(g) / 255, blue: CGFloat(b) / 255, alpha: CGFloat(a) / 255) + } + + var hexString: String? { + guard let rgbColor = usingColorSpace(.sRGB) else { + return "#FFFFFF" + } + let red: CGFloat = rgbColor.redComponent + let green: CGFloat = rgbColor.greenComponent + let blue: CGFloat = rgbColor.blueComponent + let alpha: CGFloat = rgbColor.alphaComponent + + if alpha == 1.0 { + return String( + format: "#%02lX%02lX%02lX", + Int(red * 0xFF), + Int(green * 0xFF), + Int(blue * 0xFF) + ) + } + else { + return String( + format: "#%02lX%02lX%02lX%02lX", + Int(red * 0xFF), + Int(green * 0xFF), + Int(blue * 0xFF), + Int(alpha * 0xFF) + ) + } + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/NSEdgeInsets.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/NSEdgeInsets.swift new file mode 100644 index 00000000..68aadcd2 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/NSEdgeInsets.swift @@ -0,0 +1,23 @@ +// +// UIEdgeInsets.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 11/05/22. +// + +import Foundation + +extension NSEdgeInsets { + public static func fromMap(map: [String: Double]) -> NSEdgeInsets { + return NSEdgeInsets.init(top: map["top"]!, left: map["left"]!, bottom: map["bottom"]!, right: map["right"]!) + } + + public func toMap () -> [String:Any?] { + return [ + "top": top, + "right": self.right, + "bottom": bottom, + "left": self.left + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/NSImage.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/NSImage.swift new file mode 100644 index 00000000..096a4cea --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/NSImage.swift @@ -0,0 +1,36 @@ +// +// NSImage.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 23/05/23. +// + +import Foundation +import FlutterMacOS + +extension NSImage { + public static func fromMap(map: [String:Any?]?) -> NSImage? { + guard let map = map else { + return nil + } + if let name = map["name"] as? String { + return NSImage(named: name) + } + if #available(macOS 11.0, *), let systemName = map["systemName"] as? String { + return NSImage(systemSymbolName: systemName, accessibilityDescription: nil) + } + if let data = map["data"] as? FlutterStandardTypedData { + return NSImage(data: data.data) + } + return nil + } + + func tint(color: NSColor) -> NSImage { + return NSImage(size: size, flipped: false) { (rect) -> Bool in + color.set() + rect.fill() + self.draw(in: rect, from: NSRect(origin: .zero, size: self.size), operation: .destinationIn, fraction: 1.0) + return true + } + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/NSPrinter.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/NSPrinter.swift new file mode 100644 index 00000000..58717866 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/NSPrinter.swift @@ -0,0 +1,18 @@ +// +// NSPrinter.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 17/10/22. +// + +import Foundation + +extension NSPrinter { + public func toMap () -> [String:Any?] { + return [ + "type": type.rawValue, + "languageLevel": languageLevel, + "name": name + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/PermissionRequest.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/PermissionRequest.swift new file mode 100644 index 00000000..a286a3f3 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/PermissionRequest.swift @@ -0,0 +1,29 @@ +// +// PermissionRequest.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 21/04/22. +// + +import Foundation +import WebKit + +public class PermissionRequest: NSObject { + var origin: String + var resources: [StringOrInt] + var frame: WKFrameInfo + + public init(origin: String, resources: [StringOrInt], frame: WKFrameInfo) { + self.origin = origin + self.resources = resources + self.frame = frame + } + + public func toMap () -> [String:Any?] { + return [ + "origin": origin, + "resources": resources, + "frame": frame.toMap() + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/PermissionResponse.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/PermissionResponse.swift new file mode 100644 index 00000000..171817bb --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/PermissionResponse.swift @@ -0,0 +1,27 @@ +// +// PermissionResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class PermissionResponse: NSObject { + var resources: [Any] + var action: Int? + + public init(resources: [Any], action: Int? = nil) { + self.resources = resources + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> PermissionResponse? { + guard let map = map else { + return nil + } + let resources = map["resources"] as! [Any] + let action = map["action"] as? Int + return PermissionResponse(resources: resources, action: action) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/PluginScript.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/PluginScript.swift new file mode 100644 index 00000000..7218099b --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/PluginScript.swift @@ -0,0 +1,109 @@ +// +// PluginScript.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 17/02/21. +// + +import Foundation +import WebKit + +public class PluginScript: UserScript { + var requiredInAllContentWorlds = false + var messageHandlerNames: [String] = [] + + public override init(source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly) + } + + public init(groupName: String, source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, requiredInAllContentWorlds: Bool = false, messageHandlerNames: [String] = []) { + super.init(groupName: groupName, source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly) + self.requiredInAllContentWorlds = requiredInAllContentWorlds + self.messageHandlerNames = messageHandlerNames + } + + @available(macOS 11.0, *) + public override init(source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, in contentWorld: WKContentWorld) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly, in: contentWorld) + self.contentWorld = contentWorld + } + + @available(macOS 11.0, *) + public init(source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, in contentWorld: WKContentWorld, requiredInAllContentWorlds: Bool = false, messageHandlerNames: [String] = []) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly, in: contentWorld) + self.requiredInAllContentWorlds = requiredInAllContentWorlds + self.messageHandlerNames = messageHandlerNames + } + + @available(macOS 11.0, *) + public init(groupName: String, source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, in contentWorld: WKContentWorld, requiredInAllContentWorlds: Bool = false, messageHandlerNames: [String] = []) { + super.init(groupName: groupName, source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly, in: contentWorld) + self.requiredInAllContentWorlds = requiredInAllContentWorlds + self.messageHandlerNames = messageHandlerNames + } + + public func copyAndSet(groupName: String? = nil, + source: String? = nil, + injectionTime: WKUserScriptInjectionTime? = nil, + forMainFrameOnly: Bool? = nil, + requiredInAllContentWorlds: Bool? = nil, + messageHandlerNames: [String]? = nil) -> PluginScript { + if #available(macOS 11.0, *) { + return PluginScript( + groupName: groupName ?? self.groupName!, + source: source ?? self.source, + injectionTime: injectionTime ?? self.injectionTime, + forMainFrameOnly: forMainFrameOnly ?? self.isForMainFrameOnly, + in: self.contentWorld, + requiredInAllContentWorlds: requiredInAllContentWorlds ?? self.requiredInAllContentWorlds, + messageHandlerNames: messageHandlerNames ?? self.messageHandlerNames + ) + } + return PluginScript( + groupName: groupName ?? self.groupName!, + source: source ?? self.source, + injectionTime: injectionTime ?? self.injectionTime, + forMainFrameOnly: forMainFrameOnly ?? self.isForMainFrameOnly, + requiredInAllContentWorlds: requiredInAllContentWorlds ?? self.requiredInAllContentWorlds, + messageHandlerNames: messageHandlerNames ?? self.messageHandlerNames + ) + } + + @available(macOS 11.0, *) + public func copyAndSet(groupName: String? = nil, + source: String? = nil, + injectionTime: WKUserScriptInjectionTime? = nil, + forMainFrameOnly: Bool? = nil, + contentWorld: WKContentWorld? = nil, + requiredInAllContentWorlds: Bool? = nil, + messageHandlerNames: [String]? = nil) -> PluginScript { + return PluginScript( + groupName: groupName ?? self.groupName!, + source: source ?? self.source, + injectionTime: injectionTime ?? self.injectionTime, + forMainFrameOnly: forMainFrameOnly ?? self.isForMainFrameOnly, + in: contentWorld ?? self.contentWorld, + requiredInAllContentWorlds: requiredInAllContentWorlds ?? self.requiredInAllContentWorlds, + messageHandlerNames: messageHandlerNames ?? self.messageHandlerNames + ) + } + + static func == (lhs: PluginScript, rhs: PluginScript) -> Bool { + if #available(macOS 11.0, *) { + return lhs.groupName == rhs.groupName && + lhs.source == rhs.source && + lhs.injectionTime == rhs.injectionTime && + lhs.isForMainFrameOnly == rhs.isForMainFrameOnly && + lhs.contentWorld == rhs.contentWorld && + lhs.requiredInAllContentWorlds == rhs.requiredInAllContentWorlds && + lhs.messageHandlerNames == rhs.messageHandlerNames + } else { + return lhs.groupName == rhs.groupName && + lhs.source == rhs.source && + lhs.injectionTime == rhs.injectionTime && + lhs.isForMainFrameOnly == rhs.isForMainFrameOnly && + lhs.requiredInAllContentWorlds == rhs.requiredInAllContentWorlds && + lhs.messageHandlerNames == rhs.messageHandlerNames + } + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/SecCertificate.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/SecCertificate.swift new file mode 100644 index 00000000..e7292f22 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/SecCertificate.swift @@ -0,0 +1,15 @@ +// +// SecCertificate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension SecCertificate { + var data: Data { + let serverCertificateCFData = SecCertificateCopyData(self) + return serverCertificateCFData as Data + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/ServerTrustAuthResponse.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/ServerTrustAuthResponse.swift new file mode 100644 index 00000000..2a998057 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/ServerTrustAuthResponse.swift @@ -0,0 +1,24 @@ +// +// ServerTrustAuthResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 07/05/22. +// + +import Foundation + +public class ServerTrustAuthResponse: NSObject { + var action: Int? + + public init(action: Int? = nil) { + self.action = action + } + + public static func fromMap(map: [String:Any?]?) -> ServerTrustAuthResponse? { + guard let map = map else { + return nil + } + let action = map["action"] as? Int + return ServerTrustAuthResponse(action: action) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/ServerTrustChallenge.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/ServerTrustChallenge.swift new file mode 100644 index 00000000..190c831b --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/ServerTrustChallenge.swift @@ -0,0 +1,22 @@ +// +// ServerTrustChallenge.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/02/21. +// + +import Foundation + +public class ServerTrustChallenge: NSObject { + var protectionSpace: URLProtectionSpace! + + public init(fromChallenge: URLAuthenticationChallenge) { + protectionSpace = fromChallenge.protectionSpace + } + + public func toMap () -> [String:Any?] { + return [ + "protectionSpace": protectionSpace.toMap() + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/Size2D.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/Size2D.swift new file mode 100644 index 00000000..de13ffc2 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/Size2D.swift @@ -0,0 +1,35 @@ +// +// Size.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 26/03/21. +// + +import Foundation + +public class Size2D: NSObject { + var width: Double + var height: Double + + public init(width: Double, height: Double) { + self.width = width + self.height = height + } + + public static func fromMap(map: [String:Any?]?) -> Size2D? { + guard let map = map else { + return nil + } + return Size2D( + width: map["width"] as? Double ?? -1.0, + height: map["height"] as? Double ?? -1.0 + ) + } + + public func toMap() -> [String:Any?] { + return [ + "width": width, + "height": height + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/SslCertificate.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/SslCertificate.swift new file mode 100644 index 00000000..6b9d9a17 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/SslCertificate.swift @@ -0,0 +1,30 @@ +// +// SslCertificate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/02/21. +// + +import Foundation + +public class SslCertificate: NSObject { + var x509Certificate: Data + var issuedBy: Any? + var issuedTo: Any? + var validNotAfterDate: Any? + var validNotBeforeDate: Any? + + public init(x509Certificate: Data) { + self.x509Certificate = x509Certificate + } + + public func toMap () -> [String:Any?] { + return [ + "x509Certificate": x509Certificate, + "issuedBy": issuedBy, + "issuedTo": issuedTo, + "validNotAfterDate": validNotAfterDate, + "validNotBeforeDate": validNotBeforeDate + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/SslError.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/SslError.swift new file mode 100644 index 00000000..58e3cf94 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/SslError.swift @@ -0,0 +1,50 @@ +// +// SslError.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 15/02/21. +// + +import Foundation + +public class SslError: NSObject { + var errorType: SecTrustResultType? + var message: String? + + public init(errorType: SecTrustResultType?) { + self.errorType = errorType + + var sslErrorMessage: String? = nil + switch errorType { + case .deny: + sslErrorMessage = "Indicates a user-configured deny; do not proceed." + break + case .fatalTrustFailure: + sslErrorMessage = "Indicates a trust failure which cannot be overridden by the user." + break + case .invalid: + sslErrorMessage = "Indicates an invalid setting or result." + break + case .otherError: + sslErrorMessage = "Indicates a failure other than that of trust evaluation." + break + case .recoverableTrustFailure: + sslErrorMessage = "Indicates a trust policy failure which can be overridden by the user." + break + case .unspecified: + sslErrorMessage = "Indicates the evaluation succeeded and the certificate is implicitly trusted, but user intent was not explicitly specified." + break + default: + sslErrorMessage = nil + } + + self.message = sslErrorMessage + } + + public func toMap () -> [String:Any?] { + return [ + "code": errorType?.rawValue, + "message": message + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/StringOrInt.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/StringOrInt.swift new file mode 100644 index 00000000..f7cd40c7 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/StringOrInt.swift @@ -0,0 +1,13 @@ +// +// StringOrInt.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 21/04/22. +// + +import Foundation + +public protocol StringOrInt { } + +extension Int: StringOrInt { } +extension String: StringOrInt { } diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/URLAuthenticationChallenge.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/URLAuthenticationChallenge.swift new file mode 100644 index 00000000..39b21fce --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/URLAuthenticationChallenge.swift @@ -0,0 +1,20 @@ +// +// URLAuthenticationChallenge.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension URLAuthenticationChallenge { + public func toMap () -> [String:Any?] { + return [ + "protectionSpace": protectionSpace.toMap(), + "previousFailureCount": previousFailureCount, + "failureResponse": failureResponse?.toMap(), + "error": error?.localizedDescription, + "proposedCredential": proposedCredential?.toMap(), + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/URLCredential.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/URLCredential.swift new file mode 100644 index 00000000..469f1e15 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/URLCredential.swift @@ -0,0 +1,26 @@ +// +// URLCredential.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension URLCredential { + public func toMap () -> [String:Any?] { + var x509Certificates: [Data] = [] + // certificates could be nil!!! + if certificates != nil { + for certificate in certificates { + x509Certificates.append((certificate as! SecCertificate).data) + } + } + return [ + "password": password, + "username": user, + "certificates": x509Certificates, + "persistence": persistence.rawValue + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/URLProtectionSpace.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/URLProtectionSpace.swift new file mode 100644 index 00000000..3d6b9022 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/URLProtectionSpace.swift @@ -0,0 +1,63 @@ +// +// URLProtectionSpace.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension URLProtectionSpace { + + var x509Certificate: Data? { + guard let serverTrust = serverTrust else { + return nil + } + + var secResult = SecTrustResultType.invalid + let secTrustEvaluateStatus = SecTrustEvaluate(serverTrust, &secResult); + + if secTrustEvaluateStatus == errSecSuccess, let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) { + return serverCertificate.data + } + return nil + } + + var sslCertificate: SslCertificate? { + var sslCertificate: SslCertificate? = nil + if let x509Certificate = x509Certificate { + sslCertificate = SslCertificate(x509Certificate: x509Certificate) + } + return sslCertificate + } + + var sslError: SslError? { + guard let serverTrust = serverTrust else { + return nil + } + + var secResult = SecTrustResultType.invalid + SecTrustEvaluate(serverTrust, &secResult); + + guard let sslErrorType = secResult != SecTrustResultType.proceed ? secResult : nil else { + return nil + } + + return SslError(errorType: sslErrorType) + } + + public func toMap () -> [String:Any?] { + return [ + "host": host, + "protocol": self.protocol, + "realm": realm, + "port": port, + "sslCertificate": sslCertificate?.toMap(), + "sslError": sslError?.toMap(), + "authenticationMethod": authenticationMethod, + "distinguishedNames": distinguishedNames, + "receivesCredentialSecurely": receivesCredentialSecurely, + "proxyType": proxyType + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/URLRequest.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/URLRequest.swift new file mode 100644 index 00000000..54a251d2 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/URLRequest.swift @@ -0,0 +1,99 @@ +// +// URLRequest.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import FlutterMacOS + +extension URLRequest { + public init(fromPluginMap: [String:Any?]) { + if let urlString = fromPluginMap["url"] as? String, let url = URL(string: urlString) { + self.init(url: url) + } else { + self.init(url: URL(string: "about:blank")!) + } + + if let method = fromPluginMap["method"] as? String { + httpMethod = method + } + if let body = fromPluginMap["body"] as? FlutterStandardTypedData { + httpBody = body.data + } + if let headers = fromPluginMap["headers"] as? [String:String] { + for (key, value) in headers { + setValue(value, forHTTPHeaderField: key) + } + } + if let _allowsCellularAccess = fromPluginMap["allowsCellularAccess"] as? Bool { + allowsCellularAccess = _allowsCellularAccess + } + if #available(macOS 10.15, *), let _allowsConstrainedNetworkAccess = fromPluginMap["allowsConstrainedNetworkAccess"] as? Bool { + allowsConstrainedNetworkAccess = _allowsConstrainedNetworkAccess + } + if #available(macOS 10.15, *), let _allowsExpensiveNetworkAccess = fromPluginMap["allowsExpensiveNetworkAccess"] as? Bool { + allowsExpensiveNetworkAccess = _allowsExpensiveNetworkAccess + } + if let _cachePolicy = fromPluginMap["cachePolicy"] as? Int { + cachePolicy = CachePolicy.init(rawValue: UInt(_cachePolicy)) ?? .useProtocolCachePolicy + } + if let _httpShouldHandleCookies = fromPluginMap["httpShouldHandleCookies"] as? Bool { + httpShouldHandleCookies = _httpShouldHandleCookies + } + if let _httpShouldUsePipelining = fromPluginMap["httpShouldUsePipelining"] as? Bool { + httpShouldUsePipelining = _httpShouldUsePipelining + } + if let _networkServiceType = fromPluginMap["networkServiceType"] as? Int { + networkServiceType = NetworkServiceType.init(rawValue: UInt(_networkServiceType)) ?? .default + } + if let _timeoutInterval = fromPluginMap["timeoutInterval"] as? Double { + timeoutInterval = _timeoutInterval + } + if let _mainDocumentURL = fromPluginMap["mainDocumentURL"] as? String { + mainDocumentURL = URL(string: _mainDocumentURL)! + } + if #available(macOS 11.3, *), let _assumesHTTP3Capable = fromPluginMap["assumesHTTP3Capable"] as? Bool { + assumesHTTP3Capable = _assumesHTTP3Capable + } + if #available(macOS 12.0, *), let attributionRawValue = fromPluginMap["attribution"] as? UInt, + let _attribution = URLRequest.Attribution(rawValue: attributionRawValue) { + attribution = _attribution + } + } + + public func toMap () -> [String:Any?] { + var _allowsConstrainedNetworkAccess: Bool? = nil + var _allowsExpensiveNetworkAccess: Bool? = nil + if #available(macOS 10.15, *) { + _allowsConstrainedNetworkAccess = allowsConstrainedNetworkAccess + _allowsExpensiveNetworkAccess = allowsExpensiveNetworkAccess + } + var _assumesHTTP3Capable: Bool? = nil + if #available(macOS 11.3, *) { + _assumesHTTP3Capable = assumesHTTP3Capable + } + var _attribution: UInt? = nil + if #available(macOS 12.0, *) { + _attribution = attribution.rawValue + } + return [ + "url": url?.absoluteString, + "method": httpMethod, + "headers": allHTTPHeaderFields, + "body": httpBody.map(FlutterStandardTypedData.init(bytes:)), + "allowsCellularAccess": allowsCellularAccess, + "allowsConstrainedNetworkAccess": _allowsConstrainedNetworkAccess, + "allowsExpensiveNetworkAccess": _allowsExpensiveNetworkAccess, + "cachePolicy": cachePolicy.rawValue, + "httpShouldHandleCookies": httpShouldHandleCookies, + "httpShouldUsePipelining": httpShouldUsePipelining, + "networkServiceType": networkServiceType.rawValue, + "timeoutInterval": timeoutInterval, + "mainDocumentURL": mainDocumentURL?.absoluteString, + "assumesHTTP3Capable": _assumesHTTP3Capable, + "attribution": _attribution + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/URLResponse.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/URLResponse.swift new file mode 100644 index 00000000..0a6a4835 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/URLResponse.swift @@ -0,0 +1,31 @@ +// +// URLResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation + +extension URLResponse { + public convenience init?(fromPluginMap: [String:Any?]) { + let url = URL(string: fromPluginMap["url"] as? String ?? "about:blank")! + let mimeType = fromPluginMap["mimeType"] as? String + let expectedContentLength = fromPluginMap["expectedContentLength"] as? Int64 ?? 0 + let textEncodingName = fromPluginMap["textEncodingName"] as? String + self.init(url: url, mimeType: mimeType, expectedContentLength: Int(expectedContentLength), textEncodingName: textEncodingName) + } + + public func toMap () -> [String:Any?] { + let httpResponse: HTTPURLResponse? = self as? HTTPURLResponse + return [ + "expectedContentLength": expectedContentLength, + "mimeType": mimeType, + "suggestedFilename": suggestedFilename, + "textEncodingName": textEncodingName, + "url": url?.absoluteString, + "headers": httpResponse?.allHeaderFields, + "statusCode": httpResponse?.statusCode + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/UserScript.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/UserScript.swift new file mode 100644 index 00000000..d350836e --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/UserScript.swift @@ -0,0 +1,71 @@ +// +// InAppWebViewUserScript.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation +import WebKit + +public class UserScript: WKUserScript { + var groupName: String? + + private var contentWorldWrapper: Any? + @available(macOS 11.0, *) + var contentWorld: WKContentWorld { + get { + if let value = contentWorldWrapper as? WKContentWorld { + return value + } + return .page + } + set { contentWorldWrapper = newValue } + } + + public override init(source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly) + } + + public init(groupName: String?, source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly) + self.groupName = groupName + } + + @available(macOS 11.0, *) + public override init(source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, in contentWorld: WKContentWorld) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly, in: contentWorld) + self.contentWorld = contentWorld + } + + @available(macOS 11.0, *) + public init(groupName: String?, source: String, injectionTime: WKUserScriptInjectionTime, forMainFrameOnly: Bool, in contentWorld: WKContentWorld) { + super.init(source: source, injectionTime: injectionTime, forMainFrameOnly: forMainFrameOnly, in: contentWorld) + self.groupName = groupName + self.contentWorld = contentWorld + } + + public static func fromMap(map: [String:Any?]?, windowId: Int64?) -> UserScript? { + guard let map = map else { + return nil + } + + let contentWorldMap = map["contentWorld"] as? [String:Any?] + if #available(macOS 11.0, *), let contentWorldMap = contentWorldMap { + let contentWorld = WKContentWorld.fromMap(map: contentWorldMap, windowId: windowId)! + return UserScript( + groupName: map["groupName"] as? String, + source: map["source"] as! String, + injectionTime: WKUserScriptInjectionTime.init(rawValue: map["injectionTime"] as! Int) ?? .atDocumentStart, + forMainFrameOnly: map["forMainFrameOnly"] as! Bool, + in: contentWorld + ) + } + return UserScript( + groupName: map["groupName"] as? String, + source: map["source"] as! String, + injectionTime: WKUserScriptInjectionTime.init(rawValue: map["injectionTime"] as! Int) ?? .atDocumentStart, + forMainFrameOnly: map["forMainFrameOnly"] as! Bool + ) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WKContentWorld.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WKContentWorld.swift new file mode 100644 index 00000000..f94cc81a --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WKContentWorld.swift @@ -0,0 +1,41 @@ +// +// WKContentWorld.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +@available(macOS 11.0, *) +extension WKContentWorld { + // Workaround to create stored properties in an extension: + // https://valv0.medium.com/computed-properties-and-extensions-a-pure-swift-approach-64733768112c + + private static var _windowId = [String: Int64?]() + + var windowId: Int64? { + get { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + return WKContentWorld._windowId[tmpAddress] ?? nil + } + set(newValue) { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + WKContentWorld._windowId[tmpAddress] = newValue + } + } + + public static func fromMap(map: [String:Any?]?, windowId: Int64?) -> WKContentWorld? { + guard let map = map else { + return nil + } + var name = map["name"] as! String + name = windowId != nil && name != "page" ? + WKUserContentController.WINDOW_ID_PREFIX + String(windowId!) + "-" + name : + name + let contentWorld = Util.getContentWorld(name: name) + contentWorld.windowId = name != "page" ? windowId : nil + return contentWorld + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WKFrameInfo.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WKFrameInfo.swift new file mode 100644 index 00000000..b5a96207 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WKFrameInfo.swift @@ -0,0 +1,24 @@ +// +// WKFrameInfo.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +extension WKFrameInfo { + + public func toMap () -> [String:Any?] { + var securityOrigin: [String:Any?]? = nil + securityOrigin = self.securityOrigin.toMap() + // fix: self.request throws EXC_BREAKPOINT when coming from WKNavigationAction.sourceFrame + let request: URLRequest? = self.value(forKey: "request") as? URLRequest + return [ + "isMainFrame": isMainFrame, + "request": request?.toMap(), + "securityOrigin": securityOrigin + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WKNavigationAction.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WKNavigationAction.swift new file mode 100644 index 00000000..f93080f7 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WKNavigationAction.swift @@ -0,0 +1,28 @@ +// +// WKNavigationAction.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +extension WKNavigationAction { + public func toMap () -> [String:Any?] { + var shouldPerformDownload: Bool? = nil + if #available(macOS 11.3, *) { + shouldPerformDownload = self.shouldPerformDownload + } + return [ + "request": request.toMap(), + "isForMainFrame": targetFrame?.isMainFrame ?? false, + "hasGesture": nil, + "isRedirect": nil, + "navigationType": navigationType.rawValue, + "sourceFrame": sourceFrame.toMap(), + "targetFrame": targetFrame?.toMap(), + "shouldPerformDownload": shouldPerformDownload + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WKNavigationResponse.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WKNavigationResponse.swift new file mode 100644 index 00000000..0e685291 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WKNavigationResponse.swift @@ -0,0 +1,19 @@ +// +// WKNavigationResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +extension WKNavigationResponse { + public func toMap () -> [String:Any?] { + return [ + "response": response.toMap(), + "isForMainFrame": isForMainFrame, + "canShowMIMEType": canShowMIMEType, + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WKSecurityOrigin.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WKSecurityOrigin.swift new file mode 100644 index 00000000..a7e8c714 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WKSecurityOrigin.swift @@ -0,0 +1,20 @@ +// +// WKSecurityOrigin.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +@available(iOS 9.0, *) +extension WKSecurityOrigin { + public func toMap () -> [String:Any?] { + return [ + "host": host, + "port": port, + "protocol": self.protocol + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WKUserContentController.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WKUserContentController.swift new file mode 100644 index 00000000..21d0b62c --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WKUserContentController.swift @@ -0,0 +1,375 @@ +// +// UserContentController.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 17/02/21. +// + +import Foundation +import WebKit +import OrderedSet + +extension WKUserContentController { + static var WINDOW_ID_PREFIX = "WINDOW-ID-" + + // Workaround to create stored properties in an extension: + // https://valv0.medium.com/computed-properties-and-extensions-a-pure-swift-approach-64733768112c + + @available(macOS 11.0, *) + private static var _contentWorlds = [String: Set]() + @available(macOS 11.0, *) + var contentWorlds: Set { + get { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + return WKUserContentController._contentWorlds[tmpAddress] ?? [] + } + set(newValue) { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + WKUserContentController._contentWorlds[tmpAddress] = newValue + } + } + + private static var _userOnlyScripts = [String: [WKUserScriptInjectionTime:OrderedSet]]() + var userOnlyScripts: [WKUserScriptInjectionTime:OrderedSet] { + get { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + return WKUserContentController._userOnlyScripts[tmpAddress] ?? [:] + } + set(newValue) { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + WKUserContentController._userOnlyScripts[tmpAddress] = newValue + } + } + + private static var _pluginScripts = [String: [WKUserScriptInjectionTime:OrderedSet]]() + var pluginScripts: [WKUserScriptInjectionTime:OrderedSet] { + get { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + return WKUserContentController._pluginScripts[tmpAddress] ?? [:] + } + set(newValue) { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + WKUserContentController._pluginScripts[tmpAddress] = newValue + } + } + + public func initialize () { + if #available(macOS 11.0, *) { + contentWorlds = Set([WKContentWorld.page]) + } + pluginScripts = [ + .atDocumentStart: OrderedSet(sequence: []), + .atDocumentEnd: OrderedSet(sequence: []), + ] + userOnlyScripts = [ + .atDocumentStart: OrderedSet(sequence: []), + .atDocumentEnd: OrderedSet(sequence: []), + ] + } + + public func dispose (windowId: Int64?) { + if windowId == nil { + let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self)) + if #available(macOS 11.0, *) { + contentWorlds.removeAll() + WKUserContentController._contentWorlds.removeValue(forKey: tmpAddress) + } + pluginScripts.removeAll() + WKUserContentController._pluginScripts.removeValue(forKey: tmpAddress) + userOnlyScripts.removeAll() + WKUserContentController._userOnlyScripts.removeValue(forKey: tmpAddress) + } + else if #available(macOS 11.0, *), let windowId = windowId { + let contentWorldsToRemove = contentWorlds.filter({ $0.windowId == windowId }) + for contentWorld in contentWorldsToRemove { + contentWorlds.remove(contentWorld) + removeAllScriptMessageHandlers(from: contentWorld) + } + } + } + + public func sync(scriptMessageHandler: WKScriptMessageHandler) { + let pluginScriptsList = pluginScripts.compactMap({ $0.value }).joined() + for pluginScript in pluginScriptsList { + if !containsPluginScript(pluginScript: pluginScript) { + addUserScript(pluginScript) + for messageHandlerName in pluginScript.messageHandlerNames { + removeScriptMessageHandler(forName: messageHandlerName) + add(scriptMessageHandler, name: messageHandlerName) + } + } + if #available(macOS 11.0, *), pluginScript.requiredInAllContentWorlds { + for contentWorld in contentWorlds { + if !containsPluginScript(pluginScript: pluginScript, in: contentWorld) { + let pluginScriptWithContentWorld = pluginScript.copyAndSet(contentWorld: contentWorld) + addUserScript(pluginScriptWithContentWorld) + for messageHandlerName in pluginScriptWithContentWorld.messageHandlerNames { + removeScriptMessageHandler(forName: messageHandlerName, contentWorld: contentWorld) + add(scriptMessageHandler, contentWorld: contentWorld, name: messageHandlerName) + } + } + } + } + } + + let userOnlyScriptsList = userOnlyScripts.compactMap({ $0.value }).joined() + for userOnlyScript in userOnlyScriptsList { + if !userScripts.contains(userOnlyScript) { + addUserScript(userOnlyScript) + } + } + } + + public func addUserOnlyScript(_ userOnlyScript: UserScript) { + if #available(macOS 11.0, *) { + contentWorlds.insert(userOnlyScript.contentWorld) + } + userOnlyScripts[userOnlyScript.injectionTime]!.append(userOnlyScript) + } + + public func addUserOnlyScripts(_ userOnlyScripts: [UserScript]) { + for userOnlyScript in userOnlyScripts { + addUserOnlyScript(userOnlyScript) + } + } + + public func addPluginScript(_ pluginScript: PluginScript) { + if #available(macOS 11.0, *) { + contentWorlds.insert(pluginScript.contentWorld) + } + pluginScripts[pluginScript.injectionTime]!.append(pluginScript) + } + + public func addPluginScripts(_ pluginScripts: [PluginScript]) { + for pluginScript in pluginScripts { + addPluginScript(pluginScript) + } + } + + public func getPluginScriptsRequiredInAllContentWorlds() -> [PluginScript] { + return pluginScripts.compactMap({ $0.value }) + .joined() + .filter({ $0.injectionTime == .atDocumentStart && $0.requiredInAllContentWorlds }) + } + + @available(macOS 11.0, *) + public func generateCodeForScriptEvaluation(scriptMessageHandler: WKScriptMessageHandler, source: String, contentWorld: WKContentWorld) -> String { + let (inserted, _) = contentWorlds.insert(contentWorld) + if inserted { + var generatedCode = "" + let pluginScriptsRequired = getPluginScriptsRequiredInAllContentWorlds() + for pluginScript in pluginScriptsRequired { + generatedCode += pluginScript.source + "\n" + for messageHandlerName in pluginScript.messageHandlerNames { + removeScriptMessageHandler(forName: messageHandlerName, contentWorld: contentWorld) + add(scriptMessageHandler, contentWorld: contentWorld, name: messageHandlerName) + } + } + if let windowId = contentWorld.windowId { + generatedCode += "\(WINDOW_ID_VARIABLE_JS_SOURCE) = \(String(windowId));\n" + } + return generatedCode + "\n" + source + } + return source + } + + public func removeUserOnlyScript(_ userOnlyScript: UserScript) { + userOnlyScripts[userOnlyScript.injectionTime]!.remove(userOnlyScript) + removeUserScript(scriptToRemove: userOnlyScript) + } + + public func removeUserOnlyScript(at index: Int, injectionTime: WKUserScriptInjectionTime) { + let scriptToRemove = userOnlyScripts[injectionTime]![index] + userOnlyScripts[injectionTime]!.removeObject(at: index) + removeUserScript(scriptToRemove: scriptToRemove) + } + + public func removeAllUserOnlyScripts() { + let allUserOnlyScripts = Array(userOnlyScripts.compactMap({ $0.value }).joined()) + + userOnlyScripts[.atDocumentStart]!.removeAllObjects() + userOnlyScripts[.atDocumentEnd]!.removeAllObjects() + + removeUserScripts(scriptsToRemove: allUserOnlyScripts) + } + + public func removePluginScript(_ pluginScript: PluginScript) { + pluginScripts[pluginScript.injectionTime]!.remove(pluginScript) + for messageHandlerName in pluginScript.messageHandlerNames { + removeScriptMessageHandler(forName: messageHandlerName) + if #available(macOS 11.0, *) { + for contentWorld in contentWorlds { + removeScriptMessageHandler(forName: messageHandlerName, contentWorld: contentWorld) + } + } + } + removeUserScript(scriptToRemove: pluginScript) + } + + public func removeAllPluginScripts() { + let allPluginScripts = Array(pluginScripts.compactMap({ $0.value }).joined()) + + pluginScripts[.atDocumentStart]!.removeAllObjects() + pluginScripts[.atDocumentEnd]!.removeAllObjects() + + removeUserScripts(scriptsToRemove: allPluginScripts) + } + + public func removeAllPluginScriptMessageHandlers() { + let allPluginScripts = pluginScripts.compactMap({ $0.value }).joined() + for pluginScript in allPluginScripts { + for messageHandlerName in pluginScript.messageHandlerNames { + removeScriptMessageHandler(forName: messageHandlerName) + } + } + if #available(macOS 11.0, *) { + removeAllScriptMessageHandlers() + for contentWorld in contentWorlds { + removeAllScriptMessageHandlers(from: contentWorld) + } + } + } + + @available(macOS 11.0, *) + public func resetContentWorlds(windowId: Int64?) { + let allUserOnlyScripts = userOnlyScripts.compactMap({ $0.value }).joined() + let contentWorldsFiltered = contentWorlds.filter({ $0.windowId == windowId && $0 != WKContentWorld.page }) + for contentWorld in contentWorldsFiltered { + var found = false + for script in allUserOnlyScripts { + if script.contentWorld == contentWorld { + found = true + break + } + } + if !found { + contentWorlds.remove(contentWorld) + } + } + } + + private func removeUserScript(scriptToRemove: WKUserScript, shouldAddPreviousScripts: Bool = true) -> Void { + // there isn't a way to remove a specific user script using WKUserContentController, + // so we remove all the user scripts and, then, we add them again without the one that has been removed + let userScripts = useCopyOfUserScripts() + + var userScriptsUpdated: [WKUserScript] = [] + for script in userScripts { + if script != scriptToRemove { + userScriptsUpdated.append(script) + } + } + + removeAllUserScripts() + + if shouldAddPreviousScripts { + for script in userScriptsUpdated { + addUserScript(script) + } + } + } + + private func removeUserScripts(scriptsToRemove: [WKUserScript], shouldAddPreviousScripts: Bool = true) -> Void { + // there isn't a way to remove a specific user script using WKUserContentController, + // so we remove all the user scripts and, then, we add them again without the one that has been removed + let userScripts = useCopyOfUserScripts() + + var userScriptsUpdated: [WKUserScript] = [] + for script in userScripts { + if !scriptsToRemove.contains(script) { + userScriptsUpdated.append(script) + } + } + + removeAllUserScripts() + + if shouldAddPreviousScripts { + for script in userScriptsUpdated { + addUserScript(script) + } + } + } + + public func removeUserOnlyScripts(with groupName: String, shouldAddPreviousScripts: Bool = true) -> Void { + let allUserOnlyScripts = userOnlyScripts.compactMap({ $0.value }).joined() + var scriptsToRemove: [UserScript] = [] + for script in allUserOnlyScripts { + if let scriptName = script.groupName, scriptName == groupName { + scriptsToRemove.append(script) + userOnlyScripts[script.injectionTime]!.remove(script) + } + } + removeUserScripts(scriptsToRemove: scriptsToRemove, shouldAddPreviousScripts: shouldAddPreviousScripts) + } + + public func removePluginScripts(with groupName: String, shouldAddPreviousScripts: Bool = true) -> Void { + let allPluginScripts = pluginScripts.compactMap({ $0.value }).joined() + var scriptsToRemove: [PluginScript] = [] + for script in allPluginScripts { + if let scriptName = script.groupName, scriptName == groupName { + scriptsToRemove.append(script) + pluginScripts[script.injectionTime]!.remove(script) + } + } + removeUserScripts(scriptsToRemove: scriptsToRemove, shouldAddPreviousScripts: shouldAddPreviousScripts) + } + + public func containsPluginScript(pluginScript: PluginScript) -> Bool { + let userScripts = useCopyOfUserScripts() + for script in userScripts { + if let script = script as? PluginScript, script == pluginScript { + return true + } + } + return false + } + + public func containsPluginScript(with groupName: String) -> Bool { + let userScripts = useCopyOfUserScripts() + for script in userScripts { + if let script = script as? PluginScript, script.groupName == groupName { + return true + } + } + return false + } + + @available(macOS 11.0, *) + public func containsPluginScript(pluginScript: PluginScript, in contentWorld: WKContentWorld) -> Bool { + let userScripts = useCopyOfUserScripts() + for script in userScripts { + if let script = script as? PluginScript, script == pluginScript, script.contentWorld == contentWorld { + return true + } + } + return false + } + + @available(macOS 11.0, *) + public func containsPluginScript(with groupName: String, in contentWorld: WKContentWorld) -> Bool { + let userScripts = useCopyOfUserScripts() + for script in userScripts { + if let script = script as? PluginScript, script.groupName == groupName, script.contentWorld == contentWorld { + return true + } + } + return false + } + + @available(macOS 11.0, *) + public func getContentWorlds(with windowId: Int64?) -> Set { + var contentWorldsFiltered = Set([WKContentWorld.page]) + let contentWorlds = Array(self.contentWorlds) + for contentWorld in contentWorlds { + if contentWorld.windowId == windowId { + contentWorldsFiltered.insert(contentWorld) + } + } + return contentWorldsFiltered + } + + // use a copy of self.userScripts to avoid EXC_BREAKPOINT at runtime if self.userScripts gets removed when another code is looping them + private func useCopyOfUserScripts() -> [WKUserScript] { + return Array(self.userScripts) + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WKWindowFeatures.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WKWindowFeatures.swift new file mode 100644 index 00000000..469e2be1 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WKWindowFeatures.swift @@ -0,0 +1,24 @@ +// +// WKWindowFeatures.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/02/21. +// + +import Foundation +import WebKit + +extension WKWindowFeatures { + public func toMap () -> [String:Any?] { + return [ + "allowsResizing": allowsResizing, + "height": height, + "menuBarVisibility": menuBarVisibility, + "statusBarVisibility": statusBarVisibility, + "toolbarsVisibility": toolbarsVisibility, + "width": width, + "x": x, + "y": y + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WebMessage.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WebMessage.swift new file mode 100644 index 00000000..634c7edc --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WebMessage.swift @@ -0,0 +1,71 @@ +// +// WebMessage.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/03/21. +// + +import Foundation +import FlutterMacOS + +public class WebMessage: NSObject, Disposable { + var data: Any? + var type: WebMessageType + var ports: [WebMessagePort]? + + var jsData: String { + var jsData: String = "null" + if let messageData = data { + if type == .arrayBuffer, let messageDataArrayBuffer = messageData as? FlutterStandardTypedData { + jsData = "new Uint8Array(\(Array(messageDataArrayBuffer.data))).buffer" + } else if let messageDataString = messageData as? String { + jsData = "'\(messageDataString.replacingOccurrences(of: "\'", with: "\\'"))'" + } + } + return jsData + } + + public init(data: Any?, type: WebMessageType, ports: [WebMessagePort]?) { + self.type = type + super.init() + self.data = data + self.ports = ports + } + + public static func fromMap(map: [String: Any?]) -> WebMessage { + let portMapList = map["ports"] as? [[String: Any?]] + var ports: [WebMessagePort]? = nil + if let portMapList = portMapList, !portMapList.isEmpty { + ports = [] + portMapList.forEach { (portMap) in + ports?.append(WebMessagePort.fromMap(map: portMap)) + } + } + + return WebMessage( + data: map["data"] as? Any, + type: WebMessageType.init(rawValue: map["type"] as! Int)!, + ports: ports) + } + + public func toMap () -> [String: Any?] { + return [ + "data": type == .arrayBuffer && data is [UInt8] ? Data(data as! [UInt8]) : data, + "type": type.rawValue + ] + } + + public func dispose() { + ports?.removeAll() + } + + deinit { + debugPrint("WebMessage - dealloc") + dispose() + } +} + +public enum WebMessageType: Int { + case string = 0 + case arrayBuffer = 1 +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WebMessagePort.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WebMessagePort.swift new file mode 100644 index 00000000..b7a6e53c --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WebMessagePort.swift @@ -0,0 +1,147 @@ +// +// WebMessagePort.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 10/03/21. +// + +import Foundation + +public class WebMessagePort: NSObject { + var name: String + var index: Int64 + var webMessageChannelId: String + var webMessageChannel: WebMessageChannel? + var isClosed = false + var isTransferred = false + var isStarted = false + + public init(name: String, index: Int64, webMessageChannelId: String, webMessageChannel: WebMessageChannel?) { + self.name = name + self.index = index + self.webMessageChannelId = webMessageChannelId + super.init() + self.webMessageChannel = webMessageChannel + } + + public func setWebMessageCallback(completionHandler: ((Any?) -> Void)? = nil) throws { + if isClosed || isTransferred { + throw NSError(domain: "Port is already closed or transferred", code: 0) + } + self.isStarted = true + if let webMessageChannel = webMessageChannel, let webView = webMessageChannel.webView { + let index = name == "port1" ? 0 : 1 + webView.evaluateJavascript(source: """ + (function() { + var webMessageChannel = \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(webMessageChannel.id)"]; + if (webMessageChannel != null) { + webMessageChannel.\(self.name).onmessage = function (event) { + window.webkit.messageHandlers["onWebMessagePortMessageReceived"].postMessage({ + "webMessageChannelId": "\(webMessageChannel.id)", + "index": \(String(index)), + "message": { + "data": window.ArrayBuffer != null && event.data instanceof ArrayBuffer ? Array.from(new Uint8Array(event.data)) : (event.data != null ? event.data.toString() : null), + "type": window.ArrayBuffer != null && event.data instanceof ArrayBuffer ? 1 : 0 + } + }); + } + } + })(); + """) { (_) in + completionHandler?(nil) + } + } else { + completionHandler?(nil) + } + } + + public func postMessage(message: WebMessage, completionHandler: ((Any?) -> Void)? = nil) throws { + if isClosed || isTransferred { + throw NSError(domain: "Port is already closed or transferred", code: 0) + } + if let webMessageChannel = webMessageChannel, let webView = webMessageChannel.webView { + var portsString = "null" + if let ports = message.ports { + var portArrayString: [String] = [] + for port in ports { + if port == self { + throw NSError(domain: "Source port cannot be transferred", code: 0) + } + if port.isStarted { + throw NSError(domain: "Port is already started", code: 0) + } + if port.isClosed || port.isTransferred { + throw NSError(domain: "Port is already closed or transferred", code: 0) + } + port.isTransferred = true + portArrayString.append("\(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)['\(port.webMessageChannel!.id)'].\(port.name)") + } + portsString = "[" + portArrayString.joined(separator: ", ") + "]" + } + + let source = """ + (function() { + var webMessageChannel = \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(webMessageChannel.id)"]; + if (webMessageChannel != null) { + webMessageChannel.\(self.name).postMessage(\(message.jsData), \(portsString)); + } + })(); + """ + webView.evaluateJavascript(source: source) { (_) in + completionHandler?(nil) + } + } else { + completionHandler?(nil) + } + message.dispose() + } + + public func close(completionHandler: ((Any?) -> Void)? = nil) throws { + if isTransferred { + throw NSError(domain: "Port is already transferred", code: 0) + } + isClosed = true + if let webMessageChannel = webMessageChannel, let webView = webMessageChannel.webView { + let source = """ + (function() { + var webMessageChannel = \(WEB_MESSAGE_CHANNELS_VARIABLE_NAME)["\(webMessageChannel.id)"]; + if (webMessageChannel != null) { + webMessageChannel.\(self.name).close(); + } + })(); + """ + webView.evaluateJavascript(source: source) { (_) in + completionHandler?(nil) + } + } else { + completionHandler?(nil) + } + } + + public static func fromMap(map: [String: Any?]) -> WebMessagePort { + let index = map["index"] as! Int64 + return WebMessagePort( + name: "port\(String(index + 1))", + index: index, + webMessageChannelId: map["webMessageChannelId"] as! String, + webMessageChannel: nil) + } + + public func toMap () -> [String: Any?] { + return [ + "name": name, + "index": index, + "webMessageChannelId": webMessageChannelId + ] + } + + public func dispose() { + isClosed = true + webMessageChannel = nil + } + + deinit { + debugPrint("WebMessagePort - dealloc") + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WebResourceError.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WebResourceError.swift new file mode 100644 index 00000000..3183944b --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WebResourceError.swift @@ -0,0 +1,25 @@ +// +// WebResourceError.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 01/05/22. +// + +import Foundation + +public class WebResourceError: NSObject { + var type: Int + var errorDescription: String + + public init(type: Int, errorDescription: String) { + self.type = type + self.errorDescription = errorDescription + } + + public func toMap () -> [String:Any?] { + return [ + "type": type, + "description": errorDescription + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WebResourceRequest.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WebResourceRequest.swift new file mode 100644 index 00000000..ad7635c4 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WebResourceRequest.swift @@ -0,0 +1,53 @@ +// +// WebResourceRequest.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 01/05/22. +// + +import Foundation +import WebKit + +public class WebResourceRequest: NSObject { + var url: URL + var headers: [AnyHashable:Any]? + var isRedirect = false + var hasGesture = false + var isForMainFrame = true + var method = "GET" + + public init(url: URL, headers: [AnyHashable:Any]?) { + self.url = url + self.headers = headers + } + + public init(url: URL, headers: [AnyHashable:Any]?, isForMainFrame: Bool) { + self.url = url + self.headers = headers + self.isForMainFrame = isForMainFrame + } + + public init(fromURLRequest: URLRequest) { + self.url = fromURLRequest.url ?? URL(string: "about:blank")! + self.headers = fromURLRequest.allHTTPHeaderFields + self.method = fromURLRequest.httpMethod ?? "GET" + } + + public init(fromWKNavigationResponse: WKNavigationResponse) { + let response = fromWKNavigationResponse.response as? HTTPURLResponse + self.url = response?.url ?? URL(string: "about:blank")! + self.headers = response?.allHeaderFields + self.isForMainFrame = fromWKNavigationResponse.isForMainFrame + } + + public func toMap () -> [String:Any?] { + return [ + "url": url.absoluteString, + "headers": headers, + "isRedirect": isRedirect, + "hasGesture": hasGesture, + "isForMainFrame": isForMainFrame, + "method": method + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WebResourceResponse.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WebResourceResponse.swift new file mode 100644 index 00000000..4fcf13e7 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WebResourceResponse.swift @@ -0,0 +1,47 @@ +// +// WebResourceResponse.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 01/05/22. +// + +import Foundation +import WebKit + +public class WebResourceResponse: NSObject { + var contentType: String + var contentEncoding: String + var data: Data? + var headers: [AnyHashable:Any]? + var statusCode: Int? + var reasonPhrase: String? + + public init(contentType: String, contentEncoding: String, data: Data?, + headers: [AnyHashable:Any]?, statusCode: Int?, reasonPhrase: String?) { + self.contentType = contentType + self.contentEncoding = contentEncoding + self.data = data + self.headers = headers + self.statusCode = statusCode + self.reasonPhrase = reasonPhrase + } + + public init(fromWKNavigationResponse: WKNavigationResponse) { + let response = fromWKNavigationResponse.response as? HTTPURLResponse + self.contentType = response?.mimeType ?? "" + self.contentEncoding = response?.textEncodingName ?? "" + self.headers = response?.allHeaderFields + self.statusCode = response?.statusCode + } + + public func toMap () -> [String:Any?] { + return [ + "contentType": contentType, + "contentEncoding": contentEncoding, + "data": data, + "headers": headers, + "statusCode": statusCode, + "reasonPhrase": reasonPhrase + ] + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WebViewTransport.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WebViewTransport.swift new file mode 100644 index 00000000..7e21f7e6 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Types/WebViewTransport.swift @@ -0,0 +1,18 @@ +// +// WebViewTransport.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 16/02/21. +// + +import Foundation + +public class WebViewTransport: NSObject { + var webView: InAppWebView + var request: URLRequest + + init(webView: InAppWebView, request: URLRequest) { + self.webView = webView + self.request = request + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Util.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Util.swift new file mode 100644 index 00000000..bf9d91cc --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/Util.swift @@ -0,0 +1,155 @@ +// +// Util.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 12/02/21. +// + +import Foundation +import WebKit +import FlutterMacOS + +public class Util { + public static func getUrlAsset(assetFilePath: String) throws -> URL { +// let key = SwiftFlutterPlugin.instance?.registrar?.lookupKey(forAsset: assetFilePath) + let assetFilePath = "../Frameworks/App.framework/Resources/flutter_assets/\(assetFilePath)" + guard let assetURL = Bundle.main.url(forResource: assetFilePath, withExtension: nil) else { + throw NSError(domain: assetFilePath + " asset file cannot be found!", code: 0) + } + return assetURL + } + + public static func getAbsPathAsset(assetFilePath: String) throws -> String { +// let key = SwiftFlutterPlugin.instance?.registrar?.lookupKey(forAsset: assetFilePath) + let assetFilePath = "../Frameworks/App.framework/Resources/flutter_assets/\(assetFilePath)" + guard let assetAbsPath = Bundle.main.path(forResource: assetFilePath, ofType: nil) else { + throw NSError(domain: assetFilePath + " asset file cannot be found!", code: 0) + } + return assetAbsPath + } + + public static func convertToDictionary(text: String) -> [String: Any]? { + if let data = text.data(using: .utf8) { + do { + return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] + } catch { + print(error.localizedDescription) + } + } + return nil + } + + public static func JSONStringify(value: Any, prettyPrinted: Bool = false) -> String { + let options: JSONSerialization.WritingOptions = prettyPrinted ? .prettyPrinted : .init(rawValue: 0) + if JSONSerialization.isValidJSONObject(value) { + let data = try? JSONSerialization.data(withJSONObject: value, options: options) + if data != nil { + if let string = String(data: data!, encoding: .utf8) { + return string + } + } + } + return "" + } + + @available(macOS 11.0, *) + public static func getContentWorld(name: String) -> WKContentWorld { + switch name { + case "defaultClient": + return WKContentWorld.defaultClient + case "page": + return WKContentWorld.page + default: + return WKContentWorld.world(name: name) + } + } + + public static func getNSPrintInfoJobDisposition(name: String) -> NSPrintInfo.JobDisposition { + switch name { + case "save": + return NSPrintInfo.JobDisposition.save + case "cancel": + return NSPrintInfo.JobDisposition.cancel + case "preview": + return NSPrintInfo.JobDisposition.preview + case "spool": + return NSPrintInfo.JobDisposition.spool + default: + return NSPrintInfo.JobDisposition.spool + } + } + + public static func isIPv4(address: String) -> Bool { + var sin = sockaddr_in() + return address.withCString({ cstring in inet_pton(AF_INET, cstring, &sin.sin_addr) }) == 1 + } + + public static func isIPv6(address: String) -> Bool { + var sin6 = sockaddr_in6() + return address.withCString({ cstring in inet_pton(AF_INET6, cstring, &sin6.sin6_addr) }) == 1 + } + + public static func isIpAddress(address: String) -> Bool { + return Util.isIPv6(address: address) || Util.isIPv4(address: address) + } + + public static func normalizeIPv6(address: String) throws -> String { + if !Util.isIPv6(address: address) { + throw NSError(domain: "Invalid address: \(address)", code: 0) + } + var ipString = address + // replace ipv4 address if any + let ipv4Regex = try! NSRegularExpression(pattern: "(.*:)([0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$)") + if let match = ipv4Regex.firstMatch(in: address, options: [], range: NSRange(location: 0, length: address.utf16.count)) { + if let ipv6PartRange = Range(match.range(at: 1), in: address) { + ipString = String(address[ipv6PartRange]) + } + if let ipv4Range = Range(match.range(at: 2), in: address) { + let ipv4 = address[ipv4Range] + let ipv4Split = ipv4.split(separator: ".") + var ipv4Converted = Array(repeating: "0000", count: 4) + for i in 0...3 { + let byte = Int(ipv4Split[i])! + let hex = ("0" + String(byte, radix: 16)) + var offset = hex.count - 3 + offset = offset < 0 ? 0 : offset + let fromIndex = hex.index(hex.startIndex, offsetBy: offset) + let toIndex = hex.index(hex.startIndex, offsetBy: hex.count - 1) + let indexRange = Range(uncheckedBounds: (lower: fromIndex, upper: toIndex)) + ipv4Converted[i] = String(hex[indexRange]) + } + ipString += ipv4Converted[0] + ipv4Converted[1] + ":" + ipv4Converted[2] + ipv4Converted[3] + } + } + + // take care of leading and trailing :: + let regex = try! NSRegularExpression(pattern: "^:|:$") + ipString = regex.stringByReplacingMatches(in: ipString, options: [], range: NSRange(location: 0, length: ipString.count), withTemplate: "") + + let ipv6 = ipString.split(separator: ":", omittingEmptySubsequences: false) + var fullIPv6 = Array(repeating: "0000", count: ipv6.count) + + for (i, hex) in ipv6.enumerated() { + if !hex.isEmpty { + // normalize leading zeros + let hexString = String("0000" + hex) + var offset = hexString.count - 5 + offset = offset < 0 ? 0 : offset + let fromIndex = hexString.index(hexString.startIndex, offsetBy: offset) + let toIndex = hexString.index(hexString.startIndex, offsetBy: hexString.count - 1) + let indexRange = Range(uncheckedBounds: (lower: fromIndex, upper: toIndex)) + fullIPv6[i] = String(hexString[indexRange]) + } else { + // normalize grouped zeros :: + var zeros: [String] = [] + for _ in ipv6.count...8 { + zeros.append("0000") + } + fullIPv6[i] = zeros.joined(separator: ":") + } + } + + return fullIPv6.joined(separator: ":") + } + +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/WKProcessPoolManager.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/WKProcessPoolManager.swift new file mode 100755 index 00000000..038fb4d7 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/WKProcessPoolManager.swift @@ -0,0 +1,13 @@ +// +// WKProcessPoolManager.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 19/11/2019. +// + +import Foundation +import WebKit + +public class WKProcessPoolManager { + static let sharedProcessPool = WKProcessPool() +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/WebAuthenticationSession/WebAuthenticationSession.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/WebAuthenticationSession/WebAuthenticationSession.swift new file mode 100644 index 00000000..92140fe1 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/WebAuthenticationSession/WebAuthenticationSession.swift @@ -0,0 +1,102 @@ +// +// WebAuthenticationSession.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/05/22. +// + +import Foundation +import AuthenticationServices +import SafariServices +import FlutterMacOS + +public class WebAuthenticationSession: NSObject, ASWebAuthenticationPresentationContextProviding, Disposable { + static let METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_webauthenticationsession_" + var id: String + var plugin: InAppWebViewFlutterPlugin? + var url: URL + var callbackURLScheme: String? + var settings: WebAuthenticationSessionSettings + var session: Any? + var channelDelegate: WebAuthenticationSessionChannelDelegate? + private var _canStart = true + + public init(plugin: InAppWebViewFlutterPlugin, id: String, url: URL, callbackURLScheme: String?, settings: WebAuthenticationSessionSettings) { + self.id = id + self.plugin = plugin + self.url = url + self.settings = settings + super.init() + self.callbackURLScheme = callbackURLScheme + if #available(macOS 10.15, *) { + let session = ASWebAuthenticationSession(url: self.url, callbackURLScheme: self.callbackURLScheme, completionHandler: self.completionHandler) + session.presentationContextProvider = self + self.session = session + } + let channel = FlutterMethodChannel(name: WebAuthenticationSession.METHOD_CHANNEL_NAME_PREFIX + id, + binaryMessenger: plugin.registrar!.messenger) + self.channelDelegate = WebAuthenticationSessionChannelDelegate(webAuthenticationSession: self, channel: channel) + } + + public func prepare() { + if #available(macOS 10.15, *), let session = session as? ASWebAuthenticationSession { + session.prefersEphemeralWebBrowserSession = settings.prefersEphemeralWebBrowserSession + } + } + + public func completionHandler(url: URL?, error: Error?) -> Void { + channelDelegate?.onComplete(url: url, errorCode: error?._code) + } + + public func canStart() -> Bool { + guard let session = session else { + return false + } + if #available(macOS 10.15.4, *), let session = session as? ASWebAuthenticationSession { + return session.canStart + } + return _canStart + } + + public func start() -> Bool { + guard let session = session else { + return false + } + var started = false + if #available(macOS 10.15, *), let session = session as? ASWebAuthenticationSession { + started = session.start() + } + if started { + _canStart = false + } + return started + } + + public func cancel() { + guard let session = session else { + return + } + if #available(macOS 10.15, *), let session = session as? ASWebAuthenticationSession { + session.cancel() + } + } + + @available(macOS 10.15, *) + public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { + return NSApplication.shared.windows.first { $0.isKeyWindow } ?? ASPresentationAnchor() + } + + public func dispose() { + cancel() + channelDelegate?.dispose() + channelDelegate = nil + session = nil + plugin?.webAuthenticationSessionManager?.sessions[id] = nil + plugin = nil + } + + deinit { + debugPrint("WebAuthenticationSession - dealloc") + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionChannelDelegate.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionChannelDelegate.swift new file mode 100644 index 00000000..ddd8d528 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionChannelDelegate.swift @@ -0,0 +1,76 @@ +// +// WebAuthenticationSessionChannelDelegate.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/05/22. +// + +import Foundation +import FlutterMacOS + +public class WebAuthenticationSessionChannelDelegate: ChannelDelegate { + private weak var webAuthenticationSession: WebAuthenticationSession? + + public init(webAuthenticationSession: WebAuthenticationSession, channel: FlutterMethodChannel) { + super.init(channel: channel) + self.webAuthenticationSession = webAuthenticationSession + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + // let arguments = call.arguments as? NSDictionary + switch call.method { + case "canStart": + if let webAuthenticationSession = webAuthenticationSession { + result(webAuthenticationSession.canStart()) + } else { + result(false) + } + break + case "start": + if let webAuthenticationSession = webAuthenticationSession { + result(webAuthenticationSession.start()) + } else { + result(false) + } + break + case "cancel": + if let webAuthenticationSession = webAuthenticationSession { + webAuthenticationSession.cancel() + result(true) + } else { + result(false) + } + break + case "dispose": + if let webAuthenticationSession = webAuthenticationSession { + webAuthenticationSession.dispose() + result(true) + } else { + result(false) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func onComplete(url: URL?, errorCode: Int?) { + let arguments: [String: Any?] = [ + "url": url?.absoluteString, + "errorCode": errorCode + ] + DispatchQueue.main.async { [weak self] in + self?.channel?.invokeMethod("onComplete", arguments: arguments) + } + } + + public override func dispose() { + super.dispose() + webAuthenticationSession = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionManager.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionManager.swift new file mode 100644 index 00000000..102438ff --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionManager.swift @@ -0,0 +1,78 @@ +// +// WebAuthenticationSessionManager.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/05/22. +// + +import FlutterMacOS +import AppKit +import WebKit +import Foundation +import AVFoundation +import SafariServices + +public class WebAuthenticationSessionManager: ChannelDelegate { + static let METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_webauthenticationsession" + var plugin: InAppWebViewFlutterPlugin? + var sessions: [String: WebAuthenticationSession?] = [:] + + init(plugin: InAppWebViewFlutterPlugin) { + super.init(channel: FlutterMethodChannel(name: WebAuthenticationSessionManager.METHOD_CHANNEL_NAME, binaryMessenger: plugin.registrar!.messenger)) + self.plugin = plugin + } + + public override func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + let arguments = call.arguments as? NSDictionary + + switch call.method { + case "create": + let id = arguments!["id"] as! String + let url = arguments!["url"] as! String + let callbackURLScheme = arguments!["callbackURLScheme"] as? String + let initialSettings = arguments!["initialSettings"] as! [String: Any?] + create(id: id, url: url, callbackURLScheme: callbackURLScheme, settings: initialSettings, result: result) + break + case "isAvailable": + if #available(macOS 10.15, *) { + result(true) + } else { + result(false) + } + break + default: + result(FlutterMethodNotImplemented) + break + } + } + + public func create(id: String, url: String, callbackURLScheme: String?, settings: [String: Any?], result: @escaping FlutterResult) { + if #available(macOS 10.15, *), let plugin = plugin { + let sessionUrl = URL(string: url) ?? URL(string: "about:blank")! + let initialSettings = WebAuthenticationSessionSettings() + let _ = initialSettings.parse(settings: settings) + let session = WebAuthenticationSession(plugin: plugin, id: id, url: sessionUrl, callbackURLScheme: callbackURLScheme, settings: initialSettings) + session.prepare() + sessions[id] = session + result(true) + return + } + + result(FlutterError.init(code: "WebAuthenticationSessionManager", message: "WebAuthenticationSession is not available!", details: nil)) + } + + public override func dispose() { + super.dispose() + let sessionValues = sessions.values + sessionValues.forEach { (session: WebAuthenticationSession?) in + session?.cancel() + session?.dispose() + } + sessions.removeAll() + plugin = nil + } + + deinit { + dispose() + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionSettings.swift b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionSettings.swift new file mode 100644 index 00000000..ac262a31 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Classes/WebAuthenticationSession/WebAuthenticationSessionSettings.swift @@ -0,0 +1,28 @@ +// +// WebAuthenticationSessionSettings.swift +// flutter_inappwebview +// +// Created by Lorenzo Pichilli on 08/05/22. +// + +import Foundation +import AuthenticationServices +import SafariServices + +@objcMembers +public class WebAuthenticationSessionSettings: ISettings { + + var prefersEphemeralWebBrowserSession = false + + override init(){ + super.init() + } + + override func getRealSettings(obj: WebAuthenticationSession?) -> [String: Any?] { + var realOptions: [String: Any?] = toMap() + if #available(macOS 10.15, *), let session = obj?.session as? ASWebAuthenticationSession { + realOptions["prefersEphemeralWebBrowserSession"] = session.prefersEphemeralWebBrowserSession + } + return realOptions + } +} diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/Resources/PrivacyInfo.xcprivacy b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 00000000..0eca193e --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyTrackingDomains + + NSPrivacyAccessedAPITypes + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + \ No newline at end of file diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/macos/flutter_inappwebview_macos.podspec b/ui/flutter/third_party/flutter_inappwebview_macos/macos/flutter_inappwebview_macos.podspec new file mode 100644 index 00000000..7589c5f4 --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/macos/flutter_inappwebview_macos.podspec @@ -0,0 +1,28 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint flutter_inappwebview_macos.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'flutter_inappwebview_macos' + s.version = '0.0.1' + s.summary = 'A new Flutter plugin project.' + s.description = <<-DESC +A new Flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.resources = 'Storyboards/**/*.storyboard' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'FlutterMacOS' + s.resource_bundles = {'flutter_inappwebview_macos_privacy' => ['Resources/PrivacyInfo.xcprivacy']} + + s.platform = :osx, '10.14' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.swift_version = '5.0' + + s.dependency 'OrderedSet', '~>6.0.3' +end diff --git a/ui/flutter/third_party/flutter_inappwebview_macos/pubspec.yaml b/ui/flutter/third_party/flutter_inappwebview_macos/pubspec.yaml new file mode 100644 index 00000000..0c7f6d6b --- /dev/null +++ b/ui/flutter/third_party/flutter_inappwebview_macos/pubspec.yaml @@ -0,0 +1,82 @@ +name: flutter_inappwebview_macos +description: macOS implementation of the flutter_inappwebview plugin. +version: 1.1.2 +homepage: https://inappwebview.dev/ +repository: https://github.com/pichillilorenzo/flutter_inappwebview/tree/master/flutter_inappwebview_macos +issue_tracker: https://github.com/pichillilorenzo/flutter_inappwebview/issues +funding: + - https://inappwebview.dev/donate/ +topics: + - html + - webview + - webview-flutter + - inappwebview + - browser + +environment: + sdk: ^3.5.0 + flutter: ">=3.24.0" + +dependencies: + flutter: + sdk: flutter + flutter_inappwebview_platform_interface: ^1.3.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^4.0.0 + plugin_platform_interface: ^2.1.8 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + implements: flutter_inappwebview + platforms: + macos: + pluginClass: InAppWebViewFlutterPlugin + dartPluginClass: MacOSInAppWebViewPlatform + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/ui/flutter/tool/webview_profile_check.dart b/ui/flutter/tool/webview_profile_check.dart new file mode 100644 index 00000000..ac416c31 --- /dev/null +++ b/ui/flutter/tool/webview_profile_check.dart @@ -0,0 +1,252 @@ +// Real native smoke test: flutter run -d macos -t tool/webview_profile_check.dart +import 'dart:async'; +import 'dart:io'; +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:gopeed/app/rpc/webview_profile.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + runApp(const MaterialApp(home: SizedBox())); + try { + await checkProfiles(); + stdout.writeln('PASS: native proxy, concurrent profiles, cookies, LocalStorage, IndexedDB and reopening'); + exit(0); + } catch (error, stack) { + stderr.writeln('FAIL: $error\n$stack'); + exit(1); + } +} + +void expectValue(dynamic actual, dynamic expected) { + if (actual != expected) throw StateError('Expected $expected, got $actual'); +} + +Future checkProfiles() async { + final context = SecurityContext() + ..useCertificateChainBytes(utf8.encode(fixtureCertificate)) + ..usePrivateKeyBytes(utf8.encode(fixtureKey)); + final origin = await HttpServer.bindSecure(InternetAddress.loopbackIPv4, 0, context); + origin.listen((request) async { + request.response.headers.contentType = ContentType.html; + request.response.write('profile fixture'); + await request.response.close(); + }); + final plainOrigin = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); + plainOrigin.listen((request) async { + request.response.headers.contentType = ContentType.html; + request.response.write('profile fixture'); + await request.response.close(); + }); + var proxyHits = 0; + final proxy = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + proxy.listen((socket) async { + final input = StreamIterator(socket); + final buffer = []; + Future> read(int count) async { + while (buffer.length < count) { + if (!await input.moveNext()) throw StateError('Incomplete SOCKS handshake'); + buffer.addAll(input.current); + } + final value = buffer.sublist(0, count); + buffer.removeRange(0, count); + return value; + } + + try { + final hello = await read(2); + await read(hello[1]); + socket.add([5, 0]); + await socket.flush(); + final header = await read(4); + if (header[3] == 1) { + await read(4); + } else if (header[3] == 4) { + await read(16); + } else { + final size = await read(1); + await read(size[0]); + } + final port = await read(2); + final targetPort = port[0] * 256 + port[1]; + proxyHits++; + final remote = await Socket.connect( + InternetAddress.loopbackIPv4, + targetPort == 443 ? origin.port : plainOrigin.port, + ); + socket.add([5, 0, 0, 1, 127, 0, 0, 1, 0, 0]); + await socket.flush(); + remote.listen(socket.add, onDone: socket.destroy, onError: (_) => socket.destroy()); + if (buffer.isNotEmpty) remote.add(buffer); + while (await input.moveNext()) { + remote.add(input.current); + } + remote.destroy(); + } finally { + await input.cancel(); + socket.destroy(); + } + }); + final suffix = DateTime.now().microsecondsSinceEpoch.toRadixString(16).padLeft(12, '0'); + final a = WebViewProfile('11111111-2222-3333-4444-${suffix.substring(suffix.length - 12)}'); + final b = WebViewProfile('22222222-2222-3333-4444-${suffix.substring(suffix.length - 12)}'); + final proxyUrl = 'socks5://127.0.0.1:${proxy.port}'; + await a.prepare(proxyUrl); + await b.prepare(proxyUrl); + final target = WebUri('https://profiles.invalid/'); + final pages = []; + Future open(WebViewProfile profile) async { + final ready = Completer(); + final page = HeadlessInAppWebView( + onReceivedServerTrustAuthRequest: (_, _) async => + ServerTrustAuthResponse(action: ServerTrustAuthResponseAction.PROCEED), + initialSettings: WebViewProfileSettings(profileId: profile.id, debug: true), + onWebViewCreated: (controller) { + controller.loadUrl(urlRequest: URLRequest(url: target)); + }, + onLoadStop: (controller, url) { + if (url?.host == target.host && !ready.isCompleted) ready.complete(controller); + }, + onReceivedError: (_, _, error) { + if (!ready.isCompleted) ready.completeError(StateError(error.description)); + }, + ); + pages.add(page); + await page.run(); + return ready.future.timeout(const Duration(seconds: 20)); + } + + Future js(InAppWebViewController c, String source) => c.evaluateJavascript(source: source); + Future database(InAppWebViewController c, bool write) async { + final result = await c.callAsyncJavaScript( + functionBody: + ''' + return await new Promise((resolve,reject)=>{ + const request=indexedDB.open('profile-check',1); + request.onupgradeneeded=()=>request.result.createObjectStore('state'); + request.onerror=()=>reject(String(request.error)); + request.onsuccess=()=>{ + const db=request.result; + const tx=db.transaction('state','readwrite'); + const store=tx.objectStore('state'); + const op=${write ? "store.put('saved','owner')" : "store.get('owner')"}; + let value; + op.onsuccess=()=>value=op.result; + tx.oncomplete=()=>{db.close();resolve(value===undefined?null:value);}; + tx.onerror=()=>{db.close();reject(String(tx.error));}; + }; + }); + ''', + ); + if (result?.error != null) throw StateError(result!.error.toString()); + return result?.value; + } + + try { + final plainReady = Completer(); + final plain = HeadlessInAppWebView( + initialSettings: WebViewProfileSettings(profileId: a.id, debug: true), + initialUrlRequest: URLRequest(url: WebUri('http://profiles.invalid/')), + onLoadStop: (_, url) { + if (url?.host == 'profiles.invalid' && !plainReady.isCompleted) plainReady.complete(); + }, + onReceivedError: (_, _, error) { + if (!plainReady.isCompleted) plainReady.completeError(StateError(error.description)); + }, + ); + pages.add(plain); + await plain.run(); + await plainReady.future.timeout(const Duration(seconds: 20)); + if (proxyHits == 0) throw StateError('HTTP bypassed proxy'); + final first = await open(a); + await js(first, "localStorage.setItem('owner','saved')"); + await database(first, true); + await a.setCookie( + url: target, + name: 'session', + value: 'a', + domain: 'profiles.invalid', + isHttpOnly: true, + expiresDate: DateTime.now().add(const Duration(days: 1)).millisecondsSinceEpoch, + ); + final second = await open(b); + expectValue(await js(second, "localStorage.getItem('owner')"), null); + expectValue(await database(second, false), null); + expectValue((await b.getCookies(url: target)).length, 0); + final shared = await open(a); + expectValue(await js(shared, "localStorage.getItem('owner')"), 'saved'); + expectValue(await database(shared, false), 'saved'); + expectValue((await a.getCookies(url: target)).single.value, 'a'); + await b.deleteAllCookies(); + expectValue((await a.getCookies(url: target)).single.value, 'a'); + for (final page in pages) { + await page.dispose(); + } + pages.clear(); + final reopened = await open(a); + expectValue(await js(reopened, "localStorage.getItem('owner')"), 'saved'); + expectValue((await a.getCookies(url: target)).single.isHttpOnly, true); + await a.deleteCookie(url: target, name: 'session', domain: 'profiles.invalid'); + expectValue((await a.getCookies(url: target)).length, 0); + if (proxyHits == 0) throw StateError('Browser bypassed proxy'); + } finally { + for (final page in pages) { + await page.dispose(); + } + await proxy.close(); + await plainOrigin.close(force: true); + await origin.close(force: true); + } +} + +// Self-signed test fixture; trusted only by the smoke-test callback. +const fixtureCertificate = '''-----BEGIN CERTIFICATE----- +MIIDFzCCAf+gAwIBAgIUTVTmWtXNVthZEKsowr8XXq+WPf0wDQYJKoZIhvcNAQEL +BQAwGzEZMBcGA1UEAwwQcHJvZmlsZXMuaW52YWxpZDAeFw0yNjA5MTMxNjE4NTFa +Fw0yNjA5MTYxNjE4NTFaMBsxGTAXBgNVBAMMEHByb2ZpbGVzLmludmFsaWQwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDM8AJ8i1fy++T4VjH1pyRo96Hw +7yQnjAKhbh4kD4w145bc1RgmWgJLqSSZaegXKsn5Cw2FKxCjA0TyZCTGq+fkDp45 +ERWN2TACMWGyz/7ROuAOVfIo92AnDMpdinn+wn9DdWo8z9UEnzCjs613CLCXmGOQ +9R/rTO2VlTlAM5VJygqOpBrtPzrx1ojJhL3fY9e5yYP0yxJqCf7ukCG/lrhHj5fK +7twD+tJMkkGkEMHqavmmTPlD8TsUYKB2Qf9orRPQIxNZkbtG+DEzckRhUJDBprI/ +pKVwG680d/40AZ3NvAjbz8WjmWGj2gBkSC1NdQ1k6a6MFJOPGnKfDepMk4S7AgMB +AAGjUzBRMB0GA1UdDgQWBBTwL0eACgwkw0isYAWpZwp1uqULWjAfBgNVHSMEGDAW +gBTwL0eACgwkw0isYAWpZwp1uqULWjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQCplXaffZy1mYexyq744NFnIDBlnb3FLbBSG7tJt4SAY1dDysr9 +lNd8C5udrPxlmezUmlihzv8BqJcUNA0T9iYX6rOlNmD6xaaHs7akAGxvd8upIuM6 +iuKt3lXrE+xhVKGLs1XNYVOC+03H+sI1RxSOIpVPY43zSZXRXd8ADQ/+74KLsjIP +uQ1qUPG59C36Tfmp8UBA4pb3inPyRxcpGAREbXeX1zMuyoOs4c80CsV26Jh+sQFZ +VdSqtQrwzCLxb51D1MzeOakD69CWgM1hMIG12wqKvOz0d7uHLCXgIYLIYbaqgsF3 +FdpVXgGYktDl/GfVa2i9RLCZKUGOl2OuPFie +-----END CERTIFICATE----- +'''; +const fixtureKey = '''-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDM8AJ8i1fy++T4 +VjH1pyRo96Hw7yQnjAKhbh4kD4w145bc1RgmWgJLqSSZaegXKsn5Cw2FKxCjA0Ty +ZCTGq+fkDp45ERWN2TACMWGyz/7ROuAOVfIo92AnDMpdinn+wn9DdWo8z9UEnzCj +s613CLCXmGOQ9R/rTO2VlTlAM5VJygqOpBrtPzrx1ojJhL3fY9e5yYP0yxJqCf7u +kCG/lrhHj5fK7twD+tJMkkGkEMHqavmmTPlD8TsUYKB2Qf9orRPQIxNZkbtG+DEz +ckRhUJDBprI/pKVwG680d/40AZ3NvAjbz8WjmWGj2gBkSC1NdQ1k6a6MFJOPGnKf +DepMk4S7AgMBAAECggEACWGTO5BFXmIZgSSw9MXJ7OQpDZZk2UqXdDxONOhhSrY4 +WbOHf3nvUPdVLZCjhyv2Qug2njnycQPdTBs5c766xr3EkGgvzGZ8xBzuk/jheUJw +rzdP9oAcbq0vNkj4fKj6mmPtkQsSfTusRlIbRq+nJclcBW6zH1pOJeLG1CGIW/BG +wskEc9/86434Kmmot14f/nIGHPBPEL7MgsoXtMoM7lm/bOPekxd481SOA9j2h8Wl +wLVyFrtzTqDOeNBCZNBPpQeaWgBg4nSeX8TRUMAxDYhlMu0ENmYKp2LDfw6XXEii +yJsO07JNfcHQNyFPkW/E840KAvj+RzEUkQ4VhczLgQKBgQDncicAlvRWhCD//Jd7 +1FFMx8zaz6hKRrbdI0zzpzIle3Ebe7zXL84KMzLSP3pwD/ABwktoEhQjTjvo/VCh +pkvnqOgH8k9QUEyuzVhlhwC5RZOVfz3vnKTJMuCjsQ66OY1ljNVDWWLv5MkbVPlu +wmziIWqdnitN7X0F9oHnnshuiQKBgQDireplA0+aZfQFsiWm/YSaQa/QJUVBkA0N +La+C12O+dCr2n3gJsjVPteqVloJRymwpLtOW+3XWD4U4W7JJh3B+TNdtkgjT2jYF +GK/29B/mspB9aCXuMimq1XAyFC27vSR6PhiYPdQJ+dWtACigRXTZvjTdC7EvxSsZ +MNqjdO8oIwKBgHsXbATVQ/frZ72dcldqUR7buGi7Pk7akxDDYH7JclMa9sneIk3z +38nu9t144z5MS6Iz2nTsAIfSaOx+JO8ECaSYYWcwiNw3CMGC9rtwdIMUrOw6cw9H +qSBUjcKhPSrvxvK2VwnttT/O1uJRbCNrBguyKAjSAUf9wZt1QhuUiBa5AoGAc18p +Sbhhr6fsh2mgmFm4P/rmzP3rPe9fdTGnfuS5s5nUtCl+IxE86REiEHjGY3KvklZv +Aw2BcC3+FC03lQ0GSII7s1z8eTc6/2UNSpf+FKEVwX5cr9uAMZ7ot9RlYoRmKIGq +avItFda+1oqaIti3HIwew9LLoBCuWufun/tILhkCgYACBuavVB5q0JW520hb5KeA +ScD4+OLyxfKWYPvBNaIZLBP/lWuXQmxoXHvPRbxNjGQTldj1SbGS6yD5yvHs17vb +cFF+y2NS4hTD6VMMnl5sZT9OQjEEgj5/nnq+TlcHljpQ3q+OQ7rtJJFt832ZAXLd +ts2luZQYoJ+6aosodHGjCA== +-----END PRIVATE KEY----- +''';