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@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 
HtmlActivityTask(RpcReceiverManager manager, String androidJsSource, String jsonSource, String url, boolean destroyManager)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 
getRpcReceiverManager()105   public RpcReceiverManager getRpcReceiverManager() {
106     return mReceiverManager;
107   }
108 
109   /*
110    * New WebviewClient
111    */
112   private class MyWebViewClient extends WebViewClient {
113     @Override
shouldOverrideUrlLoading(WebView view, String url)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
onCreate()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 
load()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
onDestroy()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 
shutdown()193   public static void shutdown() {
194     if (HtmlActivityTask.reference != null) {
195       HtmlActivityTask.reference.finish();
196     }
197   }
198 
199   @Override
onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo)200   public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
201     mUiFacade.onCreateContextMenu(menu, v, menuInfo);
202   }
203 
204   @Override
onPrepareOptionsMenu(Menu menu)205   public boolean onPrepareOptionsMenu(Menu menu) {
206     return mUiFacade.onPrepareOptionsMenu(menu);
207   }
208 
generateAPIWrapper()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")
call(String data)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")
dismiss()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 
register(String eventName, Integer id)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
onEventReceived(Event event)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")
dismiss()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 
ChromeClient(Activity activity)293     public ChromeClient(Activity activity) {
294       mActivity = activity;
295       mResources = mActivity.getResources();
296       mmExecutor = new SingleThreadExecutor();
297     }
298 
299     @Override
onReceivedTitle(WebView view, String title)300     public void onReceivedTitle(WebView view, String title) {
301       mActivity.setTitle(title);
302     }
303 
304     @Override
onReceivedIcon(WebView view, Bitmap icon)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
onJsAlert(WebView view, String url, String message, final JsResult result)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
onJsConfirm(WebView view, String url, String message, final JsResult result)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
onJsPrompt(WebView view, String url, final String message, final String defaultValue, final JsPromptResult result)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