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

Java tutorial

Introduction

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

Source

/*
 * Copyright (C) 2009 - 2012 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 android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Environment;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

/**
 * This class deals with the database interaction with the settings database.
 * Currently the settings database only holds information for favourite bus
 * stops.
 *
 * @author Niall Scott
 */
public class SettingsDatabase extends SQLiteOpenHelper {

    /** The name of the file on disk for this database. */
    protected static final String SETTINGS_DB_NAME = "settings.db";
    private static final int SETTINGS_DB_VERSION = 2;

    private static final String FAVOURITE_STOPS_TABLE = "favourite_stops";
    private static final String FAVOURITE_STOPS_STOPCODE = "_id";
    public static final String FAVOURITE_STOPS_STOPNAME = "stopName";

    private static final String ALERTS_TABLE = "active_alerts";
    private static final String ALERTS_ID = "_id";
    private static final String ALERTS_TYPE = "type";
    private static final String ALERTS_TIME_ADDED = "timeAdded";
    private static final String ALERTS_STOPCODE = "stopCode";
    private static final String ALERTS_DISTANCE_FROM = "distanceFrom";
    private static final String ALERTS_SERVICE_NAMES = "serviceNames";
    private static final String ALERTS_TIME_TRIGGER = "timeTrigger";

    private static final String BACKUP_DB_VERSION = "dbVersion";
    private static final String BACKUP_SCHEMA_VERSION = "jsonSchemaVersion";
    private static final String BACKUP_CREATE_TIME = "createTime";
    private static final String BACKUP_FAVOURITE_STOPS = "favouriteStops";
    private static final String BACKUP_STOPCODE = "stopCode";
    private static final String BACKUP_STOPNAME = "stopName";
    private static final String BACKUP_DIRECTORY = "/mybusedinburgh/";
    private static final String BACKUP_FILE_NAME = "settings.backup";

    /** A proximity alert type. */
    public static final byte ALERTS_TYPE_PROXIMITY = 1;
    /** A time alert type. */
    public static final byte ALERTS_TYPE_TIME = 2;

    private static SettingsDatabase instance;

    private Context context;

    /**
     * Creates a new SettingsDatabase object. If the DB does not already exist,
     * it will be created.
     *
     * @param context The activity context.
     */
    private SettingsDatabase(final Context context) {
        super(context, SETTINGS_DB_NAME, null, SETTINGS_DB_VERSION);
        this.context = context;
    }

    /**
     * Get the singleton instance of this class.
     * 
     * @param context The application context.
     * @return A reference to the singleton instance of this class.
     */
    public static SettingsDatabase getInstance(final Context context) {
        if (instance == null)
            instance = new SettingsDatabase(context);
        return instance;
    }

    /**
     * This is called when the DB does not exist.
     *
     * @param db The database object to interface with.
     */
    @Override
    public void onCreate(final SQLiteDatabase db) {
        // This is what happens when the application cannot find a database
        // with the database name.
        db.execSQL("CREATE TABLE " + FAVOURITE_STOPS_TABLE + " (" + FAVOURITE_STOPS_STOPCODE + " TEXT PRIMARY KEY,"
                + FAVOURITE_STOPS_STOPNAME + " TEXT NOT NULL);");
        db.execSQL("CREATE TABLE " + ALERTS_TABLE + " (" + ALERTS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
                + ALERTS_TYPE + " NUMERIC NOT NULL," + ALERTS_TIME_ADDED + " INTEGER NOT NULL," + ALERTS_STOPCODE
                + " TEXT NOT NULL," + ALERTS_DISTANCE_FROM + " INTEGER," + ALERTS_SERVICE_NAMES + " TEXT,"
                + ALERTS_TIME_TRIGGER + " INTEGER);");
    }

