uk.org.rivernile.edinburghbustracker.android.Application.java Source code

Java tutorial

Introduction

Here is the source code for uk.org.rivernile.edinburghbustracker.android.Application.java

Source

/*
 * Copyright (C) 2009 - 2013 Niall 'Rivernile' Scott
 *
 * This software is provided 'as-is', without any express or implied
 * warranty.  In no event will the authors or contributors be held liable for
 * any damages arising from the use of this software.
 *
 * The aforementioned copyright holder(s) hereby grant you a
 * non-transferrable right to use this software for any purpose (including
 * commercial applications), and to modify it and redistribute it, subject to
 * the following conditions:
 *
 *  1. This notice may not be removed or altered from any file it appears in.
 *
 *  2. Any modifications made to this software, except those defined in
 *     clause 3 of this agreement, must be released under this license, and
 *     the source code of any modifications must be made available on a
 *     publically accessible (and locateable) website, or sent to the
 *     original author of this software.
 *
 *  3. Software modifications that do not alter the functionality of the
 *     software but are simply adaptations to a specific environment are
 *     exempt from clause 2.
 */

package uk.org.rivernile.edinburghbustracker.android;

import android.app.backup.BackupManager;
import static uk.org.rivernile.edinburghbustracker.android.PreferencesActivity.PREF_DATABASE_AUTO_UPDATE;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.os.Build;
import android.os.Looper;
import android.widget.Toast;
import com.bugsense.trace.BugSenseHandler;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Random;
import org.json.JSONException;
import org.json.JSONObject;

/**
 * This code is the very first code that will be executed when the application
 * is started. It is used to register the BugSense handler, put a listener on
 * the SharedPreferences for Google Backup on Froyo upwards, and check for bus
 * stop database updates.
 * 
 * The Android developer documentation discourages the usage of this class, but
 * as it is unpredictable where the user will enter the application the code is
 * put here as this class is always instantiated when this application's process
 * is created.
 * 
 * @author Niall Scott
 */
public class Application extends android.app.Application {

    private static final String DB_API_CHECK_URL = "http://www.mybustracker.co.uk/ws.php?module=json&function="
            + "getTopoId&key=";
    private static final String DB_UPDATE_CHECK_URL = "http://edinb.us/api/DatabaseVersion?schemaType="
            + BusStopDatabase.SCHEMA_NAME + "&random=";

    private static final Random random = new Random(System.currentTimeMillis());

    /**
     * {@inheritDoc}
     */
    @Override
    public void onCreate() {
        super.onCreate();
        // Register the BugSense handler.
        BugSenseHandler.initAndStartSession(this, ApiKey.BUGSENSE_KEY);
        // Cause the bus stop database to be extracted straight away.
        BusStopDatabase.getInstance(this);

        // If the API level is Froyo or greater, then register the
        // SharedPreference listener.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO)
            getSharedPreferences(PreferencesActivity.PREF_FILE, 0)
                    .registerOnSharedPreferenceChangeListener(new SharedPreferencesListener(this));

        // Start the thread to check for bus stop database updates.
        new Thread(stopDBTasks).start();
    }

    private Runnable stopDBTasks = new Runnable() {
        @Override
        public void run() {
            // Delete old database files if they exist.
            File toDelete = getDatabasePath("busstops.db");
            if (toDelete.exists())
                toDelete.delete();

            toDelete = getDatabasePath("busstops.db-journal");
            if (toDelete.exists())
                toDelete.delete();

            toDelete = getDatabasePath("busstops2.db");
            if (toDelete.exists())
                toDelete.delete();

            toDelete = getDatabasePath("busstops2.db-journal");
            if (toDelete.exists())
                toDelete.delete();

            toDelete = getDatabasePath("busstops8.db");
            if (toDelete.exists())
                toDelete.delete();

            toDelete = getDatabasePath("busstops8.db-journal");
            if (toDelete.exists())
                toDelete.delete();

            // Start update task.
            checkForDBUpdates(getApplicationContext(), false);
        }
    };

