blob: fa5d768dbf28066abf422809ed187eb58f78c169 [file] [log] [blame]
/*
* Copyright (C) 2016 Google Inc.
*
* 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.
*/
package com.googlecode.android_scripting.interpreter.html;
import android.app.Activity;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.Menu;
import android.view.View;
import android.view.Window;
import android.webkit.JsPromptResult;
import android.webkit.JsResult;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import com.googlecode.android_scripting.FileUtils;
import com.googlecode.android_scripting.Log;
import com.googlecode.android_scripting.SingleThreadExecutor;
import com.googlecode.android_scripting.event.Event;
import com.googlecode.android_scripting.event.EventObserver;
import com.googlecode.android_scripting.facade.EventFacade;
import com.googlecode.android_scripting.facade.ui.UiFacade;
import com.googlecode.android_scripting.future.FutureActivityTask;
import com.googlecode.android_scripting.interpreter.InterpreterConstants;
import com.googlecode.android_scripting.jsonrpc.JsonBuilder;
import com.googlecode.android_scripting.jsonrpc.JsonRpcResult;
import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
import com.googlecode.android_scripting.jsonrpc.RpcReceiverManager;
import com.googlecode.android_scripting.rpc.MethodDescriptor;
import com.googlecode.android_scripting.rpc.RpcError;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
/**
* @author Alexey Reznichenko (alexey.reznichenko@gmail.com)
*/
public class HtmlActivityTask extends FutureActivityTask<Void> {
private static final String HTTP = "http";
private static final String ANDROID_PROTOTYPE_JS =
"Android.prototype.%1$s = function(var_args) { "
+ "return this._call(\"%1$s\", Array.prototype.slice.call(arguments)); };";
private static final String PREFIX = "file://";
private static final String BASE_URL = PREFIX + InterpreterConstants.SCRIPTS_ROOT;
private final RpcReceiverManager mReceiverManager;
private final String mJsonSource;
private final String mAndroidJsSource;
private final String mAPIWrapperSource;
private final String mUrl;
private final JavaScriptWrapper mWrapper;
private final HtmlEventObserver mObserver;
private final UiFacade mUiFacade;
private ChromeClient mChromeClient;
private WebView mView;
private MyWebViewClient mWebViewClient;
private static HtmlActivityTask reference;
private boolean mDestroyManager;
public HtmlActivityTask(RpcReceiverManager manager, String androidJsSource, String jsonSource,
String url, boolean destroyManager) {
reference = this;
mReceiverManager = manager;
mJsonSource = jsonSource;
mAndroidJsSource = androidJsSource;
mAPIWrapperSource = generateAPIWrapper();
mWrapper = new JavaScriptWrapper();
mObserver = new HtmlEventObserver();
mReceiverManager.getReceiver(EventFacade.class).addGlobalEventObserver(mObserver);
mUiFacade = mReceiverManager.getReceiver(UiFacade.class);
mUrl = url;
mDestroyManager = destroyManager;
}
public RpcReceiverManager getRpcReceiverManager() {
return mReceiverManager;
}
/*
* New WebviewClient
*/
private class MyWebViewClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
/*
* if (Uri.parse(url).getHost().equals("www.example.com")) {
* // This is my web site, so do not
* override; let my WebView load the page return false; }
* // Otherwise, the link is not for a
* page on my site, so launch another Activity that handles URLs Intent intent = new
* Intent(Intent.ACTION_VIEW, Uri.parse(url)); startActivity(intent);
*/
if (!HTTP.equals(Uri.parse(url).getScheme())) {
String source = null;
try {
source = FileUtils.readToString(new File(Uri.parse(url).getPath()));
} catch (IOException e) {
throw new RuntimeException(e);
}
source =
"<script>" + mJsonSource + "</script>" + "<script>" + mAndroidJsSource + "</script>"
+ "<script>" + mAPIWrapperSource + "</script>" + source;
mView.loadDataWithBaseURL(BASE_URL, source, "text/html", "utf-8", null);
} else {
mView.loadUrl(url);
}
return true;
}
}
@Override
public void onCreate() {
mView = new WebView(getActivity());
mView.setId(1);
mView.getSettings().setJavaScriptEnabled(true);
mView.addJavascriptInterface(mWrapper, "_rpc_wrapper");
mView.addJavascriptInterface(new Object() {
@SuppressWarnings("unused")
public void register(String event, int id) {
mObserver.register(event, id);
}
}, "_callback_wrapper");
getActivity().setContentView(mView);
mView.setOnCreateContextMenuListener(getActivity());
mChromeClient = new ChromeClient(getActivity());
mWebViewClient = new MyWebViewClient();
mView.setWebChromeClient(mChromeClient);
mView.setWebViewClient(mWebViewClient);
mView.loadUrl("javascript:" + mJsonSource);
mView.loadUrl("javascript:" + mAndroidJsSource);
mView.loadUrl("javascript:" + mAPIWrapperSource);
load();
}
private void load() {
if (!HTTP.equals(Uri.parse(mUrl).getScheme())) {
String source = null;
try {
source = FileUtils.readToString(new File(Uri.parse(mUrl).getPath()));
} catch (IOException e) {
throw new RuntimeException(e);
}
mView.loadDataWithBaseURL(BASE_URL, source, "text/html", "utf-8", null);
} else {
mView.loadUrl(mUrl);
}
}
@Override
public void onDestroy() {
mReceiverManager.getReceiver(EventFacade.class).removeEventObserver(mObserver);
if (mDestroyManager) {
mReceiverManager.shutdown();
}
mView.destroy();
mView = null;
reference = null;
setResult(null);
}
public static void shutdown() {
if (HtmlActivityTask.reference != null) {
HtmlActivityTask.reference.finish();
}
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
mUiFacade.onCreateContextMenu(menu, v, menuInfo);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
return mUiFacade.onPrepareOptionsMenu(menu);
}
private String generateAPIWrapper() {
StringBuilder wrapper = new StringBuilder();
for (Class<? extends RpcReceiver> clazz : mReceiverManager.getRpcReceiverClasses()) {
for (MethodDescriptor rpc : MethodDescriptor.collectFrom(clazz)) {
wrapper.append(String.format(ANDROID_PROTOTYPE_JS, rpc.getName()));
}
}
return wrapper.toString();
}
private class JavaScriptWrapper {
@SuppressWarnings("unused")
public String call(String data) throws JSONException {
Log.v("Received: " + data);
JSONObject request = new JSONObject(data);
int id = request.getInt("id");
String method = request.getString("method");
JSONArray params = request.getJSONArray("params");
MethodDescriptor rpc = mReceiverManager.getMethodDescriptor(method);
if (rpc == null) {
return JsonRpcResult.error(id, new RpcError("Unknown RPC.")).toString();
}
try {
return JsonRpcResult.result(id, rpc.invoke(mReceiverManager, params)).toString();
} catch (Throwable t) {
Log.e("Invocation error.", t);
return JsonRpcResult.error(id, t).toString();
}
}
@SuppressWarnings("unused")
public void dismiss() {
Activity parent = getActivity();
parent.finish();
}
}
private class HtmlEventObserver implements EventObserver {
private Map<String, Set<Integer>> mEventMap = new HashMap<String, Set<Integer>>();
public void register(String eventName, Integer id) {
if (mEventMap.containsKey(eventName)) {
mEventMap.get(eventName).add(id);
} else {
Set<Integer> idSet = new HashSet<Integer>();
idSet.add(id);
mEventMap.put(eventName, idSet);
}
}
@Override
public void onEventReceived(Event event) {
final JSONObject json = new JSONObject();
try {
json.put("data", JsonBuilder.build(event.getData()));
} catch (JSONException e) {
Log.e(e);
}
if (mEventMap.containsKey(event.getName())) {
for (final Integer id : mEventMap.get(event.getName())) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
mView.loadUrl(String.format("javascript:droid._callback(%d, %s);", id, json));
}
});
}
}
}
@SuppressWarnings("unused")
public void dismiss() {
Activity parent = getActivity();
parent.finish();
}
}
private class ChromeClient extends WebChromeClient {
private final static String JS_TITLE = "JavaScript Dialog";
private final Activity mActivity;
private final Resources mResources;
private final ExecutorService mmExecutor;
public ChromeClient(Activity activity) {
mActivity = activity;
mResources = mActivity.getResources();
mmExecutor = new SingleThreadExecutor();
}
@Override
public void onReceivedTitle(WebView view, String title) {
mActivity.setTitle(title);
}
@Override
public void onReceivedIcon(WebView view, Bitmap icon) {
mActivity.getWindow().requestFeature(Window.FEATURE_RIGHT_ICON);
mActivity.getWindow().setFeatureDrawable(Window.FEATURE_RIGHT_ICON,
new BitmapDrawable(mActivity.getResources(), icon));
}
@Override
public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
final UiFacade uiFacade = mReceiverManager.getReceiver(UiFacade.class);
uiFacade.dialogCreateAlert(JS_TITLE, message);
uiFacade.dialogSetPositiveButtonText(mResources.getString(android.R.string.ok));
mmExecutor.execute(new Runnable() {
@Override
public void run() {
try {
uiFacade.dialogShow();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
uiFacade.dialogGetResponse();
result.confirm();
}
});
return true;
}
@SuppressWarnings("unchecked")
@Override
public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {
final UiFacade uiFacade = mReceiverManager.getReceiver(UiFacade.class);
uiFacade.dialogCreateAlert(JS_TITLE, message);
uiFacade.dialogSetPositiveButtonText(mResources.getString(android.R.string.ok));
uiFacade.dialogSetNegativeButtonText(mResources.getString(android.R.string.cancel));
mmExecutor.execute(new Runnable() {
@Override
public void run() {
try {
uiFacade.dialogShow();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Map<String, Object> mResultMap = (Map<String, Object>) uiFacade.dialogGetResponse();
if ("positive".equals(mResultMap.get("which"))) {
result.confirm();
} else {
result.cancel();
}
}
});
return true;
}
@Override
public boolean onJsPrompt(WebView view, String url, final String message,
final String defaultValue, final JsPromptResult result) {
final UiFacade uiFacade = mReceiverManager.getReceiver(UiFacade.class);
mmExecutor.execute(new Runnable() {
@Override
public void run() {
String value = null;
try {
value = uiFacade.dialogGetInput(JS_TITLE, message, defaultValue);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (value != null) {
result.confirm(value);
} else {
result.cancel();
}
}
});
return true;
}
}
}