Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Android WebView #1017

Merged
merged 3 commits into from
Aug 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions examples/webview/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ requires = []

[tool.briefcase.app.webview.macOS]
requires = [
'../../src/core',
'../../src/cocoa',
'toga-cocoa',
]

[tool.briefcase.app.webview.linux]
Expand All @@ -35,8 +34,7 @@ requires = [
# Mobile deployments
[tool.briefcase.app.webview.iOS]
requires = [
'../../src/core',
'../../src/ios',
'toga-ios',
]

[tool.briefcase.app.webview.android]
Expand Down
2 changes: 2 additions & 0 deletions src/android/toga_android/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -38,6 +39,7 @@ def not_implemented(feature):
"Slider",
"Switch",
"TextInput",
"WebView",
"Window",
"not_implemented",
"paths",
Expand Down
3 changes: 3 additions & 0 deletions src/android/toga_android/libs/android_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
87 changes: 87 additions & 0 deletions src/android/toga_android/widgets/webview.py
Original file line number Diff line number Diff line change
@@ -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()