PageRenderTime 53ms CodeModel.GetById 12ms app.highlight 36ms RepoModel.GetById 1ms app.codeStats 0ms

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