Skip to content
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
132 changes: 132 additions & 0 deletions android/modules/ui/src/java/ti/modules/titanium/ui/WebViewProxy.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import android.print.PrintManager;
import android.util.DisplayMetrics;
import android.view.View;
import android.webkit.JavascriptInterface;
import android.webkit.ValueCallback;
import android.webkit.WebView;

Expand All @@ -34,12 +35,17 @@
import org.appcelerator.titanium.io.TiFileFactory;
import org.appcelerator.titanium.util.TiConvert;
import org.appcelerator.titanium.view.TiUIView;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.lang.ref.WeakReference;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import ti.modules.titanium.ui.widget.webview.TiUIWebView;

Expand Down Expand Up @@ -76,6 +82,7 @@ public class WebViewProxy extends ViewProxy implements Handler.Callback, OnLifec
private static String fpassword;
PrintManager printManager;
private Message postCreateMessage;
private JSInterface jsInterface;

@Kroll.constant
public static final int PDF_PAGE_DIN_A4 = 0;
Expand Down Expand Up @@ -127,6 +134,11 @@ public TiUIView createView(Activity activity)
postCreateMessage = null;
}

// Add script message handlers if attempted before the web-view could be created.
if (jsInterface != null) {
jsInterface.initializeJSInterface(webView);
}

return webView;
}

Expand Down Expand Up @@ -157,6 +169,24 @@ public Object evalJS(String code, @Kroll.argument(optional = true) KrollFunction
return view.getJSValue(code);
}

@Kroll.method
public void addScriptMessageHandler(String name)
{
if (jsInterface == null) {
jsInterface = new JSInterface(this);
}

jsInterface.addScriptMessageHandler(name);
}

@Kroll.method
public void removeScriptMessageHandler(String name)
{
if (jsInterface != null) {
jsInterface.removeScriptMessageHandler(name);
}
}

@Kroll.getProperty
public String getHtml()
{
Expand Down Expand Up @@ -211,6 +241,7 @@ public boolean handleMessage(Message msg)
getWebView().stopLoading();
return true;
case MSG_RELEASE:
destroyJSInterface();
TiUIWebView webView = (TiUIWebView) peekView();
if (webView != null) {
webView.destroyWebViewBinding();
Expand Down Expand Up @@ -634,6 +665,8 @@ public void onStop(Activity activity)
@Override
public void onDestroy(Activity activity)
{
destroyJSInterface();

TiUIWebView webView = (TiUIWebView) peekView();
if (webView == null) {
return;
Expand All @@ -659,6 +692,14 @@ public String getApiName()
return "Ti.UI.WebView";
}

private void destroyJSInterface()
{
if (jsInterface != null) {
jsInterface.destroy();
jsInterface = null;
}
}

private static class EvalJSRunnable implements Runnable
{
private final TiUIWebView view;
Expand Down Expand Up @@ -693,4 +734,95 @@ public void onReceiveValue(String value)
});
}
}

