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