/CVox/src/com/cvox/browser/BrowserActivity.java

http://eyes-free.googlecode.com/ · Java · 488 lines · 321 code · 100 blank · 67 comment · 43 complexity · d5846d226b6a686f8ab92c35e3297639 MD5 · raw file

  1. /*
  2. * Copyright (C) 2008 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. package com.cvox.browser;
  17. import java.io.InputStream;
  18. import java.util.Iterator;
  19. import java.util.List;
  20. import java.util.Random;
  21. import java.util.concurrent.Semaphore;
  22. import org.json.JSONArray;
  23. import org.json.JSONException;
  24. import org.json.JSONObject;
  25. import android.app.Activity;
  26. import android.app.AlertDialog;
  27. import android.app.SearchManager;
  28. import android.content.ActivityNotFoundException;
  29. import android.content.DialogInterface;
  30. import android.content.Intent;
  31. import android.content.SharedPreferences;
  32. import android.content.DialogInterface.OnClickListener;
  33. import android.content.SharedPreferences.Editor;
  34. import android.database.Cursor;
  35. import android.graphics.Bitmap;
  36. import android.media.AudioManager;
  37. import android.net.Uri;
  38. import android.os.Bundle;
  39. import android.preference.PreferenceManager;
  40. import android.speech.tts.TextToSpeech;
  41. import android.util.Log;
  42. import android.view.Menu;
  43. import android.view.MenuItem;
  44. import android.view.View;
  45. import android.view.Window;
  46. import android.view.MenuItem.OnMenuItemClickListener;
  47. import android.webkit.JsResult;
  48. import android.webkit.WebChromeClient;
  49. import android.webkit.WebSettings;
  50. import android.webkit.WebView;
  51. import android.webkit.WebViewClient;
  52. import android.widget.FrameLayout;
  53. import android.widget.SimpleCursorAdapter;
  54. import android.widget.Toast;
  55. public class BrowserActivity extends Activity {
  56. public final static String TAG = BrowserActivity.class.toString();
  57. private TextToSpeech mTts;
  58. private WebView webview = null;
  59. private long magickey = new Random().nextLong();
  60. private Semaphore resultWait = new Semaphore(0);
  61. private int resultCode = Activity.RESULT_CANCELED;
  62. private Intent resultData = null;
  63. private ScriptDatabase scriptdb = null;
  64. public final static String LAST_VIEWED = "lastviewed";
  65. public void onCreate(Bundle icicle) {
  66. super.onCreate(icicle);
  67. setVolumeControlStream(AudioManager.STREAM_MUSIC);
  68. mTts = new TextToSpeech(this, null);
  69. requestWindowFeature(Window.FEATURE_PROGRESS);
  70. setContentView(R.layout.act_browse);
  71. scriptdb = new ScriptDatabase(this);
  72. scriptdb.onUpgrade(scriptdb.getWritableDatabase(), -10, 10);
  73. webview = (WebView) findViewById(R.id.browse_webview);
  74. WebSettings settings = webview.getSettings();
  75. settings.setSavePassword(false);
  76. settings.setSaveFormData(false);
  77. settings.setJavaScriptEnabled(true);
  78. settings.setSupportZoom(true);
  79. settings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
  80. FrameLayout zoomholder = (FrameLayout) this.findViewById(R.id.browse_zoom);
  81. zoomholder.addView(webview.getZoomControls());
  82. webview.getZoomControls().setVisibility(View.GONE);
  83. webview.setWebViewClient(new OilCanClient());
  84. webview.setWebChromeClient(new OilCanChrome());
  85. webview.addJavascriptInterface(new IntentHelper(), "intentHelper");
  86. webview.addJavascriptInterface(new TtsHelper(), "ttsHelper");
  87. // load the last-viewed page into browser
  88. String url = "http://www.google.com/search?q=clcworld";
  89. if (icicle != null && icicle.containsKey(LAST_VIEWED)) url = icicle.getString(LAST_VIEWED);
  90. // or watch for incoming requested urls
  91. if (getIntent().getExtras() != null && getIntent().getExtras().containsKey(SearchManager.QUERY))
  92. url = getIntent().getStringExtra(SearchManager.QUERY);
  93. webview.loadUrl(url);
  94. }
  95. private void loadNewPage(String url) {
  96. // reset blocked flag (when implemented) and load new page
  97. webview.loadUrl(url);
  98. }
  99. public void onNewIntent(Intent intent) {
  100. // pull new url from query
  101. String url = intent.getStringExtra(SearchManager.QUERY);
  102. this.loadNewPage(url);
  103. }
  104. protected void onSaveInstanceState(Bundle outState) {
  105. outState.putString(LAST_VIEWED, webview.getUrl());
  106. }
  107. public void onDestroy() {
  108. super.onDestroy();
  109. this.scriptdb.close();
  110. }
  111. public boolean onCreateOptionsMenu(Menu menu) {
  112. super.onCreateOptionsMenu(menu);
  113. MenuItem gourl = menu.add(R.string.browse_gotourl);
  114. gourl.setIcon(R.drawable.ic_menu_goto);
  115. gourl.setOnMenuItemClickListener(new OnMenuItemClickListener() {
  116. public boolean onMenuItemClick(MenuItem item) {
  117. BrowserActivity.this.startSearch(webview.getUrl(), true, null, false);
  118. return true;
  119. }
  120. });
  121. MenuItem refresh = menu.add(R.string.browse_refresh);
  122. refresh.setIcon(R.drawable.ic_menu_refresh);
  123. refresh.setOnMenuItemClickListener(new OnMenuItemClickListener() {
  124. public boolean onMenuItemClick(MenuItem item) {
  125. webview.reload();
  126. return true;
  127. }
  128. });
  129. MenuItem scripts = menu.add(R.string.browse_manage);
  130. scripts.setIcon(android.R.drawable.ic_menu_agenda);
  131. scripts.setIntent(new Intent(BrowserActivity.this, ScriptListActivity.class));
  132. MenuItem example = menu.add(R.string.browse_example);
  133. example.setIcon(R.drawable.ic_menu_bookmark);
  134. example.setOnMenuItemClickListener(new OnMenuItemClickListener() {
  135. public boolean onMenuItemClick(MenuItem item) {
  136. final String[] examples =
  137. BrowserActivity.this.getResources().getStringArray(R.array.list_examples);
  138. new AlertDialog.Builder(BrowserActivity.this).setTitle(R.string.browse_example_title)
  139. .setItems(examples, new OnClickListener() {
  140. public void onClick(DialogInterface dialog, int which) {
  141. BrowserActivity.this.loadNewPage(examples[which]);
  142. }
  143. }).create().show();
  144. return true;
  145. }
  146. });
  147. return true;
  148. }
  149. /**
  150. * Pass a resulting intent down to the waiting script call.
  151. */
  152. protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  153. this.resultCode = resultCode;
  154. this.resultData = data;
  155. this.resultWait.release();
  156. }
  157. public final static String MATCH = "intentHelper.startActivity(",
  158. MATCH_RESULT = "intentHelper.startActivityForResult(";
  159. /**
  160. * Prepare the given script for execution, specifically by injecting our magic
  161. * key for any {@link IntentHelper} calls.
  162. */
  163. private String prepareScript(String script) {
  164. String jsonlib = "";
  165. try {
  166. jsonlib = Util.getRawString(getResources(), R.raw.json2);
  167. } catch (Exception e) {
  168. Log.e(TAG, "Problem loading raw json library", e);
  169. }
  170. script = String.format("javascript:(function() { %s %s })();", jsonlib, script);
  171. script = script.replace(MATCH, String.format("%s'%d',", MATCH, magickey));
  172. script = script.replace(MATCH_RESULT, String.format("%s'%d',", MATCH_RESULT, magickey));
  173. return script;
  174. }
  175. /**
  176. * Javascript bridge to help launch intents and return results. Any callers
  177. * will need to provide the "magic key" to help protect against intent calls
  178. * from non-injected code.
  179. *
  180. * @author jsharkey
  181. */
  182. final class IntentHelper {
  183. /**
  184. * Resolve Intent constants, like Intent.ACTION_PICK
  185. */
  186. private String getConstant(String key) {
  187. try {
  188. key = (String) Intent.class.getField(key).get(null);
  189. } catch (Exception e) {
  190. }
  191. return key;
  192. }
  193. /**
  194. * Parse the given JSON string into an Intent. This would be a good place to
  195. * add security checks in the future.
  196. */
  197. private Intent parse(String jsonraw) {
  198. Intent intent = new Intent();
  199. Log.d(TAG, String.format("parse(jsonraw=%s)", jsonraw));
  200. try {
  201. JSONObject json = new JSONObject(jsonraw);
  202. // look for specific known variables, otherwise assume extras
  203. // {"action":"ACTION_PICK","category":["CATEGORY_DEFAULT"],"type":"image/*"}
  204. Iterator keys = json.keys();
  205. while (keys.hasNext()) {
  206. String key = (String) keys.next();
  207. if ("action".equals(key)) {
  208. intent.setAction(getConstant(json.optString(key)));
  209. } else if ("category".equals(key)) {
  210. JSONArray categ = json.optJSONArray(key);
  211. for (int i = 0; i < categ.length(); i++)
  212. intent.addCategory(getConstant(categ.optString(i)));
  213. } else if ("type".equals(key)) {
  214. intent.setType(json.optString(key));
  215. } else if ("data".equals(key)) {
  216. intent.setData(Uri.parse(json.optString(key)));
  217. } else if ("class".equals(key)) {
  218. intent.setClassName(BrowserActivity.this, json.optString(key));
  219. } else {
  220. // first try parsing extra as number, otherwise fallback to string
  221. Object obj = json.get(key);
  222. if (obj instanceof Integer)
  223. intent.putExtra(getConstant(key), json.optInt(key));
  224. else if (obj instanceof Double)
  225. intent.putExtra(getConstant(key), (float) json.optDouble(key));
  226. else
  227. intent.putExtra(getConstant(key), json.optString(key));
  228. }
  229. }
  230. } catch (Exception e) {
  231. Log.e(TAG, "Problem while parsing JSON into Intent", e);
  232. intent = null;
  233. }
  234. return intent;
  235. }
  236. /**
  237. * Launch the intent described by JSON. Will only launch if magic key
  238. * matches for this browser instance.
  239. */
  240. public void startActivity(String trykey, String json) {
  241. if (magickey != Long.parseLong(trykey)) {
  242. Log.e(TAG, "Magic key from caller doesn't match, so we might have a malicious caller.");
  243. return;
  244. }
  245. Intent intent = parse(json);
  246. if (intent == null) return;
  247. try {
  248. BrowserActivity.this.startActivity(intent);
  249. } catch (ActivityNotFoundException e) {
  250. Log.e(TAG, "Couldn't find activity to handle the requested intent", e);
  251. Toast.makeText(BrowserActivity.this, R.string.browse_nointent, Toast.LENGTH_SHORT).show();
  252. }
  253. }
  254. /**
  255. * Launch the intent described by JSON and block until result is returned.
  256. * Will package and return the result as a JSON string. Will only launch if
  257. * the magic key matches for this browser instance.
  258. */
  259. public String startActivityForResult(String trykey, String json) {
  260. if (magickey != Long.parseLong(trykey)) {
  261. Log.e(TAG, "Magic key from caller doesn't match, so we might have a malicous caller.");
  262. return null;
  263. }
  264. Intent intent = parse(json);
  265. if (intent == null) return null;
  266. resultCode = Activity.RESULT_CANCELED;
  267. // start this intent and wait for result
  268. synchronized (this) {
  269. try {
  270. BrowserActivity.this.startActivityForResult(intent, 1);
  271. resultWait.acquire();
  272. } catch (ActivityNotFoundException e) {
  273. Log.e(TAG, "Couldn't find activity to handle the requested intent", e);
  274. Toast.makeText(BrowserActivity.this, R.string.browse_nointent, Toast.LENGTH_SHORT).show();
  275. } catch (Exception e) {
  276. Log.e(TAG, "Problem while waiting for activity result", e);
  277. }
  278. }
  279. JSONObject result = new JSONObject();
  280. result.optInt("resultCode", resultCode);
  281. // parse our response into json before handing back
  282. if (resultCode == Activity.RESULT_OK) {
  283. if (resultData.getExtras() != null) {
  284. try {
  285. JSONObject extras = new JSONObject();
  286. for (String key : resultData.getExtras().keySet())
  287. extras.put(key, resultData.getExtras().get(key));
  288. result.put("extras", extras);
  289. } catch (JSONException e1) {
  290. Log.e(TAG, "Problem while parsing extras", e1);
  291. }
  292. }
  293. if (resultData.getData() != null) {
  294. try {
  295. // assume that we are handling one contentresolver response
  296. Cursor cur = managedQuery(resultData.getData(), null, null, null, null);
  297. cur.moveToFirst();
  298. JSONObject data = new JSONObject();
  299. for (int i = 0; i < cur.getColumnCount(); i++)
  300. data.put(cur.getColumnName(i), cur.getString(i));
  301. result.put("data", data);
  302. } catch (Exception e) {
  303. Log.e(TAG, "Problem while parsing data result", e);
  304. }
  305. }
  306. }
  307. String resultraw = result.toString();
  308. Log.d(TAG, String.format("startActivityForResult() result=%s", resultraw));
  309. return resultraw;
  310. }
  311. }
  312. final class TtsHelper {
  313. public void speak(String message, int queueMode) {
  314. mTts.speak(message, queueMode, null);
  315. }
  316. public boolean isSpeaking() {
  317. return mTts.isSpeaking();
  318. }
  319. public int stop() {
  320. return mTts.stop();
  321. }
  322. }
  323. final class OilCanChrome extends WebChromeClient {
  324. public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
  325. new AlertDialog.Builder(BrowserActivity.this).setMessage(message).setPositiveButton(
  326. android.R.string.ok, null).create().show();
  327. result.confirm();
  328. return true;
  329. }
  330. public void onProgressChanged(WebView view, int newProgress) {
  331. BrowserActivity.this.setProgress(newProgress * 100);
  332. }
  333. public void onReceivedTitle(WebView view, String title) {
  334. BrowserActivity.this.setTitle(BrowserActivity.this.getString(R.string.browse_title, title));
  335. }
  336. };
  337. public final static String USERSCRIPT_EXTENSION = ".user.js";
  338. final class OilCanClient extends WebViewClient {
  339. /**
  340. * Watch each newly loaded page for userscript extensions (.user.js) to
  341. * prompt user with install helper.
  342. */
  343. public void onPageStarted(WebView view, final String url, Bitmap favicon) {
  344. // if url matches userscript extension, launch installer helper dialog
  345. if (url.endsWith(BrowserActivity.USERSCRIPT_EXTENSION)) {
  346. new AlertDialog.Builder(BrowserActivity.this).setTitle(R.string.install_title).setMessage(
  347. getString(R.string.install_message, url)).setPositiveButton(android.R.string.ok,
  348. new OnClickListener() {
  349. public void onClick(DialogInterface dialog, int which) {
  350. try {
  351. String raw = Util.getUrlString(url);
  352. scriptdb.insertScript(null, raw);
  353. Toast.makeText(BrowserActivity.this, R.string.manage_import_success,
  354. Toast.LENGTH_SHORT).show();
  355. } catch (Exception e) {
  356. Log.e(TAG, "Problem while trying to import script", e);
  357. Toast.makeText(BrowserActivity.this, R.string.manage_import_fail,
  358. Toast.LENGTH_SHORT).show();
  359. }
  360. }
  361. }).setNegativeButton(android.R.string.cancel, null).create().show();
  362. }
  363. }
  364. /**
  365. * Handle finished loading of each page. Specifically this checks for any
  366. * active scripts based on the URL. When found a matching site, we inject
  367. * the JSON library and the applicable script.
  368. */
  369. public void onPageFinished(WebView view, String url) {
  370. if (scriptdb == null) {
  371. Log.e(TAG, "ScriptDatabase wasn't ready for finished page");
  372. return;
  373. }
  374. // for each finished page, try looking for active scripts
  375. List<String> active = scriptdb.getActive(url);
  376. Log.d(TAG, String.format("Found %d active scripts on url=%s", active.size(), url));
  377. if (active.size() == 0) return;
  378. // inject each applicable script into page
  379. for (String script : active) {
  380. script = BrowserActivity.this.prepareScript(script);
  381. webview.loadUrl(script);
  382. }
  383. }
  384. public boolean shouldOverrideUrlLoading(WebView view, String url) {
  385. return false;
  386. }
  387. }
  388. }