Replaced all usages of DatabaseManager with AlarmsTableManager

This commit is contained in:
Phillip Hsu 2016-08-01 19:06:46 -07:00
parent 5ac5b34640
commit 992e091db7
20 changed files with 66 additions and 949 deletions

View File

@ -75,7 +75,7 @@ public abstract class Alarm extends ObjectWithId implements JsonSerializable, Pa
} }
/** <b>ONLY CALL THIS WHEN CREATING AN ALARM INSTANCE FROM A CURSOR</b> */ /** <b>ONLY CALL THIS WHEN CREATING AN ALARM INSTANCE FROM A CURSOR</b> */
// TODO: To be even more safe, create a ctor that takes the two Cursors and // TODO: To be even more safe, create a ctor that takes a Cursor and
// initialize the instance here instead of in AlarmDatabaseHelper. // initialize the instance here instead of in AlarmDatabaseHelper.
public void setSnoozing(long snoozingUntilMillis) { public void setSnoozing(long snoozingUntilMillis) {
this.snoozingUntilMillis = snoozingUntilMillis; this.snoozingUntilMillis = snoozingUntilMillis;

View File

@ -4,8 +4,8 @@ import android.app.IntentService;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import com.philliphsu.clock2.model.AlarmDatabaseHelper.AlarmCursor; import com.philliphsu.clock2.model.AlarmCursor;
import com.philliphsu.clock2.model.DatabaseManager; import com.philliphsu.clock2.model.AlarmsTableManager;
import com.philliphsu.clock2.util.AlarmController; import com.philliphsu.clock2.util.AlarmController;
/** /**
@ -61,9 +61,9 @@ public class OnBootUpAlarmScheduler extends IntentService {
if (intent != null) { if (intent != null) {
AlarmController controller = new AlarmController(this, null); AlarmController controller = new AlarmController(this, null);
// IntentService works in a background thread, so this won't hold us up. // IntentService works in a background thread, so this won't hold us up.
AlarmCursor cursor = DatabaseManager.getInstance(this).queryEnabledAlarms(); AlarmCursor cursor = new AlarmsTableManager(this).queryEnabledAlarms();
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
Alarm alarm = cursor.getAlarm(); Alarm alarm = cursor.getItem();
if (!alarm.isEnabled()) { if (!alarm.isEnabled()) {
throw new IllegalStateException( throw new IllegalStateException(
"queryEnabledAlarms() returned alarm(s) that aren't enabled"); "queryEnabledAlarms() returned alarm(s) that aren't enabled");

View File

@ -4,7 +4,8 @@ import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import com.philliphsu.clock2.model.DatabaseManager; import com.philliphsu.clock2.model.AlarmCursor;
import com.philliphsu.clock2.model.AlarmsTableManager;
import com.philliphsu.clock2.util.AlarmController; import com.philliphsu.clock2.util.AlarmController;
import static com.philliphsu.clock2.util.Preconditions.checkNotNull; import static com.philliphsu.clock2.util.Preconditions.checkNotNull;
@ -41,8 +42,8 @@ public class PendingAlarmScheduler extends BroadcastReceiver {
new Thread(new Runnable() { new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
Alarm alarm = checkNotNull(DatabaseManager AlarmCursor cursor = new AlarmsTableManager(context).queryItem(id);
.getInstance(context).getAlarm(id)); Alarm alarm = checkNotNull(cursor.getItem());
if (!alarm.isEnabled()) { if (!alarm.isEnabled()) {
throw new IllegalStateException("Alarm must be enabled!"); throw new IllegalStateException("Alarm must be enabled!");
} }

View File

@ -9,7 +9,8 @@ import android.content.Intent;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
import com.philliphsu.clock2.model.DatabaseManager; import com.philliphsu.clock2.model.AlarmCursor;
import com.philliphsu.clock2.model.AlarmsTableManager;
import com.philliphsu.clock2.util.AlarmController; import com.philliphsu.clock2.util.AlarmController;
import static android.app.PendingIntent.FLAG_ONE_SHOT; import static android.app.PendingIntent.FLAG_ONE_SHOT;
@ -41,7 +42,8 @@ public class UpcomingAlarmReceiver extends BroadcastReceiver {
new AsyncTask<Void, Void, Alarm>() { new AsyncTask<Void, Void, Alarm>() {
@Override @Override
protected Alarm doInBackground(Void... params) { protected Alarm doInBackground(Void... params) {
return checkNotNull(DatabaseManager.getInstance(context).getAlarm(id)); AlarmCursor cursor = new AlarmsTableManager(context).queryItem(id);
return checkNotNull(cursor.getItem());
} }
@Override @Override

View File

@ -1,313 +0,0 @@
package com.philliphsu.clock2.model;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.CursorWrapper;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.util.LocalBroadcastHelper;
import static com.philliphsu.clock2.DaysOfWeek.FRIDAY;
import static com.philliphsu.clock2.DaysOfWeek.MONDAY;
import static com.philliphsu.clock2.DaysOfWeek.SATURDAY;
import static com.philliphsu.clock2.DaysOfWeek.SUNDAY;
import static com.philliphsu.clock2.DaysOfWeek.THURSDAY;
import static com.philliphsu.clock2.DaysOfWeek.TUESDAY;
import static com.philliphsu.clock2.DaysOfWeek.WEDNESDAY;
/**
* Created by Phillip Hsu on 6/24/2016.
*
* TODO: We can generalize this class to all data models, not just Alarms.
*/
@Deprecated
public class AlarmDatabaseHelper extends SQLiteOpenHelper {
private static final String TAG = "AlarmDatabaseHelper";
private static final String DB_NAME = "alarms.db";
private static final int VERSION_1 = 1;
// TODO: Consider creating an inner class that implements BaseColumns
// and defines all the columns.
// TODO: Consider defining index constants for each column,
// and then removing all cursor getColumnIndex() calls.
// TODO: Consider making these public, so callers can customize their
// WHERE queries.
private static final String TABLE_ALARMS = "alarms";
private static final String COLUMN_ID = "_id";
private static final String COLUMN_HOUR = "hour";
private static final String COLUMN_MINUTES = "minutes";
private static final String COLUMN_LABEL = "label";
private static final String COLUMN_RINGTONE = "ringtone";
private static final String COLUMN_VIBRATES = "vibrates";
private static final String COLUMN_ENABLED = "enabled";
// TODO: Delete this column, becuase new sort order does not consider it
@Deprecated
private static final String COLUMN_RING_TIME_MILLIS = "ring_time_millis";
private static final String COLUMN_SNOOZING_UNTIL_MILLIS = "snoozing_until_millis";
private static final String COLUMN_SUNDAY = "sunday";
private static final String COLUMN_MONDAY = "monday";
private static final String COLUMN_TUESDAY = "tuesday";
private static final String COLUMN_WEDNESDAY = "wednesday";
private static final String COLUMN_THURSDAY = "thursday";
private static final String COLUMN_FRIDAY = "friday";
private static final String COLUMN_SATURDAY = "saturday";
private static final String COLUMN_IGNORE_UPCOMING_RING_TIME = "ignore_upcoming_ring_time";
// https://www.sqlite.org/lang_select.html#orderby
// Rows are first sorted based on the results of evaluating the left-most expression in the
// ORDER BY list, then ties are broken by evaluating the second left-most expression and so on.
// The order in which two rows for which all ORDER BY expressions evaluate to equal values are
// returned is undefined. Each ORDER BY expression may be optionally followed by one of the keywords
// ASC (smaller values are returned first) or DESC (larger values are returned first). If neither
// ASC or DESC are specified, rows are sorted in ascending (smaller values first) order by default.
// First sort by ring time in ascending order (smaller values first),
// then break ties by sorting by id in ascending order.
@Deprecated
private static final String SORT_ORDER =
COLUMN_RING_TIME_MILLIS + " ASC, " + COLUMN_ID + " ASC";
private static final String NEW_SORT_ORDER = COLUMN_HOUR + " ASC, "
+ COLUMN_MINUTES + " ASC, "
// TOneverDO: Sort COLUMN_ENABLED or else alarms could be reordered
// if you toggle them on/off, which looks confusing.
// TODO: Figure out how to get the order to be:
// No recurring days ->
// Recurring earlier in user's weekday order ->
// Recurring everyday
// As written now, this is incorrect! For one, it assumes
// the standard week order (starting on Sunday).
// DESC gives us (Sunday -> Saturday -> No recurring days),
// ASC gives us the reverse (No recurring days -> Saturday -> Sunday).
// TODO: If assuming standard week order, try ASC for all days but
// write COLUMN_SATURDAY first, then COLUMN_FRIDAY, ... , COLUMN_SUNDAY.
// Check if that gives us (No recurring days -> Sunday -> Saturday).
// + COLUMN_SUNDAY + " DESC, "
// + COLUMN_MONDAY + " DESC, "
// + COLUMN_TUESDAY + " DESC, "
// + COLUMN_WEDNESDAY + " DESC, "
// + COLUMN_THURSDAY + " DESC, "
// + COLUMN_FRIDAY + " DESC, "
// + COLUMN_SATURDAY + " DESC, "
// All else equal, newer alarms first
+ COLUMN_ID + " DESC"; // TODO: If duplicate alarm times disallowed, delete this
private final Context mAppContext;
public AlarmDatabaseHelper(Context context) {
super(context.getApplicationContext(), DB_NAME, null, VERSION_1);
mAppContext = context.getApplicationContext();
// TODO: Here is where you could compute the sort expression
// for the recurring days order, based on the user's defined
// weekday order. For example, if we read the first day of
// the week as Sunday, then we build a String called RECURRENCE_ORDER:
// RECURRENCE_ORDER =
// COLUMN_SATURDAY + " ASC, "
// + ...
// + COLUMN_SUNDAY + " ASC";
// Note how the weekday order is reversed when
// we refer to the columns. We should also include
// ordering by id as the last piece of this string:
// + COLUMN_ID + " DESC";
// and remove that piece from the NEW_SORT_ORDER
// constant. This is so we can later concatenate
// NEW_SORT_ORDER and RECURRENCE_ORDER but maintain
// the original order of the sort expressions.
// We should also rename that constant
// to BASE_SORT_ORDER. Last, in the query() methods,
// we can pass in
// BASE_SORT_ORDER + RECURRENCE_ORDER
// to its orderBy parameter.
}
@Override
public void onCreate(SQLiteDatabase db) {
// https://www.sqlite.org/datatype3.html
// INTEGER data type is stored in 1, 2, 3, 4, 6, or 8 bytes depending on the magnitude
// of the value. As soon as INTEGER values are read off of disk and into memory for processing,
// they are converted to the most general datatype (8-byte signed integer).
// 8 byte == 64 bits so this means they are read as longs...?
db.execSQL("CREATE TABLE " + TABLE_ALARMS + " ("
+ COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ COLUMN_HOUR + " INTEGER NOT NULL, "
+ COLUMN_MINUTES + " INTEGER NOT NULL, "
+ COLUMN_LABEL + " TEXT, "
+ COLUMN_RINGTONE + " TEXT NOT NULL, "
+ COLUMN_VIBRATES + " INTEGER NOT NULL, "
+ COLUMN_ENABLED + " INTEGER NOT NULL, "
+ COLUMN_RING_TIME_MILLIS + " INTEGER NOT NULL, "
+ COLUMN_SNOOZING_UNTIL_MILLIS + " INTEGER, "
+ COLUMN_SUNDAY + " INTEGER NOT NULL DEFAULT 0, "
+ COLUMN_MONDAY + " INTEGER NOT NULL DEFAULT 0, "
+ COLUMN_TUESDAY + " INTEGER NOT NULL DEFAULT 0, "
+ COLUMN_WEDNESDAY + " INTEGER NOT NULL DEFAULT 0, "
+ COLUMN_THURSDAY + " INTEGER NOT NULL DEFAULT 0, "
+ COLUMN_FRIDAY + " INTEGER NOT NULL DEFAULT 0, "
+ COLUMN_SATURDAY + " INTEGER NOT NULL DEFAULT 0, "
+ COLUMN_IGNORE_UPCOMING_RING_TIME + " INTEGER NOT NULL);");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// I don't think we need to drop current tables unless you make structural changes
// to the schema in the new version.
}
public long insertAlarm(Alarm alarm) {
long id = getWritableDatabase().insert(TABLE_ALARMS,
null, toContentValues(alarm));
alarm.setId(id);
notifyContentChanged();
return id;
}
/**
* @deprecated Use {@link #updateAlarm(long, Alarm)} instead
*/
@Deprecated
public int updateAlarm(Alarm oldAlarm, Alarm newAlarm) {
newAlarm.setId(oldAlarm.id());
SQLiteDatabase db = getWritableDatabase();
int rowsUpdated = db.update(TABLE_ALARMS,
toContentValues(newAlarm),
COLUMN_ID + " = " + newAlarm.id(),
null);
notifyContentChanged();
return rowsUpdated;
}
public int updateAlarm(long id, Alarm newAlarm) {
newAlarm.setId(id);
SQLiteDatabase db = getWritableDatabase();
int rowsUpdated = db.update(TABLE_ALARMS,
toContentValues(newAlarm),
COLUMN_ID + " = " + id,
null);
notifyContentChanged();
return rowsUpdated;
}
public int deleteAlarm(Alarm alarm) {
SQLiteDatabase db = getWritableDatabase();
int rowsDeleted = db.delete(TABLE_ALARMS,
COLUMN_ID + " = " + alarm.id(),
null);
notifyContentChanged();
return rowsDeleted;
}
public AlarmCursor queryAlarm(long id) {
Cursor c = getReadableDatabase().query(TABLE_ALARMS,
null, // All columns
COLUMN_ID + " = " + id, // Selection for this alarm id
null, // selection args, none b/c id already specified in selection
null, // group by
null, // order/sort by
null, // having
"1"); // limit 1 row
return new AlarmCursor(c);
}
public AlarmCursor queryAlarms() {
// Select all rows and columns
return queryAlarms(null);
}
public AlarmCursor queryEnabledAlarms() {
return queryAlarms(COLUMN_ENABLED + " = 1");
}
private AlarmCursor queryAlarms(String where) {
Cursor c = getReadableDatabase().query(TABLE_ALARMS,
null, where, null, null, null, NEW_SORT_ORDER);
return new AlarmCursor(c);
}
private ContentValues toContentValues(Alarm alarm) {
ContentValues values = new ContentValues();
values.put(COLUMN_HOUR, alarm.hour());
values.put(COLUMN_MINUTES, alarm.minutes());
values.put(COLUMN_LABEL, alarm.label());
values.put(COLUMN_RINGTONE, alarm.ringtone());
values.put(COLUMN_VIBRATES, alarm.vibrates());
values.put(COLUMN_ENABLED, alarm.isEnabled());
values.put(COLUMN_RING_TIME_MILLIS, alarm.ringsAt());
values.put(COLUMN_SNOOZING_UNTIL_MILLIS, alarm.snoozingUntil());
values.put(COLUMN_SUNDAY, alarm.isRecurring(SUNDAY));
values.put(COLUMN_MONDAY, alarm.isRecurring(MONDAY));
values.put(COLUMN_TUESDAY, alarm.isRecurring(TUESDAY));
values.put(COLUMN_WEDNESDAY, alarm.isRecurring(WEDNESDAY));
values.put(COLUMN_THURSDAY, alarm.isRecurring(THURSDAY));
values.put(COLUMN_FRIDAY, alarm.isRecurring(FRIDAY));
values.put(COLUMN_SATURDAY, alarm.isRecurring(SATURDAY));
values.put(COLUMN_IGNORE_UPCOMING_RING_TIME, alarm.isIgnoringUpcomingRingTime());
return values;
}
private void notifyContentChanged() {
Log.d(TAG, "notifyContentChanged()");
LocalBroadcastHelper.sendBroadcast(mAppContext,
SQLiteCursorLoader.ACTION_CHANGE_CONTENT);
}
// An alternative method to creating an Alarm from a cursor is to
// make an Alarm constructor that takes an Cursor param. However,
// this method has the advantage of keeping all the constants
// contained within this file. Another advantage is the contents of
// the Alarm class remain as pure Java, which can facilitate unit testing
// because it has no dependence on Cursor, which is part of the Android
// SDK.
public static class AlarmCursor extends CursorWrapper {
public AlarmCursor(Cursor c) {
super(c);
}
/**
* @return an Alarm instance configured for the current row,
* or null if the current row is invalid
*/
public Alarm getAlarm() {
if (isBeforeFirst() || isAfterLast())
return null;
// TODO: Use getColumnIndexOrThrow()
Alarm alarm = Alarm.builder()
.hour(getInt(getColumnIndex(COLUMN_HOUR)))
.minutes(getInt(getColumnIndex(COLUMN_MINUTES)))
.vibrates(isTrue(COLUMN_VIBRATES))
.ringtone(getString(getColumnIndex(COLUMN_RINGTONE)))
.label(getString(getColumnIndex(COLUMN_LABEL)))
.build();
alarm.setId(getLong(getColumnIndex(COLUMN_ID)));
alarm.setEnabled(isTrue(COLUMN_ENABLED));
alarm.setSnoozing(getLong(getColumnIndex(COLUMN_SNOOZING_UNTIL_MILLIS)));
alarm.setRecurring(SUNDAY, isTrue(COLUMN_SUNDAY));
alarm.setRecurring(MONDAY, isTrue(COLUMN_MONDAY));
alarm.setRecurring(TUESDAY, isTrue(COLUMN_TUESDAY));
alarm.setRecurring(WEDNESDAY, isTrue(COLUMN_WEDNESDAY));
alarm.setRecurring(THURSDAY, isTrue(COLUMN_THURSDAY));
alarm.setRecurring(FRIDAY, isTrue(COLUMN_FRIDAY));
alarm.setRecurring(SATURDAY, isTrue(COLUMN_SATURDAY));
alarm.ignoreUpcomingRingTime(isTrue(COLUMN_IGNORE_UPCOMING_RING_TIME));
return alarm;
}
public long getId() {
if (isBeforeFirst() || isAfterLast()) {
Log.e(TAG, "Failed to retrieve id, cursor out of range");
return -1;
}
return getLong(getColumnIndexOrThrow(COLUMN_ID));
}
private boolean isTrue(String columnName) {
return getInt(getColumnIndex(columnName)) == 1;
}
}
}

View File

@ -1,44 +0,0 @@
package com.philliphsu.clock2.model;
import android.content.Context;
import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.model.AlarmDatabaseHelper.AlarmCursor;
import java.util.ArrayList;
import java.util.List;
/**
* Created by Phillip Hsu on 7/2/2016.
*/
@Deprecated
public class AlarmListLoader extends DataListLoader<Alarm, AlarmCursor> {
public AlarmListLoader(Context context) {
super(context);
}
// Why not just have one method where we just call DatabaseManager.getAlarms()?
// I.e. why not load the cursor and extract the Alarms from it all in one?
// I figure if the loader is interrupted in the middle of loading, the underlying
// cursor won't be closed...
// TODO: If we end up doing it this way, then delete the redundant methods in DatabaseManager.
@Override
protected AlarmCursor loadCursor() {
return DatabaseManager.getInstance(getContext()).queryAlarms();
}
@Override
protected List<Alarm> loadItems(AlarmCursor cursor) {
ArrayList<Alarm> alarms = new ArrayList<>();
if (cursor != null) {
while (cursor.moveToNext()) {
alarms.add(cursor.getAlarm());
}
cursor.close();
}
return alarms;
}
}

View File

@ -18,6 +18,6 @@ public class AlarmLoader extends DataLoader<Alarm> {
@Override @Override
public Alarm loadInBackground() { public Alarm loadInBackground() {
return DatabaseManager.getInstance(getContext()).getAlarm(mAlarmId); return new AlarmsTableManager(getContext()).queryItem(mAlarmId).getItem();
} }
} }

View File

@ -7,7 +7,7 @@ import com.philliphsu.clock2.Alarm;
/** /**
* Created by Phillip Hsu on 6/28/2016. * Created by Phillip Hsu on 6/28/2016.
*/ */
public class AlarmsListCursorLoader extends NewSQLiteCursorLoader<Alarm, AlarmCursor> { public class AlarmsListCursorLoader extends SQLiteCursorLoader<Alarm, AlarmCursor> {
public static final String ACTION_CHANGE_CONTENT public static final String ACTION_CHANGE_CONTENT
= "com.philliphsu.clock2.model.AlarmsListCursorLoader.action.CHANGE_CONTENT"; = "com.philliphsu.clock2.model.AlarmsListCursorLoader.action.CHANGE_CONTENT";

View File

@ -1,115 +0,0 @@
package com.philliphsu.clock2.model;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import com.philliphsu.clock2.util.LocalBroadcastHelper;
/**
* Created by Phillip Hsu on 7/29/2016.
*/
@Deprecated
public abstract class BaseDatabaseHelper<T extends ObjectWithId> extends SQLiteOpenHelper {
public static final String COLUMN_ID = "_id";
private final Context mAppContext;
/**
* @param context the Context with which the application context will be retrieved
* @param name the name of the database file. Because this is required by the SQLiteOpenHelper
* constructor, we can't, for instance, have an abstract getDatabaseFileName() that
* subclasses implement and the base class can call on their behalf.
* @param version the version
*/
public BaseDatabaseHelper(Context context, String name,
/*SQLiteDatabase.CursorFactory factory,*/
int version) {
super(context.getApplicationContext(), name, null, version);
mAppContext = context.getApplicationContext();
}
/**
* @return the table managed by this helper
*/
protected abstract String getTableName();
/**
* @return the ContentValues representing the item's properties.
* You do not need to put a mapping for {@link #COLUMN_ID}, since
* the database manages ids for us (if you created your table
* with {@link #COLUMN_ID} as an {@code INTEGER PRIMARY KEY}).
*/
protected abstract ContentValues toContentValues(T item);
/**
* @return optional String specifying the sort order
* to use when querying the database. The default
* implementation returns null, which may return
* queries unordered.
*/
protected String getQuerySortOrder() {
return null;
}
public long insertItem(T item) {
long id = getWritableDatabase().insert(
getTableName(), null, toContentValues(item));
item.setId(id);
notifyContentChanged();
return id;
}
public int updateItem(long id, T newItem) {
newItem.setId(id);
SQLiteDatabase db = getWritableDatabase();
int rowsUpdated = db.update(getTableName(),
toContentValues(newItem),
COLUMN_ID + " = " + id,
null);
notifyContentChanged();
return rowsUpdated;
}
public int deleteItem(T item) {
SQLiteDatabase db = getWritableDatabase();
int rowsDeleted = db.delete(getTableName(),
COLUMN_ID + " = " + item.getId(),
null);
notifyContentChanged();
return rowsDeleted;
}
public Cursor queryItem(long id) {
return queryItems(COLUMN_ID + " = " + id, "1");
}
public Cursor queryItems() {
// Select all rows and columns
return queryItems(null, null);
}
protected Cursor queryItems(String where, String limit) {
return getReadableDatabase().query(getTableName(),
null, // All columns
where, // Selection, i.e. where COLUMN_* = [value we're looking for]
null, // selection args, none b/c id already specified in selection
null, // group by
null, // having
getQuerySortOrder(), // order/sort by
limit); // limit
}
/**
* Broadcasts to any registered receivers that the data backed
* by this helper has changed, and so they should requery and
* update themselves as necessary.
*/
private void notifyContentChanged() {
LocalBroadcastHelper.sendBroadcast(mAppContext,
SQLiteCursorLoader.ACTION_CHANGE_CONTENT);
}
}

View File

@ -12,10 +12,18 @@ public class ClockAppDatabaseHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "clock_app.db"; private static final String DB_NAME = "clock_app.db";
private static final int VERSION_1 = 1; private static final int VERSION_1 = 1;
private static ClockAppDatabaseHelper sDatabaseHelper;
public static ClockAppDatabaseHelper getInstance(Context context) {
if (sDatabaseHelper == null)
sDatabaseHelper = new ClockAppDatabaseHelper(context);
return sDatabaseHelper;
}
/** /**
* @param context the Context with which the application context will be retrieved * @param context the Context with which the application context will be retrieved
*/ */
public ClockAppDatabaseHelper(Context context) { private ClockAppDatabaseHelper(Context context) {
super(context.getApplicationContext(), DB_NAME, null, VERSION_1); super(context.getApplicationContext(), DB_NAME, null, VERSION_1);
} }

View File

@ -1,98 +0,0 @@
package com.philliphsu.clock2.model;
import android.content.Context;
import android.database.CursorWrapper;
import android.support.v4.content.AsyncTaskLoader;
import java.util.List;
/**
* Created by Phillip Hsu on 7/2/2016.
*/
@Deprecated
// TODO: Consider C extends MyTypeBoundedCursorWrapper<D>
public abstract class DataListLoader<D, C extends CursorWrapper> extends AsyncTaskLoader<List<D>> {
private C mCursor;
private List<D> mItems;
public DataListLoader(Context context) {
super(context);
}
protected abstract C loadCursor();
protected abstract List<D> loadItems(C cursor);
@Override
public List<D> loadInBackground() {
mCursor = loadCursor();
if (mCursor != null) {
// Ensure that the content window is filled
// Ensure that the data is available in memory once it is
// passed to the main thread
mCursor.getCount();
}
return loadItems(mCursor);
}
@Override
public void deliverResult(List<D> items) {
if (isReset()) {
// An async query came in while the loader is stopped
if (mCursor != null) {
mCursor.close();
}
return;
}
mItems = items;
if (isStarted()) {
super.deliverResult(items);
}
// TODO: might not be necessary. The analogue of this was
// to close the *old* cursor after assigning the new cursor.
// This is closing the current cursor? But then again, we don't
// care about the cursor after we've extracted the items from it..
// Close the cursor because it is no longer needed.
if (mCursor != null && !mCursor.isClosed()) {
mCursor.close();
}
}
@Override
protected void onStartLoading() {
if (mCursor != null && mItems != null) {
// Deliver any previously loaded data immediately.
deliverResult(mItems);
}
if (takeContentChanged() || mCursor == null || mItems == null) {
forceLoad();
}
}
@Override
protected void onStopLoading() {
// Attempt to cancel the current load task if possible.
cancelLoad();
}
@Override
public void onCanceled(List<D> data) {
if (mCursor != null && !mCursor.isClosed()) {
mCursor.close();
}
}
@Override
protected void onReset() {
super.onReset();
// Ensure the loader is stopped
onStopLoading();
if (mCursor != null && !mCursor.isClosed()) {
mCursor.close();
}
mCursor = null;
mItems = null;
}
}

View File

@ -1,97 +0,0 @@
package com.philliphsu.clock2.model;
import android.content.Context;
import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.model.AlarmDatabaseHelper.AlarmCursor;
import java.util.ArrayList;
/**
* Created by Phillip Hsu on 6/25/2016.
*/
@Deprecated
public class DatabaseManager {
private static DatabaseManager sDatabaseManager;
private final Context mContext;
private final AlarmDatabaseHelper mHelper; // TODO: Call close() when *the app* is exiting.
private DatabaseManager(Context context) {
// TODO: Do we need to hold onto this?
mContext = context.getApplicationContext();
// This will internally get the application context
mHelper = new AlarmDatabaseHelper(context);
}
public static DatabaseManager getInstance(Context context) {
if (sDatabaseManager == null) {
sDatabaseManager = new DatabaseManager(context);
}
return sDatabaseManager;
}
public long insertAlarm(Alarm alarm) {
return mHelper.insertAlarm(alarm);
}
/**
* @deprecated Use {@link #updateAlarm(long, Alarm)} instead, because all
* that is needed from the oldAlarm is its id.
*/
@Deprecated
public int updateAlarm(Alarm oldAlarm, Alarm newAlarm) {
return mHelper.updateAlarm(oldAlarm, newAlarm);
}
public int updateAlarm(long id, Alarm newAlarm) {
return mHelper.updateAlarm(id, newAlarm);
}
public int deleteAlarm(Alarm alarm) {
return mHelper.deleteAlarm(alarm);
}
// Since the query returns at most one row, just return the Alarm the row represents.
public Alarm getAlarm(long id) {
Alarm alarm = null;
AlarmCursor cursor = mHelper.queryAlarm(id);
if (cursor != null && cursor.moveToFirst()) {
alarm = cursor.getAlarm();
cursor.close();
}
return alarm;
}
/** @deprecated Use {@link #queryAlarms()} */
// TODO: Possible redundant. See AlarmListLoader.
@Deprecated
public ArrayList<Alarm> getAlarms() {
return getAlarms(mHelper.queryAlarms());
}
// TODO: Possible redundant. See AlarmListLoader.
public ArrayList<Alarm> getEnabledAlarms() {
return getAlarms(mHelper.queryEnabledAlarms());
}
// TODO: Possible redundant. See AlarmListLoader.
private ArrayList<Alarm> getAlarms(AlarmCursor cursor) {
ArrayList<Alarm> alarms = new ArrayList<>();
if (cursor != null) {
while (cursor.moveToNext()) {
alarms.add(cursor.getAlarm());
}
cursor.close();
}
return alarms;
}
public AlarmCursor queryAlarms() {
return mHelper.queryAlarms();
}
public AlarmCursor queryEnabledAlarms() {
return mHelper.queryEnabledAlarms();
}
}

View File

@ -23,7 +23,7 @@ public abstract class DatabaseTableManager<T extends ObjectWithId> {
public DatabaseTableManager(Context context) { public DatabaseTableManager(Context context) {
// Internally uses the app context // Internally uses the app context
mDbHelper = new ClockAppDatabaseHelper(context); mDbHelper = ClockAppDatabaseHelper.getInstance(context);
mAppContext = context.getApplicationContext(); mAppContext = context.getApplicationContext();
} }
@ -42,7 +42,7 @@ public abstract class DatabaseTableManager<T extends ObjectWithId> {
/** /**
* @return the Intent action that will be used to send broadcasts * @return the Intent action that will be used to send broadcasts
* to our designated {@link NewSQLiteCursorLoader} whenever an * to our designated {@link SQLiteCursorLoader} whenever an
* underlying change to our data is detected. The Loader should * underlying change to our data is detected. The Loader should
* receive the broadcast and reload its data. * receive the broadcast and reload its data.
*/ */
@ -87,7 +87,15 @@ public abstract class DatabaseTableManager<T extends ObjectWithId> {
} }
public Cursor queryItem(long id) { public Cursor queryItem(long id) {
return queryItems(COLUMN_ID + " = " + id, "1"); Cursor c = queryItems(COLUMN_ID + " = " + id, "1");
// Since the query returns at most one row, move the cursor to that row.
// Most callers of this method will not know they have to move the cursor.
// How come we don't need to do this for queries that can potentially return
// multiple rows? Because those returned cursors will almost always be
// displayed by a BaseCursorAdapter, which moves cursors to the appropriate
// positions as it binds VHs.
c.moveToFirst();
return c;
} }
public Cursor queryItems() { public Cursor queryItems() {

View File

@ -1,136 +0,0 @@
package com.philliphsu.clock2.model;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.support.v4.content.AsyncTaskLoader;
import android.util.Log;
import com.philliphsu.clock2.util.LocalBroadcastHelper;
/**
* Created by Phillip Hsu on 6/28/2016.
*
* Efficiently loads and holds a Cursor.
*/
public abstract class NewSQLiteCursorLoader<
T extends ObjectWithId,
C extends BaseItemCursor<T>>
extends AsyncTaskLoader<C> {
private static final String TAG = "SQLiteCursorLoader";
private C mCursor;
private OnContentChangeReceiver mOnContentChangeReceiver;
public NewSQLiteCursorLoader(Context context) {
super(context);
}
protected abstract C loadCursor();
/**
* @return the Intent action that will be registered to this Loader
* for receiving broadcasts about underlying data changes to our
* designated database table
*/
protected abstract String getOnContentChangeAction();
/* Runs on a worker thread */
@Override
public C loadInBackground() {
C cursor = loadCursor();
if (cursor != null) {
// Ensure that the content window is filled
// Ensure that the data is available in memory once it is
// passed to the main thread
cursor.getCount();
}
return cursor;
}
/* Runs on the UI thread */
@Override
public void deliverResult(C cursor) {
if (isReset()) {
// An async query came in while the loader is stopped
if (cursor != null) {
cursor.close();
}
return;
}
Cursor oldCursor = mCursor;
mCursor = cursor;
if (isStarted()) {
super.deliverResult(cursor);
}
// Close the old cursor because it is no longer needed.
// Because an existing cursor may be cached and redelivered, it is important
// to make sure that the old cursor and the new cursor are not the
// same before the old cursor is closed.
if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) {
oldCursor.close();
}
}
// Refer to the docs if you wish to understand the rest of the API as used below.
@Override
protected void onStartLoading() {
if (mCursor != null) {
deliverResult(mCursor);
}
if (mOnContentChangeReceiver == null) {
mOnContentChangeReceiver = new OnContentChangeReceiver();
LocalBroadcastHelper.registerReceiver(getContext(),
mOnContentChangeReceiver, getOnContentChangeAction());
}
if (takeContentChanged() || mCursor == null) {
forceLoad();
}
}
@Override
protected void onStopLoading() {
// Attempt to cancel the current load task if possible.
cancelLoad();
}
@Override
public void onCanceled(C cursor) {
if (cursor != null && !cursor.isClosed()) {
cursor.close();
}
}
@Override
protected void onReset() {
super.onReset();
// Ensure the loader is stopped
onStopLoading();
if (mCursor != null && !mCursor.isClosed()) {
mCursor.close();
}
mCursor = null;
if (mOnContentChangeReceiver != null) {
LocalBroadcastHelper.unregisterReceiver(getContext(),
mOnContentChangeReceiver);
mOnContentChangeReceiver = null;
}
}
private final class OnContentChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "Received content change event");
onContentChanged();
}
}
}

View File

@ -14,24 +14,33 @@ import com.philliphsu.clock2.util.LocalBroadcastHelper;
* *
* Efficiently loads and holds a Cursor. * Efficiently loads and holds a Cursor.
*/ */
public abstract class SQLiteCursorLoader extends AsyncTaskLoader<Cursor> { public abstract class SQLiteCursorLoader<
T extends ObjectWithId,
C extends BaseItemCursor<T>>
extends AsyncTaskLoader<C> {
private static final String TAG = "SQLiteCursorLoader"; private static final String TAG = "SQLiteCursorLoader";
public static final String ACTION_CHANGE_CONTENT = "com.philliphsu.clock2.model.action.CHANGE_CONTENT"; private C mCursor;
private Cursor mCursor;
private OnContentChangeReceiver mOnContentChangeReceiver; private OnContentChangeReceiver mOnContentChangeReceiver;
public SQLiteCursorLoader(Context context) { public SQLiteCursorLoader(Context context) {
super(context); super(context);
} }
protected abstract Cursor loadCursor(); protected abstract C loadCursor();
/**
* @return the Intent action that will be registered to this Loader
* for receiving broadcasts about underlying data changes to our
* designated database table
*/
protected abstract String getOnContentChangeAction();
/* Runs on a worker thread */ /* Runs on a worker thread */
@Override @Override
public Cursor loadInBackground() { public C loadInBackground() {
Cursor cursor = loadCursor(); C cursor = loadCursor();
if (cursor != null) { if (cursor != null) {
// Ensure that the content window is filled // Ensure that the content window is filled
// Ensure that the data is available in memory once it is // Ensure that the data is available in memory once it is
@ -43,7 +52,7 @@ public abstract class SQLiteCursorLoader extends AsyncTaskLoader<Cursor> {
/* Runs on the UI thread */ /* Runs on the UI thread */
@Override @Override
public void deliverResult(Cursor cursor) { public void deliverResult(C cursor) {
if (isReset()) { if (isReset()) {
// An async query came in while the loader is stopped // An async query came in while the loader is stopped
if (cursor != null) { if (cursor != null) {
@ -78,7 +87,7 @@ public abstract class SQLiteCursorLoader extends AsyncTaskLoader<Cursor> {
if (mOnContentChangeReceiver == null) { if (mOnContentChangeReceiver == null) {
mOnContentChangeReceiver = new OnContentChangeReceiver(); mOnContentChangeReceiver = new OnContentChangeReceiver();
LocalBroadcastHelper.registerReceiver(getContext(), LocalBroadcastHelper.registerReceiver(getContext(),
mOnContentChangeReceiver, ACTION_CHANGE_CONTENT); mOnContentChangeReceiver, getOnContentChangeAction());
} }
if (takeContentChanged() || mCursor == null) { if (takeContentChanged() || mCursor == null) {
@ -93,7 +102,7 @@ public abstract class SQLiteCursorLoader extends AsyncTaskLoader<Cursor> {
} }
@Override @Override
public void onCanceled(Cursor cursor) { public void onCanceled(C cursor) {
if (cursor != null && !cursor.isClosed()) { if (cursor != null && !cursor.isClosed()) {
cursor.close(); cursor.close();
} }

View File

@ -1,112 +0,0 @@
package com.philliphsu.clock2.model;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import com.philliphsu.clock2.Timer;
/**
* Created by Phillip Hsu on 7/29/2016.
*/
@Deprecated
public class TimerDatabaseHelper extends BaseDatabaseHelper<Timer> {
private static final String TAG = "TimerDatabaseHelper";
private static final String DB_NAME = "timers.db";
private static final int VERSION_1 = 1;
private static final String TABLE_TIMERS = "timers";
// TODO: Consider making these public, so we can move TimerCursor to its own top-level class.
private static final String COLUMN_HOUR = "hour";
private static final String COLUMN_MINUTE = "minute";
private static final String COLUMN_SECOND = "second";
private static final String COLUMN_LABEL = "label";
// http://stackoverflow.com/q/24183958/5055032
// https://www.sqlite.org/lang_keywords.html
// GROUP is a reserved keyword, so your CREATE TABLE statement
// will not compile if you include this!
// private static final String COLUMN_GROUP = "group";
private static final String COLUMN_END_TIME = "end_time";
private static final String COLUMN_PAUSE_TIME = "pause_time";
private static final String SORT_ORDER =
COLUMN_HOUR + " ASC, "
+ COLUMN_MINUTE + " ASC, "
+ COLUMN_SECOND + " ASC, "
// All else equal, newer timers first
+ COLUMN_ID + " DESC";
public TimerDatabaseHelper(Context context) {
super(context, DB_NAME, VERSION_1);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + TABLE_TIMERS + " ("
+ COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ COLUMN_HOUR + " INTEGER NOT NULL, "
+ COLUMN_MINUTE + " INTEGER NOT NULL, "
+ COLUMN_SECOND + " INTEGER NOT NULL, "
+ COLUMN_LABEL + " TEXT NOT NULL, "
// + COLUMN_GROUP + " TEXT NOT NULL, "
+ COLUMN_END_TIME + " INTEGER NOT NULL, "
+ COLUMN_PAUSE_TIME + " INTEGER NOT NULL);");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// I don't think we need to drop current tables unless you make structural changes
// to the schema in the new version.
}
// =============================================================================================
// Overridden methods can have a more specific return type, as long as that type
// is a subtype of the original return type.
@Override
public TimerCursor queryItem(long id) {
return wrapInTimerCursor(queryItem(id));
}
@Override
public TimerCursor queryItems() {
return wrapInTimerCursor(super.queryItems());
}
@Override
protected TimerCursor queryItems(String where, String limit) {
return wrapInTimerCursor(super.queryItems(where, limit));
}
// =============================================================================================
@Override
protected String getTableName() {
return TABLE_TIMERS;
}
@Override
protected ContentValues toContentValues(Timer timer) {
ContentValues cv = new ContentValues();
cv.put(COLUMN_HOUR, timer.hour());
cv.put(COLUMN_MINUTE, timer.minute());
cv.put(COLUMN_SECOND, timer.second());
cv.put(COLUMN_LABEL, timer.label());
// cv.put(COLUMN_GROUP, timer.group());
cv.put(COLUMN_END_TIME, timer.endTime());
cv.put(COLUMN_PAUSE_TIME, timer.pauseTime());
return cv;
}
@Override
protected String getQuerySortOrder() {
return SORT_ORDER;
}
private TimerCursor wrapInTimerCursor(Cursor c) {
return new TimerCursor(c);
}
}

View File

@ -7,7 +7,7 @@ import com.philliphsu.clock2.Timer;
/** /**
* Created by Phillip Hsu on 7/29/2016. * Created by Phillip Hsu on 7/29/2016.
*/ */
public class TimersListCursorLoader extends NewSQLiteCursorLoader<Timer, TimerCursor> { public class TimersListCursorLoader extends SQLiteCursorLoader<Timer, TimerCursor> {
public static final String ACTION_CHANGE_CONTENT public static final String ACTION_CHANGE_CONTENT
= "com.philliphsu.clock2.model.TimersListCursorLoader.action.CHANGE_CONTENT"; = "com.philliphsu.clock2.model.TimersListCursorLoader.action.CHANGE_CONTENT";

View File

@ -21,7 +21,8 @@ import android.util.Log;
import com.philliphsu.clock2.Alarm; import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.R; import com.philliphsu.clock2.R;
import com.philliphsu.clock2.model.DatabaseManager; import com.philliphsu.clock2.model.AlarmCursor;
import com.philliphsu.clock2.model.AlarmsTableManager;
import com.philliphsu.clock2.util.AlarmController; import com.philliphsu.clock2.util.AlarmController;
import com.philliphsu.clock2.util.AlarmUtils; import com.philliphsu.clock2.util.AlarmUtils;
import com.philliphsu.clock2.util.LocalBroadcastHelper; import com.philliphsu.clock2.util.LocalBroadcastHelper;
@ -96,8 +97,8 @@ public class RingtoneService extends Service { // TODO: abstract this, make subc
new Thread(new Runnable() { new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
mAlarm = checkNotNull(DatabaseManager AlarmCursor cursor = new AlarmsTableManager(RingtoneService.this).queryItem(id);
.getInstance(RingtoneService.this).getAlarm(id)); mAlarm = checkNotNull(cursor.getItem());
playRingtone(); playRingtone();
} }
}).start(); }).start();

View File

@ -12,7 +12,7 @@ import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.PendingAlarmScheduler; import com.philliphsu.clock2.PendingAlarmScheduler;
import com.philliphsu.clock2.R; import com.philliphsu.clock2.R;
import com.philliphsu.clock2.UpcomingAlarmReceiver; import com.philliphsu.clock2.UpcomingAlarmReceiver;
import com.philliphsu.clock2.model.DatabaseManager; import com.philliphsu.clock2.model.AlarmsTableManager;
import com.philliphsu.clock2.ringtone.RingtoneActivity; import com.philliphsu.clock2.ringtone.RingtoneActivity;
import com.philliphsu.clock2.ringtone.RingtoneService; import com.philliphsu.clock2.ringtone.RingtoneService;
@ -34,6 +34,7 @@ public final class AlarmController {
private final Context mAppContext; private final Context mAppContext;
private final View mSnackbarAnchor; private final View mSnackbarAnchor;
private final AlarmsTableManager mTableManager;
/** /**
* *
@ -43,6 +44,7 @@ public final class AlarmController {
public AlarmController(Context context, View snackbarAnchor) { public AlarmController(Context context, View snackbarAnchor) {
mAppContext = context.getApplicationContext(); mAppContext = context.getApplicationContext();
mSnackbarAnchor = snackbarAnchor; mSnackbarAnchor = snackbarAnchor;
mTableManager = new AlarmsTableManager(context);
} }
/** /**
@ -174,7 +176,7 @@ public final class AlarmController {
new Thread(new Runnable() { new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
DatabaseManager.getInstance(mAppContext).updateAlarm(alarm.id(), alarm); mTableManager.updateItem(alarm.id(), alarm);
} }
}).start(); }).start();
} }

View File

@ -13,7 +13,7 @@ import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.PendingAlarmScheduler; import com.philliphsu.clock2.PendingAlarmScheduler;
import com.philliphsu.clock2.R; import com.philliphsu.clock2.R;
import com.philliphsu.clock2.UpcomingAlarmReceiver; import com.philliphsu.clock2.UpcomingAlarmReceiver;
import com.philliphsu.clock2.model.DatabaseManager; import com.philliphsu.clock2.model.AlarmsTableManager;
import com.philliphsu.clock2.ringtone.RingtoneActivity; import com.philliphsu.clock2.ringtone.RingtoneActivity;
import com.philliphsu.clock2.ringtone.RingtoneService; import com.philliphsu.clock2.ringtone.RingtoneService;
@ -30,6 +30,7 @@ import static java.util.concurrent.TimeUnit.HOURS;
* managing the upcoming alarm notification. * managing the upcoming alarm notification.
* *
* TODO: Adapt this to Timers too... * TODO: Adapt this to Timers too...
* TODO: Keep only utility methods. Not the scheduling and cancelling methods.
*/ */
public final class AlarmUtils { public final class AlarmUtils {
private static final String TAG = "AlarmUtils"; private static final String TAG = "AlarmUtils";
@ -219,7 +220,7 @@ public final class AlarmUtils {
new Thread(new Runnable() { new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
DatabaseManager.getInstance(c).updateAlarm(alarm.id(), alarm); new AlarmsTableManager(c).updateItem(alarm.id(), alarm);
} }
}).start(); }).start();
} }