| /* |
| * 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; |
| |
| /** |
| */ |
| 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; |
| } |
| } |
| } |