    /**
     * Check for updates to the bus stop database. This may happen automatically
     * if 24 hours have elapsed since the last check, or if the user has forced
     * the action. If a database update is found, then the new database is
     * downloaded and placed in the correct location.
     * 
     * @param context The context.
     * @param force True if the user forced the check, false if not.
     */
    public static void checkForDBUpdates(final Context context, final boolean force) {
        // Check to see if the user wants their database automatically updated.
        final SharedPreferences sp = context.getSharedPreferences(PreferencesActivity.PREF_FILE, 0);
        final boolean autoUpdate = sp.getBoolean(PREF_DATABASE_AUTO_UPDATE, true);
        final SharedPreferences.Editor edit = sp.edit();

        // Continue to check if the user has enabled it, or a check has been
        // forced (from the Preferences).
        if (autoUpdate || force) {
            if (!force) {
                // If it has not been forced, check the last update time. It is
                // only checked once per day. Abort if it is too soon.
                long lastCheck = sp.getLong("lastUpdateCheck", 0);
                if ((System.currentTimeMillis() - lastCheck) < 86400000)
                    return;
            }

            // Construct the checking URL.
            final StringBuilder sb = new StringBuilder();
            sb.append(DB_API_CHECK_URL);
            sb.append(ApiKey.getHashedKey());
            sb.append("&random=");
            // A random number is used so networks don't cache the HTTP
            // response.
            sb.append(random.nextInt());
            try {
                // Do connection stuff.
                final URL url = new URL(sb.toString());
                sb.setLength(0);
                final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                try {
                    final BufferedInputStream is = new BufferedInputStream(conn.getInputStream());

                    if (!url.getHost().equals(conn.getURL().getHost())) {
                        is.close();
                        conn.disconnect();
                        return;
                    }

                    // Read the incoming data.
                    int data;
                    while ((data = is.read()) != -1) {
                        sb.append((char) data);
                    }
                } finally {
                    // Whether there's an error or not, disconnect.
                    conn.disconnect();
                }
            } catch (MalformedURLException e) {
                return;
            } catch (IOException e) {
                return;
            }

            String topoId;
            try {
                // Parse the JSON and get the topoId from it.
                final JSONObject jo = new JSONObject(sb.toString());
                topoId = jo.getString("topoId");
            } catch (JSONException e) {
                return;
            }

            // If there's topoId then it cannot continue.
            if (topoId == null || topoId.length() == 0)
                return;

            // Get the current topoId from the database.
            final BusStopDatabase bsd = BusStopDatabase.getInstance(context.getApplicationContext());
            final String dbTopoId = bsd.getTopoId();

            // If the topoIds match, write our check time to SharedPreferences.
            if (topoId.equals(dbTopoId)) {
                edit.putLong("lastUpdateCheck", System.currentTimeMillis());
                edit.commit();
                if (force) {
                    // It was forced, alert the user there is no update
                    // available.
                    Looper.prepare();
                    Toast.makeText(context, R.string.bus_stop_db_no_updates, Toast.LENGTH_LONG).show();
                    Looper.loop();
                }
                return;
            }

            // There is an update available. Empty the StringBuilder then create
            // the URL to get the new database information.
            sb.setLength(0);
            sb.append(DB_UPDATE_CHECK_URL);
            sb.append(random.nextInt());
            sb.append("&key=");
            sb.append(ApiKey.getHashedKey());

            try {
                // Connection stuff.
                final URL url = new URL(sb.toString());
                sb.setLength(0);
                final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                try {
                    final BufferedInputStream is = new BufferedInputStream(conn.getInputStream());

                    if (!url.getHost().equals(conn.getURL().getHost())) {
                        is.close();
                        conn.disconnect();
                        return;
                    }

                    int data;
                    // Read the incoming data.
                    while ((data = is.read()) != -1) {
                        sb.append((char) data);
                    }
                } finally {
                    // Whether there's an error or not, disconnect.
                    conn.disconnect();
                }
            } catch (MalformedURLException e) {
                return;
            } catch (IOException e) {
                return;
            }

            String dbUrl, schemaVersion, checksum;
            try {
                // Get the data from tje returned JSON.
                final JSONObject jo = new JSONObject(sb.toString());
                dbUrl = jo.getString("db_url");
                schemaVersion = jo.getString("db_schema_version");
                topoId = jo.getString("topo_id");
                checksum = jo.getString("checksum");
            } catch (JSONException e) {
                // There was an error parsing the JSON, it cannot continue.
                return;
            }

            // Make sure the returned schema name is compatible with the one
            // the app uses.
            if (!BusStopDatabase.SCHEMA_NAME.equals(schemaVersion))
                return;
            // Some basic sanity checking on the parameters.
            if (topoId == null || topoId.length() == 0)
                return;
            if (dbUrl == null || dbUrl.length() == 0)
                return;
            if (checksum == null || checksum.length() == 0)
                return;

            // Make sure an update really is available.
            if (!topoId.equals(dbTopoId)) {
                // Update the database.
                updateStopsDB(context, dbUrl, checksum);
            } else if (force) {
                // Tell the user there is no update available.
                Looper.prepare();
                Toast.makeText(context, R.string.bus_stop_db_no_updates, Toast.LENGTH_LONG).show();
                Looper.loop();
            }

            // Write to the SharedPreferences the last update time.
            edit.putLong("lastUpdateCheck", System.currentTimeMillis());
            edit.commit();
        }
    }

