org.jsharkey.oilcan.BrowserActivity.java Source code

Java tutorial

Introduction

Here is the source code for org.jsharkey.oilcan.BrowserActivity.java

Source

/*
   Copyright (C) 2008 Jeffrey Sharkey, http://jsharkey.org/
       
   This program is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.
       
   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.
       
   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package org.jsharkey.oilcan;

import java.io.InputStream;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Semaphore;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.SearchManager;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.DialogInterface.OnClickListener;
import android.content.SharedPreferences.Editor;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.view.MenuItem.OnMenuItemClickListener;
import android.webkit.JsResult;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.FrameLayout;
import android.widget.SimpleCursorAdapter;
import android.widget.Toast;

public class BrowserActivity extends Activity {

    public final static String TAG = BrowserActivity.class.toString();

    private WebView webview = null;

    private long magickey = new Random().nextLong();

    private Semaphore resultWait = new Semaphore(0);
    private int resultCode = Activity.RESULT_CANCELED;
    private Intent resultData = null;

    private ScriptDatabase scriptdb = null;

    public final static String LAST_VIEWED = "lastviewed";

    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);

        requestWindowFeature(Window.FEATURE_PROGRESS);
        setContentView(R.layout.act_browse);

        scriptdb = new ScriptDatabase(this);
        scriptdb.onUpgrade(scriptdb.getWritableDatabase(), -10, 10);

        webview = (WebView) findViewById(R.id.browse_webview);

        WebSettings settings = webview.getSettings();
        settings.setSavePassword(false);
        settings.setSaveFormData(false);
        settings.setJavaScriptEnabled(true);
        settings.setSupportZoom(true);
        settings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);

        FrameLayout zoomholder = (FrameLayout) this.findViewById(R.id.browse_zoom);
        zoomholder.addView(webview.getZoomControls());
        webview.getZoomControls().setVisibility(View.GONE);

        webview.setWebViewClient(new OilCanClient());
        webview.setWebChromeClient(new OilCanChrome());

        webview.addJavascriptInterface(new IntentHelper(), "intentHelper");

        // load the last-viewed page into browser
        String url = "http://m.half.com/";
        if (icicle != null && icicle.containsKey(LAST_VIEWED))
            url = icicle.getString(LAST_VIEWED);

        // or watch for incoming requested urls
        if (getIntent().getExtras() != null && getIntent().getExtras().containsKey(SearchManager.QUERY))
            url = getIntent().getStringExtra(SearchManager.QUERY);

        webview.loadUrl(url);

    }

    private void loadNewPage(String url) {
        // reset blocked flag (when implemented) and load new page
        webview.loadUrl(url);
    }

    public void onNewIntent(Intent intent) {
        // pull new url from query
        String url = intent.getStringExtra(SearchManager.QUERY);
        this.loadNewPage(url);
    }

    protected void onSaveInstanceState(Bundle outState) {
        outState.putString(LAST_VIEWED, webview.getUrl());
    }

    public void onDestroy() {
        super.onDestroy();
        this.scriptdb.close();
    }

    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);

        MenuItem gourl = menu.add(R.string.browse_gotourl);
        gourl.setIcon(R.drawable.ic_menu_goto);
        gourl.setOnMenuItemClickListener(new OnMenuItemClickListener() {
            public boolean onMenuItemClick(MenuItem item) {
                BrowserActivity.this.startSearch(webview.getUrl(), true, null, false);
                return true;
            }
        });

        MenuItem refresh = menu.add(R.string.browse_refresh);
        refresh.setIcon(R.drawable.ic_menu_refresh);
        refresh.setOnMenuItemClickListener(new OnMenuItemClickListener() {
            public boolean onMenuItemClick(MenuItem item) {
                webview.reload();
                return true;
            }
        });

        MenuItem scripts = menu.add(R.string.browse_manage);
        scripts.setIcon(android.R.drawable.ic_menu_agenda);
        scripts.setIntent(new Intent(BrowserActivity.this, ScriptListActivity.class));

        MenuItem example = menu.add(R.string.browse_example);
        example.setIcon(R.drawable.ic_menu_bookmark);
        example.setOnMenuItemClickListener(new OnMenuItemClickListener() {
            public boolean onMenuItemClick(MenuItem item) {
                final String[] examples = BrowserActivity.this.getResources().getStringArray(R.array.list_examples);
                new AlertDialog.Builder(BrowserActivity.this).setTitle(R.string.browse_example_title)
                        .setItems(examples, new OnClickListener() {
                            public void onClick(DialogInterface dialog, int which) {
                                BrowserActivity.this.loadNewPage(examples[which]);
                            }
                        }).create().show();

                return true;
            }
        });

        return true;
    }

    /**
     * Pass a resulting intent down to the waiting script call.
     */
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        this.resultCode = resultCode;
        this.resultData = data;
        this.resultWait.release();
    }

    public final static String MATCH = "intentHelper.startActivity(",
            MATCH_RESULT = "intentHelper.startActivityForResult(";

    /**
     * Prepare the given script for execution, specifically by injecting our
     * magic key for any {@link IntentHelper} calls.
     */
    private String prepareScript(String script) {
        String jsonlib = "";
        try {
            jsonlib = Util.getRawString(getResources(), R.raw.json2);
        } catch (Exception e) {
            Log.e(TAG, "Problem loading raw json library", e);
        }

        script = String.format("javascript:(function() { %s %s })();", jsonlib, script);

        script = script.replace(MATCH, String.format("%s'%d',", MATCH, magickey));
        script = script.replace(MATCH_RESULT, String.format("%s'%d',", MATCH_RESULT, magickey));

        return script;

    }

    /**
     * Javascript bridge to help launch intents and return results. Any callers
     * will need to provide the "magic key" to help protect against intent calls
     * from non-injected code.
     * 
     * @author jsharkey
     */
    final class IntentHelper {

        /**
         * Resolve Intent constants, like Intent.ACTION_PICK
         */
        private String getConstant(String key) {
            try {
                key = (String) Intent.class.getField(key).get(null);
            } catch (Exception e) {
            }
            return key;
        }

        /**
         * Parse the given JSON string into an Intent. This would be a good
         * place to add security checks in the future.
         */
        private Intent parse(String jsonraw) {
            Intent intent = new Intent();

            Log.d(TAG, String.format("parse(jsonraw=%s)", jsonraw));

            try {
                JSONObject json = new JSONObject(jsonraw);

                // look for specific known variables, otherwise assume extras
                // {"action":"ACTION_PICK","category":["CATEGORY_DEFAULT"],"type":"image/*"}

                Iterator keys = json.keys();
                while (keys.hasNext()) {
                    String key = (String) keys.next();

                    if ("action".equals(key)) {
                        intent.setAction(getConstant(json.optString(key)));
                    } else if ("category".equals(key)) {
                        JSONArray categ = json.optJSONArray(key);
                        for (int i = 0; i < categ.length(); i++)
                            intent.addCategory(getConstant(categ.optString(i)));
                    } else if ("type".equals(key)) {
                        intent.setType(json.optString(key));
                    } else if ("data".equals(key)) {
                        intent.setData(Uri.parse(json.optString(key)));
                    } else if ("class".equals(key)) {
                        intent.setClassName(BrowserActivity.this, json.optString(key));
                    } else {
                        // first try parsing extra as number, otherwise fallback to string
                        Object obj = json.get(key);
                        if (obj instanceof Integer)
                            intent.putExtra(getConstant(key), json.optInt(key));
                        else if (obj instanceof Double)
                            intent.putExtra(getConstant(key), (float) json.optDouble(key));
                        else
                            intent.putExtra(getConstant(key), json.optString(key));
                    }

                }

            } catch (Exception e) {
                Log.e(TAG, "Problem while parsing JSON into Intent", e);
                intent = null;
            }

            return intent;
        }

        /**
         * Launch the intent described by JSON. Will only launch if magic key
         * matches for this browser instance.
         */
        public void startActivity(String trykey, String json) {
            if (magickey != Long.parseLong(trykey)) {
                Log.e(TAG, "Magic key from caller doesn't match, so we might have a malicious caller.");
                return;
            }

            Intent intent = parse(json);
            if (intent == null)
                return;

            try {
                BrowserActivity.this.startActivity(intent);
            } catch (ActivityNotFoundException e) {
                Log.e(TAG, "Couldn't find activity to handle the requested intent", e);
                Toast.makeText(BrowserActivity.this, R.string.browse_nointent, Toast.LENGTH_SHORT).show();
            }

        }

        /**
         * Launch the intent described by JSON and block until result is
         * returned. Will package and return the result as a JSON string. Will
         * only launch if the magic key matches for this browser instance.
         */
        public String startActivityForResult(String trykey, String json) {
            if (magickey != Long.parseLong(trykey)) {
                Log.e(TAG, "Magic key from caller doesn't match, so we might have a malicous caller.");
                return null;
            }

            Intent intent = parse(json);
            if (intent == null)
                return null;
            resultCode = Activity.RESULT_CANCELED;

            // start this intent and wait for result
            synchronized (this) {
                try {
                    BrowserActivity.this.startActivityForResult(intent, 1);
                    resultWait.acquire();
                } catch (ActivityNotFoundException e) {
                    Log.e(TAG, "Couldn't find activity to handle the requested intent", e);
                    Toast.makeText(BrowserActivity.this, R.string.browse_nointent, Toast.LENGTH_SHORT).show();
                } catch (Exception e) {
                    Log.e(TAG, "Problem while waiting for activity result", e);
                }
            }

            JSONObject result = new JSONObject();
            result.optInt("resultCode", resultCode);

            // parse our response into json before handing back
            if (resultCode == Activity.RESULT_OK) {
                if (resultData.getExtras() != null) {
                    try {
                        JSONObject extras = new JSONObject();
                        for (String key : resultData.getExtras().keySet())
                            extras.put(key, resultData.getExtras().get(key));
                        result.put("extras", extras);

                    } catch (JSONException e1) {
                        Log.e(TAG, "Problem while parsing extras", e1);
                    }
                }

                if (resultData.getData() != null) {
                    try {
                        // assume that we are handling one contentresolver response
                        Cursor cur = managedQuery(resultData.getData(), null, null, null, null);
                        cur.moveToFirst();

                        JSONObject data = new JSONObject();
                        for (int i = 0; i < cur.getColumnCount(); i++)
                            data.put(cur.getColumnName(i), cur.getString(i));
                        result.put("data", data);

                    } catch (Exception e) {
                        Log.e(TAG, "Problem while parsing data result", e);
                    }
                }
            }

            String resultraw = result.toString();
            Log.d(TAG, String.format("startActivityForResult() result=%s", resultraw));
            return resultraw;

        }

    }

    final class OilCanChrome extends WebChromeClient {

        public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
            new AlertDialog.Builder(BrowserActivity.this).setMessage(message)
                    .setPositiveButton(android.R.string.ok, null).create().show();

            result.confirm();
            return true;

        }

        public void onProgressChanged(WebView view, int newProgress) {
            BrowserActivity.this.setProgress(newProgress * 100);
        }

        public void onReceivedTitle(WebView view, String title) {
            BrowserActivity.this.setTitle(BrowserActivity.this.getString(R.string.browse_title, title));
        }

    };

    public final static String USERSCRIPT_EXTENSION = ".user.js";

    final class OilCanClient extends WebViewClient {

        /**
         * Watch each newly loaded page for userscript extensions (.user.js) to
         * prompt user with install helper.
         */
        public void onPageStarted(WebView view, final String url, Bitmap favicon) {

            // if url matches userscript extension, launch installer helper dialog
            if (url.endsWith(BrowserActivity.USERSCRIPT_EXTENSION)) {
                new AlertDialog.Builder(BrowserActivity.this).setTitle(R.string.install_title)
                        .setMessage(getString(R.string.install_message, url))
                        .setPositiveButton(android.R.string.ok, new OnClickListener() {
                            public void onClick(DialogInterface dialog, int which) {
                                try {
                                    String raw = Util.getUrlString(url);
                                    scriptdb.insertScript(null, raw);
                                    Toast.makeText(BrowserActivity.this, R.string.manage_import_success,
                                            Toast.LENGTH_SHORT).show();
                                } catch (Exception e) {
                                    Log.e(TAG, "Problem while trying to import script", e);
                                    Toast.makeText(BrowserActivity.this, R.string.manage_import_fail,
                                            Toast.LENGTH_SHORT).show();
                                }
                            }
                        }).setNegativeButton(android.R.string.cancel, null).create().show();

            }

        }

        /**
         * Handle finished loading of each page. Specifically this checks for
         * any active scripts based on the URL. When found a matching site, we
         * inject the JSON library and the applicable script.
         */
        public void onPageFinished(WebView view, String url) {
            if (scriptdb == null) {
                Log.e(TAG, "ScriptDatabase wasn't ready for finished page");
                return;
            }

            // for each finished page, try looking for active scripts
            List<String> active = scriptdb.getActive(url);
            Log.d(TAG, String.format("Found %d active scripts on url=%s", active.size(), url));
            if (active.size() == 0)
                return;

            // inject each applicable script into page
            for (String script : active) {
                script = BrowserActivity.this.prepareScript(script);
                webview.loadUrl(script);

            }

        }

        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            return false;
        }

    }

}