private class JSInterface
{
// Store references to add later the web-view is created.
private boolean isInterfaceAdded = false;
private WeakReference<WebViewProxy> proxy;
private final String JS_INTERFACE_NAME = "tisdk";
private final Set<String> scriptMessageHandlers = new HashSet<>();

public JSInterface(WebViewProxy proxy)
{
this.proxy = new WeakReference<>(proxy);
}

// Handle the interface creation if apps request the script message handler earlier before the web-view creation.
public void initializeJSInterface(TiUIWebView tiUIWebView)
{
if (isInterfaceAdded) {
return;
}

WebView webView = tiUIWebView.getWebView();
webView.addJavascriptInterface(this, JS_INTERFACE_NAME);
isInterfaceAdded = true;
}

// Apps can call this method even before native webview is created, so we use init method to initialize again.
public void addScriptMessageHandler(String name)
{
scriptMessageHandlers.add(name);

if (view != null) {
initializeJSInterface((TiUIWebView) view);
}
}

public void removeScriptMessageHandler(String name)
{
scriptMessageHandlers.remove(name);

if (view == null) {
return;
}

// Remove interface if no handlers available.
if (scriptMessageHandlers.isEmpty() && isInterfaceAdded) {
((TiUIWebView) view).getWebView().removeJavascriptInterface(JS_INTERFACE_NAME);
isInterfaceAdded = false;
}
}

@JavascriptInterface
public void emit(String name, String json)
{
if (this.proxy == null || this.proxy.get() == null) {
return;
}

// This is just to keep parity with the iOS structure, no other use.
if (name == null || !scriptMessageHandlers.contains(name)) {
Log.e(TAG, "scriptMessageHandler not available for name: " + name);
return;
}

KrollDict event = new KrollDict();
event.put("name", name);

try {
event.put("body", new KrollDict(new JSONObject(json)));
} catch (JSONException e) {
Log.e(TAG, "Error parsing scriptMessageHandler's JSON message", e);
}

this.proxy.get().fireEvent("message", event);
}

public void destroy()
{
scriptMessageHandlers.clear();

if (view != null && isInterfaceAdded) {
WebView webView = ((TiUIWebView) view).getWebView();
webView.removeJavascriptInterface(JS_INTERFACE_NAME);
}

if (this.proxy != null) {
this.proxy.clear();
this.proxy = null;
}
}
}
}
55 changes: 35 additions & 20 deletions apidoc/Titanium/UI/WebView.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ description: |
[evalJS](Titanium.UI.WebView.evalJS) method to execute a JavaScript expression
inside the web view and retrieve the value of an expression.

To trigger a message from the remote web page, refer to <Titanium.UI.WebView.addScriptMessageHandler>.

You can inject the local `Ti.App.fireEvent` bindings yourself by adding a script element using
evalJS.

Expand All @@ -51,8 +53,6 @@ description: |
```
The `binding.min.js` is available in the [repository](https://github.com/tidev/titanium-sdk/tree/master/android/modules/ui/assets/Resources/ti.internal/webview).

For iOS check the example in <Titanium.UI.WebView.addScriptMessageHandler>.

#### Local JavaScript Files

During the build process for creating a package, all JavaScript files, that is, any file with a
Expand Down Expand Up @@ -419,39 +419,54 @@ methods:
- name: addScriptMessageHandler
summary: Adds a script message handler.
description: |
Adding a script message handler with name 'name' causes the JavaScript function
window.webkit.messageHandlers.name.postMessage(messageBody) to be defined in all
frames in the web view. You use the same 'name' in your webview's html.
Example JS code for the webview page:
Adds a script message handler that enables communication between the web content and your Titanium application in all frames of the web view.

**New, cross-platform API:**
Use `window.tisdk.emit('yourHandlerName', JSON.stringify(body));` to send messages from web content. This API is available on all platforms and should be preferred for new development.

**Legacy iOS-only API:**
For backwards compatibility or when targeting SDK 13.0.0 or older, you can also use the legacy iOS-specific API:
`window.webkit.messageHandlers.yourHandlerName.postMessage(body)`
This method works only on iOS and will not be available on other platforms.

**Important:**
Always call `addScriptMessageHandler` before loading the page (setting the URL), or reload the URL after adding the handler to ensure your web content can communicate as expected.

JS code usage examples for your webpage:
```
<script type="text/javascript">
window.webkit.messageHandlers.yourHandlerName.postMessage({
message: 'This is an example string.'
});
const body = { message: 'Hello Titanium!'};

// Preferred API for all platforms in SDK 13.1.0 and later.
window.tisdk.emit('yourHandlerName', JSON.stringify(body));

