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