From a0ba3ed051446b9bbba56026f3e21db068aaee22 Mon Sep 17 00:00:00 2001 From: Asheesh Laroia Date: Sat, 1 Aug 2020 17:11:17 -0700 Subject: [PATCH 1/2] Add Android WebView --- src/android/toga_android/factory.py | 2 + .../toga_android/libs/android_widgets.py | 3 + src/android/toga_android/widgets/webview.py | 87 +++++++++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 src/android/toga_android/widgets/webview.py diff --git a/src/android/toga_android/factory.py b/src/android/toga_android/factory.py index c173453365..315db3a68a 100644 --- a/src/android/toga_android/factory.py +++ b/src/android/toga_android/factory.py @@ -14,6 +14,7 @@ from .widgets.slider import Slider from .widgets.switch import Switch from .widgets.textinput import TextInput +from .widgets.webview import WebView from .window import Window @@ -38,6 +39,7 @@ def not_implemented(feature): "Slider", "Switch", "TextInput", + "WebView", "Window", "not_implemented", "paths", diff --git a/src/android/toga_android/libs/android_widgets.py b/src/android/toga_android/libs/android_widgets.py index fc46481d0e..5dffe5ad38 100644 --- a/src/android/toga_android/libs/android_widgets.py +++ b/src/android/toga_android/libs/android_widgets.py @@ -25,8 +25,11 @@ TextWatcher = JavaInterface("android/text/TextWatcher") TypedValue = JavaClass("android/util/TypedValue") Typeface = JavaClass("android/graphics/Typeface") +ValueCallback = JavaInterface("android/webkit/ValueCallback") ViewGroup__LayoutParams = JavaClass("android/view/ViewGroup$LayoutParams") View__MeasureSpec = JavaClass("android/view/View$MeasureSpec") +WebView = JavaClass("android/webkit/WebView") +WebViewClient = JavaClass("android/webkit/WebViewClient") # Indicate to `rubicon-java` that `ArrayAdapter` can also be typecast into a # `SpinnerAdapter`. This is required until `rubicon-java` explores the interfaces diff --git a/src/android/toga_android/widgets/webview.py b/src/android/toga_android/widgets/webview.py new file mode 100644 index 0000000000..7f7135ebc5 --- /dev/null +++ b/src/android/toga_android/widgets/webview.py @@ -0,0 +1,87 @@ +import asyncio +import base64 + +from travertino.size import at_least + +from ..libs import android_widgets +from .base import Widget, align + + +class ReceiveString(android_widgets.ValueCallback): + def __init__(self, fn=None): + super().__init__() + self._fn = fn + + def onReceiveValue(self, value): + if self._fn: + if value is None: + self._fn(None) + else: + # Ensure we send a string to the function. + self._fn(value.toString()) + + +class WebView(Widget): + def create(self): + self.native = android_widgets.WebView(self._native_activity) + # Set a WebViewClient so that new links open in this activity, + # rather than triggering the phone's web browser. + self.native.setWebViewClient(android_widgets.WebViewClient()) + # Enable JS. + self.native.getSettings().setJavaScriptEnabled(True) + + def set_on_key_down(self, handler): + # Android isn't a platform that usually has a keyboard attached, so this is unimplemented for now. + self.interface.factory.not_implemented('WebView.set_on_key_down()') + + def set_on_webview_load(self, handler): + # This requires subclassing WebViewClient, which is not yet possible with rubicon-java. + self.interface.factory.not_implemented('WebView.set_on_webview_load()') + + def get_dom(self): + # Android has no straightforward way to get the DOM from the browser synchronously. + self.interface.factory.not_implemented('WebView.get_dom()') + + def set_url(self, value): + if value: + self.native.loadUrl(str(value)) + + def set_content(self, root_url, content): + # Android WebView lacks an underlying set_content() primitive, so we navigate to + # a data URL. This means we ignore the root_url parameter. + data_url = ("data:text/html; charset=utf-8; base64," + + base64.b64encode(content.encode('utf-8')).decode('ascii')) + self.set_url(data_url) + + def set_user_agent(self, value): + if value is not None: + self.native.getSettings().setUserAgentString(value) + + async def evaluate_javascript(self, javascript): + js_value = asyncio.Future() + self.native.evaluateJavascript(str(javascript), ReceiveString(js_value.set_result)) + return await js_value + + def invoke_javascript(self, javascript): + print("omg trying to invoke " + repr(javascript)) + self.native.evaluateJavascript(str(javascript), ReceiveString()) + + def set_alignment(self, value): + # Refuse to set alignment unless widget has been added to a container. + # This is because this widget's setGravity() requires LayoutParams before it can be called. + if self.native.getLayoutParams() is None: + return + self.native.setGravity(android_widgets.Gravity.CENTER_VERTICAL | align(value)) + + def rehint(self): + self.interface.intrinsic.width = at_least(self.interface.MIN_WIDTH) + # Refuse to call measure() if widget has no container, i.e., has no LayoutParams. + # Android's measure() throws NullPointerException if the widget has no LayoutParams. + if self.native.getLayoutParams() is None: + return + self.native.measure( + android_widgets.View__MeasureSpec.UNSPECIFIED, + android_widgets.View__MeasureSpec.UNSPECIFIED, + ) + self.interface.intrinsic.width = at_least(self.native.getMeasuredWidth()) + self.interface.intrinsic.height = self.native.getMeasuredHeight() From b725cc81e3368abe36417339b9f3a18c6fb6e575 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 8 Aug 2020 15:18:02 +0800 Subject: [PATCH 2/2] Corrected the requirements in the webview demo. --- examples/webview/pyproject.toml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/webview/pyproject.toml b/examples/webview/pyproject.toml index ca244c4dd0..3fa1f6b266 100644 --- a/examples/webview/pyproject.toml +++ b/examples/webview/pyproject.toml @@ -18,8 +18,7 @@ requires = [] [tool.briefcase.app.webview.macOS] requires = [ -'../../src/core', -'../../src/cocoa', + 'toga-cocoa', ] [tool.briefcase.app.webview.linux] @@ -35,8 +34,7 @@ requires = [ # Mobile deployments [tool.briefcase.app.webview.iOS] requires = [ -'../../src/core', -'../../src/ios', + 'toga-ios', ] [tool.briefcase.app.webview.android]