    /**
     * Download the stop database from the server and put it in the
     * application's working data directory.
     *
     * @param context The context to use this method with.
     * @param url The URL of the bus stop database to download.
     */
    private static void updateStopsDB(final Context context, final String url, final String checksum) {
        if (context == null || url == null || url.length() == 0 || checksum == null || checksum.length() == 0)
            return;
        try {
            // Connect to the server.
            final URL u = new URL(url);
            final HttpURLConnection con = (HttpURLConnection) u.openConnection();
            final InputStream in = con.getInputStream();

            // Make sure the URL is what we expect.
            if (!u.getHost().equals(con.getURL().getHost())) {
                in.close();
                con.disconnect();
                return;
            }

            // The location the file should be downloaded to.
            final File temp = context.getDatabasePath(BusStopDatabase.STOP_DB_NAME + "_temp");
            // The eventual destination of the file.
            final File dest = context.getDatabasePath(BusStopDatabase.STOP_DB_NAME);
            final FileOutputStream out = new FileOutputStream(temp);

            // Get the file from the server.
            byte[] buf = new byte[1024];
            int len;
            while ((len = in.read(buf)) > 0) {
                out.write(buf, 0, len);
            }

            // Make sure the stream is flushed then close resources and
            // disconnect.
            out.flush();
            out.close();
            in.close();
            con.disconnect();

            // Do a MD5 checksum on the downloaded file. Make sure it matches
            // what the server reported.
            if (!md5Checksum(temp).equalsIgnoreCase(checksum)) {
                // If it doesn't match, delete the downloaded file.
                temp.delete();
                return;
            }

            try {
                // Open the temp database and execute the index operation on it.
                final SQLiteDatabase db = SQLiteDatabase.openDatabase(temp.getAbsolutePath(), null,
                        SQLiteDatabase.OPEN_READWRITE);
                BusStopDatabase.setUpIndexes(db);
                db.close();
            } catch (SQLiteException e) {
                // If we couldn't create the index, continue anyway. The user
                // will still be able to use the database, it will just run
                // slowly if they want route lines.
            }

            // Close a currently open database. Delete the old database then
            // move the downloaded file in to its place. Do this while
            // synchronized to make sure noting else uses the database in this
            // time.
            final BusStopDatabase bsd = BusStopDatabase.getInstance(context.getApplicationContext());
            synchronized (bsd) {
                try {
                    bsd.getReadableDatabase().close();
                } catch (SQLiteException e) {
                    // Nothing to do here. Assume it's already closed.
                }

                dest.delete();
                temp.renameTo(dest);
            }

            // Delete the associated journal file because we no longer need it.
            final File journalFile = context.getDatabasePath(BusStopDatabase.STOP_DB_NAME + "_temp-journal");
            if (journalFile.exists())
                journalFile.delete();

            // Alert the user that the database has been updated.
            Looper.prepare();
            Toast.makeText(context, R.string.bus_stop_db_updated, Toast.LENGTH_LONG).show();
            Looper.loop();
        } catch (MalformedURLException e) {
        } catch (IOException e) {
        }
    }

    /**
     * Create a checksum for a File. This is used to ensure that a downloaded
     * database has not been corrupted or incomplete.
     * 
     * See: http://vyshemirsky.blogspot.com/2007/08/computing-md5-digest-checksum-in-java.html
     * This has been slightly modified.
     * 
     * @param file The file to run the MD5 checksum against.
     * @return The MD5 checksum string.
     */
    public static String md5Checksum(final File file) {
        try {
            final InputStream fin = new FileInputStream(file);
            final MessageDigest md5er = MessageDigest.getInstance("MD5");
            final byte[] buffer = new byte[1024];
            int read;

            while ((read = fin.read(buffer)) != -1) {
                if (read > 0)
                    md5er.update(buffer, 0, read);
            }
            fin.close();

            final byte[] digest = md5er.digest();
            if (digest == null)
                return null;
            final StringBuilder builder = new StringBuilder();
            for (byte a : digest) {
                builder.append(Integer.toString((a & 0xff) + 0x100, 16).substring(1));
            }

            return builder.toString();
        } catch (FileNotFoundException e) {
            return "";
        } catch (NoSuchAlgorithmException e) {
            return "";
        } catch (IOException e) {
            return "";
        }
    }

    /**
     * The SharedPreferencesListener will look out for changes to the shared
     * preferences and schedule updates with Google Backup if there is, if the
     * device is running Android 2.2 (Froyo) or greater.
     */
    public static class SharedPreferencesListener implements OnSharedPreferenceChangeListener {

        final Context context;

        /**
         * Constructor, supplying a Context instance.
         * 
         * @param context The application Context.
         */
        public SharedPreferencesListener(final Context context) {
            this.context = context;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void onSharedPreferenceChanged(final SharedPreferences sp, final String key) {
            BackupManager.dataChanged(context.getPackageName());
        }
    }
}