winio-ui-android 0.1.0

Android backend for winio.
use std::sync::{Arc, Mutex};

use cookie::Cookie;
use futures_util::{FutureExt, TryFutureExt};
use inherit_methods_macro::inherit_methods;
use jni::{
    objects::{JObject, JString},
    refs::{LoaderContext, Reference},
};
use jni_min_helper::DynamicProxy;
use winio_callback::SyncCallback;
use winio_handle::AsContainer;
use winio_primitive::{Point, Size};

use crate::{
    BaseWidget, Result, current_activity,
    java::{
        android::webkit::{
            CookieManager, ValueCallback, WebChromeClient as AWebChromeClient, WebView as AWebView,
        },
        custom::WinioWebViewClient,
        lang::JRunnable,
    },
    vm_exec,
};

#[derive(Debug)]
pub struct WebView {
    inner: BaseWidget<AWebView<'static>>,
    on_started: Arc<SyncCallback>,
    #[allow(dead_code)]
    started_proxy: DynamicProxy,
    on_finished: Arc<SyncCallback>,
    #[allow(dead_code)]
    finished_proxy: DynamicProxy,
}

#[inherit_methods(from = "self.inner")]
impl WebView {
    pub async fn new(parent: impl AsContainer) -> Result<Self> {
        vm_exec(|env| {
            CookieManager::get_instance(env)?.set_accept_cookie(env, true)?;

            let act = current_activity(env)?;
            let widget = AWebView::new(env, act)?;
            widget
                .get_settings(env)?
                .set_java_script_enabled(env, true)?;
            let chrome_client = AWebChromeClient::new(env)?;
            widget.set_web_chrome_client(env, &chrome_client)?;
            let inner = BaseWidget::new_with_env(env, parent.as_container(), widget)?;

            let client = WinioWebViewClient::new(env)?;

            let on_started = Arc::new(SyncCallback::new());
            let started_proxy =
                DynamicProxy::build(env, &LoaderContext::None, [JRunnable::class_name()], {
                    let on_started = on_started.clone();
                    move |_env, _method, _args| {
                        on_started.signal(());
                        Ok(JObject::null())
                    }
                })?;
            client.set_on_page_started(env, &started_proxy)?;

            let on_finished = Arc::new(SyncCallback::new());
            let finished_proxy =
                DynamicProxy::build(env, &LoaderContext::None, [JRunnable::class_name()], {
                    let on_finished = on_finished.clone();
                    move |_env, _method, _args| {
                        on_finished.signal(());
                        Ok(JObject::null())
                    }
                })?;
            client.set_on_page_finished(env, &finished_proxy)?;

            inner.set_web_view_client(env, client)?;

            Ok(Self {
                inner,
                on_started,
                started_proxy,
                on_finished,
                finished_proxy,
            })
        })
    }

    pub fn is_visible(&self) -> Result<bool>;

    pub fn set_visible(&mut self, v: bool) -> Result<()>;

    pub fn is_enabled(&self) -> Result<bool>;

    pub fn set_enabled(&mut self, v: bool) -> Result<()>;

    pub fn loc(&self) -> Result<Point>;

    pub fn set_loc(&mut self, p: Point) -> Result<()>;

    pub fn size(&self) -> Result<Size>;

    pub fn set_size(&mut self, v: Size) -> Result<()>;

    pub fn source(&self) -> Result<String> {
        vm_exec(|env| Ok(self.inner.get_url(env)?.try_to_string(env)?))
    }

    pub fn set_source(&mut self, s: impl AsRef<str>) -> Result<()> {
        vm_exec(|env| {
            let url = env.new_string(s.as_ref())?;
            self.inner.load_url(env, &url)?;
            Ok(())
        })
    }

    pub fn set_html(&mut self, s: impl AsRef<str>) -> Result<()> {
        use base64::prelude::*;

        let s = BASE64_STANDARD.encode(s.as_ref());
        vm_exec(|env| {
            let data = env.new_string(s)?;
            let encoding = env.new_string("base64")?;
            self.inner
                .load_data(env, &data, JString::null(), &encoding)?;
            Ok(())
        })
    }

    pub fn can_go_forward(&self) -> Result<bool> {
        vm_exec(|env| Ok(self.inner.can_go_forward(env)?))
    }