    /**
     * An upgrade of the database, an abstract method in the super class.
     *
     * @param db The database object to interface with.
     * @param oldVersion The version of the old database.
     * @param newVersion The version of the new database.
     */
    @Override
    public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
        db.execSQL("CREATE TABLE IF NOT EXISTS " + ALERTS_TABLE + " (" + ALERTS_ID
                + " INTEGER PRIMARY KEY AUTOINCREMENT," + ALERTS_TYPE + " NUMERIC NOT NULL," + ALERTS_TIME_ADDED
                + " INTEGER NOT NULL," + ALERTS_STOPCODE + " TEXT NOT NULL," + ALERTS_DISTANCE_FROM + " INTEGER,"
                + ALERTS_SERVICE_NAMES + " TEXT," + ALERTS_TIME_TRIGGER + " INTEGER);");
    }

    /**
     * {@inheritDoc} 
     */
    @Override
    public void onOpen(final SQLiteDatabase db) {
        cleanupAlerts(db);
    }

    /**
     * Get a Cursor to all the favourite stop items.
     *
     * @return A Cursor object to all the favourite stop items.
     */
    public Cursor getAllFavouriteStops() {
        final SQLiteDatabase db = getReadableDatabase();
        return db.query(true, FAVOURITE_STOPS_TABLE,
                new String[] { FAVOURITE_STOPS_STOPCODE, FAVOURITE_STOPS_STOPNAME }, null, null, null, null,
                FAVOURITE_STOPS_STOPNAME + " ASC", null);
    }

    /**
     * This method checks to see if a favourite stop already exists in the
     * database.
     *
     * @param stopCode The stopCode to check for.
     * @return True if it already exists, false if it doesn't.
     */
    public boolean getFavouriteStopExists(final String stopCode) {
        if (stopCode == null || stopCode.length() == 0)
            return false;

        final SQLiteDatabase db = getReadableDatabase();
        final Cursor c = db.query(FAVOURITE_STOPS_TABLE, new String[] { FAVOURITE_STOPS_STOPCODE },
                FAVOURITE_STOPS_STOPCODE + " = ?", new String[] { stopCode }, null, null, null);
        if (c.getCount() > 0) {
            c.close();
            return true;
        }

        c.close();
        return false;
    }

    /**
     * Insert a new favourite stop in to the database.
     *
     * @param stopCode The stop code to insert.
     * @param stopName The stop name to insert.
     * @throws SQLException If an error occurs whilst writing to the database.
     */
    public void insertFavouriteStop(final String stopCode, final String stopName) throws SQLException {
        if (stopCode == null || stopName == null || stopCode.length() == 0 || stopName.length() == 0)
            return;
        if (getFavouriteStopExists(stopCode))
            return;
        final ContentValues cv = new ContentValues();
        cv.put(FAVOURITE_STOPS_STOPCODE, stopCode);
        cv.put(FAVOURITE_STOPS_STOPNAME, stopName);

        final SQLiteDatabase db = getWritableDatabase();
        db.insertOrThrow(FAVOURITE_STOPS_TABLE, FAVOURITE_STOPS_STOPNAME, cv);

        BackupManager.dataChanged(context.getPackageName());
    }

    /**
     * Delete a favourite stop from the database.
     *
     * @param stopCode The stop code to delete from the database.
     */
    public void deleteFavouriteStop(final String stopCode) {
        if (stopCode == null || stopCode.length() == 0)
            return;
        if (!getFavouriteStopExists(stopCode))
            return;

        final SQLiteDatabase db = getWritableDatabase();
        db.delete(FAVOURITE_STOPS_TABLE, FAVOURITE_STOPS_STOPCODE + " = ?", new String[] { stopCode });

        BackupManager.dataChanged(context.getPackageName());
    }

    /**
     * Modify the stopName string of an item in the database.
     *
     * @param stopCode The stop code of the item to modify.
     * @param stopName The new stop name.
     */
    public void modifyFavouriteStop(final String stopCode, final String stopName) {
        if (stopCode == null || stopName == null || stopCode.length() == 0 || stopName.length() == 0)
            return;
        if (!getFavouriteStopExists(stopCode))
            return;
        final ContentValues cv = new ContentValues();
        cv.put(FAVOURITE_STOPS_STOPNAME, stopName);

        final SQLiteDatabase db = getWritableDatabase();
        db.update(FAVOURITE_STOPS_TABLE, cv, FAVOURITE_STOPS_STOPCODE + " = ?", new String[] { stopCode });

        BackupManager.dataChanged(context.getPackageName());
    }

    /**
     * Get the name for a bus stop.
     *
     * @param stopCode The bus stop code.
     * @return The name for the bus stop.
     */
    public String getNameForStop(final String stopCode) {
        if (stopCode == null || stopCode.length() == 0 || !getFavouriteStopExists(stopCode))
            return null;

        final SQLiteDatabase db = getReadableDatabase();
        final Cursor c = db.query(FAVOURITE_STOPS_TABLE, new String[] { FAVOURITE_STOPS_STOPNAME },
                FAVOURITE_STOPS_STOPCODE + " = ?", new String[] { stopCode }, null, null, null);
        String result = null;
        if (c.moveToFirst()) {
            result = c.getString(0);
        }

        c.close();
        return result;
    }

    /**
     * Insert a new proximity alert in to the database. We have to store this
     * because it's not possible to get information on proximity alerts we have
     * set back from the platform.
     * 
     * @param stopCode The stopCode for which the proximity alert is set.
     * @param distance The distance at which to trigger the proximity alert.
     */
    public void insertNewProximityAlert(final String stopCode, final int distance) {
        final SQLiteDatabase db = getWritableDatabase();
        cleanupAlerts(db);

        final ContentValues cv = new ContentValues();
        cv.put(ALERTS_TYPE, ALERTS_TYPE_PROXIMITY);
        cv.put(ALERTS_TIME_ADDED, System.currentTimeMillis());
        cv.put(ALERTS_STOPCODE, stopCode);
        cv.put(ALERTS_DISTANCE_FROM, distance);

        db.insertOrThrow(ALERTS_TABLE, ALERTS_DISTANCE_FROM, cv);
    }

    /**
     * Insert a new time alert in to the database. We have to store this because
     * it's not possible to get alarm information from the platform after we
     * have set up an alarm.
     * 
     * @param stopCode The stopCode for which the time alert is set.
     * @param serviceNames The names of the services which should trigger this
     * alert.
     * @param timeTrigger The time at which to trigger the time alert.
     */
    public void insertNewTimeAlert(final String stopCode, final String[] serviceNames, final int timeTrigger) {
        final SQLiteDatabase db = getWritableDatabase();
        cleanupAlerts(db);

        int len = serviceNames.length;
        final StringBuilder sb = new StringBuilder();
        for (int i = 0; i < len; i++) {
            sb.append(serviceNames[i]);
            if (i != len - 1) {
                sb.append(',');
            }
        }

        final ContentValues cv = new ContentValues();
        cv.put(ALERTS_TYPE, ALERTS_TYPE_TIME);
        cv.put(ALERTS_TIME_ADDED, System.currentTimeMillis());
        cv.put(ALERTS_STOPCODE, stopCode);
        cv.put(ALERTS_SERVICE_NAMES, sb.toString());
        cv.put(ALERTS_TIME_TRIGGER, timeTrigger);

        db.insertOrThrow(ALERTS_TABLE, ALERTS_TIME_TRIGGER, cv);
    }

    /**
     * Delete an alert of a particular type. This can be either
     * {@link SettingsDatabase#ALERTS_TYPE_PROXIMITY} or
     * {@link SettingsDatabase#ALERTS_TYPE_TIME}.
     * 
     * @param type The type of alert to delete.
     * @see SettingsDatabase#ALERTS_TYPE_PROXIMITY
     * @see SettingsDatabase#ALERTS_TYPE_TIME
     */
    public void deleteAllAlertsOfType(final byte type) {
        final SQLiteDatabase db = getWritableDatabase();
        cleanupAlerts(db);
        db.delete(ALERTS_TABLE, ALERTS_TYPE + " = ?", new String[] { String.valueOf(type) });
    }

    /**
     * Check to see if a proximity alert already exists for the given stopCode.
     * 
     * @param stopCode The stopCode to check against.
     * @return True if an alert exists, false if it doesn't.
     */
    public boolean isActiveProximityAlert(final String stopCode) {
        return isActiveAlertOfType(stopCode, ALERTS_TYPE_PROXIMITY);
    }

    /**
     * Check to see if a time alert already exists for the given stopCode.
     * 
     * @param stopCode The stopCode to check against.
     * @return True if the alert exists, false if it doesn't.
     */
    public boolean isActiveTimeAlert(final String stopCode) {
        return isActiveAlertOfType(stopCode, ALERTS_TYPE_TIME);
    }

    /**
     * Check to see if an alert of a particular type exists at the given
     * stopCode. The type is either
     * {@link SettingsDatabase#ALERTS_TYPE_PROXIMITY} or
     * {@link SettingsDatabase#ALERTS_TYPE_TIME}
     * 
     * @param stopCode The stopCode to check against.
     * @param type The type of alert. See this method's description.
     * @return True if the alert exists, false if it doesn't.
     * @see SettingsDatabase#ALERTS_TYPE_PROXIMITY
     * @see SettingsDatabase#ALERTS_TYPE_TIME
     */
    public boolean isActiveAlertOfType(final String stopCode, final int type) {
        if (stopCode == null || stopCode.length() == 0)
            return false;
        final SQLiteDatabase db = getWritableDatabase();
        cleanupAlerts(db);
        final Cursor c = db.query(ALERTS_TABLE, new String[] { ALERTS_STOPCODE },
                ALERTS_STOPCODE + " = ? AND " + ALERTS_TYPE + " = ?",
                new String[] { stopCode, String.valueOf(type) }, null, null, null);
        if (c.getCount() > 0) {
            c.close();
            return true;
        }

        c.close();
        return false;
    }

    /**
     * Return a Cursor object which contains all fields of an alert, for all
     * alerts in the database. It is perfectly possible that no alerts exist,
     * therefore an empty Cursor is returned.
     * 
     * @return A Cursor containing all alerts known in the database.
     */
    public Cursor getAllAlerts() {
        final SQLiteDatabase db = getWritableDatabase();

        cleanupAlerts(db);
        return db.query(ALERTS_TABLE, null, null, null, null, null, null);
    }

    /**
     * Clean up alerts. This removes any alerts which are older than 1 hour.
     * 
     * @param db The SQLiteDatabase on which we are operating.
     */
    public void cleanupAlerts(final SQLiteDatabase db) {
        if (!db.isReadOnly()) {
            db.delete(ALERTS_TABLE, ALERTS_TIME_ADDED + " < ?",
                    new String[] { String.valueOf(System.currentTimeMillis() - 3600000) });
        }
    }

    /**
     * Return a dump of the list of favourite bus stops as a JSONObject, to be
     * later output to file.
     * 
     * @return A JSON object describing the favourite bus stops.
     * @throws JSONException When an error occurs whilst dealing with JSON.
     */
    public JSONObject backupDatabaseAsJSON() throws JSONException {
        final JSONObject root = new JSONObject();
        final JSONArray favStops = new JSONArray();
        JSONObject stop;

        root.put(BACKUP_DB_VERSION, SETTINGS_DB_VERSION);
        root.put(BACKUP_SCHEMA_VERSION, 1);
        root.put(BACKUP_CREATE_TIME, System.currentTimeMillis());
        root.put(BACKUP_FAVOURITE_STOPS, favStops);

        final Cursor c = getAllFavouriteStops();
        while (c.moveToNext()) {
            stop = new JSONObject();
            stop.put(BACKUP_STOPCODE, c.getString(0));
            stop.put(BACKUP_STOPNAME, c.getString(1));
            favStops.put(stop);
        }

        return root;
    }

    /**
     * Restore a previous backup from JSON input and insert it in to the
     * database.
     * 
     * @param jsonString The JSON to be restored.
     * @throws JSONException When an error occurs whilst parsing the JSON text.
     */
    public void restoreDatabaseFromJSON(final String jsonString) throws JSONException {
        final SQLiteDatabase db = getWritableDatabase();
        db.delete(FAVOURITE_STOPS_TABLE, null, null);

        final JSONObject root = new JSONObject(jsonString);
        final JSONArray favStops = root.getJSONArray(BACKUP_FAVOURITE_STOPS);
        JSONObject stop;

        final int len = favStops.length();
        for (int i = 0; i < len; i++) {
            stop = favStops.getJSONObject(i);
            insertFavouriteStop(stop.getString(BACKUP_STOPCODE), stop.getString(BACKUP_STOPNAME));
        }
    }

    /**
     * Backup the JSON dump of the database to a file on external storage.
     * 
     * @return A success or failure string.
     */
    public String backupDatabase() {
        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            return context.getString(R.string.preferences_backup_error_mediaerror);
        }

        File out = new File(Environment.getExternalStorageDirectory(), BACKUP_DIRECTORY);
        out.mkdirs();
        out = new File(out, BACKUP_FILE_NAME);

        try {
            final JSONObject root = backupDatabaseAsJSON();
            final PrintWriter pw = new PrintWriter(new FileWriter(out));
            pw.println(root.toString());
            pw.flush();
            pw.close();
        } catch (JSONException e) {
            return context.getString(R.string.preferences_backup_error_json_write);
        } catch (IOException e) {
            return context.getString(R.string.preferences_backup_error_ioerror);
        }

        return "success";
    }

    /**
     * Restore a previous JSON dump of the database from a file on external
     * storage.
     * 
     * @return A success or failure string.
     */
    public String restoreDatabase() {
        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            return context.getString(R.string.preferences_backup_error_mediaerror);
        }

        final File in = new File(Environment.getExternalStorageDirectory(), BACKUP_DIRECTORY + BACKUP_FILE_NAME);
        if (!in.exists() || !in.canRead()) {
            return context.getString(R.string.preferences_backup_error_nofile);
        }

        final StringBuilder jsonString = new StringBuilder();
        try {
            String str;
            final BufferedReader reader = new BufferedReader(new FileReader(in));
            while ((str = reader.readLine()) != null) {
                jsonString.append(str);
            }
            reader.close();

            restoreDatabaseFromJSON(jsonString.toString());
        } catch (IOException e) {
            return context.getString(R.string.preferences_backup_error_ioerror);
        } catch (JSONException e) {
            return context.getString(R.string.preferences_backup_error_json_read);
        }

        return "success";
    }
}