mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-05 16:15:43 +00:00
Isolate extension WebView profiles and follow host proxy settings
This commit is contained in:
1 parent
cee5f3336e
commit
a0e84a75b1
556 files changed
+71097
-31
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# Extension browser host configuration
|
||||
|
||||
The downloader wraps each provider with the extension's stable identity before
|
||||
constructing the runtime. `OpenOptions.ProfileID`, `DataPath`, and `ProxyURL` are
|
||||
host-only fields. `parseOpenOptions` never reads them from extension JavaScript.
|
||||
The extension API is unchanged.
|
||||
|
||||
Profiles are persistent and shared across invocations/pages of the same extension.
|
||||
Different identities use different native stores. Go providers use a SHA-256-based
|
||||
path under `<StorageDir>/webview`; Apple RPC hosts use a stable UUID data store;
|
||||
Android uses a named WebView profile. Cookie operations select that same store,
|
||||
including `clearCookies`. Closing an execution closes its pages, not its profile.
|
||||
Existing shared browser data is not copied into every extension's new profile.
|
||||
|
||||
The downloader lazily owns one loopback forward proxy with HTTP and SOCKS5
|
||||
listeners. Windows/Linux/Android use HTTP; Apple uses SOCKS5 because its HTTP
|
||||
CONNECT configuration may bypass plain HTTP. The forwarder reads the current
|
||||
Gopeed proxy config for each new request/tunnel and supports direct, system,
|
||||
HTTP(S) upstream proxies with Basic authentication, and authenticated SOCKS5.
|
||||
Credentials remain in Go and are never sent over the WebView RPC.
|
||||
|
||||
Changes affect new requests/connections. Existing CONNECT/SOCKS tunnels keep their
|
||||
route until closed; opening another page in the same profile does not reset it.
|
||||
SOCKS carries host/port rather than a full URL: system proxy/PAC selection for
|
||||
Apple is host-based, with port 80 treated as HTTP and other ports as HTTPS.
|
||||
Path-dependent PAC rules cannot be reproduced from a SOCKS connection.
|
||||
|
||||
Apple requires macOS 14 / iOS 17. Android checks MULTI_PROFILE and PROXY_OVERRIDE.
|
||||
Unsupported hosts fail explicitly instead of silently sharing browser storage or
|
||||
ignoring the proxy.
|
||||
|
||||
## Validation
|
||||
|
||||
- `go test -race ./internal/webview/proxy ./pkg/download/engine/webview`
|
||||
- `go test ./pkg/download -run 'TestWebViewProfileBound|TestDownloader_ExtensionRuntimeWebView'`
|
||||
- In webview_go: `go run ./examples/profilecheck` (Linux: `xvfb-run -a ...`).
|
||||
- In ui/flutter: `flutter run -d macos -t tool/webview_profile_check.dart`.
|
||||
|
||||
The native smoke tests cover routing and persistent profile isolation. The Flutter
|
||||
smoke test also checks concurrent pages, HttpOnly cookies, LocalStorage, IndexedDB,
|
||||
profile-scoped clearing, and reopening. The self-signed TLS fixture is trusted only
|
||||
by that test's callback; production certificate validation is unchanged.
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -29,6 +29,10 @@ type Page interface {
|
||||
}
|
||||
|
||||
type OpenOptions struct {
|
||||
// Host-owned fields are never parsed from extension JavaScript options.
|
||||
ProfileID string
|
||||
DataPath string
|
||||
ProxyURL string
|
||||
Headless bool
|
||||
Debug bool
|
||||
Title string
|
||||
|
||||
@@ -336,3 +336,15 @@ func (p *fakePage) Close() error {
|
||||
p.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestExtensionCannotSupplyHostProfileOrProxy(t *testing.T) {
|
||||
opener := &fakeOpener{page: &fakePage{}}
|
||||
runtime := NewRuntime(opener, true)
|
||||
_, err := runtime.Open(map[string]any{"profileId": "other-extension", "dataPath": "/tmp/other-profile", "proxyUrl": "http://example.com:1234", "ProfileID": "other"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if opener.opts.ProfileID != "" || opener.opts.DataPath != "" || opener.opts.ProxyURL != "" {
|
||||
t.Fatalf("extension controlled host settings: %+v", opener.opts)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
webviewproxy "github.com/GopeedLab/gopeed/internal/webview/proxy"
|
||||
enginewebview "github.com/GopeedLab/gopeed/pkg/download/engine/webview"
|
||||
)
|
||||
|
||||
// extensionWebViewOpener binds browser state to the owning extension. Neither
|
||||
// identity, filesystem paths nor proxy settings are accepted from JavaScript.
|
||||
type extensionWebViewOpener struct {
|
||||
downloader *Downloader
|
||||
provider enginewebview.Provider
|
||||
identity string
|
||||
}
|
||||
|
||||
func (o *extensionWebViewOpener) Open(opts enginewebview.OpenOptions) (enginewebview.Page, error) {
|
||||
if o.identity == "" {
|
||||
return nil, fmt.Errorf("webview extension identity is required")
|
||||
}
|
||||
sum := sha256.Sum256([]byte(o.identity))
|
||||
opts.ProfileID = fmt.Sprintf("%x-%x-%x-%x-%x", sum[:4], sum[4:6], sum[6:8], sum[8:10], sum[10:16])
|
||||
path, err := filepath.Abs(filepath.Join(o.downloader.cfg.StorageDir, "webview", fmt.Sprintf("%x", sum[:])))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts.DataPath = path
|
||||
opts.ProxyURL, err = o.downloader.webviewProxyURL()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return o.provider.Open(opts)
|
||||
}
|
||||
|
||||
func (d *Downloader) webviewProxyURL() (string, error) {
|
||||
d.webviewProxyLock.Lock()
|
||||
defer d.webviewProxyLock.Unlock()
|
||||
if d.closed.Load() {
|
||||
return "", fmt.Errorf("downloader is closed")
|
||||
}
|
||||
if d.webviewProxy == nil {
|
||||
server, err := webviewproxy.New(func(r *http.Request) (*url.URL, error) {
|
||||
if d.blob != nil && d.blob.IsURL(r.URL.String()) {
|
||||
return nil, nil
|
||||
}
|
||||
d.configLock.RLock()
|
||||
var cfg base.DownloaderProxyConfig
|
||||
if d.cfg.Proxy != nil {
|
||||
cfg = *d.cfg.Proxy
|
||||
}
|
||||
d.configLock.RUnlock()
|
||||
handler := cfg.ToHandler()
|
||||
if handler == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return handler(r)
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
d.webviewProxy = server
|
||||
}
|
||||
if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
|
||||
return d.webviewProxy.SOCKSURL(), nil
|
||||
}
|
||||
return d.webviewProxy.URL(), nil
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
enginewebview "github.com/GopeedLab/gopeed/pkg/download/engine/webview"
|
||||
)
|
||||
|
||||
type profileCaptureProvider struct{ options []enginewebview.OpenOptions }
|
||||
|
||||
func (*profileCaptureProvider) IsAvailable() bool { return true }
|
||||
func (p *profileCaptureProvider) Open(opts enginewebview.OpenOptions) (enginewebview.Page, error) {
|
||||
p.options = append(p.options, opts)
|
||||
return fakeRuntimeWebViewPage{}, nil
|
||||
}
|
||||
|
||||
func TestWebViewProfileBoundToExtension(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
d := &Downloader{cfg: &DownloaderConfig{StorageDir: root, DownloaderStoreConfig: &base.DownloaderStoreConfig{}}, configLock: &sync.RWMutex{}}
|
||||
t.Cleanup(func() {
|
||||
if d.webviewProxy != nil {
|
||||
d.webviewProxy.Close()
|
||||
}
|
||||
})
|
||||
provider := &profileCaptureProvider{}
|
||||
for _, id := range []string{"extension-a", "extension-b", "extension-a", "../../escape"} {
|
||||
opener := &extensionWebViewOpener{downloader: d, provider: provider, identity: id}
|
||||
_, err := opener.Open(enginewebview.OpenOptions{ProfileID: "forged", DataPath: "/tmp/forged", ProxyURL: "http://wrong:80"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
a, b, reopen := provider.options[0], provider.options[1], provider.options[2]
|
||||
if a.ProfileID == b.ProfileID || a.DataPath == b.DataPath {
|
||||
t.Fatal("different extensions shared browser storage")
|
||||
}
|
||||
if a.ProfileID != reopen.ProfileID || a.DataPath != reopen.DataPath {
|
||||
t.Fatal("same extension lost profile identity")
|
||||
}
|
||||
for _, opts := range provider.options {
|
||||
if filepath.Dir(opts.DataPath) != filepath.Join(root, "webview") {
|
||||
t.Fatalf("profile escaped storage root: %s", opts.DataPath)
|
||||
}
|
||||
if opts.ProxyURL != d.webviewProxy.URL() && opts.ProxyURL != d.webviewProxy.SOCKSURL() {
|
||||
t.Fatal("host proxy was overridden")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,3 +29,7 @@ linter:
|
||||
|
||||
formatter:
|
||||
page_width: 120
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
- third_party/**
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
|
||||
// Private host/plugin bridge. These settings never originate from extension JS.
|
||||
class WebViewProfileSettings extends InAppWebViewSettings {
|
||||
WebViewProfileSettings({required this.profileId, required bool debug, super.userAgent})
|
||||
: super(javaScriptEnabled: true, transparentBackground: true, isInspectable: debug);
|
||||
|
||||
final String profileId;
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toMap() => {...super.toMap(), 'gopeedProfileId': profileId};
|
||||
}
|
||||
|
||||
class WebViewProfile {
|
||||
WebViewProfile(this.id);
|
||||
|
||||
final String id;
|
||||
static const _channel = MethodChannel('com.pichillilorenzo/flutter_inappwebview_cookiemanager');
|
||||
|
||||
Future<void> prepare(String proxyUrl) async {
|
||||
await _call('gopeed.prepareProfile', {'proxyUrl': proxyUrl});
|
||||
}
|
||||
|
||||
Future<dynamic> _call(String method, Map<String, dynamic> arguments) =>
|
||||
_channel.invokeMethod(method, {...arguments, 'gopeedProfileId': id});
|
||||
|
||||
Future<List<Cookie>> getCookies({required WebUri url}) async {
|
||||
final values = await _call('getCookies', {'url': url.toString()}) as List? ?? [];
|
||||
return values.map((value) => Cookie.fromMap(Map<String, dynamic>.from(value as Map))!).toList();
|
||||
}
|
||||
|
||||
Future<void> setCookie({
|
||||
required WebUri url,
|
||||
required String name,
|
||||
required String value,
|
||||
String? domain,
|
||||
String path = '/',
|
||||
int? expiresDate,
|
||||
bool isSecure = false,
|
||||
bool isHttpOnly = false,
|
||||
}) async {
|
||||
await _call('setCookie', {
|
||||
'url': url.toString(),
|
||||
'name': name,
|
||||
'value': value,
|
||||
'domain': domain,
|
||||
'path': path,
|
||||
'expiresDate': expiresDate?.toString(),
|
||||
'isSecure': isSecure,
|
||||
'isHttpOnly': isHttpOnly,
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> deleteCookie({required WebUri url, required String name, String? domain, String path = '/'}) async {
|
||||
await _call('deleteCookie', {'url': url.toString(), 'name': name, 'domain': domain, 'path': path});
|
||||
}
|
||||
|
||||
Future<void> deleteAllCookies() async {
|
||||
await _call('deleteAllCookies', {});
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import '../../core/common/start_config.dart';
|
||||
import '../../util/log_util.dart';
|
||||
import '../../util/util.dart';
|
||||
import 'server.dart';
|
||||
import 'webview_profile.dart';
|
||||
|
||||
String buildWebViewExecuteScript({
|
||||
required String channelName,
|
||||
@@ -73,7 +74,6 @@ class WebViewRpcService {
|
||||
|
||||
final Map<String, WebViewRpcPageSession> _pagesById = {};
|
||||
final Completer<void> _overlayReady = Completer<void>();
|
||||
late final CookieManager _cookieManager = CookieManager.instance();
|
||||
|
||||
RpcServerHandle? _server;
|
||||
int _pageSeq = 0;
|
||||
@@ -162,10 +162,12 @@ class WebViewRpcService {
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> _openPage(Map<String, dynamic> params) async {
|
||||
final profile = WebViewProfile(_string(params, 'profileId'));
|
||||
await profile.prepare(_string(params, 'proxyUrl'));
|
||||
final pageId = 'page-${++_pageSeq}';
|
||||
final session = WebViewRpcPageSession(
|
||||
pageId: pageId,
|
||||
cookieManager: _cookieManager,
|
||||
cookieManager: profile,
|
||||
headless: params['headless'] as bool? ?? false,
|
||||
debug: params['debug'] as bool? ?? false,
|
||||
title: params['title'] as String? ?? '',
|
||||
@@ -293,7 +295,7 @@ class WebViewRpcPageSession {
|
||||
});
|
||||
|
||||
final String pageId;
|
||||
final CookieManager cookieManager;
|
||||
final WebViewProfile cookieManager;
|
||||
final bool headless;
|
||||
final bool debug;
|
||||
final String title;
|
||||
@@ -523,10 +525,9 @@ class WebViewRpcPageSession {
|
||||
);
|
||||
}
|
||||
|
||||
InAppWebViewSettings get _settings => InAppWebViewSettings(
|
||||
javaScriptEnabled: true,
|
||||
transparentBackground: true,
|
||||
isInspectable: debug,
|
||||
InAppWebViewSettings get _settings => WebViewProfileSettings(
|
||||
profileId: cookieManager.id,
|
||||
debug: debug,
|
||||
userAgent: userAgent.isNotEmpty ? userAgent : null,
|
||||
);
|
||||
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoadsInWebContent</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
|
||||
+12
-15
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
# Native WebView host bridge
|
||||
|
||||
Vendored unmodified Dart facades and native sources from pub.dev:
|
||||
|
||||
- flutter_inappwebview_macos 1.1.2
|
||||
- flutter_inappwebview_ios 1.1.2
|
||||
- flutter_inappwebview_android 1.1.3
|
||||
|
||||
Each package retains its upstream license. Versions and original archive hashes
|
||||
are available in the parent commit's pubspec.lock.
|
||||
|
||||
Local native changes are limited to:
|
||||
|
||||
- `GopeedProfiles`: prepare a persistent profile and apply the internal loopback
|
||||
proxy before creating a WebView. Apple uses SOCKS5 to cover HTTP as well as HTTPS;
|
||||
Android uses the application-wide HTTP proxy override completion callback.
|
||||
- `InAppWebViewSettings` and `InAppWebView`: read the host-only `gopeedProfileId`
|
||||
setting and attach the profile before navigation.
|
||||
- `MyCookieManager`: route cookie operations to the requested profile, capturing
|
||||
the store per operation so asynchronous calls cannot cross profiles.
|
||||
|
||||
`lib/app/rpc/webview_profile.dart` uses the existing plugin method channel and
|
||||
serializes these internal settings. No extension JavaScript API is added.
|
||||
|
||||
Apple requires macOS 14 / iOS 17 for named persistent stores and proxy settings.
|
||||
Android requires `MULTI_PROFILE` and `PROXY_OVERRIDE`; unsupported runtimes return
|
||||
an explicit unavailable error rather than opening a shared, unproxied browser.
|
||||
|
||||
When updating upstream, refresh these three packages, reapply the above changes,
|
||||
and run `tool/webview_profile_check.dart` on a native Apple host. Do not patch the
|
||||
user's pub cache: dependency_overrides selects these tracked sources reproducibly.
|
||||
@@ -0,0 +1,103 @@
|
||||
## 1.1.3
|
||||
|
||||
- Updated flutter_inappwebview_platform_interface version to ^1.3.0
|
||||
|
||||
## 1.1.2
|
||||
|
||||
- Removed webview/plugin_scripts_js/ConsoleLogJS.java file, use native WebChromeClient.onConsoleMessage instead
|
||||
|
||||
## 1.1.1
|
||||
|
||||
- Updated flutter_inappwebview_platform_interface version to ^1.2.0
|
||||
|
||||
## 1.1.0+4
|
||||
|
||||
- Updated flutter_inappwebview_platform_interface version
|
||||
|
||||
## 1.1.0+3
|
||||
|
||||
- Fixed compilation error
|
||||
|
||||
## 1.1.0+2
|
||||
|
||||
- Updated pubspec.yaml
|
||||
|
||||
## 1.1.0+1
|
||||
|
||||
- Downgraded androidx.appcompat:appcompat:1.7.0 to androidx.appcompat:appcompat:1.6.1
|
||||
- Added `-dontwarn android.window.BackEvent` proguard rule
|
||||
|
||||
## 1.1.0
|
||||
|
||||
- Updated androidx.webkit:webkit:1.8.0 to androidx.webkit:webkit:1.12.0
|
||||
- Updated androidx.browser:browser:1.6.0 to androidx.browser:browser:1.8.0
|
||||
- Updates minimum supported SDK version to Flutter 3.24/Dart 3.5.
|
||||
- Removed unsupported WebViewFeature.SUPPRESS_ERROR_PAGE
|
||||
- Merged "Remove references to deprecated v1 Android embedding" [#2176](https://github.com/pichillilorenzo/flutter_inappwebview/pull/2176) (thanks to [gmackall](https://github.com/gmackall))
|
||||
|
||||
## 1.0.13
|
||||
|
||||
- Fixed "Android emulator using API 34 fails to draw on resume sometimes" [#1981](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1981)
|
||||
|
||||
## 1.0.12
|
||||
|
||||
- Updated `flutter_inappwebview_platform_interface` version dependency to `^1.0.10`
|
||||
|
||||
## 1.0.11
|
||||
|
||||
- Updated `flutter_inappwebview_platform_interface` version dependency to `^1.0.9`
|
||||
- Fix typos (thanks to [michalsrutek](https://github.com/michalsrutek))
|
||||
|
||||
## 1.0.10
|
||||
|
||||
- Updated `flutter_inappwebview_platform_interface` version dependency to `^1.0.8`
|
||||
- Implemented `PlatformCustomPathHandler` class
|
||||
|
||||
## 1.0.9
|
||||
|
||||
- Updated `flutter_inappwebview_platform_interface` version dependency to `^1.0.7`
|
||||
- Fixed "Cloudflare Turnstile failure" [#1738](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1738)
|
||||
- Fixed `InAppWebViewController.callAsyncJavaScript` issue when the last line of the `functionBody` parameter includes a code comment
|
||||
|
||||
## 1.0.8
|
||||
|
||||
- Implemented `InAppWebViewSettings.interceptOnlyAsyncAjaxRequests`
|
||||
- Implemented `PlatformInAppWebViewController.clearFormData` method
|
||||
- Implemented `PlatformCookieManager.removeSessionCookies` method
|
||||
- Updated `useShouldInterceptAjaxRequest` automatic infer logic
|
||||
- Updated `CookieManager` methods return value
|
||||
|
||||
## 1.0.7
|
||||
|
||||
- Merged "Fixed error in InterceptAjaxRequestJS 'Failed to set responseType property'" [#1904](https://github.com/pichillilorenzo/flutter_inappwebview/pull/1904) (thanks to [EArminjon](https://github.com/EArminjon))
|
||||
- Fixed shouldInterceptAjaxRequest javascript code when overriding XMLHttpRequest.open method parameters
|
||||
|
||||
## 1.0.6
|
||||
|
||||
- Fixed "getFavicons: _TypeError: type '_Map<String, dynamic>' is not a subtype of type 'Iterable<dynamic>'" [#1897](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1897)
|
||||
- Fixed "onClosed not considering back navigation or up button / close button in ChromeSafariBrowser when using noHistory: true" [#1882](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1882)
|
||||
|
||||
## 1.0.5
|
||||
|
||||
- Call `super.dispose();` on `InAppBrowser` and `ChromeSafari` implementations
|
||||
|
||||
## 1.0.4
|
||||
|
||||
- Throw platform exception when ProcessGlobalConfig.apply throws an error on the native side to be able to catch it on Flutter side
|
||||
|
||||
## 1.0.3
|
||||
|
||||
- Updated `ContentBlockerHandler` CSS_DISPLAY_NONE action type and `JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_JS_SOURCE` javascript implementation code
|
||||
|
||||
## 1.0.2
|
||||
|
||||
- Updated `flutter_inappwebview_platform_interface` version dependency to `1.0.2`
|
||||
- Fixed "Crash when starting ChromeSafariBrowser on Android java.lang.NoSuchMethodError: No virtual method isEngagementSignalsApiAvailable" [#1881](https://github.com/pichillilorenzo/flutter_inappwebview/issues/1881)
|
||||
|
||||
## 1.0.1
|
||||
|
||||
- Updated README
|
||||
|
||||
## 1.0.0
|
||||
|
||||
Initial release.
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2023 Lorenzo Pichilli
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,13 @@
|
||||
# flutter\_inappwebview\_android
|
||||
|
||||
The Android WebView implementation of [`flutter_inappwebview`](https://pub.dev/packages/flutter_inappwebview).
|
||||
|
||||
## Usage
|
||||
|
||||
This package is [endorsed](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin),
|
||||
which means you can simply use `flutter_inappwebview`
|
||||
normally. This package will be automatically included in your app when you do,
|
||||
so you do not need to add it to your `pubspec.yaml`.
|
||||
|
||||
However, if you `import` this package to use any of its APIs directly, you
|
||||
should add it to your `pubspec.yaml` as usual.
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
group 'com.pichillilorenzo.flutter_inappwebview_android'
|
||||
version '1.0-SNAPSHOT'
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.0.1'
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
// Conditional for compatibility with AGP <4.2.
|
||||
if (project.android.hasProperty("namespace")) {
|
||||
namespace 'com.pichillilorenzo.flutter_inappwebview_android'
|
||||
}
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 19
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
consumerProguardFiles 'proguard-rules.pro'
|
||||
}
|
||||
lintOptions {
|
||||
disable 'InvalidPackage'
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
release {
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
implementation 'androidx.webkit:webkit:1.12.0'
|
||||
implementation 'androidx.browser:browser:1.8.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# WebView
|
||||
-keepattributes *JavascriptInterface*
|
||||
-keepclassmembers class * {
|
||||
@android.webkit.JavascriptInterface <methods>;
|
||||
}
|
||||
-keepclassmembers class * extends android.webkit.WebViewClient {
|
||||
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
|
||||
public boolean *(android.webkit.WebView, java.lang.String);
|
||||
public void *(android.webkit.webView, jav.lang.String);
|
||||
}
|
||||
-keepclassmembers class com.pichillilorenzo.flutter_inappwebview_android$JavaScriptBridgeInterface {
|
||||
<fields>;
|
||||
<methods>;
|
||||
public *;
|
||||
private *;
|
||||
}
|
||||
-keep class com.pichillilorenzo.flutter_inappwebview_android.** { *; }
|
||||
|
||||
-dontwarn android.window.BackEvent
|
||||
+1
@@ -0,0 +1 @@
|
||||
rootProject.name = 'flutter_inappwebview_android'
|
||||
Vendored
Executable
+43
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.pichillilorenzo.flutter_inappwebview_android">
|
||||
<application>
|
||||
<activity
|
||||
android:theme="@style/AppTheme"
|
||||
android:exported="false"
|
||||
android:name="com.pichillilorenzo.flutter_inappwebview_android.in_app_browser.InAppBrowserActivity"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density" />
|
||||
<activity
|
||||
android:theme="@style/ThemeTransparent"
|
||||
android:exported="false"
|
||||
android:name="com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs.ChromeCustomTabsActivity" />
|
||||
<activity
|
||||
android:theme="@style/ThemeTransparent"
|
||||
android:exported="false"
|
||||
android:name="com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs.TrustedWebActivity" />
|
||||
<activity
|
||||
android:theme="@style/ThemeTransparent"
|
||||
android:exported="false"
|
||||
android:name="com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs.ChromeCustomTabsActivitySingleInstance"
|
||||
android:launchMode="singleInstance"/>
|
||||
<activity
|
||||
android:theme="@style/ThemeTransparent"
|
||||
android:exported="false"
|
||||
android:name="com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs.TrustedWebActivitySingleInstance"
|
||||
android:launchMode="singleInstance"/>
|
||||
<receiver
|
||||
android:name="com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs.ActionBroadcastReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<meta-data
|
||||
android:name="io.flutter.embedded_views_preview"
|
||||
android:value="true" />
|
||||
</application>
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name=
|
||||
"android.support.customtabs.action.CustomTabsService" />
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
+33
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+11
@@ -0,0 +1,11 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface ISettings<T> {
|
||||
@NonNull ISettings<T> parse(@NonNull Map<String, Object> settings);
|
||||
@NonNull Map<String, Object> toMap();
|
||||
@NonNull Map<String, Object> getRealSettings(@NonNull T obj);
|
||||
}
|
||||
+9
@@ -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";
|
||||
|
||||
}
|
||||
Vendored
Executable
+224
@@ -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;
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+452
@@ -0,0 +1,452 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android;
|
||||
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.webkit.CookieManager;
|
||||
import android.webkit.CookieSyncManager;
|
||||
import android.webkit.ValueCallback;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.webkit.CookieManagerCompat;
|
||||
import androidx.webkit.WebViewFeature;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
public class MyCookieManager extends ChannelDelegateImpl {
|
||||
protected static final String LOG_TAG = "MyCookieManager";
|
||||
public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_cookiemanager";
|
||||
@Nullable
|
||||
public static CookieManager cookieManager;
|
||||
private CookieManager selectedCookieManager;
|
||||
@Nullable
|
||||
public InAppWebViewFlutterPlugin plugin;
|
||||
|
||||
public MyCookieManager(@NonNull final InAppWebViewFlutterPlugin plugin) {
|
||||
super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME));
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public static void init() {
|
||||
if (cookieManager == null) {
|
||||
cookieManager = getCookieManager();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||
if (call.method.equals("gopeed.prepareProfile")) {
|
||||
GopeedProfiles.prepare(call, result);
|
||||
return;
|
||||
}
|
||||
String profileID = call.argument("gopeedProfileId");
|
||||
if (profileID != null && !profileID.isEmpty()) {
|
||||
if (!androidx.webkit.WebViewFeature.isFeatureSupported(androidx.webkit.WebViewFeature.MULTI_PROFILE) ||
|
||||
androidx.webkit.ProfileStore.getInstance().getProfile(profileID) == null) {
|
||||
result.error("UNAVAILABLE", "WebView profile is not initialized", null);
|
||||
return;
|
||||
}
|
||||
selectedCookieManager = androidx.webkit.ProfileStore.getInstance().getProfile(profileID).getCookieManager();
|
||||
}
|
||||
init();
|
||||
try {
|
||||
switch (call.method) {
|
||||
case "setCookie":
|
||||
{
|
||||
String url = (String) call.argument("url");
|
||||
String name = (String) call.argument("name");
|
||||
String value = (String) call.argument("value");
|
||||
String domain = (String) call.argument("domain");
|
||||
String path = (String) call.argument("path");
|
||||
String expiresDateString = (String) call.argument("expiresDate");
|
||||
Long expiresDate = (expiresDateString != null ? new Long(expiresDateString) : null);
|
||||
Integer maxAge = (Integer) call.argument("maxAge");
|
||||
Boolean isSecure = (Boolean) call.argument("isSecure");
|
||||
Boolean isHttpOnly = (Boolean) call.argument("isHttpOnly");
|
||||
String sameSite = (String) call.argument("sameSite");
|
||||
setCookie(url,
|
||||
name,
|
||||
value,
|
||||
domain,
|
||||
path,
|
||||
expiresDate,
|
||||
maxAge,
|
||||
isSecure,
|
||||
isHttpOnly,
|
||||
sameSite,
|
||||
result);
|
||||
}
|
||||
break;
|
||||
case "getCookies":
|
||||
result.success(getCookies((String) call.argument("url")));
|
||||
break;
|
||||
case "deleteCookie":
|
||||
{
|
||||
String url = (String) call.argument("url");
|
||||
String name = (String) call.argument("name");
|
||||
String domain = (String) call.argument("domain");
|
||||
String path = (String) call.argument("path");
|
||||
deleteCookie(url, name, domain, path, result);
|
||||
}
|
||||
break;
|
||||
case "deleteCookies":
|
||||
{
|
||||
String url = (String) call.argument("url");
|
||||
String domain = (String) call.argument("domain");
|
||||
String path = (String) call.argument("path");
|
||||
deleteCookies(url, domain, path, result);
|
||||
}
|
||||
break;
|
||||
case "deleteAllCookies":
|
||||
deleteAllCookies(result);
|
||||
break;
|
||||
case "removeSessionCookies":
|
||||
removeSessionCookies(result);
|
||||
break;
|
||||
default:
|
||||
result.notImplemented();
|
||||
}
|
||||
} finally { selectedCookieManager = null; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiating CookieManager will load the Chromium task taking a 100ish ms so we do it lazily
|
||||
* to make sure it's done on a background thread as needed.
|
||||
*
|
||||
* https://github.com/facebook/react-native/blob/1903f6680d9750e244d97c3cd4a9f755a9a47c61/ReactAndroid/src/main/java/com/facebook/react/modules/network/ForwardingCookieHandler.java#L132
|
||||
*/
|
||||
static private @Nullable CookieManager getCookieManager() {
|
||||
if (cookieManager == null) {
|
||||
try {
|
||||
cookieManager = CookieManager.getInstance();
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=559720
|
||||
return null;
|
||||
} catch (Exception exception) {
|
||||
String message = exception.getMessage();
|
||||
// We cannot catch MissingWebViewPackageException as it is in a private / system API
|
||||
// class. This validates the exception's message to ensure we are only handling this
|
||||
// specific exception.
|
||||
// https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/webkit/WebViewFactory.java#348
|
||||
if (message != null
|
||||
&& exception
|
||||
.getClass()
|
||||
.getCanonicalName()
|
||||
.equals("android.webkit.WebViewFactory.MissingWebViewPackageException")) {
|
||||
return null;
|
||||
} else {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cookieManager;
|
||||
}
|
||||
|
||||
public void setCookie(String url,
|
||||
String name,
|
||||
String value,
|
||||
String domain,
|
||||
String path,
|
||||
Long expiresDate,
|
||||
Integer maxAge,
|
||||
Boolean isSecure,
|
||||
Boolean isHttpOnly,
|
||||
String sameSite,
|
||||
final MethodChannel.Result result) {
|
||||
final CookieManager cookieManager = selectedCookieManager != null ? selectedCookieManager : getCookieManager();
|
||||
if (cookieManager == null) {
|
||||
result.success(false);
|
||||
return;
|
||||
}
|
||||
|
||||
String cookieValue = name + "=" + value + "; Path=" + path;
|
||||
|
||||
if (domain != null)
|
||||
cookieValue += "; Domain=" + domain;
|
||||
|
||||
if (expiresDate != null)
|
||||
cookieValue += "; Expires=" + getCookieExpirationDate(expiresDate);
|
||||
|
||||
if (maxAge != null)
|
||||
cookieValue += "; Max-Age=" + maxAge.toString();
|
||||
|
||||
if (isSecure != null && isSecure)
|
||||
cookieValue += "; Secure";
|
||||
|
||||
if (isHttpOnly != null && isHttpOnly)
|
||||
cookieValue += "; HttpOnly";
|
||||
|
||||
if (sameSite != null)
|
||||
cookieValue += "; SameSite=" + sameSite;
|
||||
|
||||
cookieValue += ";";
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
cookieManager.setCookie(url, cookieValue, new ValueCallback<Boolean>() {
|
||||
@Override
|
||||
public void onReceiveValue(Boolean successful) {
|
||||
result.success(successful);
|
||||
}
|
||||
});
|
||||
cookieManager.flush();
|
||||
}
|
||||
else if (plugin != null) {
|
||||
CookieSyncManager cookieSyncMngr = CookieSyncManager.createInstance(plugin.applicationContext);
|
||||
cookieSyncMngr.startSync();
|
||||
cookieManager.setCookie(url, cookieValue);
|
||||
cookieSyncMngr.stopSync();
|
||||
cookieSyncMngr.sync();
|
||||
result.success(true);
|
||||
} else {
|
||||
cookieManager.setCookie(url, cookieValue);
|
||||
result.success(true);
|
||||
}
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getCookies(final String url) {
|
||||
|
||||
final List<Map<String, Object>> cookieListMap = new ArrayList<>();
|
||||
|
||||
final CookieManager cookieManager = selectedCookieManager != null ? selectedCookieManager : getCookieManager();
|
||||
if (cookieManager == null) return cookieListMap;
|
||||
|
||||
List<String> cookies = new ArrayList<>();
|
||||
if (WebViewFeature.isFeatureSupported(WebViewFeature.GET_COOKIE_INFO)) {
|
||||
cookies = CookieManagerCompat.getCookieInfo(cookieManager, url);
|
||||
} else {
|
||||
String cookiesString = cookieManager.getCookie(url);
|
||||
if (cookiesString != null) {
|
||||
cookies = Arrays.asList(cookiesString.split(";"));
|
||||
}
|
||||
}
|
||||
|
||||
for (String cookie : cookies) {
|
||||
String[] cookieParams = cookie.split(";");
|
||||
if (cookieParams.length == 0) continue;
|
||||
|
||||
String[] nameValue = cookieParams[0].split("=", 2);
|
||||
String name = nameValue[0].trim();
|
||||
String value = (nameValue.length > 1) ? nameValue[1].trim() : "";
|
||||
|
||||
Map<String, Object> cookieMap = new HashMap<>();
|
||||
cookieMap.put("name", name);
|
||||
cookieMap.put("value", value);
|
||||
cookieMap.put("expiresDate", null);
|
||||
cookieMap.put("isSessionOnly", null);
|
||||
cookieMap.put("domain", null);
|
||||
cookieMap.put("sameSite", null);
|
||||
cookieMap.put("isSecure", null);
|
||||
cookieMap.put("isHttpOnly", null);
|
||||
cookieMap.put("path", null);
|
||||
|
||||
if (WebViewFeature.isFeatureSupported(WebViewFeature.GET_COOKIE_INFO)) {
|
||||
cookieMap.put("isSecure", false);
|
||||
cookieMap.put("isHttpOnly", false);
|
||||
|
||||
for (int i = 1; i < cookieParams.length; i++) {
|
||||
String[] cookieParamNameValue = cookieParams[i].split("=", 2);
|
||||
String cookieParamName = cookieParamNameValue[0].trim();
|
||||
String cookieParamValue = (cookieParamNameValue.length > 1) ? cookieParamNameValue[1].trim() : "";
|
||||
|
||||
if (cookieParamName.equalsIgnoreCase("Expires")) {
|
||||
try {
|
||||
final SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy hh:mm:ss z", Locale.US);
|
||||
Date expiryDate = sdf.parse(cookieParamValue);
|
||||
if (expiryDate != null) {
|
||||
cookieMap.put("expiresDate", expiryDate.getTime());
|
||||
}
|
||||
} catch (ParseException e) {
|
||||
Log.e(LOG_TAG, "", e);
|
||||
}
|
||||
} else if (cookieParamName.equalsIgnoreCase("Max-Age")) {
|
||||
try {
|
||||
long maxAge = Long.parseLong(cookieParamValue);
|
||||
cookieMap.put("expiresDate", System.currentTimeMillis() + maxAge);
|
||||
} catch (NumberFormatException e) {
|
||||
Log.e(LOG_TAG, "", e);
|
||||
}
|
||||
} else if (cookieParamName.equalsIgnoreCase("Domain")) {
|
||||
cookieMap.put("domain", cookieParamValue);
|
||||
} else if (cookieParamName.equalsIgnoreCase("SameSite")) {
|
||||
cookieMap.put("sameSite", cookieParamValue);
|
||||
} else if (cookieParamName.equalsIgnoreCase("Secure")) {
|
||||
cookieMap.put("isSecure", true);
|
||||
} else if (cookieParamName.equalsIgnoreCase("HttpOnly")) {
|
||||
cookieMap.put("isHttpOnly", true);
|
||||
} else if (cookieParamName.equalsIgnoreCase("Path")) {
|
||||
cookieMap.put("path", cookieParamValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cookieListMap.add(cookieMap);
|
||||
}
|
||||
return cookieListMap;
|
||||
|
||||
}
|
||||
|
||||
public void deleteCookie(String url, String name, String domain, String path, final MethodChannel.Result result) {
|
||||
final CookieManager cookieManager = selectedCookieManager != null ? selectedCookieManager : getCookieManager();
|
||||
if (cookieManager == null) {
|
||||
result.success(false);
|
||||
return;
|
||||
}
|
||||
|
||||
String cookieValue = name + "=; Path=" + path + "; Max-Age=-1";
|
||||
|
||||
if (domain != null)
|
||||
cookieValue += "; Domain=" + domain;
|
||||
|
||||
cookieValue += ";";
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
cookieManager.setCookie(url, cookieValue, new ValueCallback<Boolean>() {
|
||||
@Override
|
||||
public void onReceiveValue(Boolean successful) {
|
||||
result.success(successful);
|
||||
}
|
||||
});
|
||||
cookieManager.flush();
|
||||
}
|
||||
else if (plugin != null) {
|
||||
CookieSyncManager cookieSyncMngr = CookieSyncManager.createInstance(plugin.applicationContext);
|
||||
cookieSyncMngr.startSync();
|
||||
cookieManager.setCookie(url, cookieValue);
|
||||
cookieSyncMngr.stopSync();
|
||||
cookieSyncMngr.sync();
|
||||
result.success(true);
|
||||
} else {
|
||||
cookieManager.setCookie(url, cookieValue);
|
||||
result.success(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteCookies(String url, String domain, String path, final MethodChannel.Result result) {
|
||||
final CookieManager cookieManager = selectedCookieManager != null ? selectedCookieManager : getCookieManager();
|
||||
if (cookieManager == null) {
|
||||
result.success(false);
|
||||
return;
|
||||
}
|
||||
|
||||
CookieSyncManager cookieSyncMngr = null;
|
||||
|
||||
String cookiesString = cookieManager.getCookie(url);
|
||||
if (cookiesString != null) {
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP && plugin != null) {
|
||||
cookieSyncMngr = CookieSyncManager.createInstance(plugin.applicationContext);
|
||||
cookieSyncMngr.startSync();
|
||||
}
|
||||
|
||||
String[] cookies = cookiesString.split(";");
|
||||
for (String cookie : cookies) {
|
||||
String[] nameValue = cookie.split("=", 2);
|
||||
String name = nameValue[0].trim();
|
||||
|
||||
String cookieValue = name + "=; Path=" + path + "; Max-Age=-1";
|
||||
|
||||
if (domain != null)
|
||||
cookieValue += "; Domain=" + domain;
|
||||
|
||||
cookieValue += ";";
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
|
||||
cookieManager.setCookie(url, cookieValue, null);
|
||||
else
|
||||
cookieManager.setCookie(url, cookieValue);
|
||||
}
|
||||
|
||||
if (cookieSyncMngr != null) {
|
||||
cookieSyncMngr.stopSync();
|
||||
cookieSyncMngr.sync();
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
|
||||
cookieManager.flush();
|
||||
}
|
||||
result.success(true);
|
||||
}
|
||||
|
||||
public void deleteAllCookies(final MethodChannel.Result result) {
|
||||
final CookieManager cookieManager = selectedCookieManager != null ? selectedCookieManager : getCookieManager();
|
||||
if (cookieManager == null) {
|
||||
result.success(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
cookieManager.removeAllCookies(new ValueCallback<Boolean>() {
|
||||
@Override
|
||||
public void onReceiveValue(Boolean successful) {
|
||||
result.success(successful);
|
||||
}
|
||||
});
|
||||
cookieManager.flush();
|
||||
}
|
||||
else if (plugin != null) {
|
||||
CookieSyncManager cookieSyncMngr = CookieSyncManager.createInstance(plugin.applicationContext);
|
||||
cookieSyncMngr.startSync();
|
||||
cookieManager.removeAllCookie();
|
||||
cookieSyncMngr.stopSync();
|
||||
cookieSyncMngr.sync();
|
||||
result.success(true);
|
||||
} else {
|
||||
cookieManager.removeAllCookie();
|
||||
result.success(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeSessionCookies(final MethodChannel.Result result) {
|
||||
final CookieManager cookieManager = selectedCookieManager != null ? selectedCookieManager : getCookieManager();
|
||||
if (cookieManager == null) {
|
||||
result.success(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
cookieManager.removeSessionCookies(new ValueCallback<Boolean>() {
|
||||
@Override
|
||||
public void onReceiveValue(Boolean successful) {
|
||||
result.success(successful);
|
||||
}
|
||||
});
|
||||
cookieManager.flush();
|
||||
}
|
||||
else if (plugin != null) {
|
||||
CookieSyncManager cookieSyncMngr = CookieSyncManager.createInstance(plugin.applicationContext);
|
||||
cookieSyncMngr.startSync();
|
||||
cookieManager.removeSessionCookie();
|
||||
cookieSyncMngr.stopSync();
|
||||
cookieSyncMngr.sync();
|
||||
result.success(true);
|
||||
} else {
|
||||
cookieManager.removeSessionCookie();
|
||||
result.success(true);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getCookieExpirationDate(Long timestamp) {
|
||||
final SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy hh:mm:ss z", Locale.US);
|
||||
sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
|
||||
return sdf.format(new Date(timestamp));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
plugin = null;
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+138
@@ -0,0 +1,138 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android;
|
||||
|
||||
import android.webkit.ValueCallback;
|
||||
import android.webkit.WebStorage;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
public class MyWebStorage extends ChannelDelegateImpl {
|
||||
protected static final String LOG_TAG = "MyWebStorage";
|
||||
public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_webstoragemanager";
|
||||
|
||||
@Nullable
|
||||
public static WebStorage webStorageManager;
|
||||
@Nullable
|
||||
public InAppWebViewFlutterPlugin plugin;
|
||||
|
||||
public MyWebStorage(@NonNull final InAppWebViewFlutterPlugin plugin) {
|
||||
super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME));
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public static void init() {
|
||||
if (webStorageManager == null) {
|
||||
webStorageManager = WebStorage.getInstance();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||
init();
|
||||
|
||||
switch (call.method) {
|
||||
case "getOrigins":
|
||||
getOrigins(result);
|
||||
break;
|
||||
case "deleteAllData":
|
||||
if (webStorageManager != null) {
|
||||
webStorageManager.deleteAllData();
|
||||
result.success(true);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
case "deleteOrigin":
|
||||
{
|
||||
if (webStorageManager != null) {
|
||||
String origin = (String) call.argument("origin");
|
||||
webStorageManager.deleteOrigin(origin);
|
||||
result.success(true);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "getQuotaForOrigin":
|
||||
{
|
||||
String origin = (String) call.argument("origin");
|
||||
getQuotaForOrigin(origin, result);
|
||||
}
|
||||
break;
|
||||
case "getUsageForOrigin":
|
||||
{
|
||||
String origin = (String) call.argument("origin");
|
||||
getUsageForOrigin(origin, result);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
result.notImplemented();
|
||||
}
|
||||
}
|
||||
|
||||
public void getOrigins(final MethodChannel.Result result) {
|
||||
if (webStorageManager == null) {
|
||||
result.success(new ArrayList<>());
|
||||
return;
|
||||
}
|
||||
webStorageManager.getOrigins(new ValueCallback<Map>() {
|
||||
@Override
|
||||
public void onReceiveValue(Map value) {
|
||||
List<Map<String, Object>> origins = new ArrayList<>();
|
||||
for(Object key : value.keySet()) {
|
||||
WebStorage.Origin originObj = (WebStorage.Origin) value.get(key);
|
||||
|
||||
Map<String, Object> originInfo = new HashMap<>();
|
||||
originInfo.put("origin", originObj.getOrigin());
|
||||
originInfo.put("quota", originObj.getQuota());
|
||||
originInfo.put("usage", originObj.getUsage());
|
||||
|
||||
origins.add(originInfo);
|
||||
}
|
||||
result.success(origins);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void getQuotaForOrigin(String origin, final MethodChannel.Result result) {
|
||||
if (webStorageManager == null) {
|
||||
result.success(0);
|
||||
return;
|
||||
}
|
||||
webStorageManager.getQuotaForOrigin(origin, new ValueCallback<Long>() {
|
||||
@Override
|
||||
public void onReceiveValue(Long value) {
|
||||
result.success(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void getUsageForOrigin(String origin, final MethodChannel.Result result) {
|
||||
if (webStorageManager == null) {
|
||||
result.success(0);
|
||||
return;
|
||||
}
|
||||
webStorageManager.getUsageForOrigin(origin, new ValueCallback<Long>() {
|
||||
@Override
|
||||
public void onReceiveValue(Long value) {
|
||||
result.success(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
plugin = null;
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+73
@@ -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;
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+386
@@ -0,0 +1,386 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Insets;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.http.SslCertificate;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
import android.view.WindowMetrics;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.Size2D;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.SyncBaseCallbackResultImpl;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URL;
|
||||
import java.net.UnknownHostException;
|
||||
import java.security.Key;
|
||||
import java.security.KeyStore;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
public class Util {
|
||||
|
||||
static final String LOG_TAG = "Util";
|
||||
public static final String ANDROID_ASSET_URL = "file:///android_asset/";
|
||||
|
||||
private Util() {}
|
||||
|
||||
public static String getUrlAsset(InAppWebViewFlutterPlugin plugin, String assetFilePath) throws IOException {
|
||||
String key = plugin.flutterAssets.getAssetFilePathByName(assetFilePath);
|
||||
InputStream is = null;
|
||||
IOException e = null;
|
||||
|
||||
try {
|
||||
is = getFileAsset(plugin, assetFilePath);
|
||||
} catch (IOException ex) {
|
||||
e = ex;
|
||||
} finally {
|
||||
if (is != null) {
|
||||
try {
|
||||
is.close();
|
||||
} catch (IOException ex) {
|
||||
e = ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (e != null) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
return ANDROID_ASSET_URL + key;
|
||||
}
|
||||
|
||||
public static InputStream getFileAsset(InAppWebViewFlutterPlugin plugin, String assetFilePath) throws IOException {
|
||||
String key = plugin.flutterAssets.getAssetFilePathByName(assetFilePath);
|
||||
AssetManager mg = plugin.applicationContext.getResources().getAssets();
|
||||
return mg.open(key);
|
||||
}
|
||||
|
||||
public static <T> T invokeMethodAndWaitResult(final @NonNull MethodChannel channel,
|
||||
final @NonNull String method, final @Nullable Object arguments,
|
||||
final @NonNull SyncBaseCallbackResultImpl<T> callback) throws InterruptedException {
|
||||
Handler handler = new Handler(Looper.getMainLooper());
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
channel.invokeMethod(method, arguments, callback);
|
||||
}
|
||||
});
|
||||
callback.latch.await();
|
||||
return callback.result;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static PrivateKeyAndCertificates loadPrivateKeyAndCertificate(@NonNull InAppWebViewFlutterPlugin plugin,
|
||||
@NonNull String certificatePath,
|
||||
@Nullable String certificatePassword,
|
||||
@NonNull String keyStoreType) {
|
||||
PrivateKeyAndCertificates privateKeyAndCertificates = null;
|
||||
InputStream certificateFileStream = null;
|
||||
|
||||
try {
|
||||
certificateFileStream = getFileAsset(plugin, certificatePath);
|
||||
} catch (IOException ignored) {}
|
||||
|
||||
try {
|
||||
if (certificateFileStream == null) {
|
||||
certificateFileStream = new FileInputStream(certificatePath);
|
||||
}
|
||||
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
|
||||
keyStore.load(certificateFileStream, (certificatePassword != null ? certificatePassword : "").toCharArray());
|
||||
|
||||
Enumeration<String> aliases = keyStore.aliases();
|
||||
String alias = aliases.nextElement();
|
||||
|
||||
Key key = keyStore.getKey(alias, (certificatePassword != null ? certificatePassword : "").toCharArray());
|
||||
if (key instanceof PrivateKey) {
|
||||
PrivateKey privateKey = (PrivateKey)key;
|
||||
Certificate cert = keyStore.getCertificate(alias);
|
||||
X509Certificate[] certificates = new X509Certificate[1];
|
||||
certificates[0] = (X509Certificate)cert;
|
||||
privateKeyAndCertificates = new PrivateKeyAndCertificates(privateKey, certificates);
|
||||
}
|
||||
certificateFileStream.close();
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "", e);
|
||||
} finally {
|
||||
if (certificateFileStream != null) {
|
||||
try {
|
||||
certificateFileStream.close();
|
||||
} catch (IOException ex) {
|
||||
Log.e(LOG_TAG, "", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return privateKeyAndCertificates;
|
||||
}
|
||||
|
||||
public static class PrivateKeyAndCertificates {
|
||||
|
||||
public X509Certificate[] certificates;
|
||||
public PrivateKey privateKey;
|
||||
|
||||
public PrivateKeyAndCertificates(PrivateKey privateKey, X509Certificate[] certificates) {
|
||||
this.privateKey = privateKey;
|
||||
this.certificates = certificates;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static HttpURLConnection makeHttpRequest(String urlString, String method, @Nullable Map<String, String> headers) {
|
||||
HttpURLConnection urlConnection = null;
|
||||
try {
|
||||
URL url = new URL(urlString);
|
||||
urlConnection = (HttpURLConnection) url.openConnection();
|
||||
urlConnection.setRequestMethod(method);
|
||||
if (headers != null) {
|
||||
for (Map.Entry<String, String> header : headers.entrySet()) {
|
||||
urlConnection.setRequestProperty(header.getKey(), header.getValue());
|
||||
}
|
||||
}
|
||||
urlConnection.setConnectTimeout(15000); // 15 seconds
|
||||
urlConnection.setReadTimeout(15000); // 15 seconds
|
||||
urlConnection.setDoInput(true);
|
||||
urlConnection.setInstanceFollowRedirects(true);
|
||||
if ("GET".equalsIgnoreCase(method)) {
|
||||
urlConnection.setDoOutput(false);
|
||||
}
|
||||
urlConnection.connect();
|
||||
return urlConnection;
|
||||
}
|
||||
catch (Exception e) {
|
||||
if (!(e instanceof SSLHandshakeException)) {
|
||||
Log.e(LOG_TAG, "", e);
|
||||
}
|
||||
if (urlConnection != null) {
|
||||
urlConnection.disconnect();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* SslCertificate class does not has a public getter for the underlying
|
||||
* X509Certificate, we can only do this by hack. This only works for Android 4.0+
|
||||
* https://groups.google.com/forum/#!topic/android-developers/eAPJ6b7mrmg
|
||||
*/
|
||||
public static X509Certificate getX509CertFromSslCertHack(SslCertificate sslCert) {
|
||||
X509Certificate x509Certificate = null;
|
||||
|
||||
Bundle bundle = SslCertificate.saveState(sslCert);
|
||||
byte[] bytes = bundle.getByteArray("x509-certificate");
|
||||
|
||||
if (bytes == null) {
|
||||
x509Certificate = null;
|
||||
} else {
|
||||
try {
|
||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
|
||||
Certificate cert = certFactory.generateCertificate(new ByteArrayInputStream(bytes));
|
||||
x509Certificate = (X509Certificate) cert;
|
||||
} catch (CertificateException e) {
|
||||
x509Certificate = null;
|
||||
}
|
||||
}
|
||||
|
||||
return x509Certificate;
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
|
||||
public static String JSONStringify(@Nullable Object value) {
|
||||
if (value == null) {
|
||||
return "null";
|
||||
}
|
||||
if (value instanceof Map) {
|
||||
return new JSONObject((Map<String, Object>) value).toString();
|
||||
} else if (value instanceof List) {
|
||||
return new JSONArray((List<Object>) value).toString();
|
||||
} else if (value instanceof String) {
|
||||
return JSONObject.quote((String) value);
|
||||
} else {
|
||||
return JSONObject.wrap(value).toString();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean objEquals(@Nullable Object a, @Nullable Object b) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
return Objects.equals(a, b);
|
||||
}
|
||||
return (a == b) || (a != null && a.equals(b));
|
||||
}
|
||||
|
||||
public static String replaceAll(String s, String oldString, String newString) {
|
||||
return TextUtils.join(newString, s.split(Pattern.quote(oldString)));
|
||||
}
|
||||
|
||||
public static void log(String tag, String message) {
|
||||
// Split by line, then ensure each line can fit into Log's maximum length.
|
||||
for (int i = 0, length = message.length(); i < length; i++) {
|
||||
int newline = message.indexOf('\n', i);
|
||||
newline = newline != -1 ? newline : length;
|
||||
do {
|
||||
int end = Math.min(newline, i + 4000);
|
||||
Log.d(tag, message.substring(i, end));
|
||||
i = end;
|
||||
} while (i < newline);
|
||||
}
|
||||
}
|
||||
|
||||
public static float getPixelDensity(Context context) {
|
||||
return context.getResources().getDisplayMetrics().density;
|
||||
}
|
||||
|
||||
public static Size2D getFullscreenSize(Context context) {
|
||||
Size2D fullscreenSize = new Size2D(-1, -1);
|
||||
WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
|
||||
if (wm != null) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
final WindowMetrics metrics = wm.getCurrentWindowMetrics();
|
||||
// Gets all excluding insets
|
||||
final WindowInsets windowInsets = metrics.getWindowInsets();
|
||||
Insets insets = windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars()
|
||||
| WindowInsets.Type.displayCutout());
|
||||
int insetsWidth = insets.right + insets.left;
|
||||
int insetsHeight = insets.top + insets.bottom;
|
||||
final Rect bounds = metrics.getBounds();
|
||||
fullscreenSize.setWidth(bounds.width() - insetsWidth);
|
||||
fullscreenSize.setHeight(bounds.height() - insetsHeight);
|
||||
} else {
|
||||
DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||
wm.getDefaultDisplay().getMetrics(displayMetrics);
|
||||
fullscreenSize.setWidth(displayMetrics.widthPixels);
|
||||
fullscreenSize.setHeight(displayMetrics.heightPixels);
|
||||
}
|
||||
}
|
||||
return fullscreenSize;
|
||||
}
|
||||
|
||||
public static boolean isClass(String className) {
|
||||
try {
|
||||
Class.forName(className);
|
||||
return true;
|
||||
} catch (ClassNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isIPv6(String address) {
|
||||
try {
|
||||
Inet6Address.getByName(address);
|
||||
} catch (UnknownHostException e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static String normalizeIPv6(String address) throws Exception {
|
||||
if (!Util.isIPv6(address)) {
|
||||
throw new Exception("Invalid address: " + address);
|
||||
}
|
||||
return InetAddress.getByName(address).getCanonicalHostName();
|
||||
}
|
||||
|
||||
public static <T> T getOrDefault(Map<String, Object> map, String key, T defaultValue) {
|
||||
return map.containsKey(key) ? (T) map.get(key) : defaultValue;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static byte[] readAllBytes(@Nullable InputStream inputStream) {
|
||||
if (inputStream == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final int bufLen = 4 * 0x400; // 4KB
|
||||
byte[] buf = new byte[bufLen];
|
||||
int readLen;
|
||||
IOException exception = null;
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
byte[] data = null;
|
||||
|
||||
try {
|
||||
while ((readLen = inputStream.read(buf, 0, bufLen)) != -1)
|
||||
outputStream.write(buf, 0, readLen);
|
||||
|
||||
data = outputStream.toByteArray();
|
||||
} catch (IOException e) {
|
||||
exception = e;
|
||||
} finally {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && exception != null) {
|
||||
exception.addSuppressed(e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
outputStream.close();
|
||||
} catch (IOException e) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && exception != null) {
|
||||
exception.addSuppressed(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static <O> Object invokeMethodIfExists(final O o, final String methodName, Object... args) {
|
||||
Method[] methods = o.getClass().getMethods();
|
||||
for (Method method : methods) {
|
||||
if (method.getName().equals(methodName)) {
|
||||
try {
|
||||
return method.invoke(o, args);
|
||||
} catch (IllegalAccessException e) {
|
||||
return null;
|
||||
} catch (InvocationTargetException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Drawable drawableFromBytes(Context context, byte[] data) {
|
||||
return new BitmapDrawable(context.getResources(), BitmapFactory.decodeByteArray(data, 0, data.length));
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+47
@@ -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;
|
||||
}
|
||||
}
|
||||
+47
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+462
@@ -0,0 +1,462 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.widget.RemoteViews;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams;
|
||||
import androidx.browser.customtabs.CustomTabsCallback;
|
||||
import androidx.browser.customtabs.CustomTabsIntent;
|
||||
import androidx.browser.customtabs.CustomTabsService;
|
||||
import androidx.browser.customtabs.CustomTabsSession;
|
||||
import androidx.browser.customtabs.EngagementSignalsCallback;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.R;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.AndroidResource;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.CustomTabsActionButton;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.CustomTabsMenuItem;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.CustomTabsSecondaryToolbar;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
public class ChromeCustomTabsActivity extends Activity implements Disposable {
|
||||
protected static final String LOG_TAG = "CustomTabsActivity";
|
||||
public static final String METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_chromesafaribrowser_";
|
||||
|
||||
public String id;
|
||||
@Nullable
|
||||
public CustomTabsIntent.Builder builder;
|
||||
public ChromeCustomTabsSettings customSettings = new ChromeCustomTabsSettings();
|
||||
public CustomTabActivityHelper customTabActivityHelper = new CustomTabActivityHelper();
|
||||
@Nullable
|
||||
public CustomTabsSession customTabsSession;
|
||||
public final static int CHROME_CUSTOM_TAB_REQUEST_CODE = 100;
|
||||
public final static int NO_HISTORY_CHROME_CUSTOM_TAB_REQUEST_CODE = 101;
|
||||
protected boolean onOpened = false;
|
||||
protected boolean onCompletedInitialLoad = false;
|
||||
protected boolean isBindSuccess = false;
|
||||
@Nullable
|
||||
public ChromeSafariBrowserManager manager;
|
||||
@Nullable
|
||||
public String initialUrl;
|
||||
@Nullable
|
||||
public List<String> initialOtherLikelyURLs;
|
||||
@Nullable
|
||||
public Map<String, String> initialHeaders;
|
||||
@Nullable
|
||||
public String initialReferrer;
|
||||
public List<CustomTabsMenuItem> menuItems = new ArrayList<>();
|
||||
@Nullable
|
||||
public CustomTabsActionButton actionButton;
|
||||
@Nullable
|
||||
public CustomTabsSecondaryToolbar secondaryToolbar;
|
||||
@Nullable
|
||||
public ChromeCustomTabsChannelDelegate channelDelegate;
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.chrome_custom_tabs_layout);
|
||||
|
||||
Bundle b = getIntent().getExtras();
|
||||
if (b == null) return;
|
||||
|
||||
id = b.getString("id");
|
||||
|
||||
String managerId = b.getString("managerId");
|
||||
manager = ChromeSafariBrowserManager.shared.get(managerId);
|
||||
if (manager == null || manager.plugin == null || manager.plugin.messenger == null) return;
|
||||
|
||||
manager.browsers.put(id, this);
|
||||
|
||||
MethodChannel channel = new MethodChannel(manager.plugin.messenger, METHOD_CHANNEL_NAME_PREFIX + id);
|
||||
channelDelegate = new ChromeCustomTabsChannelDelegate(this, channel);
|
||||
|
||||
initialUrl = b.getString("url");
|
||||
initialHeaders = (Map<String, String>) b.getSerializable("headers");
|
||||
initialReferrer = b.getString("referrer");
|
||||
initialOtherLikelyURLs = b.getStringArrayList("otherLikelyURLs");
|
||||
|
||||
customSettings = new ChromeCustomTabsSettings();
|
||||
customSettings.parse((HashMap<String, Object>) b.getSerializable("settings"));
|
||||
actionButton = CustomTabsActionButton.fromMap((Map<String, Object>) b.getSerializable("actionButton"));
|
||||
secondaryToolbar = CustomTabsSecondaryToolbar.fromMap((Map<String, Object>) b.getSerializable("secondaryToolbar"));
|
||||
List<Map<String, Object>> menuItemList = (List<Map<String, Object>>) b.getSerializable("menuItemList");
|
||||
for (Map<String, Object> menuItem : menuItemList) {
|
||||
menuItems.add(CustomTabsMenuItem.fromMap(menuItem));
|
||||
}
|
||||
|
||||
if (customSettings.noHistory && manager.plugin.noHistoryCustomTabsActivityCallbacks != null) {
|
||||
manager.plugin.noHistoryCustomTabsActivityCallbacks.noHistoryBrowserIDs.put(id, id);
|
||||
}
|
||||
|
||||
final ChromeCustomTabsActivity chromeCustomTabsActivity = this;
|
||||
|
||||
customTabActivityHelper.setConnectionCallback(new CustomTabActivityHelper.ConnectionCallback() {
|
||||
@Override
|
||||
public void onCustomTabsConnected() {
|
||||
customTabsConnected();
|
||||
if (channelDelegate != null) {
|
||||
channelDelegate.onServiceConnected();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCustomTabsDisconnected() {
|
||||
chromeCustomTabsActivity.close();
|
||||
dispose();
|
||||
}
|
||||
});
|
||||
|
||||
customTabActivityHelper.setCustomTabsCallback(new CustomTabsCallback() {
|
||||
@Override
|
||||
public void onNavigationEvent(int navigationEvent, Bundle extras) {
|
||||
if (navigationEvent == TAB_SHOWN && !onOpened) {
|
||||
onOpened = true;
|
||||
if (channelDelegate != null) {
|
||||
channelDelegate.onOpened();
|
||||
}
|
||||
}
|
||||
|
||||
if (navigationEvent == NAVIGATION_FINISHED && !onCompletedInitialLoad) {
|
||||
onCompletedInitialLoad = true;
|
||||
if (channelDelegate != null) {
|
||||
channelDelegate.onCompletedInitialLoad();
|
||||
}
|
||||
}
|
||||
|
||||
if (channelDelegate != null) {
|
||||
channelDelegate.onNavigationEvent(navigationEvent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extraCallback(@NonNull String callbackName, Bundle args) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageChannelReady(Bundle extras) {
|
||||
if (channelDelegate != null) {
|
||||
channelDelegate.onMessageChannelReady();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostMessage(@NonNull String message, Bundle extras) {
|
||||
if (channelDelegate != null) {
|
||||
channelDelegate.onPostMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRelationshipValidationResult(@CustomTabsService.Relation int relation,
|
||||
@NonNull Uri requestedOrigin,
|
||||
boolean result, Bundle extras) {
|
||||
if (channelDelegate != null) {
|
||||
channelDelegate.onRelationshipValidationResult(relation, requestedOrigin, result);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void launchUrl(@NonNull String url,
|
||||
@Nullable Map<String, String> headers,
|
||||
@Nullable String referrer,
|
||||
@Nullable List<String> otherLikelyURLs) {
|
||||
launchUrlWithSession(customTabsSession, url, headers, referrer, otherLikelyURLs);
|
||||
}
|
||||
|
||||
public void launchUrlWithSession(@Nullable CustomTabsSession session,
|
||||
@NonNull String url,
|
||||
@Nullable Map<String, String> headers,
|
||||
@Nullable String referrer,
|
||||
@Nullable List<String> otherLikelyURLs) {
|
||||
mayLaunchUrl(url, otherLikelyURLs);
|
||||
builder = new CustomTabsIntent.Builder(session);
|
||||
prepareCustomTabs();
|
||||
|
||||
CustomTabsIntent customTabsIntent = builder.build();
|
||||
prepareCustomTabsIntent(customTabsIntent);
|
||||
|
||||
CustomTabActivityHelper.openCustomTab(this, customTabsIntent, Uri.parse(url), headers,
|
||||
referrer != null ? Uri.parse(referrer) : null, CHROME_CUSTOM_TAB_REQUEST_CODE);
|
||||
}
|
||||
|
||||
public boolean mayLaunchUrl(@Nullable String url, @Nullable List<String> otherLikelyURLs) {
|
||||
Uri uri = url != null ? Uri.parse(url) : null;
|
||||
|
||||
List<Bundle> bundleOtherLikelyURLs = new ArrayList<>();
|
||||
if (otherLikelyURLs != null) {
|
||||
Bundle bundleOtherLikelyURL = new Bundle();
|
||||
for (String otherLikelyURL : otherLikelyURLs) {
|
||||
bundleOtherLikelyURL.putString(CustomTabsService.KEY_URL, otherLikelyURL);
|
||||
}
|
||||
}
|
||||
return customTabActivityHelper.mayLaunchUrl(uri, null, bundleOtherLikelyURLs);
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
public void customTabsConnected() {
|
||||
customTabsSession = customTabActivityHelper.getSession();
|
||||
|
||||
if (customTabsSession != null) {
|
||||
try {
|
||||
Bundle bundle = new Bundle();
|
||||
if (customTabsSession.isEngagementSignalsApiAvailable(bundle)) {
|
||||
customTabsSession.setEngagementSignalsCallback(new EngagementSignalsCallback() {
|
||||
@Override
|
||||
public void onVerticalScrollEvent(boolean isDirectionUp, @NonNull Bundle extras) {
|
||||
if (channelDelegate != null) {
|
||||
channelDelegate.onVerticalScrollEvent(isDirectionUp);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGreatestScrollPercentageIncreased(int scrollPercentage, @NonNull Bundle extras) {
|
||||
if (channelDelegate != null) {
|
||||
channelDelegate.onGreatestScrollPercentageIncreased(scrollPercentage);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionEnded(boolean didUserInteract, @NonNull Bundle extras) {
|
||||
if (channelDelegate != null) {
|
||||
channelDelegate.onSessionEnded(didUserInteract);
|
||||
}
|
||||
}
|
||||
}, bundle);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
Log.d(LOG_TAG, "Custom Tabs Engagement Signals API not supported", e);
|
||||
}
|
||||
}
|
||||
|
||||
// avoid webpage reopen if isBindSuccess is false: onServiceConnected->launchUrl
|
||||
if (isBindSuccess && initialUrl != null) {
|
||||
launchUrl(initialUrl, initialHeaders, initialReferrer, initialOtherLikelyURLs);
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareCustomTabs() {
|
||||
if (builder == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (customSettings.addDefaultShareMenuItem != null) {
|
||||
builder.setShareState(customSettings.addDefaultShareMenuItem ?
|
||||
CustomTabsIntent.SHARE_STATE_ON : CustomTabsIntent.SHARE_STATE_OFF);
|
||||
} else {
|
||||
builder.setShareState(customSettings.shareState);
|
||||
}
|
||||
|
||||
CustomTabColorSchemeParams.Builder defaultColorSchemeBuilder = new CustomTabColorSchemeParams.Builder();
|
||||
if (customSettings.toolbarBackgroundColor != null && !customSettings.toolbarBackgroundColor.isEmpty()) {
|
||||
defaultColorSchemeBuilder.setToolbarColor(Color.parseColor(customSettings.toolbarBackgroundColor));
|
||||
}
|
||||
if (customSettings.navigationBarColor != null && !customSettings.navigationBarColor.isEmpty()) {
|
||||
defaultColorSchemeBuilder.setNavigationBarColor(Color.parseColor(customSettings.navigationBarColor));
|
||||
}
|
||||
if (customSettings.navigationBarDividerColor != null && !customSettings.navigationBarDividerColor.isEmpty()) {
|
||||
defaultColorSchemeBuilder.setNavigationBarDividerColor(Color.parseColor(customSettings.navigationBarDividerColor));
|
||||
}
|
||||
if (customSettings.secondaryToolbarColor != null && !customSettings.secondaryToolbarColor.isEmpty()) {
|
||||
defaultColorSchemeBuilder.setSecondaryToolbarColor(Color.parseColor(customSettings.secondaryToolbarColor));
|
||||
}
|
||||
builder.setDefaultColorSchemeParams(defaultColorSchemeBuilder.build());
|
||||
|
||||
builder.setShowTitle(customSettings.showTitle);
|
||||
builder.setUrlBarHidingEnabled(customSettings.enableUrlBarHiding);
|
||||
builder.setInstantAppsEnabled(customSettings.instantAppsEnabled);
|
||||
if (customSettings.startAnimations.size() == 2) {
|
||||
builder.setStartAnimations(this,
|
||||
customSettings.startAnimations.get(0).getIdentifier(this),
|
||||
customSettings.startAnimations.get(1).getIdentifier(this));
|
||||
}
|
||||
if (customSettings.exitAnimations.size() == 2) {
|
||||
builder.setExitAnimations(this,
|
||||
customSettings.exitAnimations.get(0).getIdentifier(this),
|
||||
customSettings.exitAnimations.get(1).getIdentifier(this));
|
||||
}
|
||||
|
||||
for (CustomTabsMenuItem menuItem : menuItems) {
|
||||
builder.addMenuItem(menuItem.getLabel(),
|
||||
createPendingIntent(menuItem.getId()));
|
||||
}
|
||||
|
||||
if (actionButton != null) {
|
||||
byte[] data = actionButton.getIcon();
|
||||
BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
|
||||
bitmapOptions.inMutable = true;
|
||||
Bitmap bmp = BitmapFactory.decodeByteArray(
|
||||
data, 0, data.length, bitmapOptions
|
||||
);
|
||||
builder.setActionButton(bmp, actionButton.getDescription(),
|
||||
createPendingIntent(actionButton.getId()),
|
||||
actionButton.isShouldTint());
|
||||
}
|
||||
|
||||
if (secondaryToolbar != null) {
|
||||
AndroidResource layout = secondaryToolbar.getLayout();
|
||||
RemoteViews remoteViews = new RemoteViews(layout.getDefPackage(), layout.getIdentifier(this));
|
||||
int[] clickableIDs = new int[secondaryToolbar.getClickableIDs().size()];
|
||||
for (int i = 0, length = secondaryToolbar.getClickableIDs().size(); i < length; i++) {
|
||||
AndroidResource clickableID = secondaryToolbar.getClickableIDs().get(i);
|
||||
clickableIDs[i] = clickableID.getIdentifier(this);
|
||||
}
|
||||
builder.setSecondaryToolbarViews(remoteViews, clickableIDs, getSecondaryToolbarOnClickPendingIntent());
|
||||
}
|
||||
}
|
||||
|
||||
public PendingIntent getSecondaryToolbarOnClickPendingIntent() {
|
||||
Intent broadcastIntent = new Intent(this, ActionBroadcastReceiver.class);
|
||||
|
||||
Bundle extras = new Bundle();
|
||||
extras.putString(ActionBroadcastReceiver.KEY_ACTION_VIEW_ID, id);
|
||||
extras.putString(ActionBroadcastReceiver.KEY_ACTION_MANAGER_ID, manager != null ? manager.id : null);
|
||||
broadcastIntent.putExtras(extras);
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
return PendingIntent.getBroadcast(
|
||||
this, 0, broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||
} else {
|
||||
return PendingIntent.getBroadcast(
|
||||
this, 0, broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareCustomTabsIntent(CustomTabsIntent customTabsIntent) {
|
||||
if (customSettings.packageName != null)
|
||||
customTabsIntent.intent.setPackage(customSettings.packageName);
|
||||
else
|
||||
customTabsIntent.intent.setPackage(CustomTabsHelper.getPackageNameToUse(this));
|
||||
|
||||
if (customSettings.keepAliveEnabled)
|
||||
CustomTabsHelper.addKeepAliveExtra(this, customTabsIntent.intent);
|
||||
|
||||
if (customSettings.alwaysUseBrowserUI)
|
||||
CustomTabsIntent.setAlwaysUseBrowserUI(customTabsIntent.intent);
|
||||
}
|
||||
|
||||
public void updateActionButton(@NonNull byte[] icon, @NonNull String description) {
|
||||
if (customTabsSession == null || actionButton == null) {
|
||||
return;
|
||||
}
|
||||
BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
|
||||
bitmapOptions.inMutable = true;
|
||||
Bitmap bmp = BitmapFactory.decodeByteArray(
|
||||
icon, 0, icon.length, bitmapOptions
|
||||
);
|
||||
customTabsSession.setActionButton(bmp, description);
|
||||
actionButton.setIcon(icon);
|
||||
actionButton.setDescription(description);
|
||||
}
|
||||
|
||||
public void updateSecondaryToolbar(CustomTabsSecondaryToolbar secondaryToolbar) {
|
||||
if (customTabsSession == null) {
|
||||
return;
|
||||
}
|
||||
AndroidResource layout = secondaryToolbar.getLayout();
|
||||
RemoteViews remoteViews = new RemoteViews(layout.getDefPackage(), layout.getIdentifier(this));
|
||||
int[] clickableIDs = new int[secondaryToolbar.getClickableIDs().size()];
|
||||
for (int i = 0, length = secondaryToolbar.getClickableIDs().size(); i < length; i++) {
|
||||
AndroidResource clickableID = secondaryToolbar.getClickableIDs().get(i);
|
||||
clickableIDs[i] = clickableID.getIdentifier(this);
|
||||
}
|
||||
customTabsSession.setSecondaryToolbarViews(remoteViews, clickableIDs, getSecondaryToolbarOnClickPendingIntent());
|
||||
this.secondaryToolbar = secondaryToolbar;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
isBindSuccess = customTabActivityHelper.bindCustomTabsService(this);
|
||||
|
||||
if (!isBindSuccess && initialUrl != null) {
|
||||
// chrome process not running, start tab directly
|
||||
launchUrlWithSession(null, initialUrl, initialHeaders, initialReferrer, initialOtherLikelyURLs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
customTabActivityHelper.unbindCustomTabsService(this);
|
||||
isBindSuccess = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == CHROME_CUSTOM_TAB_REQUEST_CODE) {
|
||||
close();
|
||||
dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private PendingIntent createPendingIntent(int actionSourceId) {
|
||||
Intent actionIntent = new Intent(this, ActionBroadcastReceiver.class);
|
||||
|
||||
Bundle extras = new Bundle();
|
||||
extras.putInt(ActionBroadcastReceiver.KEY_ACTION_ID, actionSourceId);
|
||||
extras.putString(ActionBroadcastReceiver.KEY_ACTION_VIEW_ID, id);
|
||||
extras.putString(ActionBroadcastReceiver.KEY_ACTION_MANAGER_ID, manager != null ? manager.id : null);
|
||||
actionIntent.putExtras(extras);
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
return PendingIntent.getBroadcast(
|
||||
this, actionSourceId, actionIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
|
||||
} else {
|
||||
return PendingIntent.getBroadcast(
|
||||
this, actionSourceId, actionIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
onStop();
|
||||
onDestroy();
|
||||
if (channelDelegate != null) {
|
||||
channelDelegate.dispose();
|
||||
channelDelegate = null;
|
||||
}
|
||||
if (manager != null) {
|
||||
if (manager.browsers.containsKey(id)) {
|
||||
manager.browsers.put(id, null);
|
||||
}
|
||||
}
|
||||
manager = null;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
onStop();
|
||||
onDestroy();
|
||||
customTabsSession = null;
|
||||
finish();
|
||||
if (channelDelegate != null) {
|
||||
channelDelegate.onClosed();
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -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";
|
||||
|
||||
}
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.browser.customtabs.CustomTabsService;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.CustomTabsSecondaryToolbar;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
public class ChromeCustomTabsChannelDelegate extends ChannelDelegateImpl {
|
||||
@Nullable
|
||||
private ChromeCustomTabsActivity chromeCustomTabsActivity;
|
||||
|
||||
public ChromeCustomTabsChannelDelegate(@NonNull ChromeCustomTabsActivity chromeCustomTabsActivity, @NonNull MethodChannel channel) {
|
||||
super(channel);
|
||||
this.chromeCustomTabsActivity = chromeCustomTabsActivity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(@NonNull final MethodCall call, @NonNull final MethodChannel.Result result) {
|
||||
switch (call.method) {
|
||||
case "launchUrl":
|
||||
if (chromeCustomTabsActivity != null) {
|
||||
String url = (String) call.argument("url");
|
||||
if (url != null) {
|
||||
Map<String, String> headers = (Map<String, String>) call.argument("headers");
|
||||
String referrer = (String) call.argument("referrer");
|
||||
List<String> otherLikelyURLs = (List<String>) call.argument("otherLikelyURLs");
|
||||
chromeCustomTabsActivity.launchUrl(url, headers, referrer, otherLikelyURLs);
|
||||
result.success(true);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
case "mayLaunchUrl":
|
||||
if (chromeCustomTabsActivity != null) {
|
||||
String url = (String) call.argument("url");
|
||||
List<String> otherLikelyURLs = (List<String>) call.argument("otherLikelyURLs");
|
||||
result.success(chromeCustomTabsActivity.mayLaunchUrl(url, otherLikelyURLs));
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
case "updateActionButton":
|
||||
if (chromeCustomTabsActivity != null) {
|
||||
byte[] icon = (byte[]) call.argument("icon");
|
||||
String description = (String) call.argument("description");
|
||||
chromeCustomTabsActivity.updateActionButton(icon, description);
|
||||
result.success(true);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
case "validateRelationship":
|
||||
if (chromeCustomTabsActivity != null && chromeCustomTabsActivity.customTabsSession != null) {
|
||||
Integer relation = (Integer) call.argument("relation");
|
||||
String origin = (String) call.argument("origin");
|
||||
result.success(chromeCustomTabsActivity.customTabsSession.validateRelationship(relation, Uri.parse(origin), null));
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
case "updateSecondaryToolbar":
|
||||
if (chromeCustomTabsActivity != null) {
|
||||
CustomTabsSecondaryToolbar secondaryToolbar = CustomTabsSecondaryToolbar.fromMap((Map<String, Object>) call.argument("secondaryToolbar"));
|
||||
chromeCustomTabsActivity.updateSecondaryToolbar(secondaryToolbar);
|
||||
result.success(true);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
case "requestPostMessageChannel":
|
||||
if (chromeCustomTabsActivity != null && chromeCustomTabsActivity.customTabsSession != null) {
|
||||
String sourceOrigin = (String) call.argument("sourceOrigin");
|
||||
String targetOrigin = (String) call.argument("targetOrigin");
|
||||
result.success(chromeCustomTabsActivity.customTabsSession.requestPostMessageChannel(Uri.parse(sourceOrigin),
|
||||
targetOrigin != null ? Uri.parse(targetOrigin) : null, new Bundle()));
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
case "postMessage":
|
||||
if (chromeCustomTabsActivity != null && chromeCustomTabsActivity.customTabsSession != null) {
|
||||
String message = (String) call.argument("message");
|
||||
result.success(chromeCustomTabsActivity.customTabsSession.postMessage(message, new Bundle()));
|
||||
} else {
|
||||
result.success(CustomTabsService.RESULT_FAILURE_MESSAGING_ERROR);
|
||||
}
|
||||
break;
|
||||
case "isEngagementSignalsApiAvailable":
|
||||
if (chromeCustomTabsActivity != null && chromeCustomTabsActivity.customTabsSession != null) {
|
||||
try {
|
||||
result.success(chromeCustomTabsActivity.customTabsSession.isEngagementSignalsApiAvailable(new Bundle()));
|
||||
} catch (Throwable e) {
|
||||
result.success(false);
|
||||
}
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
case "close":
|
||||
if (chromeCustomTabsActivity != null) {
|
||||
chromeCustomTabsActivity.onStop();
|
||||
chromeCustomTabsActivity.onDestroy();
|
||||
chromeCustomTabsActivity.close();
|
||||
|
||||
if (chromeCustomTabsActivity.manager != null && chromeCustomTabsActivity.manager.plugin != null &&
|
||||
chromeCustomTabsActivity.manager.plugin.activity != null) {
|
||||
Activity activity = chromeCustomTabsActivity.manager.plugin.activity;
|
||||
// https://stackoverflow.com/a/41596629/4637638
|
||||
Intent myIntent = new Intent(activity, activity.getClass());
|
||||
myIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
myIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
activity.startActivity(myIntent);
|
||||
}
|
||||
chromeCustomTabsActivity.dispose();
|
||||
result.success(true);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
result.notImplemented();
|
||||
}
|
||||
}
|
||||
|
||||
public void onServiceConnected() {
|
||||
MethodChannel channel = getChannel();
|
||||
if (channel == null) return;
|
||||
Map<String, Object> obj = new HashMap<>();
|
||||
channel.invokeMethod("onServiceConnected", obj);
|
||||
}
|
||||
|
||||
public void onOpened() {
|
||||
MethodChannel channel = getChannel();
|
||||
if (channel == null) return;
|
||||
Map<String, Object> obj = new HashMap<>();
|
||||
channel.invokeMethod("onOpened", obj);
|
||||
}
|
||||
|
||||
public void onCompletedInitialLoad() {
|
||||
MethodChannel channel = getChannel();
|
||||
if (channel == null) return;
|
||||
Map<String, Object> obj = new HashMap<>();
|
||||
channel.invokeMethod("onCompletedInitialLoad", obj);
|
||||
}
|
||||
|
||||
public void onNavigationEvent(int navigationEvent) {
|
||||
MethodChannel channel = getChannel();
|
||||
if (channel == null) return;
|
||||
Map<String, Object> obj = new HashMap<>();
|
||||
obj.put("navigationEvent", navigationEvent);
|
||||
channel.invokeMethod("onNavigationEvent", obj);
|
||||
}
|
||||
|
||||
public void onClosed() {
|
||||
MethodChannel channel = getChannel();
|
||||
if (channel == null) return;
|
||||
Map<String, Object> obj = new HashMap<>();
|
||||
channel.invokeMethod("onClosed", obj);
|
||||
}
|
||||
|
||||
public void onItemActionPerform(int id, String url, String title) {
|
||||
MethodChannel channel = getChannel();
|
||||
if (channel == null) return;
|
||||
Map<String, Object> obj = new HashMap<>();
|
||||
obj.put("id", id);
|
||||
obj.put("url", url);
|
||||
obj.put("title", title);
|
||||
channel.invokeMethod("onItemActionPerform", obj);
|
||||
}
|
||||
|
||||
public void onSecondaryItemActionPerform(String name, String url) {
|
||||
MethodChannel channel = getChannel();
|
||||
if (channel == null) return;
|
||||
Map<String, Object> obj = new HashMap<>();
|
||||
obj.put("name", name);
|
||||
obj.put("url", url);
|
||||
channel.invokeMethod("onSecondaryItemActionPerform", obj);
|
||||
}
|
||||
|
||||
public void onRelationshipValidationResult(int relation, @NonNull Uri requestedOrigin, boolean result) {
|
||||
MethodChannel channel = getChannel();
|
||||
if (channel == null) return;
|
||||
Map<String, Object> obj = new HashMap<>();
|
||||
obj.put("relation", relation);
|
||||
obj.put("requestedOrigin", requestedOrigin.toString());
|
||||
obj.put("result", result);
|
||||
channel.invokeMethod("onRelationshipValidationResult", obj);
|
||||
}
|
||||
|
||||
public void onMessageChannelReady() {
|
||||
MethodChannel channel = getChannel();
|
||||
if (channel == null) return;
|
||||
Map<String, Object> obj = new HashMap<>();
|
||||
channel.invokeMethod("onMessageChannelReady", obj);
|
||||
}
|
||||
|
||||
public void onPostMessage(@NonNull String message) {
|
||||
MethodChannel channel = getChannel();
|
||||
if (channel == null) return;
|
||||
Map<String, Object> obj = new HashMap<>();
|
||||
obj.put("message", message);
|
||||
channel.invokeMethod("onPostMessage", obj);
|
||||
}
|
||||
|
||||
public void onVerticalScrollEvent(boolean isDirectionUp) {
|
||||
MethodChannel channel = getChannel();
|
||||
if (channel == null) return;
|
||||
Map<String, Object> obj = new HashMap<>();
|
||||
obj.put("isDirectionUp", isDirectionUp);
|
||||
channel.invokeMethod("onVerticalScrollEvent", obj);
|
||||
}
|
||||
|
||||
public void onGreatestScrollPercentageIncreased(int scrollPercentage) {
|
||||
MethodChannel channel = getChannel();
|
||||
if (channel == null) return;
|
||||
Map<String, Object> obj = new HashMap<>();
|
||||
obj.put("scrollPercentage", scrollPercentage);
|
||||
channel.invokeMethod("onGreatestScrollPercentageIncreased", obj);
|
||||
}
|
||||
|
||||
public void onSessionEnded(boolean didUserInteract) {
|
||||
MethodChannel channel = getChannel();
|
||||
if (channel == null) return;
|
||||
Map<String, Object> obj = new HashMap<>();
|
||||
obj.put("didUserInteract", didUserInteract);
|
||||
channel.invokeMethod("onSessionEnded", obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
chromeCustomTabsActivity = null;
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+187
@@ -0,0 +1,187 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.browser.customtabs.CustomTabsIntent;
|
||||
import androidx.browser.trusted.ScreenOrientation;
|
||||
import androidx.browser.trusted.TrustedWebActivityDisplayMode;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.ISettings;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.AndroidResource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ChromeCustomTabsSettings implements ISettings<ChromeCustomTabsActivity> {
|
||||
|
||||
final static String LOG_TAG = "ChromeCustomTabsSettings";
|
||||
|
||||
@Deprecated
|
||||
public Boolean addDefaultShareMenuItem;
|
||||
public Integer shareState = CustomTabsIntent.SHARE_STATE_DEFAULT;
|
||||
public Boolean showTitle = true;
|
||||
@Nullable
|
||||
public String toolbarBackgroundColor;
|
||||
@Nullable
|
||||
public String navigationBarColor;
|
||||
@Nullable
|
||||
public String navigationBarDividerColor;
|
||||
@Nullable
|
||||
public String secondaryToolbarColor;
|
||||
public Boolean enableUrlBarHiding = false;
|
||||
public Boolean instantAppsEnabled = false;
|
||||
public String packageName;
|
||||
public Boolean keepAliveEnabled = false;
|
||||
public Boolean isSingleInstance = false;
|
||||
public Boolean noHistory = false;
|
||||
public Boolean isTrustedWebActivity = false;
|
||||
public List<String> additionalTrustedOrigins = new ArrayList<>();
|
||||
public TrustedWebActivityDisplayMode displayMode = null;
|
||||
public Integer screenOrientation = ScreenOrientation.DEFAULT;
|
||||
public List<AndroidResource> startAnimations = new ArrayList<>();
|
||||
public List<AndroidResource> exitAnimations = new ArrayList<>();
|
||||
public Boolean alwaysUseBrowserUI = false;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ChromeCustomTabsSettings parse(@NonNull Map<String, Object> options) {
|
||||
for (Map.Entry<String, Object> pair : options.entrySet()) {
|
||||
String key = pair.getKey();
|
||||
Object value = pair.getValue();
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case "addDefaultShareMenuItem":
|
||||
addDefaultShareMenuItem = (Boolean) value;
|
||||
break;
|
||||
case "shareState":
|
||||
shareState = (Integer) value;
|
||||
break;
|
||||
case "showTitle":
|
||||
showTitle = (Boolean) value;
|
||||
break;
|
||||
case "toolbarBackgroundColor":
|
||||
toolbarBackgroundColor = (String) value;
|
||||
break;
|
||||
case "navigationBarColor":
|
||||
navigationBarColor = (String) value;
|
||||
break;
|
||||
case "navigationBarDividerColor":
|
||||
navigationBarDividerColor = (String) value;
|
||||
break;
|
||||
case "secondaryToolbarColor":
|
||||
secondaryToolbarColor = (String) value;
|
||||
break;
|
||||
case "enableUrlBarHiding":
|
||||
enableUrlBarHiding = (Boolean) value;
|
||||
break;
|
||||
case "instantAppsEnabled":
|
||||
instantAppsEnabled = (Boolean) value;
|
||||
break;
|
||||
case "packageName":
|
||||
packageName = (String) value;
|
||||
break;
|
||||
case "keepAliveEnabled":
|
||||
keepAliveEnabled = (Boolean) value;
|
||||
break;
|
||||
case "isSingleInstance":
|
||||
isSingleInstance = (Boolean) value;
|
||||
break;
|
||||
case "noHistory":
|
||||
noHistory = (Boolean) value;
|
||||
break;
|
||||
case "isTrustedWebActivity":
|
||||
isTrustedWebActivity = (Boolean) value;
|
||||
break;
|
||||
case "additionalTrustedOrigins":
|
||||
additionalTrustedOrigins = (List<String>) value;
|
||||
break;
|
||||
case "displayMode":
|
||||
Map<String, Object> displayModeMap = (Map<String, Object>) value;
|
||||
String displayModeType = (String) displayModeMap.get("type");
|
||||
if (displayModeType != null) {
|
||||
switch (displayModeType) {
|
||||
case "IMMERSIVE_MODE":
|
||||
boolean isSticky = (boolean) displayModeMap.get("isSticky");
|
||||
int layoutInDisplayCutoutMode = (int) displayModeMap.get("displayCutoutMode");
|
||||
displayMode = new TrustedWebActivityDisplayMode.ImmersiveMode(isSticky, layoutInDisplayCutoutMode);
|
||||
break;
|
||||
case "DEFAULT_MODE":
|
||||
displayMode = new TrustedWebActivityDisplayMode.DefaultMode();
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "screenOrientation":
|
||||
screenOrientation = (Integer) value;
|
||||
break;
|
||||
case "startAnimations":
|
||||
List<Map<String, Object>> startAnimationsList = (List<Map<String, Object>>) value;
|
||||
for (Map<String, Object> startAnimation : startAnimationsList) {
|
||||
AndroidResource androidResource = AndroidResource.fromMap(startAnimation);
|
||||
if (androidResource != null) {
|
||||
startAnimations.add(AndroidResource.fromMap(startAnimation));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "exitAnimations":
|
||||
List<Map<String, Object>> exitAnimationsList = (List<Map<String, Object>>) value;
|
||||
for (Map<String, Object> exitAnimation : exitAnimationsList) {
|
||||
AndroidResource androidResource = AndroidResource.fromMap(exitAnimation);
|
||||
if (androidResource != null) {
|
||||
exitAnimations.add(AndroidResource.fromMap(exitAnimation));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "alwaysUseBrowserUI":
|
||||
alwaysUseBrowserUI = (Boolean) value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Map<String, Object> toMap() {
|
||||
Map<String, Object> options = new HashMap<>();
|
||||
options.put("addDefaultShareMenuItem", addDefaultShareMenuItem);
|
||||
options.put("showTitle", showTitle);
|
||||
options.put("toolbarBackgroundColor", toolbarBackgroundColor);
|
||||
options.put("navigationBarColor", navigationBarColor);
|
||||
options.put("navigationBarDividerColor", navigationBarDividerColor);
|
||||
options.put("secondaryToolbarColor", secondaryToolbarColor);
|
||||
options.put("enableUrlBarHiding", enableUrlBarHiding);
|
||||
options.put("instantAppsEnabled", instantAppsEnabled);
|
||||
options.put("packageName", packageName);
|
||||
options.put("keepAliveEnabled", keepAliveEnabled);
|
||||
options.put("isSingleInstance", isSingleInstance);
|
||||
options.put("noHistory", noHistory);
|
||||
options.put("isTrustedWebActivity", isTrustedWebActivity);
|
||||
options.put("additionalTrustedOrigins", additionalTrustedOrigins);
|
||||
options.put("screenOrientation", screenOrientation);
|
||||
options.put("alwaysUseBrowserUI", alwaysUseBrowserUI);
|
||||
return options;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Map<String, Object> getRealSettings(@NonNull ChromeCustomTabsActivity chromeCustomTabsActivity) {
|
||||
Map<String, Object> realOptions = toMap();
|
||||
if (chromeCustomTabsActivity != null) {
|
||||
Intent intent = chromeCustomTabsActivity.getIntent();
|
||||
if (intent != null) {
|
||||
realOptions.put("packageName", intent.getPackage());
|
||||
realOptions.put("keepAliveEnabled", intent.hasExtra(CustomTabsHelper.EXTRA_CUSTOM_TABS_KEEP_ALIVE));
|
||||
}
|
||||
}
|
||||
return realOptions;
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+139
@@ -0,0 +1,139 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.browser.customtabs.CustomTabsClient;
|
||||
import androidx.browser.customtabs.CustomTabsIntent;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.Util;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
public class ChromeSafariBrowserManager extends ChannelDelegateImpl {
|
||||
protected static final String LOG_TAG = "ChromeBrowserManager";
|
||||
public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_chromesafaribrowser";
|
||||
|
||||
@Nullable
|
||||
public InAppWebViewFlutterPlugin plugin;
|
||||
public String id;
|
||||
public static final Map<String, ChromeSafariBrowserManager> shared = new HashMap<>();
|
||||
public final Map<String, ChromeCustomTabsActivity> browsers = new HashMap<>();
|
||||
|
||||
public ChromeSafariBrowserManager(final InAppWebViewFlutterPlugin plugin) {
|
||||
super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME));
|
||||
this.id = UUID.randomUUID().toString();
|
||||
this.plugin = plugin;
|
||||
shared.put(this.id, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(final MethodCall call, final MethodChannel.Result result) {
|
||||
final String viewId = (String) call.argument("id");
|
||||
|
||||
switch (call.method) {
|
||||
case "open":
|
||||
if (plugin != null && plugin.activity != null) {
|
||||
String url = (String) call.argument("url");
|
||||
HashMap<String, Object> headers = (HashMap<String, Object>) call.argument("headers");
|
||||
String referrer = (String) call.argument("referrer");
|
||||
ArrayList<String> otherLikelyURLs = (ArrayList<String>) call.argument("otherLikelyURLs");
|
||||
HashMap<String, Object> settings = (HashMap<String, Object>) call.argument("settings");
|
||||
HashMap<String, Object> actionButton = (HashMap<String, Object>) call.argument("actionButton");
|
||||
HashMap<String, Object> secondaryToolbar = (HashMap<String, Object>) call.argument("secondaryToolbar");
|
||||
List<HashMap<String, Object>> menuItemList = (List<HashMap<String, Object>>) call.argument("menuItemList");
|
||||
open(plugin.activity, viewId, url, headers, referrer, otherLikelyURLs, settings, actionButton, secondaryToolbar, menuItemList, result);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
case "isAvailable":
|
||||
if (plugin != null && plugin.activity != null) {
|
||||
result.success(CustomTabActivityHelper.isAvailable(plugin.activity));
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
case "getMaxToolbarItems":
|
||||
result.success(CustomTabsIntent.getMaxToolbarItems());
|
||||
break;
|
||||
case "getPackageName":
|
||||
if (plugin != null && plugin.activity != null) {
|
||||
ArrayList<String> packages = (ArrayList<String>) call.argument("packages");
|
||||
Boolean ignoreDefault = (Boolean) call.argument("ignoreDefault");
|
||||
result.success(CustomTabsClient.getPackageName(plugin.activity, packages, ignoreDefault));
|
||||
} else {
|
||||
result.success(null);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
result.notImplemented();
|
||||
}
|
||||
}
|
||||
|
||||
public void open(Activity activity, String viewId, @Nullable String url, @Nullable HashMap<String, Object> headers,
|
||||
@Nullable String referrer, @Nullable ArrayList<String> otherLikelyURLs,
|
||||
HashMap<String, Object> settings, HashMap<String, Object> actionButton,
|
||||
HashMap<String, Object> secondaryToolbar,
|
||||
List<HashMap<String, Object>> menuItemList, MethodChannel.Result result) {
|
||||
|
||||
Intent intent = null;
|
||||
Bundle extras = new Bundle();
|
||||
extras.putString("url", url);
|
||||
extras.putString("id", viewId);
|
||||
extras.putString("managerId", this.id);
|
||||
extras.putSerializable("headers", headers);
|
||||
extras.putString("referrer", referrer);
|
||||
extras.putSerializable("otherLikelyURLs", otherLikelyURLs);
|
||||
extras.putSerializable("settings", settings);
|
||||
extras.putSerializable("actionButton", (Serializable) actionButton);
|
||||
extras.putSerializable("secondaryToolbar", (Serializable) secondaryToolbar);
|
||||
extras.putSerializable("menuItemList", (Serializable) menuItemList);
|
||||
|
||||
Boolean isSingleInstance = Util.<Boolean>getOrDefault(settings, "isSingleInstance", false);
|
||||
Boolean isTrustedWebActivity = Util.<Boolean>getOrDefault(settings, "isTrustedWebActivity", false);
|
||||
if (CustomTabActivityHelper.isAvailable(activity)) {
|
||||
intent = new Intent(activity, !isSingleInstance ?
|
||||
(!isTrustedWebActivity ? ChromeCustomTabsActivity.class : TrustedWebActivity.class) :
|
||||
(!isTrustedWebActivity ? ChromeCustomTabsActivitySingleInstance.class : TrustedWebActivitySingleInstance.class));
|
||||
intent.putExtras(extras);
|
||||
Boolean noHistory = Util.<Boolean>getOrDefault(settings, "noHistory", false);
|
||||
if (noHistory) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
|
||||
}
|
||||
activity.startActivity(intent);
|
||||
result.success(true);
|
||||
return;
|
||||
}
|
||||
|
||||
result.error(LOG_TAG, "ChromeCustomTabs is not available!", null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
Collection<ChromeCustomTabsActivity> browserList = browsers.values();
|
||||
for (ChromeCustomTabsActivity browser : browserList) {
|
||||
if (browser != null) {
|
||||
browser.close();
|
||||
browser.dispose();
|
||||
}
|
||||
}
|
||||
browsers.clear();
|
||||
shared.remove(this.id);
|
||||
plugin = null;
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+177
@@ -0,0 +1,177 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Browser;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.browser.customtabs.CustomTabsCallback;
|
||||
import androidx.browser.customtabs.CustomTabsClient;
|
||||
import androidx.browser.customtabs.CustomTabsIntent;
|
||||
import androidx.browser.customtabs.CustomTabsServiceConnection;
|
||||
import androidx.browser.customtabs.CustomTabsSession;
|
||||
import androidx.browser.trusted.TrustedWebActivityIntent;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* This is a helper class to manage the connection to the Custom Tabs Service.
|
||||
*/
|
||||
public class CustomTabActivityHelper implements ServiceConnectionCallback {
|
||||
private CustomTabsSession mCustomTabsSession;
|
||||
private CustomTabsClient mClient;
|
||||
private CustomTabsServiceConnection mConnection;
|
||||
private ConnectionCallback mConnectionCallback;
|
||||
private CustomTabsCallback mCustomTabsCallback;
|
||||
|
||||
/**
|
||||
* Opens the URL on a Custom Tab if possible.
|
||||
*
|
||||
* @param activity The host activity.
|
||||
* @param intent a intent to be used if Custom Tabs is available.
|
||||
* @param uri the Uri to be opened.
|
||||
*/
|
||||
public static void openCustomTab(Activity activity,
|
||||
Intent intent,
|
||||
Uri uri,
|
||||
@Nullable Map<String, String> headers,
|
||||
@Nullable Uri referrer,
|
||||
int requestCode) {
|
||||
intent.setData(uri);
|
||||
if (headers != null) {
|
||||
Bundle bundleHeaders = new Bundle();
|
||||
for (Map.Entry<String, String> header : headers.entrySet()) {
|
||||
bundleHeaders.putString(header.getKey(), header.getValue());
|
||||
}
|
||||
intent.putExtra(Browser.EXTRA_HEADERS, bundleHeaders);
|
||||
}
|
||||
if (referrer != null) {
|
||||
intent.putExtra(Intent.EXTRA_REFERRER, referrer);
|
||||
}
|
||||
activity.startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
public static void openCustomTab(Activity activity,
|
||||
CustomTabsIntent customTabsIntent,
|
||||
Uri uri,
|
||||
@Nullable Map<String, String> headers,
|
||||
@Nullable Uri referrer,
|
||||
int requestCode) {
|
||||
CustomTabActivityHelper.openCustomTab(activity, customTabsIntent.intent, uri,
|
||||
headers, referrer, requestCode);
|
||||
}
|
||||
|
||||
public static void openTrustedWebActivity(Activity activity,
|
||||
TrustedWebActivityIntent trustedWebActivityIntent,
|
||||
Uri uri,
|
||||
@Nullable Map<String, String> headers,
|
||||
@Nullable Uri referrer,
|
||||
int requestCode) {
|
||||
CustomTabActivityHelper.openCustomTab(activity, trustedWebActivityIntent.getIntent(), uri,
|
||||
headers, referrer, requestCode);
|
||||
}
|
||||
|
||||
|
||||
public static boolean isAvailable(Activity activity) {
|
||||
return CustomTabsHelper.getPackageNameToUse(activity) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unbinds the Activity from the Custom Tabs Service.
|
||||
* @param activity the activity that is connected to the service.
|
||||
*/
|
||||
public void unbindCustomTabsService(Activity activity) {
|
||||
if (mConnection == null) return;
|
||||
activity.unbindService(mConnection);
|
||||
mClient = null;
|
||||
mCustomTabsSession = null;
|
||||
mConnection = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or retrieves an exiting CustomTabsSession.
|
||||
*
|
||||
* @return a CustomTabsSession.
|
||||
*/
|
||||
@Nullable
|
||||
public CustomTabsSession getSession() {
|
||||
if (mClient == null) {
|
||||
mCustomTabsSession = null;
|
||||
} else if (mCustomTabsSession == null) {
|
||||
mCustomTabsSession = mClient.newSession(mCustomTabsCallback);
|
||||
}
|
||||
return mCustomTabsSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a Callback to be called when connected or disconnected from the Custom Tabs Service.
|
||||
* @param connectionCallback
|
||||
*/
|
||||
public void setConnectionCallback(ConnectionCallback connectionCallback) {
|
||||
this.mConnectionCallback = connectionCallback;
|
||||
}
|
||||
|
||||
public void setCustomTabsCallback(CustomTabsCallback customTabsCallback) {
|
||||
this.mCustomTabsCallback = customTabsCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the Activity to the Custom Tabs Service.
|
||||
* @param activity the activity to be binded to the service.
|
||||
*/
|
||||
public boolean bindCustomTabsService(Activity activity) {
|
||||
if (mClient != null) return true;
|
||||
|
||||
String packageName = CustomTabsHelper.getPackageNameToUse(activity);
|
||||
if (packageName == null) return false;
|
||||
|
||||
mConnection = new ServiceConnection(this);
|
||||
return CustomTabsClient.bindCustomTabsService(activity, packageName, mConnection);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see {@link CustomTabsSession#mayLaunchUrl(Uri, Bundle, List)}.
|
||||
* @return true if call to mayLaunchUrl was accepted.
|
||||
*/
|
||||
public boolean mayLaunchUrl(Uri uri, Bundle extras, List<Bundle> otherLikelyBundles) {
|
||||
if (mClient == null) return false;
|
||||
|
||||
CustomTabsSession session = getSession();
|
||||
if (session == null) return false;
|
||||
|
||||
return session.mayLaunchUrl(uri, extras, otherLikelyBundles);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(CustomTabsClient client) {
|
||||
mClient = client;
|
||||
mClient.warmup(0L);
|
||||
if (mConnectionCallback != null) mConnectionCallback.onCustomTabsConnected();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected() {
|
||||
mClient = null;
|
||||
mCustomTabsSession = null;
|
||||
if (mConnectionCallback != null) mConnectionCallback.onCustomTabsDisconnected();
|
||||
}
|
||||
|
||||
/**
|
||||
* A Callback for when the service is connected or disconnected. Use those callbacks to
|
||||
* handle UI changes when the service is connected or disconnected.
|
||||
*/
|
||||
public interface ConnectionCallback {
|
||||
/**
|
||||
* Called when the service is connected.
|
||||
*/
|
||||
void onCustomTabsConnected();
|
||||
|
||||
/**
|
||||
* Called when the service is disconnected.
|
||||
*/
|
||||
void onCustomTabsDisconnected();
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+134
@@ -0,0 +1,134 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.browser.customtabs.CustomTabsService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Helper class for Custom Tabs.
|
||||
*/
|
||||
public class CustomTabsHelper {
|
||||
protected static final String TAG = "CustomTabsHelper";
|
||||
static final String STABLE_PACKAGE = "com.android.chrome";
|
||||
static final String BETA_PACKAGE = "com.chrome.beta";
|
||||
static final String DEV_PACKAGE = "com.chrome.dev";
|
||||
static final String LOCAL_PACKAGE = "com.google.android.apps.chrome";
|
||||
protected static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE =
|
||||
"android.support.customtabs.extra.KEEP_ALIVE";
|
||||
|
||||
private static String sPackageNameToUse;
|
||||
|
||||
private CustomTabsHelper() {}
|
||||
|
||||
public static void addKeepAliveExtra(Context context, Intent intent) {
|
||||
Intent keepAliveIntent = new Intent().setClassName(
|
||||
context.getPackageName(), KeepAliveService.class.getCanonicalName());
|
||||
intent.putExtra(EXTRA_CUSTOM_TABS_KEEP_ALIVE, keepAliveIntent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes through all apps that handle VIEW intents and have a warmup service. Picks
|
||||
* the one chosen by the user if there is one, otherwise makes a best effort to return a
|
||||
* valid package name.
|
||||
*
|
||||
* This is <strong>not</strong> threadsafe.
|
||||
*
|
||||
* @param context {@link Context} to use for accessing {@link PackageManager}.
|
||||
* @return The package name recommended to use for connecting to custom tabs related components.
|
||||
*/
|
||||
public static String getPackageNameToUse(Context context) {
|
||||
if (sPackageNameToUse != null) return sPackageNameToUse;
|
||||
|
||||
PackageManager pm = context.getPackageManager();
|
||||
// Get default VIEW intent handler.
|
||||
Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com"));
|
||||
activityIntent.addCategory(Intent.CATEGORY_BROWSABLE);
|
||||
ResolveInfo defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0);
|
||||
String defaultViewHandlerPackageName = null;
|
||||
if (defaultViewHandlerInfo != null) {
|
||||
defaultViewHandlerPackageName = defaultViewHandlerInfo.activityInfo.packageName;
|
||||
}
|
||||
|
||||
// Get all apps that can handle VIEW intents.
|
||||
int flags = 0;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
flags |= PackageManager.MATCH_ALL;
|
||||
}
|
||||
List<ResolveInfo> resolvedActivityList = pm.queryIntentActivities(activityIntent, flags);
|
||||
List<String> packagesSupportingCustomTabs = new ArrayList<>();
|
||||
for (ResolveInfo info : resolvedActivityList) {
|
||||
Intent serviceIntent = new Intent();
|
||||
serviceIntent.setAction(CustomTabsService.ACTION_CUSTOM_TABS_CONNECTION);
|
||||
serviceIntent.setPackage(info.activityInfo.packageName);
|
||||
if (pm.resolveService(serviceIntent, 0) != null) {
|
||||
packagesSupportingCustomTabs.add(info.activityInfo.packageName);
|
||||
}
|
||||
}
|
||||
|
||||
// Now packagesSupportingCustomTabs contains all apps that can handle both VIEW intents
|
||||
// and service calls.
|
||||
if (packagesSupportingCustomTabs.isEmpty()) {
|
||||
sPackageNameToUse = null;
|
||||
} else if (packagesSupportingCustomTabs.size() == 1) {
|
||||
sPackageNameToUse = packagesSupportingCustomTabs.get(0);
|
||||
} else if (!TextUtils.isEmpty(defaultViewHandlerPackageName)
|
||||
&& !hasSpecializedHandlerIntents(context, activityIntent)
|
||||
&& packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName)) {
|
||||
sPackageNameToUse = defaultViewHandlerPackageName;
|
||||
} else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) {
|
||||
sPackageNameToUse = STABLE_PACKAGE;
|
||||
} else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) {
|
||||
sPackageNameToUse = BETA_PACKAGE;
|
||||
} else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) {
|
||||
sPackageNameToUse = DEV_PACKAGE;
|
||||
} else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) {
|
||||
sPackageNameToUse = LOCAL_PACKAGE;
|
||||
}
|
||||
return sPackageNameToUse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to check whether there is a specialized handler for a given intent.
|
||||
* @param intent The intent to check with.
|
||||
* @return Whether there is a specialized handler for the given intent.
|
||||
*/
|
||||
private static boolean hasSpecializedHandlerIntents(Context context, Intent intent) {
|
||||
try {
|
||||
PackageManager pm = context.getPackageManager();
|
||||
List<ResolveInfo> handlers = pm.queryIntentActivities(
|
||||
intent,
|
||||
PackageManager.GET_RESOLVED_FILTER);
|
||||
if (handlers == null || handlers.size() == 0) {
|
||||
return false;
|
||||
}
|
||||
for (ResolveInfo resolveInfo : handlers) {
|
||||
IntentFilter filter = resolveInfo.filter;
|
||||
if (filter == null) continue;
|
||||
if (filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0) continue;
|
||||
if (resolveInfo.activityInfo == null) continue;
|
||||
return true;
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
Log.e(TAG, "Runtime exception while getting specialized handlers");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return All possible chrome package names that provide custom tabs feature.
|
||||
*/
|
||||
public static String[] getPackages() {
|
||||
return new String[]{"", STABLE_PACKAGE, BETA_PACKAGE, DEV_PACKAGE, LOCAL_PACKAGE};
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+18
@@ -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;
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity;
|
||||
|
||||
public class NoHistoryCustomTabsActivityCallbacks implements Disposable {
|
||||
@Nullable
|
||||
public InAppWebViewFlutterPlugin plugin;
|
||||
|
||||
public final Map<String, String> noHistoryBrowserIDs = new HashMap<>();
|
||||
|
||||
public NoHistoryCustomTabsActivityCallbacks(@NonNull final InAppWebViewFlutterPlugin plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public Application.ActivityLifecycleCallbacks activityLifecycleCallbacks = new Application.ActivityLifecycleCallbacks() {
|
||||
@Override
|
||||
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStarted(@NonNull Activity activity) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResumed(@NonNull Activity activity) {
|
||||
if (activity instanceof FlutterActivity && plugin != null && plugin.chromeSafariBrowserManager != null) {
|
||||
Collection<String> browserIds = noHistoryBrowserIDs.values();
|
||||
for (String browserId : browserIds) {
|
||||
if (browserId != null) {
|
||||
noHistoryBrowserIDs.put(browserId, null);
|
||||
ChromeCustomTabsActivity browser = plugin.chromeSafariBrowserManager.browsers.get(browserId);
|
||||
if (browser != null) {
|
||||
browser.close();
|
||||
browser.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityPaused(@NonNull Activity activity) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStopped(@NonNull Activity activity) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityDestroyed(@NonNull Activity activity) {
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
noHistoryBrowserIDs.clear();
|
||||
plugin = null;
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+32
@@ -0,0 +1,32 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import androidx.browser.customtabs.CustomTabsClient;
|
||||
import androidx.browser.customtabs.CustomTabsServiceConnection;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* Implementation for the CustomTabsServiceConnection that avoids leaking the
|
||||
* ServiceConnectionCallback
|
||||
*/
|
||||
public class ServiceConnection extends CustomTabsServiceConnection {
|
||||
// A weak reference to the ServiceConnectionCallback to avoid leaking it.
|
||||
private WeakReference<ServiceConnectionCallback> mConnectionCallback;
|
||||
|
||||
public ServiceConnection(ServiceConnectionCallback connectionCallback) {
|
||||
mConnectionCallback = new WeakReference<>(connectionCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCustomTabsServiceConnected(ComponentName name, CustomTabsClient client) {
|
||||
ServiceConnectionCallback connectionCallback = mConnectionCallback.get();
|
||||
if (connectionCallback != null) connectionCallback.onServiceConnected(client);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
ServiceConnectionCallback connectionCallback = mConnectionCallback.get();
|
||||
if (connectionCallback != null) connectionCallback.onServiceDisconnected();
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+19
@@ -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();
|
||||
}
|
||||
Vendored
Executable
+84
@@ -0,0 +1,84 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.chrome_custom_tabs;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams;
|
||||
import androidx.browser.customtabs.CustomTabsIntent;
|
||||
import androidx.browser.trusted.TrustedWebActivityIntent;
|
||||
import androidx.browser.trusted.TrustedWebActivityIntentBuilder;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class TrustedWebActivity extends ChromeCustomTabsActivity {
|
||||
|
||||
protected static final String LOG_TAG = "TrustedWebActivity";
|
||||
|
||||
public TrustedWebActivityIntentBuilder builder;
|
||||
|
||||
@Override
|
||||
public void launchUrl(@NonNull String url,
|
||||
@Nullable Map<String, String> headers,
|
||||
@Nullable String referrer,
|
||||
@Nullable List<String> otherLikelyURLs) {
|
||||
if (customTabsSession == null) {
|
||||
return;
|
||||
}
|
||||
Uri uri = Uri.parse(url);
|
||||
|
||||
mayLaunchUrl(url, otherLikelyURLs);
|
||||
builder = new TrustedWebActivityIntentBuilder(uri);
|
||||
prepareCustomTabs();
|
||||
|
||||
TrustedWebActivityIntent trustedWebActivityIntent = builder.build(customTabsSession);
|
||||
prepareCustomTabsIntent(trustedWebActivityIntent);
|
||||
|
||||
CustomTabActivityHelper.openTrustedWebActivity(this, trustedWebActivityIntent, uri, headers,
|
||||
referrer != null ? Uri.parse(referrer) : null, CHROME_CUSTOM_TAB_REQUEST_CODE);
|
||||
}
|
||||
|
||||
private void prepareCustomTabs() {
|
||||
CustomTabColorSchemeParams.Builder defaultColorSchemeBuilder = new CustomTabColorSchemeParams.Builder();
|
||||
if (customSettings.toolbarBackgroundColor != null && !customSettings.toolbarBackgroundColor.isEmpty()) {
|
||||
defaultColorSchemeBuilder.setToolbarColor(Color.parseColor(customSettings.toolbarBackgroundColor));
|
||||
}
|
||||
if (customSettings.navigationBarColor != null && !customSettings.navigationBarColor.isEmpty()) {
|
||||
defaultColorSchemeBuilder.setNavigationBarColor(Color.parseColor(customSettings.navigationBarColor));
|
||||
}
|
||||
if (customSettings.navigationBarDividerColor != null && !customSettings.navigationBarDividerColor.isEmpty()) {
|
||||
defaultColorSchemeBuilder.setNavigationBarDividerColor(Color.parseColor(customSettings.navigationBarDividerColor));
|
||||
}
|
||||
if (customSettings.secondaryToolbarColor != null && !customSettings.secondaryToolbarColor.isEmpty()) {
|
||||
defaultColorSchemeBuilder.setSecondaryToolbarColor(Color.parseColor(customSettings.secondaryToolbarColor));
|
||||
}
|
||||
builder.setDefaultColorSchemeParams(defaultColorSchemeBuilder.build());
|
||||
|
||||
if (customSettings.additionalTrustedOrigins != null && !customSettings.additionalTrustedOrigins.isEmpty()) {
|
||||
builder.setAdditionalTrustedOrigins(customSettings.additionalTrustedOrigins);
|
||||
}
|
||||
|
||||
if (customSettings.displayMode != null) {
|
||||
builder.setDisplayMode(customSettings.displayMode);
|
||||
}
|
||||
|
||||
builder.setScreenOrientation(customSettings.screenOrientation);
|
||||
}
|
||||
|
||||
private void prepareCustomTabsIntent(TrustedWebActivityIntent trustedWebActivityIntent) {
|
||||
Intent intent = trustedWebActivityIntent.getIntent();
|
||||
if (customSettings.packageName != null)
|
||||
intent.setPackage(customSettings.packageName);
|
||||
else
|
||||
intent.setPackage(CustomTabsHelper.getPackageNameToUse(this));
|
||||
|
||||
if (customSettings.keepAliveEnabled)
|
||||
CustomTabsHelper.addKeepAliveExtra(this, intent);
|
||||
|
||||
if (customSettings.alwaysUseBrowserUI)
|
||||
CustomTabsIntent.setAlwaysUseBrowserUI(intent);
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+7
@@ -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";
|
||||
|
||||
}
|
||||
Vendored
Executable
+59
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+71
@@ -0,0 +1,71 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.content_blocker;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class ContentBlockerAction {
|
||||
@NonNull
|
||||
private ContentBlockerActionType type;
|
||||
|
||||
@Nullable
|
||||
private String selector;
|
||||
|
||||
ContentBlockerAction(@NonNull ContentBlockerActionType type, @Nullable String selector) {
|
||||
this.type = type;
|
||||
if (this.type.equals(ContentBlockerActionType.CSS_DISPLAY_NONE)) {
|
||||
assert(selector != null);
|
||||
}
|
||||
this.selector = selector;
|
||||
}
|
||||
|
||||
public static ContentBlockerAction fromMap(Map<String, Object> map) {
|
||||
ContentBlockerActionType type = ContentBlockerActionType.fromValue((String) map.get("type"));
|
||||
String selector = (String) map.get("selector");
|
||||
return new ContentBlockerAction(type, selector);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public ContentBlockerActionType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(@NonNull ContentBlockerActionType type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getSelector() {
|
||||
return selector;
|
||||
}
|
||||
|
||||
public void setSelector(String selector) {
|
||||
this.selector = selector;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
ContentBlockerAction that = (ContentBlockerAction) o;
|
||||
|
||||
if (type != that.type) return false;
|
||||
return selector != null ? selector.equals(that.selector) : that.selector == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = type.hashCode();
|
||||
result = 31 * result + (selector != null ? selector.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ContentBlockerAction{" +
|
||||
"type=" + type +
|
||||
", selector='" + selector + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+30
@@ -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;
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+336
@@ -0,0 +1,336 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.content_blocker;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.webkit.WebResourceResponse;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.Util;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js.JavaScriptBridgeJS;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.WebResourceRequestExt;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebView;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
|
||||
public class ContentBlockerHandler {
|
||||
protected static final String LOG_TAG = "ContentBlockerHandler";
|
||||
|
||||
protected List<ContentBlocker> ruleList = new ArrayList<>();
|
||||
|
||||
public ContentBlockerHandler() {}
|
||||
|
||||
public ContentBlockerHandler(List<ContentBlocker> ruleList) {
|
||||
this.ruleList = ruleList;
|
||||
}
|
||||
|
||||
public List<ContentBlocker> getRuleList() {
|
||||
return this.ruleList;
|
||||
}
|
||||
|
||||
public void setRuleList(List<ContentBlocker> newRuleList) {
|
||||
this.ruleList = newRuleList;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public WebResourceResponse checkUrl(final InAppWebView webView, WebResourceRequestExt request,
|
||||
ContentBlockerTriggerResourceType responseResourceType)
|
||||
throws URISyntaxException, InterruptedException, MalformedURLException {
|
||||
if (webView.customSettings.contentBlockers == null)
|
||||
return null;
|
||||
|
||||
String url = request.getUrl();
|
||||
|
||||
URI u;
|
||||
try {
|
||||
u = new URI(url);
|
||||
} catch (URISyntaxException e) {
|
||||
String[] urlSplit = url.split(":");
|
||||
String scheme = urlSplit[0];
|
||||
URL tempUrl = new URL(url.replace(scheme, "https"));
|
||||
u = new URI(scheme, tempUrl.getUserInfo(), tempUrl.getHost(), tempUrl.getPort(), tempUrl.getPath(), tempUrl.getQuery(), tempUrl.getRef());
|
||||
}
|
||||
String host = u.getHost();
|
||||
int port = u.getPort();
|
||||
String scheme = u.getScheme();
|
||||
// thread safe copy list
|
||||
List<ContentBlocker> ruleListCopy = new CopyOnWriteArrayList<ContentBlocker>(ruleList);
|
||||
|
||||
for (ContentBlocker contentBlocker : ruleListCopy) {
|
||||
ContentBlockerTrigger trigger = contentBlocker.getTrigger();
|
||||
List<ContentBlockerTriggerResourceType> resourceTypes = trigger.getResourceType();
|
||||
if (resourceTypes.contains(ContentBlockerTriggerResourceType.IMAGE) && !resourceTypes.contains(ContentBlockerTriggerResourceType.SVG_DOCUMENT)) {
|
||||
resourceTypes.add(ContentBlockerTriggerResourceType.SVG_DOCUMENT);
|
||||
}
|
||||
|
||||
ContentBlockerAction action = contentBlocker.getAction();
|
||||
|
||||
Matcher m = trigger.getUrlFilterPatternCompiled().matcher(url);
|
||||
if (m.matches()) {
|
||||
|
||||
if (!resourceTypes.isEmpty() && !resourceTypes.contains(responseResourceType)) {
|
||||
return null;
|
||||
}
|
||||
if (!trigger.getIfDomain().isEmpty()) {
|
||||
boolean matchFound = false;
|
||||
for (String domain : trigger.getIfDomain()) {
|
||||
if ((domain.startsWith("*") && host.endsWith(domain.replace("*", ""))) || domain.equals(host)) {
|
||||
matchFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matchFound)
|
||||
return null;
|
||||
}
|
||||
if (!trigger.getUnlessDomain().isEmpty()) {
|
||||
for (String domain : trigger.getUnlessDomain())
|
||||
if ((domain.startsWith("*") && host.endsWith(domain.replace("*", ""))) || domain.equals(host))
|
||||
return null;
|
||||
}
|
||||
|
||||
final String[] webViewUrl = new String[1];
|
||||
if (!trigger.getLoadType().isEmpty() || !trigger.getIfTopUrl().isEmpty() || !trigger.getUnlessTopUrl().isEmpty()) {
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
Handler handler = new Handler(webView.getWebViewLooper());
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
webViewUrl[0] = webView.getUrl();
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
latch.await();
|
||||
}
|
||||
|
||||
if (webViewUrl[0] != null) {
|
||||
if (!trigger.getLoadType().isEmpty()) {
|
||||
URI cUrl = new URI(webViewUrl[0]);
|
||||
String cHost = cUrl.getHost();
|
||||
int cPort = cUrl.getPort();
|
||||
String cScheme = cUrl.getScheme();
|
||||
|
||||
if ( (trigger.getLoadType().contains("first-party") && cHost != null && !(cScheme.equals(scheme) && cHost.equals(host) && cPort == port)) ||
|
||||
(trigger.getLoadType().contains("third-party") && cHost != null && cHost.equals(host)) )
|
||||
return null;
|
||||
}
|
||||
if (!trigger.getIfTopUrl().isEmpty()) {
|
||||
boolean matchFound = false;
|
||||
for (String topUrl : trigger.getIfTopUrl()) {
|
||||
if (webViewUrl[0].startsWith(topUrl)) {
|
||||
matchFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matchFound)
|
||||
return null;
|
||||
}
|
||||
if (!trigger.getUnlessTopUrl().isEmpty()) {
|
||||
for (String topUrl : trigger.getUnlessTopUrl())
|
||||
if (webViewUrl[0].startsWith(topUrl))
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
switch (action.getType()) {
|
||||
|
||||
case BLOCK:
|
||||
return new WebResourceResponse("", "", null);
|
||||
|
||||
case CSS_DISPLAY_NONE:
|
||||
final String cssSelector = action.getSelector();
|
||||
final String jsScript = "(function(d) { " +
|
||||
" function hide () { " +
|
||||
" if (d.body != null && !d.getElementById('" + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "-css-display-none-style')) { " +
|
||||
" var c = d.createElement('style'); " +
|
||||
" c.id = '" + JavaScriptBridgeJS.JAVASCRIPT_BRIDGE_NAME + "-css-display-none-style'; " +
|
||||
" c.innerHTML = '" + cssSelector + " { display: none !important; }'; " +
|
||||
" d.body.appendChild(c); " +
|
||||
" }" +
|
||||
" d.querySelectorAll('" + cssSelector + "').forEach(function (item, index) { " +
|
||||
" item.setAttribute('style', 'display: none !important;'); " +
|
||||
" }); " +
|
||||
" }; " +
|
||||
" hide(); " +
|
||||
" d.addEventListener('DOMContentLoaded', function(event) { hide(); }); " +
|
||||
"})(document);";
|
||||
|
||||
final Handler handler = new Handler(webView.getWebViewLooper());
|
||||
handler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
webView.evaluateJavascript(jsScript, null);
|
||||
} else {
|
||||
webView.loadUrl("javascript:" + jsScript);
|
||||
}
|
||||
}
|
||||
}, 800);
|
||||
break;
|
||||
|
||||
case MAKE_HTTPS:
|
||||
if (scheme.equals("http") && (port == -1 || port == 80)) {
|
||||
String urlHttps = url.replace("http://", "https://");
|
||||
|
||||
HttpURLConnection urlConnection = Util.makeHttpRequest(urlHttps, request.getMethod(), request.getHeaders());
|
||||
if (urlConnection != null) {
|
||||
try {
|
||||
byte[] dataBytes = Util.readAllBytes(urlConnection.getInputStream());
|
||||
if (dataBytes == null) {
|
||||
return null;
|
||||
}
|
||||
InputStream dataStream = new ByteArrayInputStream(dataBytes);
|
||||
|
||||
String encoding = urlConnection.getContentEncoding();
|
||||
String contentType = urlConnection.getContentType();
|
||||
if (contentType == null) {
|
||||
contentType = "text/plain";
|
||||
} else {
|
||||
String[] contentTypeSplit = contentType.split(";");
|
||||
contentType = contentTypeSplit[0].trim();
|
||||
if (encoding == null) {
|
||||
encoding = (contentTypeSplit.length > 1 && contentTypeSplit[1].contains("charset="))
|
||||
? contentTypeSplit[1].replace("charset=", "").trim()
|
||||
: "utf-8";
|
||||
}
|
||||
}
|
||||
|
||||
String reasonPhrase = urlConnection.getResponseMessage();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && reasonPhrase != null) {
|
||||
Map<String, String> responseHeaders = new HashMap<>();
|
||||
for (Map.Entry<String, List<String>> responseHeader : urlConnection.getHeaderFields().entrySet()) {
|
||||
responseHeaders.put(responseHeader.getKey(), TextUtils.join(",", responseHeader.getValue()));
|
||||
}
|
||||
return new WebResourceResponse(contentType,
|
||||
encoding,
|
||||
urlConnection.getResponseCode(),
|
||||
reasonPhrase,
|
||||
responseHeaders,
|
||||
dataStream);
|
||||
} else {
|
||||
return new WebResourceResponse(contentType,
|
||||
encoding,
|
||||
dataStream);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (!(e instanceof SSLHandshakeException)) {
|
||||
Log.e(LOG_TAG, "", e);
|
||||
}
|
||||
} finally {
|
||||
urlConnection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// Request mRequest = new Request.Builder().url(urlHttps).build();
|
||||
// Response response = null;
|
||||
//
|
||||
// try {
|
||||
// response = Util.getBasicOkHttpClient().newCall(mRequest).execute();
|
||||
// byte[] dataBytes = response.body().bytes();
|
||||
// InputStream dataStream = new ByteArrayInputStream(dataBytes);
|
||||
//
|
||||
// String[] contentTypeSplit = response.header("content-type", "text/plain").split(";");
|
||||
//
|
||||
// String contentType = contentTypeSplit[0].trim();
|
||||
// String encoding = (contentTypeSplit.length > 1 && contentTypeSplit[1].contains("charset="))
|
||||
// ? contentTypeSplit[1].replace("charset=", "").trim()
|
||||
// : "utf-8";
|
||||
//
|
||||
// response.body().close();
|
||||
// response.close();
|
||||
//
|
||||
// return new WebResourceResponse(contentType, encoding, dataStream);
|
||||
//
|
||||
// } catch (Exception e) {
|
||||
// if (response != null) {
|
||||
// response.body().close();
|
||||
// response.close();
|
||||
// }
|
||||
// if (!(e instanceof SSLHandshakeException)) {
|
||||
// Log.e(LOG_TAG, "", e);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public WebResourceResponse checkUrl(final InAppWebView webView, WebResourceRequestExt request) throws URISyntaxException, InterruptedException, MalformedURLException {
|
||||
ContentBlockerTriggerResourceType responseResourceType = getResourceTypeFromUrl(request);
|
||||
return checkUrl(webView, request, responseResourceType);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public WebResourceResponse checkUrl(final InAppWebView webView, WebResourceRequestExt request, String contentType) throws URISyntaxException, InterruptedException, MalformedURLException {
|
||||
ContentBlockerTriggerResourceType responseResourceType = getResourceTypeFromContentType(contentType);
|
||||
return checkUrl(webView, request, responseResourceType);
|
||||
}
|
||||
|
||||
public ContentBlockerTriggerResourceType getResourceTypeFromUrl(WebResourceRequestExt request) {
|
||||
ContentBlockerTriggerResourceType responseResourceType = ContentBlockerTriggerResourceType.RAW;
|
||||
String url = request.getUrl();
|
||||
|
||||
if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||
// make an HTTP "HEAD" request to the server for that URL. This will not return the full content of the URL.
|
||||
HttpURLConnection urlConnection = Util.makeHttpRequest(url, "HEAD", request.getHeaders());
|
||||
if (urlConnection != null) {
|
||||
try {
|
||||
String contentType = urlConnection.getContentType();
|
||||
if (contentType != null) {
|
||||
String[] contentTypeSplit = contentType.split(";");
|
||||
contentType = contentTypeSplit[0].trim();
|
||||
responseResourceType = getResourceTypeFromContentType(contentType);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(LOG_TAG, "", e);
|
||||
} finally {
|
||||
urlConnection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
return responseResourceType;
|
||||
}
|
||||
|
||||
public ContentBlockerTriggerResourceType getResourceTypeFromContentType(String contentType) {
|
||||
ContentBlockerTriggerResourceType responseResourceType = ContentBlockerTriggerResourceType.RAW;
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
|
||||
if (contentType.equals("text/css")) {
|
||||
responseResourceType = ContentBlockerTriggerResourceType.STYLE_SHEET;
|
||||
} else if (contentType.equals("image/svg+xml")) {
|
||||
responseResourceType = ContentBlockerTriggerResourceType.SVG_DOCUMENT;
|
||||
} else if (contentType.startsWith("image/")) {
|
||||
responseResourceType = ContentBlockerTriggerResourceType.IMAGE;
|
||||
} else if (contentType.startsWith("font/")) {
|
||||
responseResourceType = ContentBlockerTriggerResourceType.FONT;
|
||||
} else if (contentType.startsWith("audio/") || contentType.startsWith("video/") || contentType.equals("application/ogg")) {
|
||||
responseResourceType = ContentBlockerTriggerResourceType.MEDIA;
|
||||
} else if (contentType.endsWith("javascript")) {
|
||||
responseResourceType = ContentBlockerTriggerResourceType.SCRIPT;
|
||||
} else if (contentType.startsWith("text/")) {
|
||||
responseResourceType = ContentBlockerTriggerResourceType.DOCUMENT;
|
||||
}
|
||||
|
||||
return responseResourceType;
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+185
@@ -0,0 +1,185 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.content_blocker;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class ContentBlockerTrigger {
|
||||
|
||||
@NonNull
|
||||
private String urlFilter;
|
||||
private Pattern urlFilterPatternCompiled;
|
||||
private Boolean urlFilterIsCaseSensitive;
|
||||
private List<ContentBlockerTriggerResourceType> resourceType = new ArrayList<>();
|
||||
private List<String> ifDomain = new ArrayList<>();
|
||||
private List<String> unlessDomain = new ArrayList<>();
|
||||
private List<String> loadType = new ArrayList<>();
|
||||
private List<String> ifTopUrl = new ArrayList<>();
|
||||
private List<String> unlessTopUrl = new ArrayList<>();
|
||||
|
||||
public ContentBlockerTrigger(@NonNull String urlFilter, @Nullable Boolean urlFilterIsCaseSensitive, @Nullable List<ContentBlockerTriggerResourceType> resourceType,
|
||||
@Nullable List<String> ifDomain, @Nullable List<String> unlessDomain, @Nullable List<String> loadType,
|
||||
@Nullable List<String> ifTopUrl, @Nullable List<String> unlessTopUrl) {
|
||||
this.urlFilterIsCaseSensitive = urlFilterIsCaseSensitive != null ? urlFilterIsCaseSensitive : false;
|
||||
|
||||
this.urlFilter = urlFilter;
|
||||
this.urlFilterPatternCompiled = Pattern.compile(this.urlFilter, this.urlFilterIsCaseSensitive ? 0 : Pattern.CASE_INSENSITIVE);
|
||||
|
||||
this.resourceType = resourceType != null ? resourceType : this.resourceType;
|
||||
this.ifDomain = ifDomain != null ? ifDomain : this.ifDomain;
|
||||
this.unlessDomain = unlessDomain != null ? unlessDomain : this.unlessDomain;
|
||||
if ((!(this.ifDomain.isEmpty() || this.unlessDomain.isEmpty()) != false))
|
||||
throw new AssertionError();
|
||||
this.loadType = loadType != null ? loadType : this.loadType;
|
||||
if ((this.loadType.size() > 2)) throw new AssertionError();
|
||||
this.ifTopUrl = ifTopUrl != null ? ifTopUrl : this.ifTopUrl;
|
||||
this.unlessTopUrl = unlessTopUrl != null ? unlessTopUrl : this.unlessTopUrl;
|
||||
if ((!(this.ifTopUrl.isEmpty() || this.unlessTopUrl.isEmpty()) != false))
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
public static ContentBlockerTrigger fromMap(Map<String, Object> map) {
|
||||
String urlFilter = (String) map.get("url-filter");
|
||||
Boolean urlFilterIsCaseSensitive = (Boolean) map.get("url-filter-is-case-sensitive");
|
||||
List<String> resourceTypeStringList = (List<String>) map.get("resource-type");
|
||||
List<ContentBlockerTriggerResourceType> resourceType = new ArrayList<>();
|
||||
if (resourceTypeStringList != null) {
|
||||
for (String type : resourceTypeStringList) {
|
||||
resourceType.add(ContentBlockerTriggerResourceType.fromValue(type));
|
||||
}
|
||||
} else {
|
||||
resourceType.addAll(Arrays.asList(ContentBlockerTriggerResourceType.values()));
|
||||
}
|
||||
List<String> ifDomain = (List<String>) map.get("if-domain");
|
||||
List<String> unlessDomain = (List<String>) map.get("unless-domain");
|
||||
List<String> loadType = (List<String>) map.get("load-type");
|
||||
List<String> ifTopUrl = (List<String>) map.get("if-top-url");
|
||||
List<String> unlessTopUrl = (List<String>) map.get("unless-top-url");
|
||||
return new ContentBlockerTrigger(urlFilter, urlFilterIsCaseSensitive, resourceType, ifDomain, unlessDomain, loadType, ifTopUrl, unlessTopUrl);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getUrlFilter() {
|
||||
return urlFilter;
|
||||
}
|
||||
|
||||
public void setUrlFilter(@NonNull String urlFilter) {
|
||||
this.urlFilter = urlFilter;
|
||||
}
|
||||
|
||||
public Pattern getUrlFilterPatternCompiled() {
|
||||
return urlFilterPatternCompiled;
|
||||
}
|
||||
|
||||
public void setUrlFilterPatternCompiled(Pattern urlFilterPatternCompiled) {
|
||||
this.urlFilterPatternCompiled = urlFilterPatternCompiled;
|
||||
}
|
||||
|
||||
public Boolean getUrlFilterIsCaseSensitive() {
|
||||
return urlFilterIsCaseSensitive;
|
||||
}
|
||||
|
||||
public void setUrlFilterIsCaseSensitive(Boolean urlFilterIsCaseSensitive) {
|
||||
this.urlFilterIsCaseSensitive = urlFilterIsCaseSensitive;
|
||||
}
|
||||
|
||||
public List<ContentBlockerTriggerResourceType> getResourceType() {
|
||||
return resourceType;
|
||||
}
|
||||
|
||||
public void setResourceType(List<ContentBlockerTriggerResourceType> resourceType) {
|
||||
this.resourceType = resourceType;
|
||||
}
|
||||
|
||||
public List<String> getIfDomain() {
|
||||
return ifDomain;
|
||||
}
|
||||
|
||||
public void setIfDomain(List<String> ifDomain) {
|
||||
this.ifDomain = ifDomain;
|
||||
}
|
||||
|
||||
public List<String> getUnlessDomain() {
|
||||
return unlessDomain;
|
||||
}
|
||||
|
||||
public void setUnlessDomain(List<String> unlessDomain) {
|
||||
this.unlessDomain = unlessDomain;
|
||||
}
|
||||
|
||||
public List<String> getLoadType() {
|
||||
return loadType;
|
||||
}
|
||||
|
||||
public void setLoadType(List<String> loadType) {
|
||||
this.loadType = loadType;
|
||||
}
|
||||
|
||||
public List<String> getIfTopUrl() {
|
||||
return ifTopUrl;
|
||||
}
|
||||
|
||||
public void setIfTopUrl(List<String> ifTopUrl) {
|
||||
this.ifTopUrl = ifTopUrl;
|
||||
}
|
||||
|
||||
public List<String> getUnlessTopUrl() {
|
||||
return unlessTopUrl;
|
||||
}
|
||||
|
||||
public void setUnlessTopUrl(List<String> unlessTopUrl) {
|
||||
this.unlessTopUrl = unlessTopUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
ContentBlockerTrigger that = (ContentBlockerTrigger) o;
|
||||
|
||||
if (!urlFilter.equals(that.urlFilter)) return false;
|
||||
if (!urlFilterPatternCompiled.equals(that.urlFilterPatternCompiled)) return false;
|
||||
if (!urlFilterIsCaseSensitive.equals(that.urlFilterIsCaseSensitive)) return false;
|
||||
if (!resourceType.equals(that.resourceType)) return false;
|
||||
if (!ifDomain.equals(that.ifDomain)) return false;
|
||||
if (!unlessDomain.equals(that.unlessDomain)) return false;
|
||||
if (!loadType.equals(that.loadType)) return false;
|
||||
if (!ifTopUrl.equals(that.ifTopUrl)) return false;
|
||||
return unlessTopUrl.equals(that.unlessTopUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = urlFilter.hashCode();
|
||||
result = 31 * result + urlFilterPatternCompiled.hashCode();
|
||||
result = 31 * result + urlFilterIsCaseSensitive.hashCode();
|
||||
result = 31 * result + resourceType.hashCode();
|
||||
result = 31 * result + ifDomain.hashCode();
|
||||
result = 31 * result + unlessDomain.hashCode();
|
||||
result = 31 * result + loadType.hashCode();
|
||||
result = 31 * result + ifTopUrl.hashCode();
|
||||
result = 31 * result + unlessTopUrl.hashCode();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ContentBlockerTrigger{" +
|
||||
"urlFilter='" + urlFilter + '\'' +
|
||||
", urlFilterPatternCompiled=" + urlFilterPatternCompiled +
|
||||
", urlFilterIsCaseSensitive=" + urlFilterIsCaseSensitive +
|
||||
", resourceType=" + resourceType +
|
||||
", ifDomain=" + ifDomain +
|
||||
", unlessDomain=" + unlessDomain +
|
||||
", loadType=" + loadType +
|
||||
", ifTopUrl=" + ifTopUrl +
|
||||
", unlessTopUrl=" + unlessTopUrl +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+36
@@ -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;
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+95
@@ -0,0 +1,95 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.credential_database;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.URLCredential;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.URLProtectionSpace;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class CredentialDatabase {
|
||||
|
||||
private static CredentialDatabase instance;
|
||||
static final String LOG_TAG = "CredentialDatabase";
|
||||
|
||||
// If you change the database schema, you must increment the database version.
|
||||
public static final int DATABASE_VERSION = 2;
|
||||
public static final String DATABASE_NAME = "CredentialDatabase.db";
|
||||
|
||||
public URLProtectionSpaceDao protectionSpaceDao;
|
||||
public URLCredentialDao credentialDao;
|
||||
public CredentialDatabaseHelper db;
|
||||
|
||||
private CredentialDatabase() {}
|
||||
|
||||
private CredentialDatabase(CredentialDatabaseHelper db, URLProtectionSpaceDao protectionSpaceDao, URLCredentialDao credentialDao) {
|
||||
this.db = db;
|
||||
this.protectionSpaceDao = protectionSpaceDao;
|
||||
this.credentialDao = credentialDao;
|
||||
}
|
||||
|
||||
public static CredentialDatabase getInstance(Context context) {
|
||||
if (instance != null)
|
||||
return instance;
|
||||
CredentialDatabaseHelper db = new CredentialDatabaseHelper(context);
|
||||
instance = new CredentialDatabase(db, new URLProtectionSpaceDao(db), new URLCredentialDao(db));
|
||||
return instance;
|
||||
}
|
||||
|
||||
public List<URLCredential> getHttpAuthCredentials(String host, String protocol, String realm, Integer port) {
|
||||
List<URLCredential> credentials = new ArrayList<>();
|
||||
URLProtectionSpace protectionSpace = protectionSpaceDao.find(host, protocol, realm, port);
|
||||
if (protectionSpace != null) {
|
||||
credentials = credentialDao.getAllByProtectionSpaceId(protectionSpace.getId());
|
||||
}
|
||||
return credentials;
|
||||
}
|
||||
|
||||
public void clearAllAuthCredentials() {
|
||||
db.clearAllTables(db.getWritableDatabase());
|
||||
}
|
||||
|
||||
public void removeHttpAuthCredentials(String host, String protocol, String realm, Integer port) {
|
||||
URLProtectionSpace URLProtectionSpace = protectionSpaceDao.find(host, protocol, realm, port);
|
||||
if (URLProtectionSpace != null) {
|
||||
protectionSpaceDao.delete(URLProtectionSpace);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeHttpAuthCredential(String host, String protocol, String realm, Integer port, String username, String password) {
|
||||
URLProtectionSpace protectionSpace = protectionSpaceDao.find(host, protocol, realm, port);
|
||||
if (protectionSpace != null) {
|
||||
URLCredential credential = credentialDao.find(username, password, protectionSpace.getId());
|
||||
credentialDao.delete(credential);
|
||||
}
|
||||
}
|
||||
|
||||
public void setHttpAuthCredential(String host, String protocol, String realm, Integer port, String username, String password) {
|
||||
URLProtectionSpace protectionSpace = protectionSpaceDao.find(host, protocol, realm, port);
|
||||
Long protectionSpaceId;
|
||||
if (protectionSpace == null) {
|
||||
protectionSpaceId = protectionSpaceDao.insert(new URLProtectionSpace(null, host, protocol, realm, port));
|
||||
} else {
|
||||
protectionSpaceId = protectionSpace.getId();
|
||||
}
|
||||
|
||||
URLCredential credential = credentialDao.find(username, password, protectionSpaceId);
|
||||
if (credential != null) {
|
||||
boolean needUpdate = false;
|
||||
if (!credential.getUsername().equals(username)) {
|
||||
credential.setUsername(username);
|
||||
needUpdate = true;
|
||||
}
|
||||
if (!credential.getPassword().equals(password)) {
|
||||
credential.setPassword(password);
|
||||
needUpdate = true;
|
||||
}
|
||||
if (needUpdate)
|
||||
credentialDao.update(credential);
|
||||
} else {
|
||||
credential = new URLCredential(null, username, password, protectionSpaceId);
|
||||
credential.setId(credentialDao.insert(credential));
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+157
@@ -0,0 +1,157 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.credential_database;
|
||||
|
||||
import android.os.Build;
|
||||
import android.webkit.WebViewDatabase;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.URLCredential;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.URLProtectionSpace;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
public class CredentialDatabaseHandler extends ChannelDelegateImpl {
|
||||
protected static final String LOG_TAG = "CredentialDatabaseHandler";
|
||||
public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_credential_database";
|
||||
|
||||
@Nullable
|
||||
public static CredentialDatabase credentialDatabase;
|
||||
@Nullable
|
||||
public InAppWebViewFlutterPlugin plugin;
|
||||
|
||||
public CredentialDatabaseHandler(@NonNull final InAppWebViewFlutterPlugin plugin) {
|
||||
super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME));
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public static void init(@NonNull InAppWebViewFlutterPlugin plugin) {
|
||||
if (credentialDatabase == null) {
|
||||
credentialDatabase = CredentialDatabase.getInstance(plugin.applicationContext);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||
if (plugin != null) {
|
||||
init(plugin);
|
||||
}
|
||||
|
||||
switch (call.method) {
|
||||
case "getAllAuthCredentials":
|
||||
{
|
||||
List<Map<String, Object>> allCredentials = new ArrayList<>();
|
||||
if (credentialDatabase != null) {
|
||||
List<URLProtectionSpace> protectionSpaces = credentialDatabase.protectionSpaceDao.getAll();
|
||||
for (URLProtectionSpace protectionSpace : protectionSpaces) {
|
||||
List<Map<String, Object>> credentials = new ArrayList<>();
|
||||
for (URLCredential credential : credentialDatabase.credentialDao.getAllByProtectionSpaceId(protectionSpace.getId())) {
|
||||
credentials.add(credential.toMap());
|
||||
}
|
||||
Map<String, Object> obj = new HashMap<>();
|
||||
obj.put("protectionSpace", protectionSpace.toMap());
|
||||
obj.put("credentials", credentials);
|
||||
allCredentials.add(obj);
|
||||
}
|
||||
}
|
||||
result.success(allCredentials);
|
||||
}
|
||||
break;
|
||||
case "getHttpAuthCredentials":
|
||||
{
|
||||
List<Map<String, Object>> credentials = new ArrayList<>();
|
||||
if (credentialDatabase != null) {
|
||||
String host = (String) call.argument("host");
|
||||
String protocol = (String) call.argument("protocol");
|
||||
String realm = (String) call.argument("realm");
|
||||
Integer port = (Integer) call.argument("port");
|
||||
|
||||
for (URLCredential credential : credentialDatabase.getHttpAuthCredentials(host, protocol, realm, port)) {
|
||||
credentials.add(credential.toMap());
|
||||
}
|
||||
}
|
||||
result.success(credentials);
|
||||
}
|
||||
break;
|
||||
case "setHttpAuthCredential":
|
||||
{
|
||||
if (credentialDatabase != null) {
|
||||
String host = (String) call.argument("host");
|
||||
String protocol = (String) call.argument("protocol");
|
||||
String realm = (String) call.argument("realm");
|
||||
Integer port = (Integer) call.argument("port");
|
||||
String username = (String) call.argument("username");
|
||||
String password = (String) call.argument("password");
|
||||
|
||||
credentialDatabase.setHttpAuthCredential(host, protocol, realm, port, username, password);
|
||||
result.success(true);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "removeHttpAuthCredential":
|
||||
{
|
||||
if (credentialDatabase != null) {
|
||||
String host = (String) call.argument("host");
|
||||
String protocol = (String) call.argument("protocol");
|
||||
String realm = (String) call.argument("realm");
|
||||
Integer port = (Integer) call.argument("port");
|
||||
String username = (String) call.argument("username");
|
||||
String password = (String) call.argument("password");
|
||||
|
||||
credentialDatabase.removeHttpAuthCredential(host, protocol, realm, port, username, password);
|
||||
result.success(true);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "removeHttpAuthCredentials":
|
||||
{
|
||||
if (credentialDatabase != null) {
|
||||
String host = (String) call.argument("host");
|
||||
String protocol = (String) call.argument("protocol");
|
||||
String realm = (String) call.argument("realm");
|
||||
Integer port = (Integer) call.argument("port");
|
||||
|
||||
credentialDatabase.removeHttpAuthCredentials(host, protocol, realm, port);
|
||||
result.success(true);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "clearAllAuthCredentials":
|
||||
if (credentialDatabase != null) {
|
||||
credentialDatabase.clearAllAuthCredentials();
|
||||
if (plugin != null && plugin.applicationContext != null) {
|
||||
WebViewDatabase.getInstance(plugin.applicationContext).clearHttpAuthUsernamePassword();
|
||||
}
|
||||
result.success(true);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
result.notImplemented();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
plugin = null;
|
||||
credentialDatabase = null;
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+66
@@ -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);
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+15
@@ -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";
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+106
@@ -0,0 +1,106 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.credential_database;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.URLCredential;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class URLCredentialDao {
|
||||
|
||||
CredentialDatabaseHelper credentialDatabaseHelper;
|
||||
String[] projection = {
|
||||
URLCredentialContract.FeedEntry._ID,
|
||||
URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME,
|
||||
URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD,
|
||||
URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID
|
||||
};
|
||||
|
||||
public URLCredentialDao(CredentialDatabaseHelper credentialDatabaseHelper) {
|
||||
this.credentialDatabaseHelper = credentialDatabaseHelper;
|
||||
}
|
||||
|
||||
public List<URLCredential> getAllByProtectionSpaceId(Long protectionSpaceId) {
|
||||
String selection = URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + " = ?";
|
||||
String[] selectionArgs = {protectionSpaceId.toString()};
|
||||
|
||||
Cursor cursor = credentialDatabaseHelper.getReadableDatabase().query(
|
||||
URLCredentialContract.FeedEntry.TABLE_NAME,
|
||||
projection,
|
||||
selection,
|
||||
selectionArgs,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
List<URLCredential> URLCredentials = new ArrayList<>();
|
||||
while (cursor.moveToNext()) {
|
||||
Long id = cursor.getLong(cursor.getColumnIndexOrThrow(URLCredentialContract.FeedEntry._ID));
|
||||
String username = cursor.getString(cursor.getColumnIndexOrThrow(URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME));
|
||||
String password = cursor.getString(cursor.getColumnIndexOrThrow(URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD));
|
||||
URLCredentials.add(new URLCredential(id, username, password, protectionSpaceId));
|
||||
}
|
||||
cursor.close();
|
||||
|
||||
return URLCredentials;
|
||||
}
|
||||
|
||||
public URLCredential find(String username, String password, Long protectionSpaceId) {
|
||||
String selection = URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME + " = ? AND " +
|
||||
URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD + " = ? AND " +
|
||||
URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + " = ?";
|
||||
String[] selectionArgs = {username, password, protectionSpaceId.toString()};
|
||||
|
||||
Cursor cursor = credentialDatabaseHelper.getReadableDatabase().query(
|
||||
URLCredentialContract.FeedEntry.TABLE_NAME,
|
||||
projection,
|
||||
selection,
|
||||
selectionArgs,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
URLCredential URLCredential = null;
|
||||
if (cursor.moveToNext()) {
|
||||
Long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(URLCredentialContract.FeedEntry._ID));
|
||||
String rowUsername = cursor.getString(cursor.getColumnIndexOrThrow(URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME));
|
||||
String rowPassword = cursor.getString(cursor.getColumnIndexOrThrow(URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD));
|
||||
URLCredential = new URLCredential(rowId, rowUsername, rowPassword, protectionSpaceId);
|
||||
}
|
||||
cursor.close();
|
||||
|
||||
return URLCredential;
|
||||
}
|
||||
|
||||
public long insert(URLCredential urlCredential) {
|
||||
ContentValues credentialValues = new ContentValues();
|
||||
credentialValues.put(URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME, urlCredential.getUsername());
|
||||
credentialValues.put(URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD, urlCredential.getPassword());
|
||||
credentialValues.put(URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID, urlCredential.getProtectionSpaceId());
|
||||
|
||||
return credentialDatabaseHelper.getWritableDatabase().insert(URLCredentialContract.FeedEntry.TABLE_NAME, null, credentialValues);
|
||||
}
|
||||
|
||||
public long update(URLCredential urlCredential) {
|
||||
ContentValues credentialValues = new ContentValues();
|
||||
credentialValues.put(URLCredentialContract.FeedEntry.COLUMN_NAME_USERNAME, urlCredential.getUsername());
|
||||
credentialValues.put(URLCredentialContract.FeedEntry.COLUMN_NAME_PASSWORD, urlCredential.getPassword());
|
||||
|
||||
String whereClause = URLCredentialContract.FeedEntry.COLUMN_NAME_PROTECTION_SPACE_ID + " = ?";
|
||||
String[] whereArgs = {urlCredential.getProtectionSpaceId().toString()};
|
||||
|
||||
return credentialDatabaseHelper.getWritableDatabase().update(URLCredentialContract.FeedEntry.TABLE_NAME, credentialValues, whereClause, whereArgs);
|
||||
}
|
||||
|
||||
public long delete(URLCredential urlCredential) {
|
||||
String whereClause = URLCredentialContract.FeedEntry._ID + " = ?";
|
||||
String[] whereArgs = {urlCredential.getId().toString()};
|
||||
|
||||
return credentialDatabaseHelper.getWritableDatabase().delete(URLCredentialContract.FeedEntry.TABLE_NAME, whereClause, whereArgs);
|
||||
}
|
||||
|
||||
}
|
||||
Vendored
Executable
+16
@@ -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";
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+100
@@ -0,0 +1,100 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.credential_database;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.URLProtectionSpace;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class URLProtectionSpaceDao {
|
||||
CredentialDatabaseHelper credentialDatabaseHelper;
|
||||
String[] projection = {
|
||||
URLProtectionSpaceContract.FeedEntry._ID,
|
||||
URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST,
|
||||
URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL,
|
||||
URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM,
|
||||
URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT
|
||||
};
|
||||
|
||||
public URLProtectionSpaceDao(CredentialDatabaseHelper credentialDatabaseHelper) {
|
||||
this.credentialDatabaseHelper = credentialDatabaseHelper;
|
||||
}
|
||||
|
||||
public List<URLProtectionSpace> getAll() {
|
||||
SQLiteDatabase readableDatabase = credentialDatabaseHelper.getReadableDatabase();
|
||||
|
||||
Cursor cursor = readableDatabase.query(
|
||||
URLProtectionSpaceContract.FeedEntry.TABLE_NAME,
|
||||
projection,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
List<URLProtectionSpace> URLProtectionSpaces = new ArrayList<>();
|
||||
while (cursor.moveToNext()) {
|
||||
Long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry._ID));
|
||||
String rowHost = cursor.getString(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST));
|
||||
String rowProtocol = cursor.getString(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL));
|
||||
String rowRealm = cursor.getString(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM));
|
||||
Integer rowPort = cursor.getInt(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT));
|
||||
URLProtectionSpaces.add(new URLProtectionSpace(rowId, rowHost, rowProtocol, rowRealm, rowPort));
|
||||
}
|
||||
cursor.close();
|
||||
|
||||
return URLProtectionSpaces;
|
||||
}
|
||||
|
||||
public URLProtectionSpace find(String host, String protocol, String realm, Integer port) {
|
||||
SQLiteDatabase readableDatabase = credentialDatabaseHelper.getReadableDatabase();
|
||||
|
||||
String selection = URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST + " = ? AND " + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL + " = ? AND " +
|
||||
URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM + " = ? AND " + URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT + " = ?";
|
||||
String[] selectionArgs = {host, protocol, realm, port.toString()};
|
||||
|
||||
Cursor cursor = readableDatabase.query(
|
||||
URLProtectionSpaceContract.FeedEntry.TABLE_NAME,
|
||||
projection,
|
||||
selection,
|
||||
selectionArgs,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
URLProtectionSpace URLProtectionSpace = null;
|
||||
if (cursor.moveToNext()) {
|
||||
Long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry._ID));
|
||||
String rowHost = cursor.getString(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST));
|
||||
String rowProtocol = cursor.getString(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL));
|
||||
String rowRealm = cursor.getString(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM));
|
||||
Integer rowPort = cursor.getInt(cursor.getColumnIndexOrThrow(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT));
|
||||
URLProtectionSpace = new URLProtectionSpace(rowId, rowHost, rowProtocol, rowRealm, rowPort);
|
||||
}
|
||||
cursor.close();
|
||||
|
||||
return URLProtectionSpace;
|
||||
}
|
||||
|
||||
public long insert(URLProtectionSpace URLProtectionSpace) {
|
||||
ContentValues protectionSpaceValues = new ContentValues();
|
||||
protectionSpaceValues.put(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_HOST, URLProtectionSpace.getHost());
|
||||
protectionSpaceValues.put(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PROTOCOL, URLProtectionSpace.getProtocol());
|
||||
protectionSpaceValues.put(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_REALM, URLProtectionSpace.getRealm());
|
||||
protectionSpaceValues.put(URLProtectionSpaceContract.FeedEntry.COLUMN_NAME_PORT, URLProtectionSpace.getPort());
|
||||
|
||||
return credentialDatabaseHelper.getWritableDatabase().insert(URLProtectionSpaceContract.FeedEntry.TABLE_NAME, null, protectionSpaceValues);
|
||||
};
|
||||
|
||||
public long delete(URLProtectionSpace URLProtectionSpace) {
|
||||
String whereClause = URLProtectionSpaceContract.FeedEntry._ID + " = ?";
|
||||
String[] whereArgs = {URLProtectionSpace.getId().toString()};
|
||||
|
||||
return credentialDatabaseHelper.getWritableDatabase().delete(URLProtectionSpaceContract.FeedEntry.TABLE_NAME, whereClause, whereArgs);
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.find_interaction;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.FindSession;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
public class FindInteractionChannelDelegate extends ChannelDelegateImpl {
|
||||
@Nullable
|
||||
private FindInteractionController findInteractionController;
|
||||
|
||||
public FindInteractionChannelDelegate(@NonNull FindInteractionController findInteractionController, @NonNull MethodChannel channel) {
|
||||
super(channel);
|
||||
this.findInteractionController = findInteractionController;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(@NonNull MethodCall call, @NonNull final MethodChannel.Result result) {
|
||||
switch (call.method) {
|
||||
case "findAll":
|
||||
if (findInteractionController != null) {
|
||||
String find = (String) call.argument("find");
|
||||
findInteractionController.findAll(find);
|
||||
}
|
||||
result.success(true);
|
||||
break;
|
||||
case "findNext":
|
||||
if (findInteractionController != null) {
|
||||
Boolean forward = (Boolean) call.argument("forward");
|
||||
findInteractionController.findNext(forward);
|
||||
}
|
||||
result.success(true);
|
||||
break;
|
||||
case "clearMatches":
|
||||
if (findInteractionController != null) {
|
||||
findInteractionController.clearMatches();
|
||||
}
|
||||
result.success(true);
|
||||
break;
|
||||
case "setSearchText":
|
||||
if (findInteractionController != null) {
|
||||
findInteractionController.searchText = (String) call.argument("searchText");
|
||||
result.success(true);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
case "getSearchText":
|
||||
if (findInteractionController != null) {
|
||||
result.success(findInteractionController.searchText);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
case "getActiveFindSession":
|
||||
if (findInteractionController != null && findInteractionController.activeFindSession != null) {
|
||||
result.success(findInteractionController.activeFindSession.toMap());
|
||||
} else {
|
||||
result.success(null);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
result.notImplemented();
|
||||
}
|
||||
}
|
||||
|
||||
public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) {
|
||||
MethodChannel channel = getChannel();
|
||||
if (channel == null) return;
|
||||
|
||||
if (isDoneCounting && findInteractionController != null && findInteractionController.webView != null) {
|
||||
findInteractionController.activeFindSession = new FindSession(numberOfMatches, activeMatchOrdinal);
|
||||
}
|
||||
|
||||
Map<String, Object> obj = new HashMap<>();
|
||||
obj.put("activeMatchOrdinal", activeMatchOrdinal);
|
||||
obj.put("numberOfMatches", numberOfMatches);
|
||||
obj.put("isDoneCounting", isDoneCounting);
|
||||
channel.invokeMethod("onFindResultReceived", obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
findInteractionController = null;
|
||||
}
|
||||
}
|
||||
+73
@@ -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;
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.find_interaction;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.ISettings;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class FindInteractionSettings implements ISettings<FindInteractionController> {
|
||||
public static final String LOG_TAG = "FindInteractionSettings";
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public FindInteractionSettings parse(@NonNull Map<String, Object> settings) {
|
||||
// for (Map.Entry<String, Object> pair : settings.entrySet()) {
|
||||
// String key = pair.getKey();
|
||||
// Object value = pair.getValue();
|
||||
// if (value == null) {
|
||||
// continue;
|
||||
// }
|
||||
//
|
||||
// switch (key) {
|
||||
//
|
||||
// }
|
||||
// }
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Map<String, Object> toMap() {
|
||||
Map<String, Object> settings = new HashMap<>();
|
||||
return settings;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Map<String, Object> getRealSettings(@NonNull FindInteractionController findInteractionController) {
|
||||
Map<String, Object> realSettings = toMap();
|
||||
return realSettings;
|
||||
}
|
||||
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.headless_in_app_webview;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.Util;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.Size2D;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.FlutterWebView;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
public class HeadlessInAppWebView implements Disposable {
|
||||
protected static final String LOG_TAG = "HeadlessInAppWebView";
|
||||
public static final String METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_headless_inappwebview_";
|
||||
|
||||
@NonNull
|
||||
public final String id;
|
||||
@Nullable
|
||||
public HeadlessWebViewChannelDelegate channelDelegate;
|
||||
@Nullable
|
||||
public FlutterWebView flutterWebView;
|
||||
@Nullable
|
||||
public InAppWebViewFlutterPlugin plugin;
|
||||
|
||||
public HeadlessInAppWebView(@NonNull final InAppWebViewFlutterPlugin plugin, @NonNull String id, @NonNull FlutterWebView flutterWebView) {
|
||||
this.id = id;
|
||||
this.plugin = plugin;
|
||||
this.flutterWebView = flutterWebView;
|
||||
final MethodChannel channel = new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME_PREFIX + id);
|
||||
this.channelDelegate = new HeadlessWebViewChannelDelegate(this, channel);
|
||||
}
|
||||
|
||||
public void onWebViewCreated() {
|
||||
if (channelDelegate != null) {
|
||||
channelDelegate.onWebViewCreated();
|
||||
}
|
||||
}
|
||||
|
||||
public void prepare(Map<String, Object> params) {
|
||||
if (flutterWebView != null) {
|
||||
View view = flutterWebView.getView();
|
||||
if (view != null) {
|
||||
final Map<String, Object> initialSize = (Map<String, Object>) params.get("initialSize");
|
||||
Size2D size = Size2D.fromMap(initialSize);
|
||||
if (size == null) {
|
||||
size = new Size2D(-1, -1);
|
||||
}
|
||||
setSize(size);
|
||||
view.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
if (plugin != null && plugin.activity != null) {
|
||||
// Add the headless WebView to the view hierarchy.
|
||||
// This way is also possible to take screenshots.
|
||||
ViewGroup contentView = (ViewGroup) plugin.activity.findViewById(android.R.id.content);
|
||||
if (contentView != null) {
|
||||
ViewGroup mainView = (ViewGroup) (contentView).getChildAt(0);
|
||||
if (mainView != null && flutterWebView != null) {
|
||||
View view = flutterWebView.getView();
|
||||
if (view != null) {
|
||||
mainView.addView(view, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setSize(@NonNull Size2D size) {
|
||||
if (flutterWebView != null && flutterWebView.webView != null) {
|
||||
View view = flutterWebView.getView();
|
||||
if (view != null) {
|
||||
float scale = Util.getPixelDensity(view.getContext());
|
||||
Size2D fullscreenSize = Util.getFullscreenSize(view.getContext());
|
||||
int width = (int) (size.getWidth() == -1 ? fullscreenSize.getWidth() : (size.getWidth() * scale));
|
||||
int height = (int) (size.getWidth() == -1 ? fullscreenSize.getHeight() : (size.getHeight() * scale));
|
||||
view.setLayoutParams(new FrameLayout.LayoutParams(width, height));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Size2D getSize() {
|
||||
if (flutterWebView != null && flutterWebView.webView != null) {
|
||||
View view = flutterWebView.getView();
|
||||
if (view != null) {
|
||||
float scale = Util.getPixelDensity(view.getContext());
|
||||
Size2D fullscreenSize = Util.getFullscreenSize(view.getContext());
|
||||
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
|
||||
return new Size2D(
|
||||
fullscreenSize.getWidth() == layoutParams.width ? layoutParams.width : (layoutParams.width / scale),
|
||||
fullscreenSize.getHeight() == layoutParams.height ? layoutParams.height : (layoutParams.height / scale)
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public FlutterWebView disposeAndGetFlutterWebView() {
|
||||
FlutterWebView newFlutterWebView = flutterWebView;
|
||||
if (flutterWebView != null) {
|
||||
View view = flutterWebView.getView();
|
||||
if (view != null) {
|
||||
// restore WebView layout params and visibility
|
||||
view.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
view.setVisibility(View.VISIBLE);
|
||||
// remove from parent
|
||||
ViewGroup parent = (ViewGroup) view.getParent();
|
||||
if (parent != null) {
|
||||
parent.removeView(view);
|
||||
}
|
||||
}
|
||||
// set to null to avoid to be disposed before calling "dispose()"
|
||||
flutterWebView = null;
|
||||
dispose();
|
||||
}
|
||||
return newFlutterWebView;
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
if (channelDelegate != null) {
|
||||
channelDelegate.dispose();
|
||||
channelDelegate = null;
|
||||
}
|
||||
if (plugin != null) {
|
||||
HeadlessInAppWebViewManager headlessInAppWebViewManager = plugin.headlessInAppWebViewManager;
|
||||
if (headlessInAppWebViewManager != null && headlessInAppWebViewManager.webViews.containsKey(id)) {
|
||||
headlessInAppWebViewManager.webViews.put(id, null);
|
||||
}
|
||||
Activity activity = plugin.activity;
|
||||
if (activity != null) {
|
||||
ViewGroup contentView = plugin.activity.findViewById(android.R.id.content);
|
||||
if (contentView != null) {
|
||||
ViewGroup mainView = (ViewGroup) (contentView).getChildAt(0);
|
||||
if (mainView != null && flutterWebView != null) {
|
||||
View view = flutterWebView.getView();
|
||||
if (view != null) {
|
||||
mainView.removeView(flutterWebView.getView());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (flutterWebView != null) {
|
||||
flutterWebView.dispose();
|
||||
}
|
||||
flutterWebView = null;
|
||||
plugin = null;
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+98
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.headless_in_app_webview;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.FlutterWebView;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
import io.flutter.plugin.common.MethodChannel.Result;
|
||||
|
||||
public class HeadlessInAppWebViewManager extends ChannelDelegateImpl {
|
||||
protected static final String LOG_TAG = "HeadlessInAppWebViewManager";
|
||||
public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_headless_inappwebview";
|
||||
|
||||
public final Map<String, HeadlessInAppWebView> webViews = new HashMap<>();
|
||||
@Nullable
|
||||
public InAppWebViewFlutterPlugin plugin;
|
||||
|
||||
public HeadlessInAppWebViewManager(final InAppWebViewFlutterPlugin plugin) {
|
||||
super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME));
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(final MethodCall call, @NonNull final Result result) {
|
||||
final String id = (String) call.argument("id");
|
||||
|
||||
switch (call.method) {
|
||||
case "run":
|
||||
{
|
||||
HashMap<String, Object> params = (HashMap<String, Object>) call.argument("params");
|
||||
run(id, params);
|
||||
}
|
||||
result.success(true);
|
||||
break;
|
||||
default:
|
||||
result.notImplemented();
|
||||
}
|
||||
}
|
||||
|
||||
public void run(String id, HashMap<String, Object> params) {
|
||||
if (plugin == null || (plugin.activity == null && plugin.applicationContext == null)) return;
|
||||
Context context = plugin.activity;
|
||||
if (context == null) {
|
||||
context = plugin.applicationContext;
|
||||
}
|
||||
FlutterWebView flutterWebView = new FlutterWebView(plugin, context, id, params);
|
||||
HeadlessInAppWebView headlessInAppWebView = new HeadlessInAppWebView(plugin, id, flutterWebView);
|
||||
webViews.put(id, headlessInAppWebView);
|
||||
|
||||
headlessInAppWebView.prepare(params);
|
||||
headlessInAppWebView.onWebViewCreated();
|
||||
flutterWebView.makeInitialLoad(params);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
Collection<HeadlessInAppWebView> headlessInAppWebViews = webViews.values();
|
||||
for (HeadlessInAppWebView headlessInAppWebView : headlessInAppWebViews) {
|
||||
if (headlessInAppWebView != null) {
|
||||
headlessInAppWebView.dispose();
|
||||
}
|
||||
}
|
||||
webViews.clear();
|
||||
plugin = null;
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.headless_in_app_webview;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.Size2D;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
public class HeadlessWebViewChannelDelegate extends ChannelDelegateImpl {
|
||||
@Nullable
|
||||
private HeadlessInAppWebView headlessWebView;
|
||||
|
||||
public HeadlessWebViewChannelDelegate(@NonNull HeadlessInAppWebView headlessWebView, @NonNull MethodChannel channel) {
|
||||
super(channel);
|
||||
this.headlessWebView = headlessWebView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||
switch (call.method) {
|
||||
case "dispose":
|
||||
if (headlessWebView != null) {
|
||||
headlessWebView.dispose();
|
||||
result.success(true);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
case "setSize":
|
||||
if (headlessWebView != null) {
|
||||
Map<String, Object> sizeMap = (Map<String, Object>) call.argument("size");
|
||||
Size2D size = Size2D.fromMap(sizeMap);
|
||||
if (size != null)
|
||||
headlessWebView.setSize(size);
|
||||
result.success(true);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
case "getSize":
|
||||
if (headlessWebView != null) {
|
||||
Size2D size = headlessWebView.getSize();
|
||||
result.success(size != null ? size.toMap() : null);
|
||||
} else {
|
||||
result.success(null);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
result.notImplemented();
|
||||
}
|
||||
}
|
||||
|
||||
public void onWebViewCreated() {
|
||||
MethodChannel channel = getChannel();
|
||||
if (channel == null) return;
|
||||
Map<String, Object> obj = new HashMap<>();
|
||||
channel.invokeMethod("onWebViewCreated", obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
headlessWebView = null;
|
||||
}
|
||||
}
|
||||
+8
@@ -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);
|
||||
}
|
||||
Vendored
Executable
+643
@@ -0,0 +1,643 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.in_app_browser;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.webkit.WebView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.SearchView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.menu.MenuBuilder;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.R;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.Util;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.find_interaction.FindInteractionController;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.pull_to_refresh.PullToRefreshChannelDelegate;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.pull_to_refresh.PullToRefreshLayout;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.pull_to_refresh.PullToRefreshSettings;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.AndroidResource;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.InAppBrowserMenuItem;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.URLRequest;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.UserScript;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.webview.WebViewChannelDelegate;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebView;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.webview.in_app_webview.InAppWebViewSettings;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
public class InAppBrowserActivity extends AppCompatActivity implements InAppBrowserDelegate, Disposable {
|
||||
protected static final String LOG_TAG = "InAppBrowserActivity";
|
||||
public static final String METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappbrowser_";
|
||||
|
||||
@Nullable
|
||||
public Integer windowId;
|
||||
public String id;
|
||||
@Nullable
|
||||
public InAppWebView webView;
|
||||
@Nullable
|
||||
public PullToRefreshLayout pullToRefreshLayout;
|
||||
@Nullable
|
||||
public ActionBar actionBar;
|
||||
@Nullable
|
||||
public Menu menu;
|
||||
@Nullable
|
||||
public SearchView searchView;
|
||||
public InAppBrowserSettings customSettings = new InAppBrowserSettings();
|
||||
@Nullable
|
||||
public ProgressBar progressBar;
|
||||
public boolean isHidden = false;
|
||||
@Nullable
|
||||
public String fromActivity;
|
||||
private List<ActivityResultListener> activityResultListeners = new ArrayList<>();
|
||||
@Nullable
|
||||
public InAppBrowserManager manager;
|
||||
@Nullable
|
||||
public InAppBrowserChannelDelegate channelDelegate;
|
||||
public List<InAppBrowserMenuItem> menuItems = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Bundle b = getIntent().getExtras();
|
||||
if (b == null) return;
|
||||
|
||||
id = b.getString("id");
|
||||
|
||||
String managerId = b.getString("managerId");
|
||||
manager = InAppBrowserManager.shared.get(managerId);
|
||||
if (manager == null || manager.plugin == null|| manager.plugin.messenger == null) return;
|
||||
|
||||
Map<String, Object> settingsMap = (Map<String, Object>) b.getSerializable("settings");
|
||||
customSettings.parse(settingsMap);
|
||||
|
||||
windowId = b.getInt("windowId");
|
||||
|
||||
setContentView(R.layout.activity_web_view);
|
||||
|
||||
Map<String, Object> pullToRefreshInitialSettings = (Map<String, Object>) b.getSerializable("pullToRefreshInitialSettings");
|
||||
MethodChannel pullToRefreshLayoutChannel = new MethodChannel(manager.plugin.messenger, PullToRefreshLayout.METHOD_CHANNEL_NAME_PREFIX + id);
|
||||
PullToRefreshSettings pullToRefreshSettings = new PullToRefreshSettings();
|
||||
pullToRefreshSettings.parse(pullToRefreshInitialSettings);
|
||||
pullToRefreshLayout = findViewById(R.id.pullToRefresh);
|
||||
pullToRefreshLayout.channelDelegate = new PullToRefreshChannelDelegate(pullToRefreshLayout, pullToRefreshLayoutChannel);
|
||||
pullToRefreshLayout.settings = pullToRefreshSettings;
|
||||
pullToRefreshLayout.prepare();
|
||||
|
||||
webView = findViewById(R.id.webView);
|
||||
webView.id = id;
|
||||
webView.windowId = windowId;
|
||||
webView.inAppBrowserDelegate = this;
|
||||
webView.plugin = manager.plugin;
|
||||
|
||||
FindInteractionController findInteractionController = new FindInteractionController(webView, manager.plugin, id, null);
|
||||
webView.findInteractionController = findInteractionController;
|
||||
findInteractionController.prepare();
|
||||
|
||||
final MethodChannel channel = new MethodChannel(manager.plugin.messenger, METHOD_CHANNEL_NAME_PREFIX + id);
|
||||
channelDelegate = new InAppBrowserChannelDelegate(channel);
|
||||
webView.channelDelegate = new WebViewChannelDelegate(webView, channel);
|
||||
|
||||
fromActivity = b.getString("fromActivity");
|
||||
|
||||
Map<String, Object> contextMenu = (Map<String, Object>) b.getSerializable("contextMenu");
|
||||
List<Map<String, Object>> initialUserScripts = (List<Map<String, Object>>) b.getSerializable("initialUserScripts");
|
||||
List<Map<String, Object>> menuItemList = (List<Map<String, Object>>) b.getSerializable("menuItems");
|
||||
for (Map<String, Object> menuItem : menuItemList) {
|
||||
menuItems.add(InAppBrowserMenuItem.fromMap(menuItem));
|
||||
}
|
||||
|
||||
InAppWebViewSettings webViewSettings = new InAppWebViewSettings();
|
||||
webViewSettings.parse(settingsMap);
|
||||
webView.customSettings = webViewSettings;
|
||||
webView.contextMenu = contextMenu;
|
||||
|
||||
List<UserScript> userScripts = new ArrayList<>();
|
||||
if (initialUserScripts != null) {
|
||||
for (Map<String, Object> initialUserScript : initialUserScripts) {
|
||||
userScripts.add(UserScript.fromMap(initialUserScript));
|
||||
}
|
||||
}
|
||||
webView.userContentController.addUserOnlyScripts(userScripts);
|
||||
|
||||
actionBar = getSupportActionBar();
|
||||
|
||||
prepareView();
|
||||
|
||||
if (windowId != -1) {
|
||||
if (webView.plugin != null && webView.plugin.inAppWebViewManager != null) {
|
||||
Message resultMsg = webView.plugin.inAppWebViewManager.windowWebViewMessages.get(windowId);
|
||||
if (resultMsg != null) {
|
||||
((WebView.WebViewTransport) resultMsg.obj).setWebView(webView);
|
||||
resultMsg.sendToTarget();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
String initialFile = b.getString("initialFile");
|
||||
Map<String, Object> initialUrlRequest = (Map<String, Object>) b.getSerializable("initialUrlRequest");
|
||||
String initialData = b.getString("initialData");
|
||||
if (initialFile != null) {
|
||||
try {
|
||||
webView.loadFile(initialFile);
|
||||
} catch (IOException e) {
|
||||
Log.e(LOG_TAG, initialFile + " asset file cannot be found!", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (initialData != null) {
|
||||
String mimeType = b.getString("initialMimeType");
|
||||
String encoding = b.getString("initialEncoding");
|
||||
String baseUrl = b.getString("initialBaseUrl");
|
||||
String historyUrl = b.getString("initialHistoryUrl");
|
||||
webView.loadDataWithBaseURL(baseUrl, initialData, mimeType, encoding, historyUrl);
|
||||
}
|
||||
else if (initialUrlRequest != null) {
|
||||
URLRequest urlRequest = URLRequest.fromMap(initialUrlRequest);
|
||||
if (urlRequest != null) {
|
||||
webView.loadUrl(urlRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (channelDelegate != null) {
|
||||
channelDelegate.onBrowserCreated();
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareView() {
|
||||
|
||||
if (webView != null) {
|
||||
webView.prepare();
|
||||
}
|
||||
|
||||
if (customSettings.hidden)
|
||||
hide();
|
||||
else
|
||||
show();
|
||||
|
||||
progressBar = findViewById(R.id.progressBar);
|
||||
|
||||
if (progressBar != null) {
|
||||
if (customSettings.hideProgressBar)
|
||||
progressBar.setMax(0);
|
||||
else
|
||||
progressBar.setMax(100);
|
||||
}
|
||||
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayShowTitleEnabled(!customSettings.hideTitleBar);
|
||||
|
||||
if (customSettings.hideToolbarTop)
|
||||
actionBar.hide();
|
||||
|
||||
if (customSettings.toolbarTopBackgroundColor != null && !customSettings.toolbarTopBackgroundColor.isEmpty())
|
||||
actionBar.setBackgroundDrawable(new ColorDrawable(Color.parseColor(customSettings.toolbarTopBackgroundColor)));
|
||||
|
||||
if (customSettings.toolbarTopFixedTitle != null && !customSettings.toolbarTopFixedTitle.isEmpty())
|
||||
actionBar.setTitle(customSettings.toolbarTopFixedTitle);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu m) {
|
||||
menu = m;
|
||||
|
||||
if (actionBar != null && (customSettings.toolbarTopFixedTitle == null || customSettings.toolbarTopFixedTitle.isEmpty()))
|
||||
actionBar.setTitle(webView != null ? webView.getTitle() : "");
|
||||
|
||||
if (menu == null)
|
||||
return super.onCreateOptionsMenu(m);
|
||||
|
||||
if (menu instanceof MenuBuilder) {
|
||||
((MenuBuilder) menu).setOptionalIconsVisible(true);
|
||||
}
|
||||
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
// Inflate menu to add items to action bar if it is present.
|
||||
inflater.inflate(R.menu.menu_main, menu);
|
||||
|
||||
MenuItem menuSearchItem = menu.findItem(R.id.menu_search);
|
||||
if (menuSearchItem != null) {
|
||||
if (customSettings.hideUrlBar)
|
||||
menuSearchItem.setVisible(false);
|
||||
|
||||
searchView = (SearchView) menuSearchItem.getActionView();
|
||||
if (searchView != null) {
|
||||
searchView.setFocusable(true);
|
||||
|
||||
searchView.setQuery(webView != null ? webView.getUrl() : "", false);
|
||||
|
||||
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String query) {
|
||||
if (!query.isEmpty()) {
|
||||
if (webView != null)
|
||||
webView.loadUrl(query);
|
||||
if (searchView != null) {
|
||||
searchView.setQuery("", false);
|
||||
searchView.setIconified(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextChange(String newText) {
|
||||
return false;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
searchView.setOnCloseListener(new SearchView.OnCloseListener() {
|
||||
@Override
|
||||
public boolean onClose() {
|
||||
if (searchView != null && searchView.getQuery().toString().isEmpty())
|
||||
searchView.setQuery(webView != null ? webView.getUrl() : "", false);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
searchView.setOnQueryTextFocusChangeListener(new View.OnFocusChangeListener() {
|
||||
@Override
|
||||
public void onFocusChange(View view, boolean b) {
|
||||
if (!b && searchView != null) {
|
||||
searchView.setQuery("", false);
|
||||
searchView.setIconified(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (customSettings.hideDefaultMenuItems) {
|
||||
MenuItem actionClose = menu.findItem(R.id.action_close);
|
||||
if (actionClose != null) {
|
||||
actionClose.setVisible(false);
|
||||
}
|
||||
MenuItem actionGoBack = menu.findItem(R.id.action_go_back);
|
||||
if (actionGoBack != null) {
|
||||
actionGoBack.setVisible(false);
|
||||
}
|
||||
MenuItem actionReload = menu.findItem(R.id.action_reload);
|
||||
if (actionReload != null) {
|
||||
actionReload.setVisible(false);
|
||||
}
|
||||
MenuItem actionGoForward = menu.findItem(R.id.action_go_forward);
|
||||
if (actionGoForward != null) {
|
||||
actionGoForward.setVisible(false);
|
||||
}
|
||||
MenuItem actionShare = menu.findItem(R.id.action_share);
|
||||
if (actionShare != null) {
|
||||
actionShare.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
for (final InAppBrowserMenuItem menuItem : menuItems) {
|
||||
int order = menuItem.getOrder() != null ? menuItem.getOrder() : Menu.NONE;
|
||||
MenuItem item = menu.add(Menu.NONE, menuItem.getId(), order, menuItem.getTitle());
|
||||
if (menuItem.isShowAsAction()) {
|
||||
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
}
|
||||
Object icon = menuItem.getIcon();
|
||||
if (icon != null) {
|
||||
if (icon instanceof AndroidResource) {
|
||||
item.setIcon(((AndroidResource) icon).getIdentifier(this));
|
||||
} else {
|
||||
item.setIcon(Util.drawableFromBytes(this, (byte[]) icon));
|
||||
}
|
||||
String iconColor = menuItem.getIconColor();
|
||||
if (iconColor != null && !iconColor.isEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
item.getIcon().setTint(Color.parseColor(iconColor));
|
||||
}
|
||||
}
|
||||
item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
|
||||
@Override
|
||||
public boolean onMenuItemClick(@NonNull MenuItem item) {
|
||||
if (channelDelegate != null) {
|
||||
channelDelegate.onMenuItemClicked(menuItem);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
if (customSettings.shouldCloseOnBackButtonPressed) {
|
||||
close(null);
|
||||
return true;
|
||||
}
|
||||
if (customSettings.allowGoBackWithBackButton) {
|
||||
if (canGoBack())
|
||||
goBack();
|
||||
else if (customSettings.closeOnCannotGoBack)
|
||||
close(null);
|
||||
return true;
|
||||
}
|
||||
if (!customSettings.shouldCloseOnBackButtonPressed) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
public void close(final MethodChannel.Result result) {
|
||||
if (channelDelegate != null) {
|
||||
channelDelegate.onExit();
|
||||
}
|
||||
|
||||
dispose();
|
||||
|
||||
if (result != null) {
|
||||
result.success(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
if (webView != null)
|
||||
webView.reload();
|
||||
}
|
||||
|
||||
public void goBack() {
|
||||
if (webView != null && canGoBack())
|
||||
webView.goBack();
|
||||
}
|
||||
|
||||
public boolean canGoBack() {
|
||||
if (webView != null)
|
||||
return webView.canGoBack();
|
||||
return false;
|
||||
}
|
||||
|
||||
public void goForward() {
|
||||
if (webView != null && canGoForward())
|
||||
webView.goForward();
|
||||
}
|
||||
|
||||
public boolean canGoForward() {
|
||||
if (webView != null)
|
||||
return webView.canGoForward();
|
||||
return false;
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
if (fromActivity != null) {
|
||||
try {
|
||||
isHidden = true;
|
||||
Intent openActivity = new Intent(this, Class.forName(fromActivity));
|
||||
openActivity.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
|
||||
startActivityIfNeeded(openActivity, 0);
|
||||
} catch (ClassNotFoundException e) {
|
||||
Log.d(LOG_TAG, "", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void show() {
|
||||
isHidden = false;
|
||||
Intent openActivity = new Intent(this, InAppBrowserActivity.class);
|
||||
openActivity.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
|
||||
startActivityIfNeeded(openActivity, 0);
|
||||
}
|
||||
|
||||
public void goBackButtonClicked(MenuItem item) {
|
||||
goBack();
|
||||
}
|
||||
|
||||
public void goForwardButtonClicked(MenuItem item) {
|
||||
goForward();
|
||||
}
|
||||
|
||||
public void shareButtonClicked(MenuItem item) {
|
||||
Intent share = new Intent(Intent.ACTION_SEND);
|
||||
share.setType("text/plain");
|
||||
share.putExtra(Intent.EXTRA_TEXT, webView != null ? webView.getUrl() : "");
|
||||
startActivity(Intent.createChooser(share, "Share"));
|
||||
}
|
||||
|
||||
public void reloadButtonClicked(MenuItem item) {
|
||||
reload();
|
||||
}
|
||||
|
||||
public void closeButtonClicked(MenuItem item) {
|
||||
close(null);
|
||||
}
|
||||
|
||||
public void setSettings(InAppBrowserSettings newSettings, HashMap<String, Object> newSettingsMap) {
|
||||
|
||||
InAppWebViewSettings newInAppWebViewSettings = new InAppWebViewSettings();
|
||||
newInAppWebViewSettings.parse(newSettingsMap);
|
||||
if (webView != null) {
|
||||
webView.setSettings(newInAppWebViewSettings, newSettingsMap);
|
||||
}
|
||||
|
||||
if (newSettingsMap.get("hidden") != null && customSettings.hidden != newSettings.hidden) {
|
||||
if (newSettings.hidden)
|
||||
hide();
|
||||
else
|
||||
show();
|
||||
}
|
||||
|
||||
if (newSettingsMap.get("hideProgressBar") != null && customSettings.hideProgressBar != newSettings.hideProgressBar && progressBar != null) {
|
||||
if (newSettings.hideProgressBar)
|
||||
progressBar.setMax(0);
|
||||
else
|
||||
progressBar.setMax(100);
|
||||
}
|
||||
|
||||
if (actionBar != null && newSettingsMap.get("hideTitleBar") != null && customSettings.hideTitleBar != newSettings.hideTitleBar)
|
||||
actionBar.setDisplayShowTitleEnabled(!newSettings.hideTitleBar);
|
||||
|
||||
if (actionBar != null && newSettingsMap.get("hideToolbarTop") != null && customSettings.hideToolbarTop != newSettings.hideToolbarTop) {
|
||||
if (newSettings.hideToolbarTop)
|
||||
actionBar.hide();
|
||||
else
|
||||
actionBar.show();
|
||||
}
|
||||
|
||||
if (actionBar != null && newSettingsMap.get("toolbarTopBackgroundColor") != null &&
|
||||
!Util.objEquals(customSettings.toolbarTopBackgroundColor, newSettings.toolbarTopBackgroundColor) &&
|
||||
newSettings.toolbarTopBackgroundColor != null && !newSettings.toolbarTopBackgroundColor.isEmpty())
|
||||
actionBar.setBackgroundDrawable(new ColorDrawable(Color.parseColor(newSettings.toolbarTopBackgroundColor)));
|
||||
|
||||
if (actionBar != null && newSettingsMap.get("toolbarTopFixedTitle") != null &&
|
||||
!Util.objEquals(customSettings.toolbarTopFixedTitle, newSettings.toolbarTopFixedTitle) &&
|
||||
newSettings.toolbarTopFixedTitle != null && !newSettings.toolbarTopFixedTitle.isEmpty())
|
||||
actionBar.setTitle(newSettings.toolbarTopFixedTitle);
|
||||
|
||||
if (menu != null && newSettingsMap.get("hideUrlBar") != null && customSettings.hideUrlBar != newSettings.hideUrlBar) {
|
||||
MenuItem menuSearchItem = menu.findItem(R.id.menu_search);
|
||||
if (menuSearchItem != null) {
|
||||
menuSearchItem.setVisible(!newSettings.hideUrlBar);
|
||||
}
|
||||
}
|
||||
|
||||
if (menu != null && newSettingsMap.get("hideDefaultMenuItems") != null && customSettings.hideDefaultMenuItems != newSettings.hideDefaultMenuItems) {
|
||||
MenuItem actionClose = menu.findItem(R.id.action_close);
|
||||
if (actionClose != null) {
|
||||
actionClose.setVisible(!newSettings.hideDefaultMenuItems);
|
||||
}
|
||||
MenuItem actionGoBack = menu.findItem(R.id.action_go_back);
|
||||
if (actionGoBack != null) {
|
||||
actionGoBack.setVisible(!newSettings.hideDefaultMenuItems);
|
||||
}
|
||||
MenuItem actionReload = menu.findItem(R.id.action_reload);
|
||||
if (actionReload != null) {
|
||||
actionReload.setVisible(!newSettings.hideDefaultMenuItems);
|
||||
}
|
||||
MenuItem actionGoForward = menu.findItem(R.id.action_go_forward);
|
||||
if (actionGoForward != null) {
|
||||
actionGoForward.setVisible(!newSettings.hideDefaultMenuItems);
|
||||
}
|
||||
MenuItem actionShare = menu.findItem(R.id.action_share);
|
||||
if (actionShare != null) {
|
||||
actionShare.setVisible(!newSettings.hideDefaultMenuItems);
|
||||
}
|
||||
}
|
||||
|
||||
customSettings = newSettings;
|
||||
}
|
||||
|
||||
public Map<String, Object> getCustomSettings() {
|
||||
Map<String, Object> webViewSettingsMap = webView != null ? webView.getCustomSettings() : null;
|
||||
if (customSettings == null || webViewSettingsMap == null)
|
||||
return null;
|
||||
|
||||
Map<String, Object> settingsMap = customSettings.getRealSettings(this);
|
||||
settingsMap.putAll(webViewSettingsMap);
|
||||
return settingsMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Activity getActivity() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void didChangeTitle(String title) {
|
||||
if (actionBar != null && (customSettings.toolbarTopFixedTitle == null || customSettings.toolbarTopFixedTitle.isEmpty())) {
|
||||
actionBar.setTitle(title);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void didStartNavigation(String url) {
|
||||
if (progressBar != null) {
|
||||
progressBar.setProgress(0);
|
||||
}
|
||||
if (searchView != null) {
|
||||
searchView.setQuery(url, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void didUpdateVisitedHistory(String url) {
|
||||
if (searchView != null) {
|
||||
searchView.setQuery(url, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void didFinishNavigation(String url) {
|
||||
if (searchView != null) {
|
||||
searchView.setQuery(url, false);
|
||||
}
|
||||
if (progressBar != null) {
|
||||
progressBar.setProgress(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void didFailNavigation(String url, int errorCode, String description) {
|
||||
if (progressBar != null) {
|
||||
progressBar.setProgress(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void didChangeProgress(int progress) {
|
||||
if (progressBar != null) {
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
progressBar.setProgress(progress, true);
|
||||
} else {
|
||||
progressBar.setProgress(progress);
|
||||
}
|
||||
if (progress == 100) {
|
||||
progressBar.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<ActivityResultListener> getActivityResultListeners() {
|
||||
return activityResultListeners;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult (int requestCode,
|
||||
int resultCode,
|
||||
Intent data) {
|
||||
for (ActivityResultListener listener : activityResultListeners) {
|
||||
if (listener.onActivityResult(requestCode, resultCode, data)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
if (channelDelegate != null) {
|
||||
channelDelegate.dispose();
|
||||
channelDelegate = null;
|
||||
}
|
||||
activityResultListeners.clear();
|
||||
if (webView != null) {
|
||||
if (manager != null && manager.plugin != null &&
|
||||
manager.plugin.activityPluginBinding != null && webView.inAppWebViewChromeClient != null) {
|
||||
manager.plugin.activityPluginBinding.removeActivityResultListener(webView.inAppWebViewChromeClient);
|
||||
}
|
||||
RelativeLayout containerView = (RelativeLayout) findViewById(R.id.container);
|
||||
if (containerView != null) {
|
||||
containerView.removeAllViews();
|
||||
}
|
||||
webView.dispose();
|
||||
webView = null;
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
dispose();
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.in_app_browser;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.InAppBrowserMenuItem;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
public class InAppBrowserChannelDelegate extends ChannelDelegateImpl {
|
||||
public InAppBrowserChannelDelegate(@NonNull MethodChannel channel) {
|
||||
super(channel);
|
||||
}
|
||||
|
||||
public void onBrowserCreated() {
|
||||
MethodChannel channel = getChannel();
|
||||
if (channel == null) return;
|
||||
Map<String, Object> obj = new HashMap<>();
|
||||
channel.invokeMethod("onBrowserCreated", obj);
|
||||
}
|
||||
|
||||
public void onMenuItemClicked(InAppBrowserMenuItem menuItem) {
|
||||
MethodChannel channel = getChannel();
|
||||
if (channel == null) return;
|
||||
Map<String, Object> obj = new HashMap<>();
|
||||
obj.put("id", menuItem.getId());
|
||||
channel.invokeMethod("onMenuItemClicked", obj);
|
||||
}
|
||||
|
||||
public void onExit() {
|
||||
MethodChannel channel = getChannel();
|
||||
if (channel == null) return;
|
||||
Map<String, Object> obj = new HashMap<>();
|
||||
channel.invokeMethod("onExit", obj);
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.in_app_browser;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface InAppBrowserDelegate {
|
||||
Activity getActivity();
|
||||
List<ActivityResultListener> getActivityResultListeners();
|
||||
void didChangeTitle(String title);
|
||||
void didStartNavigation(String url);
|
||||
void didUpdateVisitedHistory(String url);
|
||||
void didFinishNavigation(String url);
|
||||
void didFailNavigation(String url, int errorCode, String description);
|
||||
void didChangeProgress(int progress);
|
||||
}
|
||||
Vendored
Executable
+219
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.in_app_browser;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.provider.Browser;
|
||||
import android.util.Log;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
import io.flutter.plugin.common.MethodChannel.Result;
|
||||
|
||||
/**
|
||||
* InAppBrowserManager
|
||||
*/
|
||||
public class InAppBrowserManager extends ChannelDelegateImpl {
|
||||
protected static final String LOG_TAG = "InAppBrowserManager";
|
||||
public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappbrowser";
|
||||
|
||||
@Nullable
|
||||
public InAppWebViewFlutterPlugin plugin;
|
||||
public String id;
|
||||
public static final Map<String, InAppBrowserManager> shared = new HashMap<>();
|
||||
|
||||
public InAppBrowserManager(final InAppWebViewFlutterPlugin plugin) {
|
||||
super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME));
|
||||
this.id = UUID.randomUUID().toString();
|
||||
this.plugin = plugin;
|
||||
shared.put(this.id, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||
switch (call.method) {
|
||||
case "open":
|
||||
if (plugin != null && plugin.activity != null) {
|
||||
open(plugin.activity, (Map<String, Object>) call.arguments());
|
||||
result.success(true);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
case "openWithSystemBrowser":
|
||||
if (plugin != null && plugin.activity != null) {
|
||||
String url = (String) call.argument("url");
|
||||
openWithSystemBrowser(plugin.activity, url, result);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
result.notImplemented();
|
||||
}
|
||||
}
|
||||
|
||||
public static String getMimeType(String url) {
|
||||
String type = null;
|
||||
String extension = MimeTypeMap.getFileExtensionFromUrl(url);
|
||||
if (extension != null) {
|
||||
type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a new browser with the specified URL.
|
||||
*
|
||||
* @param url the url to load.
|
||||
* @param result
|
||||
* @return "" if ok, or error message.
|
||||
*/
|
||||
public void openWithSystemBrowser(Activity activity, String url, Result result) {
|
||||
try {
|
||||
Intent intent;
|
||||
intent = new Intent(Intent.ACTION_VIEW);
|
||||
// Omitting the MIME type for file: URLs causes "No Activity found to handle Intent".
|
||||
// Adding the MIME type to http: URLs causes them to not be handled by the downloader.
|
||||
Uri uri = Uri.parse(url);
|
||||
if ("file".equals(uri.getScheme())) {
|
||||
intent.setDataAndType(uri, getMimeType(url));
|
||||
} else {
|
||||
intent.setData(uri);
|
||||
}
|
||||
intent.putExtra(Browser.EXTRA_APPLICATION_ID, activity.getPackageName());
|
||||
// CB-10795: Avoid circular loops by preventing it from opening in the current app
|
||||
this.openExternalExcludeCurrentApp(activity, intent);
|
||||
result.success(true);
|
||||
// not catching FileUriExposedException explicitly because buildtools<24 doesn't know about it
|
||||
} catch (java.lang.RuntimeException e) {
|
||||
Log.d(LOG_TAG, url + " cannot be opened: " + e.toString());
|
||||
result.error(LOG_TAG, url + " cannot be opened!", null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the intent, providing a chooser that excludes the current app to avoid
|
||||
* circular loops.
|
||||
*/
|
||||
public void openExternalExcludeCurrentApp(Activity activity, Intent intent) {
|
||||
String currentPackage = activity.getPackageName();
|
||||
boolean hasCurrentPackage = false;
|
||||
PackageManager pm = activity.getPackageManager();
|
||||
List<ResolveInfo> activities = pm.queryIntentActivities(intent, 0);
|
||||
ArrayList<Intent> targetIntents = new ArrayList<Intent>();
|
||||
for (ResolveInfo ri : activities) {
|
||||
if (!currentPackage.equals(ri.activityInfo.packageName)) {
|
||||
Intent targetIntent = (Intent) intent.clone();
|
||||
targetIntent.setPackage(ri.activityInfo.packageName);
|
||||
targetIntents.add(targetIntent);
|
||||
} else {
|
||||
hasCurrentPackage = true;
|
||||
}
|
||||
}
|
||||
// If the current app package isn't a target for this URL, then use
|
||||
// the normal launch behavior
|
||||
if (!hasCurrentPackage || targetIntents.size() == 0) {
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
// If there's only one possible intent, launch it directly
|
||||
else if (targetIntents.size() == 1) {
|
||||
activity.startActivity(targetIntents.get(0));
|
||||
}
|
||||
// Otherwise, show a custom chooser without the current app listed
|
||||
else if (targetIntents.size() > 0) {
|
||||
Intent chooser = Intent.createChooser(targetIntents.remove(targetIntents.size() - 1), null);
|
||||
chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetIntents.toArray(new Parcelable[]{}));
|
||||
activity.startActivity(chooser);
|
||||
}
|
||||
}
|
||||
|
||||
public void open(Activity activity, Map<String, Object> arguments) {
|
||||
String id = (String) arguments.get("id");
|
||||
Map<String, Object> urlRequest = (Map<String, Object>) arguments.get("urlRequest");
|
||||
String assetFilePath = (String) arguments.get("assetFilePath");
|
||||
String data = (String) arguments.get("data");
|
||||
String mimeType = (String) arguments.get("mimeType");
|
||||
String encoding = (String) arguments.get("encoding");
|
||||
String baseUrl = (String) arguments.get("baseUrl");
|
||||
String historyUrl = (String) arguments.get("historyUrl");
|
||||
Map<String, Object> settings = (Map<String, Object>) arguments.get("settings");
|
||||
Map<String, Object> contextMenu = (Map<String, Object>) arguments.get("contextMenu");
|
||||
Integer windowId = (Integer) arguments.get("windowId");
|
||||
List<Map<String, Object>> initialUserScripts = (List<Map<String, Object>>) arguments.get("initialUserScripts");
|
||||
Map<String, Object> pullToRefreshInitialSettings = (Map<String, Object>) arguments.get("pullToRefreshSettings");
|
||||
List<Map<String, Object>> menuItems = (List<Map<String, Object>>) arguments.get("menuItems");
|
||||
|
||||
Bundle extras = new Bundle();
|
||||
extras.putString("fromActivity", activity.getClass().getName());
|
||||
extras.putSerializable("initialUrlRequest", (Serializable) urlRequest);
|
||||
extras.putString("initialFile", assetFilePath);
|
||||
extras.putString("initialData", data);
|
||||
extras.putString("initialMimeType", mimeType);
|
||||
extras.putString("initialEncoding", encoding);
|
||||
extras.putString("initialBaseUrl", baseUrl);
|
||||
extras.putString("initialHistoryUrl", historyUrl);
|
||||
extras.putString("id", id);
|
||||
extras.putString("managerId", this.id);
|
||||
extras.putSerializable("settings", (Serializable) settings);
|
||||
extras.putSerializable("contextMenu", (Serializable) contextMenu);
|
||||
extras.putInt("windowId", windowId != null ? windowId : -1);
|
||||
extras.putSerializable("initialUserScripts", (Serializable) initialUserScripts);
|
||||
extras.putSerializable("pullToRefreshInitialSettings", (Serializable) pullToRefreshInitialSettings);
|
||||
extras.putSerializable("menuItems", (Serializable) menuItems);
|
||||
startInAppBrowserActivity(activity, extras);
|
||||
}
|
||||
|
||||
public void startInAppBrowserActivity(Activity activity, Bundle extras) {
|
||||
Intent intent = new Intent(activity, InAppBrowserActivity.class);
|
||||
if (extras != null)
|
||||
intent.putExtras(extras);
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
shared.remove(this.id);
|
||||
plugin = null;
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+109
@@ -0,0 +1,109 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.in_app_browser;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.ISettings;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.R;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class InAppBrowserSettings implements ISettings<InAppBrowserActivity> {
|
||||
|
||||
public static final String LOG_TAG = "InAppBrowserSettings";
|
||||
|
||||
public Boolean hidden = false;
|
||||
public Boolean hideToolbarTop = false;
|
||||
@Nullable
|
||||
public String toolbarTopBackgroundColor;
|
||||
@Nullable
|
||||
public String toolbarTopFixedTitle;
|
||||
public Boolean hideUrlBar = false;
|
||||
public Boolean hideProgressBar = false;
|
||||
|
||||
public Boolean hideTitleBar = false;
|
||||
public Boolean closeOnCannotGoBack = true;
|
||||
public Boolean allowGoBackWithBackButton = true;
|
||||
public Boolean shouldCloseOnBackButtonPressed = false;
|
||||
public Boolean hideDefaultMenuItems = false;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public InAppBrowserSettings parse(@NonNull Map<String, Object> settings) {
|
||||
for (Map.Entry<String, Object> pair : settings.entrySet()) {
|
||||
String key = pair.getKey();
|
||||
Object value = pair.getValue();
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case "hidden":
|
||||
hidden = (Boolean) value;
|
||||
break;
|
||||
case "hideToolbarTop":
|
||||
hideToolbarTop = (Boolean) value;
|
||||
break;
|
||||
case "toolbarTopBackgroundColor":
|
||||
toolbarTopBackgroundColor = (String) value;
|
||||
break;
|
||||
case "toolbarTopFixedTitle":
|
||||
toolbarTopFixedTitle = (String) value;
|
||||
break;
|
||||
case "hideUrlBar":
|
||||
hideUrlBar = (Boolean) value;
|
||||
break;
|
||||
case "hideTitleBar":
|
||||
hideTitleBar = (Boolean) value;
|
||||
break;
|
||||
case "closeOnCannotGoBack":
|
||||
closeOnCannotGoBack = (Boolean) value;
|
||||
break;
|
||||
case "hideProgressBar":
|
||||
hideProgressBar = (Boolean) value;
|
||||
break;
|
||||
case "allowGoBackWithBackButton":
|
||||
allowGoBackWithBackButton = (Boolean) value;
|
||||
break;
|
||||
case "shouldCloseOnBackButtonPressed":
|
||||
shouldCloseOnBackButtonPressed = (Boolean) value;
|
||||
break;
|
||||
case "hideDefaultMenuItems":
|
||||
hideDefaultMenuItems = (Boolean) value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Map<String, Object> toMap() {
|
||||
Map<String, Object> settings = new HashMap<>();
|
||||
settings.put("hidden", hidden);
|
||||
settings.put("hideToolbarTop", hideToolbarTop);
|
||||
settings.put("toolbarTopBackgroundColor", toolbarTopBackgroundColor);
|
||||
settings.put("toolbarTopFixedTitle", toolbarTopFixedTitle);
|
||||
settings.put("hideUrlBar", hideUrlBar);
|
||||
settings.put("hideTitleBar", hideTitleBar);
|
||||
settings.put("closeOnCannotGoBack", closeOnCannotGoBack);
|
||||
settings.put("hideProgressBar", hideProgressBar);
|
||||
settings.put("allowGoBackWithBackButton", allowGoBackWithBackButton);
|
||||
settings.put("shouldCloseOnBackButtonPressed", shouldCloseOnBackButtonPressed);
|
||||
settings.put("hideDefaultMenuItems", hideDefaultMenuItems);
|
||||
return settings;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Map<String, Object> getRealSettings(@NonNull InAppBrowserActivity inAppBrowserActivity) {
|
||||
Map<String, Object> realSettings = toMap();
|
||||
realSettings.put("hidden", inAppBrowserActivity.isHidden);
|
||||
realSettings.put("hideToolbarTop", inAppBrowserActivity.actionBar == null || !inAppBrowserActivity.actionBar.isShowing());
|
||||
realSettings.put("hideUrlBar", inAppBrowserActivity.menu == null || !inAppBrowserActivity.menu.findItem(R.id.menu_search).isVisible());
|
||||
realSettings.put("hideProgressBar", inAppBrowserActivity.progressBar == null || inAppBrowserActivity.progressBar.getMax() == 0);
|
||||
return realSettings;
|
||||
}
|
||||
}
|
||||
+266
@@ -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);";
|
||||
}
|
||||
+152
@@ -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);";
|
||||
}
|
||||
+250
@@ -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;" +
|
||||
" }" +
|
||||
"})();";
|
||||
}
|
||||
+35
@@ -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']});" +
|
||||
"})();";
|
||||
}
|
||||
+22
@@ -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');" +
|
||||
" });" +
|
||||
"})();";
|
||||
}
|
||||
+22
@@ -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');" +
|
||||
" });" +
|
||||
"})();";
|
||||
}
|
||||
+89
@@ -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();" +
|
||||
" });" +
|
||||
"})();";
|
||||
}
|
||||
+24
@@ -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();" +
|
||||
" }" +
|
||||
"};";
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.plugin_scripts_js;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.PluginScript;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.UserScriptInjectionTime;
|
||||
|
||||
public class PromisePolyfillJS {
|
||||
public static final String PROMISE_POLYFILL_JS_PLUGIN_SCRIPT_GROUP_NAME = "IN_APP_WEBVIEW_PROMISE_POLYFILL_JS_PLUGIN_SCRIPT";
|
||||
public static final PluginScript PROMISE_POLYFILL_JS_PLUGIN_SCRIPT = new PluginScript(
|
||||
PromisePolyfillJS.PROMISE_POLYFILL_JS_PLUGIN_SCRIPT_GROUP_NAME,
|
||||
PromisePolyfillJS.PROMISE_POLYFILL_JS_SOURCE,
|
||||
UserScriptInjectionTime.AT_DOCUMENT_START,
|
||||
null,
|
||||
true,
|
||||
null
|
||||
);
|
||||
|
||||
// https://github.com/tildeio/rsvp.js
|
||||
public static final String PROMISE_POLYFILL_JS_SOURCE = "if (window.Promise == null) {" +
|
||||
" !function(t,e){\"object\"==typeof exports&&\"undefined\"!=typeof module?e(exports):\"function\"==typeof define&&define.amd?define([\"exports\"],e):e(t.RSVP={})}(this,function(t){\"use strict\";function e(t){var e=t._promiseCallbacks;return e||(e=t._promiseCallbacks={}),e}var r={mixin:function(t){return t.on=this.on,t.off=this.off,t.trigger=this.trigger,t._promiseCallbacks=void 0,t},on:function(t,r){if(\"function\"!=typeof r)throw new TypeError(\"Callback must be a function\");var n=e(this),o=n[t];o||(o=n[t]=[]),-1===o.indexOf(r)&&o.push(r)},off:function(t,r){var n=e(this);if(r){var o=n[t],i=o.indexOf(r);-1!==i&&o.splice(i,1)}else n[t]=[]},trigger:function(t,r,n){var o=e(this)[t];if(o)for(var i=0;i<o.length;i++)(0,o[i])(r,n)}},n={instrument:!1};function o(t,e){if(2!==arguments.length)return n[t];n[t]=e}r.mixin(n);var i=[];function s(t,e,r){1===i.push({name:t,payload:{key:e._guidKey,id:e._id,eventName:t,detail:e._result,childId:r&&r._id,label:e._label,timeStamp:Date.now(),error:n[\"instrument-with-stack\"]?new Error(e._label):null}})&&setTimeout(function(){for(var t=0;t<i.length;t++){var e=i[t],r=e.payload;r.guid=r.key+r.id,r.childGuid=r.key+r.childId,r.error&&(r.stack=r.error.stack),n.trigger(e.name,e.payload)}i.length=0},50)}function u(t,e){if(t&&\"object\"==typeof t&&t.constructor===this)return t;var r=new this(c,e);return m(r,t),r}function c(){}var a=void 0,f=1,l=2,h={error:null};function p(t){try{return t.then}catch(t){return h.error=t,h}}var y=void 0;function _(){try{var t=y;return y=null,t.apply(this,arguments)}catch(t){return h.error=t,h}}function v(t){return y=t,_}function d(t,e,r){if(e.constructor===t.constructor&&r===A&&t.constructor.resolve===u)!function(t,e){e._state===f?b(t,e._result):e._state===l?(e._onError=null,g(t,e._result)):j(e,void 0,function(r){e===r?b(t,r):m(t,r)},function(e){return g(t,e)})}(t,e);else if(r===h){var o=h.error;h.error=null,g(t,o)}else\"function\"==typeof r?function(t,e,r){n.async(function(t){var n=!1,o=v(r).call(e,function(r){n||(n=!0,e===r?b(t,r):m(t,r))},function(e){n||(n=!0,g(t,e))},\"Settle: \"+(t._label||\" unknown promise\"));if(!n&&o===h){n=!0;var i=h.error;h.error=null,g(t,i)}},t)}(t,e,r):b(t,e)}function m(t,e){var r,n;t===e?b(t,e):(n=typeof(r=e),null===r||\"object\"!==n&&\"function\"!==n?b(t,e):d(t,e,p(e)))}function w(t){t._onError&&t._onError(t._result),O(t)}function b(t,e){t._state===a&&(t._result=e,t._state=f,0===t._subscribers.length?n.instrument&&s(\"fulfilled\",t):n.async(O,t))}function g(t,e){t._state===a&&(t._state=l,t._result=e,n.async(w,t))}function j(t,e,r,o){var i=t._subscribers,s=i.length;t._onError=null,i[s]=e,i[s+f]=r,i[s+l]=o,0===s&&t._state&&n.async(O,t)}function O(t){var e=t._subscribers,r=t._state;if(n.instrument&&s(r===f?\"fulfilled\":\"rejected\",t),0!==e.length){for(var o=void 0,i=void 0,u=t._result,c=0;c<e.length;c+=3)o=e[c],i=e[c+r],o?E(r,o,i,u):i(u);t._subscribers.length=0}}function E(t,e,r,n){var o=\"function\"==typeof r,i=void 0;if(i=o?v(r)(n):n,e._state!==a);else if(i===e)g(e,new TypeError(\"A promises callback cannot return that same promise.\"));else if(i===h){var s=h.error;h.error=null,g(e,s)}else o?m(e,i):t===f?b(e,i):t===l&&g(e,i)}function A(t,e,r){var o=this._state;if(o===f&&!t||o===l&&!e)return n.instrument&&s(\"chained\",this,this),this;this._onError=null;var i=new this.constructor(c,r),u=this._result;if(n.instrument&&s(\"chained\",this,i),o===a)j(this,i,t,e);else{var h=o===f?t:e;n.async(function(){return E(o,i,h,u)})}return i}var T=function(){function t(t,e,r,n){this._instanceConstructor=t,this.promise=new t(c,n),this._abortOnReject=r,this._isUsingOwnPromise=t===k,this._isUsingOwnResolve=t.resolve===u,this._init.apply(this,arguments)}return t.prototype._init=function(t,e){var r=e.length||0;this.length=r,this._remaining=r,this._result=new Array(r),this._enumerate(e)},t.prototype._enumerate=function(t){for(var e=this.length,r=this.promise,n=0;r._state===a&&n<e;n++)this._eachEntry(t[n],n,!0);this._checkFullfillment()},t.prototype._checkFullfillment=function(){if(0===this._remaining){var t=this._result;b(this.promise,t),this._result=null}},t.prototype._settleMaybeThenable=function(t,e,r){var n=this._instanceConstructor;if(this._isUsingOwnResolve){var o=p(t);if(o===A&&t._state!==a)t._onError=null,this._settledAt(t._state,e,t._result,r);else if(\"function\"!=typeof o)this._settledAt(f,e,t,r);else if(this._isUsingOwnPromise){var i=new n(c);d(i,t,o),this._willSettleAt(i,e,r)}else this._willSettleAt(new n(function(e){return e(t)}),e,r)}else this._willSettleAt(n.resolve(t),e,r)},t.prototype._eachEntry=function(t,e,r){null!==t&&\"object\"==typeof t?this._settleMaybeThenable(t,e,r):this._setResultAt(f,e,t,r)},t.prototype._settledAt=function(t,e,r,n){var o=this.promise;o._state===a&&(this._abortOnReject&&t===l?g(o,r):(this._setResultAt(t,e,r,n),this._checkFullfillment()))},t.prototype._setResultAt=function(t,e,r,n){this._remaining--,this._result[e]=r},t.prototype._willSettleAt=function(t,e,r){var n=this;j(t,void 0,function(tLine truncated
|
||||
"}";
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.print_job;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.PrintJobInfoExt;
|
||||
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
|
||||
public class PrintJobChannelDelegate extends ChannelDelegateImpl {
|
||||
@Nullable
|
||||
private PrintJobController printJobController;
|
||||
|
||||
public PrintJobChannelDelegate(@NonNull PrintJobController printJobController, @NonNull MethodChannel channel) {
|
||||
super(channel);
|
||||
this.printJobController = printJobController;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||
switch (call.method) {
|
||||
case "cancel":
|
||||
if (printJobController != null) {
|
||||
printJobController.cancel();
|
||||
result.success(true);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
case "restart":
|
||||
if (printJobController != null) {
|
||||
printJobController.restart();
|
||||
result.success(true);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
case "getInfo":
|
||||
if (printJobController != null) {
|
||||
PrintJobInfoExt info = printJobController.getInfo();
|
||||
result.success(info != null ? info.toMap() : null);
|
||||
} else {
|
||||
result.success(null);
|
||||
}
|
||||
break;
|
||||
case "dispose":
|
||||
if (printJobController != null) {
|
||||
printJobController.dispose();
|
||||
result.success(true);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
result.notImplemented();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
printJobController = null;
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.print_job;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.PrintJobInfoExt;
|
||||
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
|
||||
public class PrintJobController implements Disposable {
|
||||
protected static final String LOG_TAG = "PrintJob";
|
||||
public static final String METHOD_CHANNEL_NAME_PREFIX = "com.pichillilorenzo/flutter_inappwebview_printjobcontroller_";
|
||||
|
||||
@NonNull
|
||||
public String id;
|
||||
@Nullable
|
||||
public InAppWebViewFlutterPlugin plugin;
|
||||
@Nullable
|
||||
public PrintJobChannelDelegate channelDelegate;
|
||||
@Nullable
|
||||
public android.print.PrintJob job;
|
||||
@Nullable
|
||||
public PrintJobSettings settings;
|
||||
|
||||
public PrintJobController(@NonNull String id, @NonNull android.print.PrintJob job,
|
||||
@Nullable PrintJobSettings settings, @NonNull InAppWebViewFlutterPlugin plugin) {
|
||||
this.id = id;
|
||||
this.plugin = plugin;
|
||||
this.job = job;
|
||||
this.settings = settings;
|
||||
final MethodChannel channel = new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME_PREFIX + id);
|
||||
this.channelDelegate = new PrintJobChannelDelegate(this, channel);
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
if (this.job != null) {
|
||||
this.job.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
public void restart() {
|
||||
if (this.job != null) {
|
||||
this.job.restart();
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PrintJobInfoExt getInfo() {
|
||||
if (this.job != null) {
|
||||
return PrintJobInfoExt.fromPrintJobInfo(this.job.getInfo());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void disposeNoCancel() {
|
||||
if (channelDelegate != null) {
|
||||
channelDelegate.dispose();
|
||||
channelDelegate = null;
|
||||
}
|
||||
if (plugin != null) {
|
||||
PrintJobManager printJobManager = plugin.printJobManager;
|
||||
if (printJobManager != null && printJobManager.jobs.containsKey(id)) {
|
||||
printJobManager.jobs.put(id, null);
|
||||
}
|
||||
}
|
||||
if (job != null) {
|
||||
job = null;
|
||||
}
|
||||
plugin = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
if (channelDelegate != null) {
|
||||
channelDelegate.dispose();
|
||||
channelDelegate = null;
|
||||
}
|
||||
if (plugin != null) {
|
||||
PrintJobManager printJobManager = plugin.printJobManager;
|
||||
if (printJobManager != null && printJobManager.jobs.containsKey(id)) {
|
||||
printJobManager.jobs.put(id, null);
|
||||
}
|
||||
}
|
||||
if (job != null) {
|
||||
job.cancel();
|
||||
job = null;
|
||||
}
|
||||
plugin = null;
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.print_job;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.Disposable;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
|
||||
public class PrintJobManager implements Disposable {
|
||||
protected static final String LOG_TAG = "PrintJobManager";
|
||||
@Nullable
|
||||
public InAppWebViewFlutterPlugin plugin;
|
||||
public final Map<String, PrintJobController> jobs = new HashMap<>();
|
||||
|
||||
public PrintJobManager(@NonNull final InAppWebViewFlutterPlugin plugin) {
|
||||
super();
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
Collection<PrintJobController> printJobControllers = jobs.values();
|
||||
for (PrintJobController job : printJobControllers) {
|
||||
if (job != null) {
|
||||
job.dispose();
|
||||
}
|
||||
}
|
||||
jobs.clear();
|
||||
plugin = null;
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+99
@@ -0,0 +1,99 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.print_job;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.ISettings;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.MediaSizeExt;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.ResolutionExt;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
|
||||
public class PrintJobSettings implements ISettings<PrintJobController> {
|
||||
|
||||
public static final String LOG_TAG = "PrintJobSettings";
|
||||
|
||||
public Boolean handledByClient = false;
|
||||
@Nullable
|
||||
public String jobName;
|
||||
@Nullable
|
||||
public Integer orientation;
|
||||
// @Nullable
|
||||
// public MarginsExt margins;
|
||||
@Nullable
|
||||
public MediaSizeExt mediaSize;
|
||||
@Nullable
|
||||
public Integer colorMode;
|
||||
@Nullable
|
||||
public Integer duplexMode;
|
||||
@Nullable
|
||||
public ResolutionExt resolution;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public PrintJobSettings parse(@NonNull Map<String, Object> settings) {
|
||||
for (Map.Entry<String, Object> pair : settings.entrySet()) {
|
||||
String key = pair.getKey();
|
||||
Object value = pair.getValue();
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case "handledByClient":
|
||||
handledByClient = (Boolean) value;
|
||||
break;
|
||||
case "jobName":
|
||||
jobName = (String) value;
|
||||
break;
|
||||
case "orientation":
|
||||
orientation = (Integer) value;
|
||||
break;
|
||||
// case "margins":
|
||||
// margins = MarginsExt.fromMap((Map<String, Object>) value);
|
||||
// break;
|
||||
case "mediaSize":
|
||||
mediaSize = MediaSizeExt.fromMap((Map<String, Object>) value);
|
||||
break;
|
||||
case "colorMode":
|
||||
colorMode = (Integer) value;
|
||||
break;
|
||||
case "duplexMode":
|
||||
duplexMode = (Integer) value;
|
||||
break;
|
||||
case "resolution":
|
||||
resolution = ResolutionExt.fromMap((Map<String, Object>) value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Map<String, Object> toMap() {
|
||||
Map<String, Object> settings = new HashMap<>();
|
||||
settings.put("handledByClient", handledByClient);
|
||||
settings.put("jobName", jobName);
|
||||
settings.put("orientation", orientation);
|
||||
// settings.put("margins", margins != null ? margins.toMap() : null);
|
||||
settings.put("mediaSize", mediaSize != null ? mediaSize.toMap() : null);
|
||||
settings.put("colorMode", colorMode);
|
||||
settings.put("duplexMode", duplexMode);
|
||||
settings.put("resolution", resolution != null ? resolution.toMap() : null);
|
||||
return settings;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Map<String, Object> getRealSettings(@NonNull PrintJobController printJobController) {
|
||||
Map<String, Object> realSettings = toMap();
|
||||
return realSettings;
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+56
@@ -0,0 +1,56 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.process_global_config;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.webkit.ProcessGlobalConfig;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
public class ProcessGlobalConfigManager extends ChannelDelegateImpl {
|
||||
protected static final String LOG_TAG = "ProcessGlobalConfigM";
|
||||
public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_processglobalconfig";
|
||||
|
||||
@Nullable
|
||||
public InAppWebViewFlutterPlugin plugin;
|
||||
|
||||
public ProcessGlobalConfigManager(@NonNull final InAppWebViewFlutterPlugin plugin) {
|
||||
super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME));
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||
switch (call.method) {
|
||||
case "apply":
|
||||
if (plugin != null && plugin.activity != null) {
|
||||
ProcessGlobalConfigSettings settings = (new ProcessGlobalConfigSettings())
|
||||
.parse((Map<String, Object>) call.argument("settings"));
|
||||
try {
|
||||
ProcessGlobalConfig.apply(settings.toProcessGlobalConfig(plugin.activity));
|
||||
result.success(true);
|
||||
} catch (Exception e) {
|
||||
result.error(LOG_TAG, "", e);
|
||||
}
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
result.notImplemented();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
plugin = null;
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.process_global_config;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.webkit.ProcessGlobalConfig;
|
||||
import androidx.webkit.WebViewFeature;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.ISettings;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ProcessGlobalConfigSettings implements ISettings<ProcessGlobalConfig> {
|
||||
public static final String LOG_TAG = "ProcessGlobalConfigSettings";
|
||||
|
||||
@Nullable
|
||||
public String dataDirectorySuffix;
|
||||
@Nullable
|
||||
public DirectoryBasePaths directoryBasePaths;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ProcessGlobalConfigSettings parse(@NonNull Map<String, Object> settings) {
|
||||
for (Map.Entry<String, Object> pair : settings.entrySet()) {
|
||||
String key = pair.getKey();
|
||||
Object value = pair.getValue();
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case "dataDirectorySuffix":
|
||||
dataDirectorySuffix = (String) value;
|
||||
break;
|
||||
case "directoryBasePaths":
|
||||
directoryBasePaths = (new DirectoryBasePaths()).parse((Map<String, Object>) value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public ProcessGlobalConfig toProcessGlobalConfig(@NonNull Context context) {
|
||||
ProcessGlobalConfig config = new ProcessGlobalConfig();
|
||||
if (dataDirectorySuffix != null &&
|
||||
WebViewFeature.isStartupFeatureSupported(context, WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX)) {
|
||||
config.setDataDirectorySuffix(context, dataDirectorySuffix);
|
||||
}
|
||||
if (directoryBasePaths != null &&
|
||||
WebViewFeature.isStartupFeatureSupported(context, WebViewFeature.STARTUP_FEATURE_SET_DIRECTORY_BASE_PATHS)) {
|
||||
config.setDirectoryBasePaths(context,
|
||||
new File(directoryBasePaths.dataDirectoryBasePath),
|
||||
new File(directoryBasePaths.cacheDirectoryBasePath));
|
||||
}
|
||||
return config;
|
||||
}
|
||||
@NonNull
|
||||
public Map<String, Object> toMap() {
|
||||
Map<String, Object> settings = new HashMap<>();
|
||||
settings.put("dataDirectorySuffix", dataDirectorySuffix);
|
||||
return settings;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Map<String, Object> getRealSettings(@NonNull ProcessGlobalConfig processGlobalConfig) {
|
||||
Map<String, Object> realSettings = toMap();
|
||||
return realSettings;
|
||||
}
|
||||
|
||||
static class DirectoryBasePaths implements ISettings<Object> {
|
||||
public static final String LOG_TAG = "ProcessGlobalConfigSettings";
|
||||
|
||||
public String cacheDirectoryBasePath;
|
||||
public String dataDirectoryBasePath;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public DirectoryBasePaths parse(@NonNull Map<String, Object> settings) {
|
||||
for (Map.Entry<String, Object> pair : settings.entrySet()) {
|
||||
String key = pair.getKey();
|
||||
Object value = pair.getValue();
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case "cacheDirectoryBasePath":
|
||||
cacheDirectoryBasePath = (String) value;
|
||||
break;
|
||||
case "dataDirectoryBasePath":
|
||||
dataDirectoryBasePath = (String) value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Map<String, Object> toMap() {
|
||||
Map<String, Object> settings = new HashMap<>();
|
||||
settings.put("cacheDirectoryBasePath", cacheDirectoryBasePath);
|
||||
settings.put("dataDirectoryBasePath", dataDirectoryBasePath);
|
||||
return settings;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Map<String, Object> getRealSettings(@NonNull Object obj) {
|
||||
Map<String, Object> realSettings = toMap();
|
||||
return realSettings;
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
Executable
+129
@@ -0,0 +1,129 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.proxy;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.webkit.ProxyConfig;
|
||||
import androidx.webkit.ProxyController;
|
||||
import androidx.webkit.WebViewFeature;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.InAppWebViewFlutterPlugin;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.ProxyRuleExt;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
public class ProxyManager extends ChannelDelegateImpl {
|
||||
protected static final String LOG_TAG = "ProxyManager";
|
||||
public static final String METHOD_CHANNEL_NAME = "com.pichillilorenzo/flutter_inappwebview_proxycontroller";
|
||||
|
||||
@Nullable
|
||||
public static ProxyController proxyController;
|
||||
@Nullable
|
||||
public InAppWebViewFlutterPlugin plugin;
|
||||
|
||||
public ProxyManager(@NonNull final InAppWebViewFlutterPlugin plugin) {
|
||||
super(new MethodChannel(plugin.messenger, METHOD_CHANNEL_NAME));
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
public static void init() {
|
||||
if (proxyController == null &&
|
||||
WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) {
|
||||
proxyController = ProxyController.getInstance();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
|
||||
init();
|
||||
|
||||
switch (call.method) {
|
||||
case "setProxyOverride":
|
||||
if (proxyController != null) {
|
||||
HashMap<String, Object> settingsMap = (HashMap<String, Object>) call.argument("settings");
|
||||
ProxySettings settings = new ProxySettings();
|
||||
if (settingsMap != null) {
|
||||
settings.parse(settingsMap);
|
||||
}
|
||||
setProxyOverride(settings, result);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
case "clearProxyOverride":
|
||||
if (proxyController != null ) {
|
||||
clearProxyOverride(result);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
result.notImplemented();
|
||||
}
|
||||
}
|
||||
|
||||
private void setProxyOverride(ProxySettings settings, final MethodChannel.Result result) {
|
||||
if (proxyController != null) {
|
||||
ProxyConfig.Builder proxyConfigBuilder = new ProxyConfig.Builder();
|
||||
for (String bypassRule : settings.bypassRules) {
|
||||
proxyConfigBuilder.addBypassRule(bypassRule);
|
||||
}
|
||||
for (String direct : settings.directs) {
|
||||
proxyConfigBuilder.addDirect(direct);
|
||||
}
|
||||
for (ProxyRuleExt proxyRule : settings.proxyRules) {
|
||||
if (proxyRule.getSchemeFilter() != null) {
|
||||
proxyConfigBuilder.addProxyRule(proxyRule.getUrl(), proxyRule.getSchemeFilter());
|
||||
} else {
|
||||
proxyConfigBuilder.addProxyRule(proxyRule.getUrl());
|
||||
}
|
||||
}
|
||||
if (settings.bypassSimpleHostnames != null && settings.bypassSimpleHostnames) {
|
||||
proxyConfigBuilder.bypassSimpleHostnames();
|
||||
}
|
||||
if (settings.removeImplicitRules != null && settings.removeImplicitRules) {
|
||||
proxyConfigBuilder.removeImplicitRules();
|
||||
}
|
||||
if (settings.reverseBypassEnabled != null && WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE_REVERSE_BYPASS)) {
|
||||
proxyConfigBuilder.setReverseBypassEnabled(settings.reverseBypassEnabled);
|
||||
}
|
||||
proxyController.setProxyOverride(proxyConfigBuilder.build(), new Executor() {
|
||||
@Override
|
||||
public void execute(Runnable command) {
|
||||
command.run();
|
||||
}
|
||||
}, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
result.success(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void clearProxyOverride(final MethodChannel.Result result) {
|
||||
if (proxyController != null) {
|
||||
proxyController.clearProxyOverride(new Executor() {
|
||||
@Override
|
||||
public void execute(Runnable command) {
|
||||
command.run();
|
||||
}
|
||||
}, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
result.success(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
plugin = null;
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.proxy;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.webkit.ProxyConfig;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.ISettings;
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.ProxyRuleExt;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ProxySettings implements ISettings<ProxyConfig> {
|
||||
List<String> bypassRules = new ArrayList<>();
|
||||
List<String> directs = new ArrayList<>();
|
||||
List<ProxyRuleExt> proxyRules = new ArrayList<>();
|
||||
Boolean bypassSimpleHostnames = null;
|
||||
Boolean removeImplicitRules = null;
|
||||
Boolean reverseBypassEnabled = false;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ProxySettings parse(@NonNull Map<String, Object> settings) {
|
||||
for (Map.Entry<String, Object> pair : settings.entrySet()) {
|
||||
String key = pair.getKey();
|
||||
Object value = pair.getValue();
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case "bypassRules":
|
||||
bypassRules = (List<String>) value;
|
||||
break;
|
||||
case "directs":
|
||||
directs = (List<String>) value;
|
||||
break;
|
||||
case "proxyRules":
|
||||
proxyRules = new ArrayList<>();
|
||||
List<Map<String, String>> proxyRuleMapList = (List<Map<String, String>>) value;
|
||||
for (Map<String, String> proxyRuleMap : proxyRuleMapList) {
|
||||
ProxyRuleExt proxyRuleExt = ProxyRuleExt.fromMap(proxyRuleMap);
|
||||
if (proxyRuleExt != null) {
|
||||
proxyRules.add(proxyRuleExt);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "bypassSimpleHostnames":
|
||||
bypassSimpleHostnames = (Boolean) value;
|
||||
break;
|
||||
case "removeImplicitRules":
|
||||
removeImplicitRules = (Boolean) value;
|
||||
break;
|
||||
case "reverseBypassEnabled":
|
||||
reverseBypassEnabled = (Boolean) value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Map<String, Object> toMap() {
|
||||
List<Map<String, String>> proxyRuleMapList = new ArrayList<>();
|
||||
for (ProxyRuleExt proxyRuleExt : proxyRules) {
|
||||
proxyRuleMapList.add(proxyRuleExt.toMap());
|
||||
}
|
||||
Map<String, Object> settings = new HashMap<>();
|
||||
settings.put("bypassRules", bypassRules);
|
||||
settings.put("directs", directs);
|
||||
settings.put("proxyRules", proxyRuleMapList);
|
||||
settings.put("bypassSimpleHostnames", bypassSimpleHostnames);
|
||||
settings.put("removeImplicitRules", removeImplicitRules);
|
||||
settings.put("reverseBypassEnabled", reverseBypassEnabled);
|
||||
return settings;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Map<String, Object> getRealSettings(@NonNull ProxyConfig proxyConfig) {
|
||||
Map<String, Object> realSettings = toMap();
|
||||
List<Map<String, String>> proxyRuleMapList = new ArrayList<>();
|
||||
List<ProxyConfig.ProxyRule> proxyRules = proxyConfig.getProxyRules();
|
||||
for (ProxyConfig.ProxyRule proxyRule : proxyRules) {
|
||||
Map<String, String> proxyRuleMap = new HashMap<>();
|
||||
proxyRuleMap.put("url", proxyRule.getUrl());
|
||||
proxyRuleMap.put("schemeFilter", proxyRule.getSchemeFilter());
|
||||
proxyRuleMapList.add(proxyRuleMap);
|
||||
}
|
||||
realSettings.put("bypassRules", proxyConfig.getBypassRules());
|
||||
realSettings.put("proxyRules", proxyRuleMapList);
|
||||
realSettings.put("reverseBypassEnabled", proxyConfig.isReverseBypassEnabled());
|
||||
return realSettings;
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.pull_to_refresh;
|
||||
|
||||
import android.graphics.Color;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.types.ChannelDelegateImpl;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
|
||||
public class PullToRefreshChannelDelegate extends ChannelDelegateImpl {
|
||||
@Nullable
|
||||
private PullToRefreshLayout pullToRefreshView;
|
||||
|
||||
public PullToRefreshChannelDelegate(@NonNull PullToRefreshLayout pullToRefreshView, @NonNull MethodChannel channel) {
|
||||
super(channel);
|
||||
this.pullToRefreshView = pullToRefreshView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(@NonNull MethodCall call, @NonNull final MethodChannel.Result result) {
|
||||
switch (call.method) {
|
||||
case "setEnabled":
|
||||
if (pullToRefreshView != null) {
|
||||
Boolean enabled = (Boolean) call.argument("enabled");
|
||||
pullToRefreshView.settings.enabled = enabled; // used by InAppWebView.onOverScrolled
|
||||
pullToRefreshView.setEnabled(enabled);
|
||||
result.success(true);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
case "isEnabled":
|
||||
if (pullToRefreshView != null) {
|
||||
result.success(pullToRefreshView.isEnabled());
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
case "setRefreshing":
|
||||
if (pullToRefreshView != null) {
|
||||
Boolean refreshing = (Boolean) call.argument("refreshing");
|
||||
pullToRefreshView.setRefreshing(refreshing);
|
||||
result.success(true);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
case "isRefreshing":
|
||||
result.success(pullToRefreshView != null && pullToRefreshView.isRefreshing());
|
||||
break;
|
||||
case "setColor":
|
||||
if (pullToRefreshView != null) {
|
||||
String color = (String) call.argument("color");
|
||||
pullToRefreshView.setColorSchemeColors(Color.parseColor(color));
|
||||
result.success(true);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
case "setBackgroundColor":
|
||||
if (pullToRefreshView != null) {
|
||||
String color = (String) call.argument("color");
|
||||
pullToRefreshView.setProgressBackgroundColorSchemeColor(Color.parseColor(color));
|
||||
result.success(true);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
case "setDistanceToTriggerSync":
|
||||
if (pullToRefreshView != null) {
|
||||
Integer distanceToTriggerSync = (Integer) call.argument("distanceToTriggerSync");
|
||||
pullToRefreshView.setDistanceToTriggerSync(distanceToTriggerSync);
|
||||
result.success(true);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
case "setSlingshotDistance":
|
||||
if (pullToRefreshView != null) {
|
||||
Integer slingshotDistance = (Integer) call.argument("slingshotDistance");
|
||||
pullToRefreshView.setSlingshotDistance(slingshotDistance);
|
||||
result.success(true);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
case "getDefaultSlingshotDistance":
|
||||
result.success(SwipeRefreshLayout.DEFAULT_SLINGSHOT_DISTANCE);
|
||||
break;
|
||||
case "setSize":
|
||||
if (pullToRefreshView != null) {
|
||||
Integer size = (Integer) call.argument("size");
|
||||
pullToRefreshView.setSize(size);
|
||||
result.success(true);
|
||||
} else {
|
||||
result.success(false);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
result.notImplemented();
|
||||
}
|
||||
}
|
||||
|
||||
public void onRefresh() {
|
||||
MethodChannel channel = getChannel();
|
||||
if (channel == null) return;
|
||||
Map<String, Object> obj = new HashMap<>();
|
||||
channel.invokeMethod("onRefresh", obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
pullToRefreshView = null;
|
||||
}
|
||||
}
|
||||
+86
@@ -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();
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package com.pichillilorenzo.flutter_inappwebview_android.pull_to_refresh;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.pichillilorenzo.flutter_inappwebview_android.ISettings;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class PullToRefreshSettings implements ISettings<PullToRefreshLayout> {
|
||||
public static final String LOG_TAG = "PullToRefreshSettings";
|
||||
|
||||
public Boolean enabled = true;
|
||||
@Nullable
|
||||
public String color;
|
||||
@Nullable
|
||||
public String backgroundColor;
|
||||
@Nullable
|
||||
public Integer distanceToTriggerSync;
|
||||
@Nullable
|
||||
public Integer slingshotDistance;
|
||||
@Nullable
|
||||
public Integer size;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public PullToRefreshSettings parse(@NonNull Map<String, Object> settings) {
|
||||
for (Map.Entry<String, Object> pair : settings.entrySet()) {
|
||||
String key = pair.getKey();
|
||||
Object value = pair.getValue();
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case "enabled":
|
||||
enabled = (Boolean) value;
|
||||
break;
|
||||
case "color":
|
||||
color = (String) value;
|
||||
break;
|
||||
case "backgroundColor":
|
||||
backgroundColor = (String) value;
|
||||
break;
|
||||
case "distanceToTriggerSync":
|
||||
distanceToTriggerSync = (Integer) value;
|
||||
break;
|
||||
case "slingshotDistance":
|
||||
slingshotDistance = (Integer) value;
|
||||
break;
|
||||
case "size":
|
||||
size = (Integer) value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Map<String, Object> toMap() {
|
||||
Map<String, Object> settings = new HashMap<>();
|
||||
settings.put("enabled", enabled);
|
||||
settings.put("color", color);
|
||||
settings.put("backgroundColor", backgroundColor);
|
||||
settings.put("distanceToTriggerSync", distanceToTriggerSync);
|
||||
settings.put("slingshotDistance", slingshotDistance);
|
||||
settings.put("size", size);
|
||||
return settings;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Map<String, Object> getRealSettings(@NonNull PullToRefreshLayout pullToRefreshLayout) {
|
||||
Map<String, Object> realSettings = toMap();
|
||||
return realSettings;
|
||||
}
|
||||
|
||||
}
|
||||
Loaded 100 of 556 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user