Home | History | Annotate | Download | only in html
      1 /*
      2  * Copyright (C) 2016 Google Inc.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
      5  * use this file except in compliance with the License. You may obtain a copy of
      6  * the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
     12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
     13  * License for the specific language governing permissions and limitations under
     14  * the License.
     15  */
     16 
     17 package com.googlecode.android_scripting.interpreter.html;
     18 
     19 import android.app.Activity;
     20 import android.content.res.Resources;
     21 import android.graphics.Bitmap;
     22 import android.graphics.drawable.BitmapDrawable;
     23 import android.net.Uri;
     24 import android.view.ContextMenu;
     25 import android.view.ContextMenu.ContextMenuInfo;
     26 import android.view.Menu;
     27 import android.view.View;
     28 import android.view.Window;
     29 import android.webkit.JsPromptResult;
     30 import android.webkit.JsResult;
     31 import android.webkit.WebChromeClient;
     32 import android.webkit.WebView;
     33 import android.webkit.WebViewClient;
     34 
     35 import com.googlecode.android_scripting.FileUtils;
     36 import com.googlecode.android_scripting.Log;
     37 import com.googlecode.android_scripting.SingleThreadExecutor;
     38 import com.googlecode.android_scripting.event.Event;
     39 import com.googlecode.android_scripting.event.EventObserver;
     40 import com.googlecode.android_scripting.facade.EventFacade;
     41 import com.googlecode.android_scripting.facade.ui.UiFacade;
     42 import com.googlecode.android_scripting.future.FutureActivityTask;
     43 import com.googlecode.android_scripting.interpreter.InterpreterConstants;
     44 import com.googlecode.android_scripting.jsonrpc.JsonBuilder;
     45 import com.googlecode.android_scripting.jsonrpc.JsonRpcResult;
     46 import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
     47 import com.googlecode.android_scripting.jsonrpc.RpcReceiverManager;
     48 import com.googlecode.android_scripting.rpc.MethodDescriptor;
     49 import com.googlecode.android_scripting.rpc.RpcError;
     50 
     51 import java.io.File;
     52 import java.io.IOException;
     53 import java.util.HashMap;
     54 import java.util.HashSet;
     55 import java.util.Map;
     56 import java.util.Set;
     57 import java.util.concurrent.ExecutorService;
     58 
     59 import org.json.JSONArray;
     60 import org.json.JSONException;
     61 import org.json.JSONObject;
     62 
     63 /**
     64  * @author Alexey Reznichenko (alexey.reznichenko (at) gmail.com)
     65  */
     66 public class HtmlActivityTask extends FutureActivityTask<Void> {
     67 
     68   private static final String HTTP = "http";
     69   private static final String ANDROID_PROTOTYPE_JS =
     70       "Android.prototype.%1$s = function(var_args) { "
     71           + "return this._call(\"%1$s\", Array.prototype.slice.call(arguments)); };";
     72 
     73   private static final String PREFIX = "file://";
     74   private static final String BASE_URL = PREFIX + InterpreterConstants.SCRIPTS_ROOT;
     75 
     76   private final RpcReceiverManager mReceiverManager;
     77   private final String mJsonSource;
     78   private final String mAndroidJsSource;
     79   private final String mAPIWrapperSource;
     80   private final String mUrl;
     81   private final JavaScriptWrapper mWrapper;
     82   private final HtmlEventObserver mObserver;
     83   private final UiFacade mUiFacade;
     84   private ChromeClient mChromeClient;
     85   private WebView mView;
     86   private MyWebViewClient mWebViewClient;
     87   private static HtmlActivityTask reference;
     88   private boolean mDestroyManager;
     89 
     90   public HtmlActivityTask(RpcReceiverManager manager, String androidJsSource, String jsonSource,
     91       String url, boolean destroyManager) {
     92     reference = this;
     93     mReceiverManager = manager;
     94     mJsonSource = jsonSource;
     95     mAndroidJsSource = androidJsSource;
     96     mAPIWrapperSource = generateAPIWrapper();
     97     mWrapper = new JavaScriptWrapper();
     98     mObserver = new HtmlEventObserver();
     99     mReceiverManager.getReceiver(EventFacade.class).addGlobalEventObserver(mObserver);
    100     mUiFacade = mReceiverManager.getReceiver(UiFacade.class);
    101     mUrl = url;
    102     mDestroyManager = destroyManager;
    103   }
    104 
    105   public RpcReceiverManager getRpcReceiverManager() {
    106     return mReceiverManager;
    107   }
    108 
    109   /*
    110    * New WebviewClient
    111    */
    112   private class MyWebViewClient extends WebViewClient {
    113     @Override
    114     public boolean shouldOverrideUrlLoading(WebView view, String url) {
    115       /*
    116        * if (Uri.parse(url).getHost().equals("www.example.com")) {
    117        * // This is my web site, so do not
    118        * override; let my WebView load the page return false; }
    119        * // Otherwise, the link is not for a
    120        * page on my site, so launch another Activity that handles URLs Intent intent = new
    121        * Intent(Intent.ACTION_VIEW, Uri.parse(url)); startActivity(intent);
    122        */
    123       if (!HTTP.equals(Uri.parse(url).getScheme())) {
    124         String source = null;
    125         try {
    126           source = FileUtils.readToString(new File(Uri.parse(url).getPath()));
    127         } catch (IOException e) {
    128           throw new RuntimeException(e);
    129         }
    130         source =
    131             "<script>" + mJsonSource + "</script>" + "<script>" + mAndroidJsSource + "</script>"
    132                 + "<script>" + mAPIWrapperSource + "</script>" + source;
    133         mView.loadDataWithBaseURL(BASE_URL, source, "text/html", "utf-8", null);
    134       } else {
    135         mView.loadUrl(url);
    136       }
    137       return true;
    138     }
    139   }
    140 
    141   @Override
    142   public void onCreate() {
    143     mView = new WebView(getActivity());
    144     mView.setId(1);
    145     mView.getSettings().setJavaScriptEnabled(true);
    146     mView.addJavascriptInterface(mWrapper, "_rpc_wrapper");
    147     mView.addJavascriptInterface(new Object() {
    148 
    149       @SuppressWarnings("unused")
    150       public void register(String event, int id) {
    151         mObserver.register(event, id);
    152       }
    153     }, "_callback_wrapper");
    154 
    155     getActivity().setContentView(mView);
    156     mView.setOnCreateContextMenuListener(getActivity());
    157     mChromeClient = new ChromeClient(getActivity());
    158     mWebViewClient = new MyWebViewClient();
    159     mView.setWebChromeClient(mChromeClient);
    160     mView.setWebViewClient(mWebViewClient);
    161     mView.loadUrl("javascript:" + mJsonSource);
    162     mView.loadUrl("javascript:" + mAndroidJsSource);
    163     mView.loadUrl("javascript:" + mAPIWrapperSource);
    164     load();
    165   }
    166 
    167   private void load() {
    168     if (!HTTP.equals(Uri.parse(mUrl).getScheme())) {
    169       String source = null;
    170       try {
    171         source = FileUtils.readToString(new File(Uri.parse(mUrl).getPath()));
    172       } catch (IOException e) {
    173         throw new RuntimeException(e);
    174       }
    175       mView.loadDataWithBaseURL(BASE_URL, source, "text/html", "utf-8", null);
    176     } else {
    177       mView.loadUrl(mUrl);
    178     }
    179   }
    180 
    181   @Override
    182   public void onDestroy() {
    183     mReceiverManager.getReceiver(EventFacade.class).removeEventObserver(mObserver);
    184     if (mDestroyManager) {
    185       mReceiverManager.shutdown();
    186     }
    187     mView.destroy();
    188     mView = null;
    189     reference = null;
    190     setResult(null);
    191   }
    192 
    193   public static void shutdown() {
    194     if (HtmlActivityTask.reference != null) {
    195       HtmlActivityTask.reference.finish();
    196     }
    197   }
    198 
    199   @Override
    200   public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
    201     mUiFacade.onCreateContextMenu(menu, v, menuInfo);
    202   }
    203 
    204   @Override
    205   public boolean onPrepareOptionsMenu(Menu menu) {
    206     return mUiFacade.onPrepareOptionsMenu(menu);
    207   }
    208 
    209   private String generateAPIWrapper() {
    210     StringBuilder wrapper = new StringBuilder();
    211     for (Class<? extends RpcReceiver> clazz : mReceiverManager.getRpcReceiverClasses()) {
    212       for (MethodDescriptor rpc : MethodDescriptor.collectFrom(clazz)) {
    213         wrapper.append(String.format(ANDROID_PROTOTYPE_JS, rpc.getName()));
    214       }
    215     }
    216     return wrapper.toString();
    217   }
    218 
    219   private class JavaScriptWrapper {
    220     @SuppressWarnings("unused")
    221     public String call(String data) throws JSONException {
    222       Log.v("Received: " + data);
    223       JSONObject request = new JSONObject(data);
    224       int id = request.getInt("id");
    225       String method = request.getString("method");
    226       JSONArray params = request.getJSONArray("params");
    227       MethodDescriptor rpc = mReceiverManager.getMethodDescriptor(method);
    228       if (rpc == null) {
    229         return JsonRpcResult.error(id, new RpcError("Unknown RPC.")).toString();
    230       }
    231       try {
    232         return JsonRpcResult.result(id, rpc.invoke(mReceiverManager, params)).toString();
    233       } catch (Throwable t) {
    234         Log.e("Invocation error.", t);
    235         return JsonRpcResult.error(id, t).toString();
    236       }
    237     }
    238 
    239     @SuppressWarnings("unused")
    240     public void dismiss() {
    241       Activity parent = getActivity();
    242       parent.finish();
    243     }
    244   }
    245 
    246   private class HtmlEventObserver implements EventObserver {
    247     private Map<String, Set<Integer>> mEventMap = new HashMap<String, Set<Integer>>();
    248 
    249     public void register(String eventName, Integer id) {
    250       if (mEventMap.containsKey(eventName)) {
    251         mEventMap.get(eventName).add(id);
    252       } else {
    253         Set<Integer> idSet = new HashSet<Integer>();
    254         idSet.add(id);
    255         mEventMap.put(eventName, idSet);
    256       }
    257     }
    258 
    259     @Override
    260     public void onEventReceived(Event event) {
    261       final JSONObject json = new JSONObject();
    262       try {
    263         json.put("data", JsonBuilder.build(event.getData()));
    264       } catch (JSONException e) {
    265         Log.e(e);
    266       }
    267       if (mEventMap.containsKey(event.getName())) {
    268         for (final Integer id : mEventMap.get(event.getName())) {
    269           getActivity().runOnUiThread(new Runnable() {
    270             @Override
    271             public void run() {
    272               mView.loadUrl(String.format("javascript:droid._callback(%d, %s);", id, json));
    273             }
    274           });
    275         }
    276       }
    277     }
    278 
    279     @SuppressWarnings("unused")
    280     public void dismiss() {
    281       Activity parent = getActivity();
    282       parent.finish();
    283     }
    284   }
    285 
    286   private class ChromeClient extends WebChromeClient {
    287     private final static String JS_TITLE = "JavaScript Dialog";
    288 
    289     private final Activity mActivity;
    290     private final Resources mResources;
    291     private final ExecutorService mmExecutor;
    292 
    293     public ChromeClient(Activity activity) {
    294       mActivity = activity;
    295       mResources = mActivity.getResources();
    296       mmExecutor = new SingleThreadExecutor();
    297     }
    298 
    299     @Override
    300     public void onReceivedTitle(WebView view, String title) {
    301       mActivity.setTitle(title);
    302     }
    303 
    304     @Override
    305     public void onReceivedIcon(WebView view, Bitmap icon) {
    306       mActivity.getWindow().requestFeature(Window.FEATURE_RIGHT_ICON);
    307       mActivity.getWindow().setFeatureDrawable(Window.FEATURE_RIGHT_ICON,
    308                                                new BitmapDrawable(mActivity.getResources(), icon));
    309     }
    310 
    311     @Override
    312     public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
    313       final UiFacade uiFacade = mReceiverManager.getReceiver(UiFacade.class);
    314       uiFacade.dialogCreateAlert(JS_TITLE, message);
    315       uiFacade.dialogSetPositiveButtonText(mResources.getString(android.R.string.ok));
    316 
    317       mmExecutor.execute(new Runnable() {
    318 
    319         @Override
    320         public void run() {
    321           try {
    322             uiFacade.dialogShow();
    323           } catch (InterruptedException e) {
    324             throw new RuntimeException(e);
    325           }
    326           uiFacade.dialogGetResponse();
    327           result.confirm();
    328         }
    329       });
    330       return true;
    331     }
    332 
    333     @SuppressWarnings("unchecked")
    334     @Override
    335     public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {
    336       final UiFacade uiFacade = mReceiverManager.getReceiver(UiFacade.class);
    337       uiFacade.dialogCreateAlert(JS_TITLE, message);
    338       uiFacade.dialogSetPositiveButtonText(mResources.getString(android.R.string.ok));
    339       uiFacade.dialogSetNegativeButtonText(mResources.getString(android.R.string.cancel));
    340 
    341       mmExecutor.execute(new Runnable() {
    342 
    343         @Override
    344         public void run() {
    345           try {
    346             uiFacade.dialogShow();
    347           } catch (InterruptedException e) {
    348             throw new RuntimeException(e);
    349           }
    350           Map<String, Object> mResultMap = (Map<String, Object>) uiFacade.dialogGetResponse();
    351           if ("positive".equals(mResultMap.get("which"))) {
    352             result.confirm();
    353           } else {
    354             result.cancel();
    355           }
    356         }
    357       });
    358 
    359       return true;
    360     }
    361 
    362     @Override
    363     public boolean onJsPrompt(WebView view, String url, final String message,
    364         final String defaultValue, final JsPromptResult result) {
    365       final UiFacade uiFacade = mReceiverManager.getReceiver(UiFacade.class);
    366       mmExecutor.execute(new Runnable() {
    367         @Override
    368         public void run() {
    369           String value = null;
    370           try {
    371             value = uiFacade.dialogGetInput(JS_TITLE, message, defaultValue);
    372           } catch (InterruptedException e) {
    373             throw new RuntimeException(e);
    374           }
    375           if (value != null) {
    376             result.confirm(value);
    377           } else {
    378             result.cancel();
    379           }
    380         }
    381       });
    382       return true;
    383     }
    384   }
    385 }
    386