// Legacy iOS-only API for SDK 13.0.0 and older.
window.webkit.messageHandlers.yourHandlerName.postMessage(body);
</script>
```
Connection to the webview:

Titanium app code usages:
```js
webView.addScriptMessageHandler('yourHandlerName');
webView.addEventListener('message', ({ name }) => {
if (name === 'yourHandlerName') {
webView.addEventListener('message', (e) => {
if (e.name === 'yourHandlerName') {
console.log(e.body.message);
}
});
webView.url = 'https://some-remote-url.org';
```
platforms: [iphone, ipad, macos]
since: {iphone: "8.0.0", ipad: "8.0.0", macos: "9.2.0"}
platforms: [android, iphone, ipad, macos]
since: {android: "13.1.0", iphone: "8.0.0", ipad: "8.0.0", macos: "9.2.0"}
parameters:
- name: handlerName
summary: |
The name of the message handler and should not be '_Ti_',
as titanium is using it for internal usage.
The name of the message handler, other than the reserved '_Ti_', or '_Ti_Cookie_'.
type: String

- name: removeScriptMessageHandler
summary: Removes a script message handler.
platforms: [iphone, ipad, macos]
since: {iphone: "8.0.0", ipad: "8.0.0", macos: "9.2.0"}
platforms: [android, iphone, ipad, macos]
since: {android: "13.1.0", iphone: "8.0.0", ipad: "8.0.0", macos: "9.2.0"}
parameters:
- name: name
summary: The name of the message handler.
Expand Down Expand Up @@ -652,8 +667,8 @@ events:
description: |
This event get fired when you have added a message handler using <Titanium.UI.WebView.addScriptMessageHandler>
and the webpage sends a message to it.
platforms: [iphone, ipad, macos]
since: {iphone: "8.0.0", ipad: "8.0.0", macos: "9.2.0"}
platforms: [android, iphone, ipad, macos]
since: {android: "13.1.0", iphone: "8.0.0", ipad: "8.0.0", macos: "9.2.0"}
properties:
- name: url
summary: URL of the web document being loaded.
Expand Down
2 changes: 1 addition & 1 deletion iphone/Classes/TiUIWebView.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
- (void)viewDidClose;
- (void)reload;
- (WKWebView *)webView;

- (WKUserScript *)userScriptForMessageHandlerParity;
- (void)fireEvent:(id)listener withObject:(id)obj remove:(BOOL)yn thisObject:(id)thisObject_;

@end
Expand Down
12 changes: 12 additions & 0 deletions iphone/Classes/TiUIWebView.m
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,18 @@ - (WKUserScript *)userScriptDisableContextMenu
forMainFrameOnly:YES] autorelease];
}

- (WKUserScript *)userScriptForMessageHandlerParity
{
NSString *tisdkScript = @"window.tisdk={ \
emit:function(handlerName, payload){ \
window.webkit.messageHandlers[handlerName].postMessage(JSON.parse(payload)); \
} \
}; \
";

return [[[WKUserScript alloc] initWithSource:tisdkScript injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO] autorelease];
}

- (WKUserScript *)userScriptTitaniumJSEvaluationFromString:(NSString *)string
{
return [[[WKUserScript alloc] initWithSource:string
Expand Down
1 change: 1 addition & 0 deletions iphone/Classes/TiUIWebViewProxy.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

- (void)refreshHTMLContent;
- (void)setPageToken:(NSString *)pageToken;

@end

#endif
14 changes: 14 additions & 0 deletions iphone/Classes/TiUIWebViewProxy.m
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
#import "TiNetworkCookieProxy.h"
#import "TiUIWebView.h"

@interface TiUIWebViewProxy ()
@property (nonatomic, assign) BOOL hasAddedTisdkScript;
@end

@implementation TiUIWebViewProxy

static NSArray *webViewKeySequence;
Expand All @@ -31,6 +35,7 @@ - (NSArray *)keySequence
- (id)_initWithPageContext:(id<TiEvaluator>)context
{
if (self = [super _initWithPageContext:context]) {
self.hasAddedTisdkScript = NO;
}

return self;
Expand Down Expand Up @@ -382,13 +387,22 @@ - (void)removeAllUserScripts:(id)unused
{
WKUserContentController *controller = [[[self wkWebView] configuration] userContentController];
[controller removeAllUserScripts];
self.hasAddedTisdkScript = NO;
}

- (void)addScriptMessageHandler:(id)value
{
ENSURE_SINGLE_ARG(value, NSString);

WKUserContentController *controller = [[[self wkWebView] configuration] userContentController];

// Enable web pages to use unified API for both Android and iOS as:
// window.tisdk.emit('handlerName', JSON.stringify(body));
if (!self.hasAddedTisdkScript) {
[controller addUserScript:[[self webView] userScriptForMessageHandlerParity]];
self.hasAddedTisdkScript = YES;
}

[controller addScriptMessageHandler:[self webView] name:value];
}

Expand Down
Loading