/*
* 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";
}
}