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