    pub fn go_forward(&mut self) -> Result<()> {
        vm_exec(|env| {
            self.inner.go_forward(env)?;
            Ok(())
        })
    }

    pub fn can_go_back(&self) -> Result<bool> {
        vm_exec(|env| Ok(self.inner.can_go_back(env)?))
    }

    pub fn go_back(&mut self) -> Result<()> {
        vm_exec(|env| {
            self.inner.go_back(env)?;
            Ok(())
        })
    }

    pub fn reload(&mut self) -> Result<()> {
        vm_exec(|env| {
            self.inner.reload(env)?;
            Ok(())
        })
    }

    pub fn stop(&mut self) -> Result<()> {
        vm_exec(|env| {
            self.inner.stop_loading(env)?;
            Ok(())
        })
    }

    pub async fn wait_navigating(&self) {
        self.on_started.wait().await;
    }

    pub async fn wait_navigated(&self) {
        self.on_finished.wait().await;
    }

    pub async fn cookies(&self) -> Result<Vec<Cookie<'static>>> {
        let source = self.source()?;
        if source.is_empty() {
            return Ok(vec![]);
        }
        vm_exec(|env| {
            let url = env.new_string(&source)?;
            let cookies = CookieManager::get_instance(env)?.get_cookie(env, &url)?;
            let cookies = cookies.try_to_string(env)?;
            if let Ok(cookie) = Cookie::parse(cookies) {
                Ok(vec![cookie])
            } else {
                Ok(vec![])
            }
        })
    }

    async fn set_cookie_impl(&mut self, cookie: &str) -> Result<()> {
        let source = self.source()?;
        if source.is_empty() {
            return Ok(());
        }
        let (rx, _proxy) = vm_exec(|env| {
            let url = env.new_string(&source)?;
            let value = env.new_string(cookie)?;
            let (tx, rx) = oneshot::channel();
            let tx = Arc::new(Mutex::new(Some(tx)));
            let proxy =
                DynamicProxy::build(env, &LoaderContext::None, [ValueCallback::class_name()], {
                    move |_env, _method, _args| {
                        if let Some(tx) = tx.lock().unwrap().take() {
                            tx.send(()).ok();
                        }
                        Ok(JObject::null())
                    }
                })?;
            CookieManager::get_instance(env)?.set_cookie(env, &url, &value, &proxy)?;
            Result::Ok((rx, proxy))
        })?;
        rx.await.map_err(std::io::Error::other)?;
        Ok(())
    }

    pub async fn set_cookie(&mut self, c: &Cookie<'_>) -> Result<()> {
        self.set_cookie_impl(&c.to_string()).await
    }

    pub async fn delete_cookie(&mut self, c: &Cookie<'_>) -> Result<()> {
        let mut cookie = c.clone();
        cookie.set_max_age(time::Duration::seconds(0));
        self.set_cookie_impl(&cookie.to_string()).await
    }

    pub fn run_javascript(
        &mut self,
        script: impl AsRef<str>,
    ) -> Result<impl Future<Output = Result<String>> + 'static> {
        let (tx, rx) = oneshot::channel();
        let _proxy = vm_exec(|env| {
            let script = env.new_string(script.as_ref())?;
            let tx = Arc::new(Mutex::new(Some(tx)));
            let callback =
                DynamicProxy::build(env, &LoaderContext::None, [ValueCallback::class_name()], {
                    move |env, method, args| {
                        let name = method.get_name(env)?.try_to_string(env)?;
                        if name == "onReceiveValue" {
                            let value = args.get_element(env, 0)?;
                            let value = env.cast_local::<JString>(value)?;
                            let result = if value.is_null() {
                                None
                            } else {
                                Some(value.try_to_string(env)?)
                            };
                            if let Some(tx) = tx.lock().unwrap().take() {
                                tx.send(result).ok();
                            }
                        }
                        Ok(JObject::null())
                    }
                })?;
            self.inner.evaluate_javascript(env, &script, &callback)?;
            Result::Ok(callback)
        })?;
        Ok(rx
            .into_future()
            .map_err(|e| std::io::Error::other(e).into())
            .map(|res| res.map(|res| res.unwrap_or_default())))
    }
}

winio_handle::impl_as_widget!(WebView, inner);