diff --git a/app/src/main/java/com/philliphsu/clock2/Alarm.java b/app/src/main/java/com/philliphsu/clock2/Alarm.java index 217d3ad..2044e01 100644 --- a/app/src/main/java/com/philliphsu/clock2/Alarm.java +++ b/app/src/main/java/com/philliphsu/clock2/Alarm.java @@ -6,6 +6,7 @@ import android.support.annotation.NonNull; import com.google.auto.value.AutoValue; import com.philliphsu.clock2.model.JsonSerializable; +import com.philliphsu.clock2.model.ObjectWithId; import org.json.JSONObject; @@ -21,11 +22,10 @@ import static com.philliphsu.clock2.DaysOfWeek.SUNDAY; * Created by Phillip Hsu on 5/26/2016. */ @AutoValue -public abstract class Alarm implements JsonSerializable, Parcelable { +public abstract class Alarm extends ObjectWithId implements JsonSerializable, Parcelable { private static final int MAX_MINUTES_CAN_SNOOZE = 30; // =================== MUTABLE ======================= - private long id; private long snoozingUntilMillis; private boolean enabled; private final boolean[] recurringDays = new boolean[NUM_DAYS]; @@ -204,18 +204,15 @@ public abstract class Alarm implements JsonSerializable, Parcelable { } public int intId() { - return (int) id; - } - - public void setId(long id) { - this.id = id; + return (int) getId(); } // TODO: Remove method signature from JsonSerializable interface. // TODO: Remove final modifier. + // TODO: Rename to getId() so usages refer to ObjectWithId#getId(), then delete this method. @Override public final long id() { - return id; + return getId(); } @Deprecated @@ -246,7 +243,7 @@ public abstract class Alarm implements JsonSerializable, Parcelable { // because when we recreate the object, we can't initialize // those mutable fields until after we call build(). Values // in the parcel are read in the order they were written. - dest.writeLong(id); + dest.writeLong(getId()); dest.writeLong(snoozingUntilMillis); dest.writeInt(enabled ? 1 : 0); dest.writeBooleanArray(recurringDays); @@ -261,7 +258,7 @@ public abstract class Alarm implements JsonSerializable, Parcelable { .ringtone(in.readString()) .vibrates(in.readInt() != 0) .build(); - alarm.id = in.readLong(); + alarm.setId(in.readLong()); alarm.snoozingUntilMillis = in.readLong(); alarm.enabled = in.readInt() != 0; in.readBooleanArray(alarm.recurringDays); diff --git a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsCursorAdapter.java b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsCursorAdapter.java index 0d579d8..064058f 100644 --- a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsCursorAdapter.java +++ b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsCursorAdapter.java @@ -1,13 +1,10 @@ package com.philliphsu.clock2.alarms; -import android.database.Cursor; -import android.support.v7.widget.RecyclerView; -import android.util.Log; import android.view.ViewGroup; import com.philliphsu.clock2.Alarm; +import com.philliphsu.clock2.BaseCursorAdapter; import com.philliphsu.clock2.OnListItemInteractionListener; -import com.philliphsu.clock2.model.AlarmDatabaseHelper.AlarmCursor; import com.philliphsu.clock2.util.AlarmController; /** @@ -15,63 +12,19 @@ import com.philliphsu.clock2.util.AlarmController; * * TODO: Extend from BaseCursorAdapter */ -public class AlarmsCursorAdapter extends RecyclerView.Adapter { +public class AlarmsCursorAdapter extends BaseCursorAdapter { private static final String TAG = "AlarmsCursorAdapter"; - private final OnListItemInteractionListener mListener; private final AlarmController mAlarmController; - private AlarmCursor mCursor; public AlarmsCursorAdapter(OnListItemInteractionListener listener, AlarmController alarmController) { - mListener = listener; + super(listener); mAlarmController = alarmController; - // Excerpt from docs of notifyDataSetChanged(): - // "RecyclerView will attempt to synthesize [artificially create?] - // visible structural change events [when items are inserted, removed or - // moved] for adapters that report that they have stable IDs when - // [notifyDataSetChanged()] is used. This can help for the purposes of - // animation and visual object persistence [?] but individual item views - // will still need to be rebound and relaid out." - setHasStableIds(true); } @Override - public AlarmViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - return new AlarmViewHolder(parent, mListener, mAlarmController); - } - - @Override - public void onBindViewHolder(AlarmViewHolder holder, int position) { - if (!mCursor.moveToPosition(position)) { - Log.e(TAG, "Failed to bind alarm " + position); - return; - } - holder.onBind(mCursor.getAlarm()); - } - - @Override - public int getItemCount() { - return mCursor == null ? 0 : mCursor.getCount(); - } - - @Override - public long getItemId(int position) { - if (mCursor == null || !mCursor.moveToPosition(position)) { - return super.getItemId(position); // -1 - } - return mCursor.getId(); - } - - // TODO: Cursor param should be the appropriate subclass? - public void swapCursor(Cursor cursor) { - if (mCursor == cursor) { - return; - } - if (mCursor != null) { - mCursor.close(); - } - mCursor = (AlarmCursor) cursor; - notifyDataSetChanged(); + protected AlarmViewHolder onCreateViewHolder(ViewGroup parent, OnListItemInteractionListener listener) { + return new AlarmViewHolder(parent, listener, mAlarmController); } } diff --git a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java index 775f427..3b41e6d 100644 --- a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java +++ b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java @@ -1,26 +1,20 @@ package com.philliphsu.clock2.alarms; import android.app.Activity; -import android.content.Context; import android.content.Intent; -import android.database.Cursor; import android.os.Bundle; import android.os.Handler; -import android.support.v4.app.Fragment; -import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; -import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; -import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; import com.philliphsu.clock2.Alarm; import com.philliphsu.clock2.AsyncItemChangeHandler; -import com.philliphsu.clock2.OnListItemInteractionListener; import com.philliphsu.clock2.R; +import com.philliphsu.clock2.RecyclerViewFragment; import com.philliphsu.clock2.editalarm.EditAlarmActivity; +import com.philliphsu.clock2.model.AlarmCursor; import com.philliphsu.clock2.model.AlarmsListCursorLoader; import com.philliphsu.clock2.util.AlarmController; import com.philliphsu.clock2.util.DelayedSnackbarHandler; @@ -28,10 +22,11 @@ import com.philliphsu.clock2.util.DelayedSnackbarHandler; import butterknife.Bind; import butterknife.ButterKnife; -// TODO: Use native fragments since we're targeting API >=19? -// TODO: Use native LoaderCallbacks. -public class AlarmsFragment extends Fragment implements LoaderCallbacks, - OnListItemInteractionListener, ScrollHandler { +public class AlarmsFragment extends RecyclerViewFragment< + Alarm, + AlarmViewHolder, + AlarmCursor, + AlarmsCursorAdapter> implements ScrollHandler { private static final String TAG = "AlarmsFragment"; private static final int REQUEST_EDIT_ALARM = 0; // Public because MainActivity needs to use it. @@ -76,23 +71,6 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks, mAlarmController = new AlarmController(getActivity(), mSnackbarAnchor); mAsyncItemChangeHandler = new AsyncItemChangeHandler(getActivity(), mSnackbarAnchor, this, mAlarmController); - - getLoaderManager().initLoader(0, null, this); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_alarms, container, false); - ButterKnife.bind(this, view); - - // Set the adapter - Context context = view.getContext(); - mList.setLayoutManager(new LinearLayoutManager(context)); - mAdapter = new AlarmsCursorAdapter(this, mAlarmController); - mList.setAdapter(mAdapter); - - return view; } @Override @@ -110,20 +88,29 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks, } @Override - public Loader onCreateLoader(int id, Bundle args) { + public Loader onCreateLoader(int id, Bundle args) { return new AlarmsListCursorLoader(getActivity()); } @Override - public void onLoadFinished(Loader loader, Cursor data) { - mAdapter.swapCursor(data); + public void onLoadFinished(Loader loader, AlarmCursor data) { + super.onLoadFinished(loader, data); // Scroll to the last modified alarm performScrollToStableId(); } @Override - public void onLoaderReset(Loader loader) { - mAdapter.swapCursor(null); + public void onFabClick() { + Intent intent = new Intent(getActivity(), EditAlarmActivity.class); + startActivityForResult(intent, REQUEST_CREATE_ALARM); + } + + @Override + protected AlarmsCursorAdapter getAdapter() { + if (mAdapter == null) { + mAdapter = new AlarmsCursorAdapter(this, mAlarmController); + } + return mAdapter; } @Override diff --git a/app/src/main/java/com/philliphsu/clock2/model/AlarmCursor.java b/app/src/main/java/com/philliphsu/clock2/model/AlarmCursor.java new file mode 100644 index 0000000..b8aed9c --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/model/AlarmCursor.java @@ -0,0 +1,58 @@ +package com.philliphsu.clock2.model; + +import android.database.Cursor; + +import com.philliphsu.clock2.Alarm; + +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 7/30/2016. + */ +// 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 the contents of +// the Alarm class as pure Java, which can facilitate unit testing +// because it has no dependence on Cursor, which is part of the Android SDK. +public class AlarmCursor extends BaseItemCursor { + private static final String TAG = "AlarmCursor"; + + public AlarmCursor(Cursor c) { + super(c); + } + + /** + * @return an Alarm instance configured for the current row, + * or null if the current row is invalid + */ + @Override + public Alarm getItem() { + if (isBeforeFirst() || isAfterLast()) + return null; + Alarm alarm = Alarm.builder() + .hour(getInt(getColumnIndexOrThrow(AlarmsTable.COLUMN_HOUR))) + .minutes(getInt(getColumnIndexOrThrow(AlarmsTable.COLUMN_MINUTES))) + .vibrates(isTrue(AlarmsTable.COLUMN_VIBRATES)) + .ringtone(getString(getColumnIndexOrThrow(AlarmsTable.COLUMN_RINGTONE))) + .label(getString(getColumnIndexOrThrow(AlarmsTable.COLUMN_LABEL))) + .build(); + alarm.setId(getLong(getColumnIndexOrThrow(AlarmsTable.COLUMN_ID))); + alarm.setEnabled(isTrue(AlarmsTable.COLUMN_ENABLED)); + alarm.setSnoozing(getLong(getColumnIndexOrThrow(AlarmsTable.COLUMN_SNOOZING_UNTIL_MILLIS))); + alarm.setRecurring(SUNDAY, isTrue(AlarmsTable.COLUMN_SUNDAY)); + alarm.setRecurring(MONDAY, isTrue(AlarmsTable.COLUMN_MONDAY)); + alarm.setRecurring(TUESDAY, isTrue(AlarmsTable.COLUMN_TUESDAY)); + alarm.setRecurring(WEDNESDAY, isTrue(AlarmsTable.COLUMN_WEDNESDAY)); + alarm.setRecurring(THURSDAY, isTrue(AlarmsTable.COLUMN_THURSDAY)); + alarm.setRecurring(FRIDAY, isTrue(AlarmsTable.COLUMN_FRIDAY)); + alarm.setRecurring(SATURDAY, isTrue(AlarmsTable.COLUMN_SATURDAY)); + alarm.ignoreUpcomingRingTime(isTrue(AlarmsTable.COLUMN_IGNORE_UPCOMING_RING_TIME)); + return alarm; + } +} diff --git a/app/src/main/java/com/philliphsu/clock2/model/AlarmDatabaseHelper.java b/app/src/main/java/com/philliphsu/clock2/model/AlarmDatabaseHelper.java index a8a5f97..5a2f6f8 100644 --- a/app/src/main/java/com/philliphsu/clock2/model/AlarmDatabaseHelper.java +++ b/app/src/main/java/com/philliphsu/clock2/model/AlarmDatabaseHelper.java @@ -24,6 +24,7 @@ import static com.philliphsu.clock2.DaysOfWeek.WEDNESDAY; * * 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"; diff --git a/app/src/main/java/com/philliphsu/clock2/model/AlarmsListCursorLoader.java b/app/src/main/java/com/philliphsu/clock2/model/AlarmsListCursorLoader.java index 327889a..460367a 100644 --- a/app/src/main/java/com/philliphsu/clock2/model/AlarmsListCursorLoader.java +++ b/app/src/main/java/com/philliphsu/clock2/model/AlarmsListCursorLoader.java @@ -1,19 +1,27 @@ package com.philliphsu.clock2.model; import android.content.Context; -import android.database.Cursor; + +import com.philliphsu.clock2.Alarm; /** * Created by Phillip Hsu on 6/28/2016. */ -public class AlarmsListCursorLoader extends SQLiteCursorLoader { +public class AlarmsListCursorLoader extends NewSQLiteCursorLoader { + public static final String ACTION_CHANGE_CONTENT + = "com.philliphsu.clock2.model.AlarmsListCursorLoader.action.CHANGE_CONTENT"; public AlarmsListCursorLoader(Context context) { super(context); } @Override - protected Cursor loadCursor() { - return DatabaseManager.getInstance(getContext()).queryAlarms(); + protected AlarmCursor loadCursor() { + return new AlarmsTableManager(getContext()).queryItems(); + } + + @Override + protected String getOnContentChangeAction() { + return ACTION_CHANGE_CONTENT; } } diff --git a/app/src/main/java/com/philliphsu/clock2/model/AlarmsTable.java b/app/src/main/java/com/philliphsu/clock2/model/AlarmsTable.java new file mode 100644 index 0000000..5c203b8 --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/model/AlarmsTable.java @@ -0,0 +1,94 @@ +package com.philliphsu.clock2.model; + +import android.database.sqlite.SQLiteDatabase; + +/** + * Created by Phillip Hsu on 7/30/2016. + */ +public final class AlarmsTable { + private AlarmsTable() {} + + // TODO: Consider defining index constants for each column, + // and then removing all cursor getColumnIndex() calls. + public static final String TABLE_ALARMS = "alarms"; + + // TODO: Consider implementing BaseColumns instead to get _id column. + public static final String COLUMN_ID = "_id"; + public static final String COLUMN_HOUR = "hour"; + public static final String COLUMN_MINUTES = "minutes"; + public static final String COLUMN_LABEL = "label"; + public static final String COLUMN_RINGTONE = "ringtone"; + public static final String COLUMN_VIBRATES = "vibrates"; + public static final String COLUMN_ENABLED = "enabled"; + + // TODO: Delete this column, becuase new sort order does not consider it + @Deprecated + public static final String COLUMN_RING_TIME_MILLIS = "ring_time_millis"; + + public static final String COLUMN_SNOOZING_UNTIL_MILLIS = "snoozing_until_millis"; + public static final String COLUMN_SUNDAY = "sunday"; + public static final String COLUMN_MONDAY = "monday"; + public static final String COLUMN_TUESDAY = "tuesday"; + public static final String COLUMN_WEDNESDAY = "wednesday"; + public static final String COLUMN_THURSDAY = "thursday"; + public static final String COLUMN_FRIDAY = "friday"; + public static final String COLUMN_SATURDAY = "saturday"; + public static final String COLUMN_IGNORE_UPCOMING_RING_TIME = "ignore_upcoming_ring_time"; + + // 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"; + + public 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 + + public static void onCreate(SQLiteDatabase db) { + 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);"); + } + + public static void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE_ALARMS); + onCreate(db); + } +} diff --git a/app/src/main/java/com/philliphsu/clock2/model/AlarmsTableManager.java b/app/src/main/java/com/philliphsu/clock2/model/AlarmsTableManager.java new file mode 100644 index 0000000..3cfd046 --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/model/AlarmsTableManager.java @@ -0,0 +1,85 @@ +package com.philliphsu.clock2.model; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; + +import com.philliphsu.clock2.Alarm; + +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 7/30/2016. + */ +public class AlarmsTableManager extends DatabaseTableManager { + + public AlarmsTableManager(Context context) { + super(context); + } + + @Override + protected String getQuerySortOrder() { + return AlarmsTable.NEW_SORT_ORDER; + } + + @Override + public AlarmCursor queryItem(long id) { + return wrapInAlarmCursor(super.queryItem(id)); + } + + @Override + public AlarmCursor queryItems() { + return wrapInAlarmCursor(super.queryItems()); + } + + public AlarmCursor queryEnabledAlarms() { + return queryItems(AlarmsTable.COLUMN_ENABLED + " = 1", null); + } + + @Override + protected AlarmCursor queryItems(String where, String limit) { + return wrapInAlarmCursor(super.queryItems(where, limit)); + } + + @Override + protected String getTableName() { + return AlarmsTable.TABLE_ALARMS; + } + + @Override + protected ContentValues toContentValues(Alarm alarm) { + ContentValues values = new ContentValues(); + values.put(AlarmsTable.COLUMN_HOUR, alarm.hour()); + values.put(AlarmsTable.COLUMN_MINUTES, alarm.minutes()); + values.put(AlarmsTable.COLUMN_LABEL, alarm.label()); + values.put(AlarmsTable.COLUMN_RINGTONE, alarm.ringtone()); + values.put(AlarmsTable.COLUMN_VIBRATES, alarm.vibrates()); + values.put(AlarmsTable.COLUMN_ENABLED, alarm.isEnabled()); + values.put(AlarmsTable.COLUMN_RING_TIME_MILLIS, alarm.ringsAt()); + values.put(AlarmsTable.COLUMN_SNOOZING_UNTIL_MILLIS, alarm.snoozingUntil()); + values.put(AlarmsTable.COLUMN_SUNDAY, alarm.isRecurring(SUNDAY)); + values.put(AlarmsTable.COLUMN_MONDAY, alarm.isRecurring(MONDAY)); + values.put(AlarmsTable.COLUMN_TUESDAY, alarm.isRecurring(TUESDAY)); + values.put(AlarmsTable.COLUMN_WEDNESDAY, alarm.isRecurring(WEDNESDAY)); + values.put(AlarmsTable.COLUMN_THURSDAY, alarm.isRecurring(THURSDAY)); + values.put(AlarmsTable.COLUMN_FRIDAY, alarm.isRecurring(FRIDAY)); + values.put(AlarmsTable.COLUMN_SATURDAY, alarm.isRecurring(SATURDAY)); + values.put(AlarmsTable.COLUMN_IGNORE_UPCOMING_RING_TIME, alarm.isIgnoringUpcomingRingTime()); + return values; + } + + @Override + protected String getOnContentChangeAction() { + return AlarmsListCursorLoader.ACTION_CHANGE_CONTENT; + } + + private AlarmCursor wrapInAlarmCursor(Cursor c) { + return new AlarmCursor(c); + } +} diff --git a/app/src/main/java/com/philliphsu/clock2/model/BaseDatabaseHelper.java b/app/src/main/java/com/philliphsu/clock2/model/BaseDatabaseHelper.java index 5537fba..d420a8e 100644 --- a/app/src/main/java/com/philliphsu/clock2/model/BaseDatabaseHelper.java +++ b/app/src/main/java/com/philliphsu/clock2/model/BaseDatabaseHelper.java @@ -11,6 +11,7 @@ import com.philliphsu.clock2.util.LocalBroadcastHelper; /** * Created by Phillip Hsu on 7/29/2016. */ +@Deprecated public abstract class BaseDatabaseHelper extends SQLiteOpenHelper { public static final String COLUMN_ID = "_id"; diff --git a/app/src/main/java/com/philliphsu/clock2/model/BaseItemCursor.java b/app/src/main/java/com/philliphsu/clock2/model/BaseItemCursor.java index 269dcc4..28067d1 100644 --- a/app/src/main/java/com/philliphsu/clock2/model/BaseItemCursor.java +++ b/app/src/main/java/com/philliphsu/clock2/model/BaseItemCursor.java @@ -25,7 +25,7 @@ public abstract class BaseItemCursor extends CursorWrapp Log.e(TAG, "Failed to retrieve id, cursor out of range"); return -1; } - return getLong(getColumnIndexOrThrow(BaseDatabaseHelper.COLUMN_ID)); + return getLong(getColumnIndexOrThrow("_id")); // TODO: Refer to a constant instead of a hardcoded value } /** diff --git a/app/src/main/java/com/philliphsu/clock2/model/ClockAppDatabaseHelper.java b/app/src/main/java/com/philliphsu/clock2/model/ClockAppDatabaseHelper.java new file mode 100644 index 0000000..6f5b1a0 --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/model/ClockAppDatabaseHelper.java @@ -0,0 +1,33 @@ +package com.philliphsu.clock2.model; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +/** + * Created by Phillip Hsu on 7/30/2016. + */ +public class ClockAppDatabaseHelper extends SQLiteOpenHelper { + private static final String TAG = "ClockAppDatabaseHelper"; + private static final String DB_NAME = "clock_app.db"; + private static final int VERSION_1 = 1; + + /** + * @param context the Context with which the application context will be retrieved + */ + public ClockAppDatabaseHelper(Context context) { + super(context.getApplicationContext(), DB_NAME, null, VERSION_1); + } + + @Override + public void onCreate(SQLiteDatabase db) { + AlarmsTable.onCreate(db); + TimersTable.onCreate(db); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + AlarmsTable.onUpgrade(db, oldVersion, newVersion); + TimersTable.onUpgrade(db, oldVersion, newVersion); + } +} diff --git a/app/src/main/java/com/philliphsu/clock2/model/DatabaseManager.java b/app/src/main/java/com/philliphsu/clock2/model/DatabaseManager.java index 36cac25..4c6f7ab 100644 --- a/app/src/main/java/com/philliphsu/clock2/model/DatabaseManager.java +++ b/app/src/main/java/com/philliphsu/clock2/model/DatabaseManager.java @@ -10,6 +10,7 @@ import java.util.ArrayList; /** * Created by Phillip Hsu on 6/25/2016. */ +@Deprecated public class DatabaseManager { private static DatabaseManager sDatabaseManager; diff --git a/app/src/main/java/com/philliphsu/clock2/model/DatabaseTableManager.java b/app/src/main/java/com/philliphsu/clock2/model/DatabaseTableManager.java new file mode 100644 index 0000000..c5dfd87 --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/model/DatabaseTableManager.java @@ -0,0 +1,112 @@ +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/30/2016. + */ +public abstract class DatabaseTableManager { + // TODO: Consider implementing BaseColumns for your table schemas. + // This column should be present in all table schemas, and the value is simple enough + // we can reproduce it here instead of relying on our subclasses to retrieve it from + // their designated table schema. + private static final String COLUMN_ID = "_id"; + + private final SQLiteOpenHelper mDbHelper; + private final Context mAppContext; + + public DatabaseTableManager(Context context) { + // Internally uses the app context + mDbHelper = new ClockAppDatabaseHelper(context); + 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 the Intent action that will be used to send broadcasts + * to our designated {@link NewSQLiteCursorLoader} whenever an + * underlying change to our data is detected. The Loader should + * receive the broadcast and reload its data. + */ + protected abstract String getOnContentChangeAction(); + + /** + * @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 = mDbHelper.getWritableDatabase().insert( + getTableName(), null, toContentValues(item)); + item.setId(id); + notifyContentChanged(); + return id; + } + + public int updateItem(long id, T newItem) { + newItem.setId(id); + SQLiteDatabase db = mDbHelper.getWritableDatabase(); + int rowsUpdated = db.update(getTableName(), + toContentValues(newItem), + COLUMN_ID + " = " + id, + null); + notifyContentChanged(); + return rowsUpdated; + } + + public int deleteItem(T item) { + SQLiteDatabase db = mDbHelper.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 mDbHelper.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 + } + + private void notifyContentChanged() { + LocalBroadcastHelper.sendBroadcast(mAppContext, getOnContentChangeAction()); + } +} diff --git a/app/src/main/java/com/philliphsu/clock2/model/NewSQLiteCursorLoader.java b/app/src/main/java/com/philliphsu/clock2/model/NewSQLiteCursorLoader.java index 353dea0..6c5048e 100644 --- a/app/src/main/java/com/philliphsu/clock2/model/NewSQLiteCursorLoader.java +++ b/app/src/main/java/com/philliphsu/clock2/model/NewSQLiteCursorLoader.java @@ -17,10 +17,9 @@ import com.philliphsu.clock2.util.LocalBroadcastHelper; public abstract class NewSQLiteCursorLoader< T extends ObjectWithId, C extends BaseItemCursor> - extends AsyncTaskLoader { - private static final String TAG = "SQLiteCursorLoader"; + extends AsyncTaskLoader { - public static final String ACTION_CHANGE_CONTENT = "com.philliphsu.clock2.model.action.CHANGE_CONTENT"; + private static final String TAG = "SQLiteCursorLoader"; private C mCursor; private OnContentChangeReceiver mOnContentChangeReceiver; @@ -31,6 +30,13 @@ public abstract class NewSQLiteCursorLoader< 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() { @@ -81,7 +87,7 @@ public abstract class NewSQLiteCursorLoader< if (mOnContentChangeReceiver == null) { mOnContentChangeReceiver = new OnContentChangeReceiver(); LocalBroadcastHelper.registerReceiver(getContext(), - mOnContentChangeReceiver, ACTION_CHANGE_CONTENT); + mOnContentChangeReceiver, getOnContentChangeAction()); } if (takeContentChanged() || mCursor == null) { diff --git a/app/src/main/java/com/philliphsu/clock2/model/TimerCursor.java b/app/src/main/java/com/philliphsu/clock2/model/TimerCursor.java new file mode 100644 index 0000000..905ca6c --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/model/TimerCursor.java @@ -0,0 +1,30 @@ +package com.philliphsu.clock2.model; + +import android.database.Cursor; + +import com.philliphsu.clock2.Timer; + +/** + * Created by Phillip Hsu on 7/30/2016. + */ +public class TimerCursor extends BaseItemCursor { + + public TimerCursor(Cursor cursor) { + super(cursor); + } + + @Override + public Timer getItem() { + if (isBeforeFirst() || isAfterLast()) + return null; + int hour = getInt(getColumnIndexOrThrow(TimersTable.COLUMN_HOUR)); + int minute = getInt(getColumnIndexOrThrow(TimersTable.COLUMN_MINUTE)); + int second = getInt(getColumnIndexOrThrow(TimersTable.COLUMN_SECOND)); + String label = getString(getColumnIndexOrThrow(TimersTable.COLUMN_LABEL)); +// String group = getString(getColumnIndexOrThrow(COLUMN_GROUP)); + Timer t = Timer.create(hour, minute, second, label, /*group*/""); + t.setEndTime(getInt(getColumnIndexOrThrow(TimersTable.COLUMN_END_TIME))); + t.setPauseTime(getInt(getColumnIndexOrThrow(TimersTable.COLUMN_PAUSE_TIME))); + return t; + } +} diff --git a/app/src/main/java/com/philliphsu/clock2/model/TimerDatabaseHelper.java b/app/src/main/java/com/philliphsu/clock2/model/TimerDatabaseHelper.java index d3f23ec..c137565 100644 --- a/app/src/main/java/com/philliphsu/clock2/model/TimerDatabaseHelper.java +++ b/app/src/main/java/com/philliphsu/clock2/model/TimerDatabaseHelper.java @@ -10,6 +10,7 @@ import com.philliphsu.clock2.Timer; /** * Created by Phillip Hsu on 7/29/2016. */ +@Deprecated public class TimerDatabaseHelper extends BaseDatabaseHelper { private static final String TAG = "TimerDatabaseHelper"; private static final String DB_NAME = "timers.db"; @@ -108,25 +109,4 @@ public class TimerDatabaseHelper extends BaseDatabaseHelper { return new TimerCursor(c); } - public static class TimerCursor extends BaseItemCursor { - - public TimerCursor(Cursor cursor) { - super(cursor); - } - - @Override - public Timer getItem() { - if (isBeforeFirst() || isAfterLast()) - return null; - int hour = getInt(getColumnIndexOrThrow(COLUMN_HOUR)); - int minute = getInt(getColumnIndexOrThrow(COLUMN_MINUTE)); - int second = getInt(getColumnIndexOrThrow(COLUMN_SECOND)); - String label = getString(getColumnIndexOrThrow(COLUMN_LABEL)); -// String group = getString(getColumnIndexOrThrow(COLUMN_GROUP)); - Timer t = Timer.create(hour, minute, second, label, /*group*/""); - t.setEndTime(getInt(getColumnIndexOrThrow(COLUMN_END_TIME))); - t.setPauseTime(getInt(getColumnIndexOrThrow(COLUMN_PAUSE_TIME))); - return t; - } - } } diff --git a/app/src/main/java/com/philliphsu/clock2/model/TimersListCursorLoader.java b/app/src/main/java/com/philliphsu/clock2/model/TimersListCursorLoader.java index a41e028..22d0755 100644 --- a/app/src/main/java/com/philliphsu/clock2/model/TimersListCursorLoader.java +++ b/app/src/main/java/com/philliphsu/clock2/model/TimersListCursorLoader.java @@ -7,14 +7,21 @@ import com.philliphsu.clock2.Timer; /** * Created by Phillip Hsu on 7/29/2016. */ -public class TimersListCursorLoader extends NewSQLiteCursorLoader { +public class TimersListCursorLoader extends NewSQLiteCursorLoader { + public static final String ACTION_CHANGE_CONTENT + = "com.philliphsu.clock2.model.TimersListCursorLoader.action.CHANGE_CONTENT"; public TimersListCursorLoader(Context context) { super(context); } @Override - protected TimerDatabaseHelper.TimerCursor loadCursor() { - return new TimerDatabaseHelper(getContext()).queryItems(); + protected TimerCursor loadCursor() { + return new TimersTableManager(getContext()).queryItems(); + } + + @Override + protected String getOnContentChangeAction() { + return ACTION_CHANGE_CONTENT; } } diff --git a/app/src/main/java/com/philliphsu/clock2/model/TimersTable.java b/app/src/main/java/com/philliphsu/clock2/model/TimersTable.java new file mode 100644 index 0000000..184ff7b --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/model/TimersTable.java @@ -0,0 +1,54 @@ +package com.philliphsu.clock2.model; + +import android.database.sqlite.SQLiteDatabase; + +/** + * Created by Phillip Hsu on 7/30/2016. + */ +public final class TimersTable { + private TimersTable() {} + + // TODO: Consider defining index constants for each column, + // and then removing all cursor getColumnIndex() calls. + public static final String TABLE_TIMERS = "timers"; + + // TODO: Consider implementing BaseColumns instead to get _id column. + public static final String COLUMN_ID = "_id"; + public static final String COLUMN_HOUR = "hour"; + public static final String COLUMN_MINUTE = "minute"; + public static final String COLUMN_SECOND = "second"; + public 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! +// public static final String COLUMN_GROUP = "group"; + + public static final String COLUMN_END_TIME = "end_time"; + public static final String COLUMN_PAUSE_TIME = "pause_time"; + + public static final String SORT_ORDER = + COLUMN_HOUR + " ASC, " + + COLUMN_MINUTE + " ASC, " + + COLUMN_SECOND + " ASC, " + // All else equal, newer timers first + + COLUMN_ID + " DESC"; + + public static 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);"); + } + + public static void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + db.execSQL("DROP TABLE IF EXISTS " + TABLE_TIMERS); + onCreate(db); + } +} diff --git a/app/src/main/java/com/philliphsu/clock2/model/TimersTableManager.java b/app/src/main/java/com/philliphsu/clock2/model/TimersTableManager.java new file mode 100644 index 0000000..d7e1e89 --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/model/TimersTableManager.java @@ -0,0 +1,64 @@ +package com.philliphsu.clock2.model; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; + +import com.philliphsu.clock2.Timer; + +/** + * Created by Phillip Hsu on 7/30/2016. + */ +public class TimersTableManager extends DatabaseTableManager { + + public TimersTableManager(Context context) { + super(context); + } + + @Override + protected String getQuerySortOrder() { + return TimersTable.SORT_ORDER; + } + + @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 TimersTable.TABLE_TIMERS; + } + + @Override + protected ContentValues toContentValues(Timer timer) { + ContentValues cv = new ContentValues(); + cv.put(TimersTable.COLUMN_HOUR, timer.hour()); + cv.put(TimersTable.COLUMN_MINUTE, timer.minute()); + cv.put(TimersTable.COLUMN_SECOND, timer.second()); + cv.put(TimersTable.COLUMN_LABEL, timer.label()); +// cv.put(TimersTable.COLUMN_GROUP, timer.group()); + cv.put(TimersTable.COLUMN_END_TIME, timer.endTime()); + cv.put(TimersTable.COLUMN_PAUSE_TIME, timer.pauseTime()); + return cv; + } + + @Override + protected String getOnContentChangeAction() { + return TimersListCursorLoader.ACTION_CHANGE_CONTENT; + } + + private TimerCursor wrapInTimerCursor(Cursor c) { + return new TimerCursor(c); + } +} diff --git a/app/src/main/java/com/philliphsu/clock2/timers/TimersCursorAdapter.java b/app/src/main/java/com/philliphsu/clock2/timers/TimersCursorAdapter.java index 9eacf5a..4778c45 100644 --- a/app/src/main/java/com/philliphsu/clock2/timers/TimersCursorAdapter.java +++ b/app/src/main/java/com/philliphsu/clock2/timers/TimersCursorAdapter.java @@ -5,12 +5,12 @@ import android.view.ViewGroup; import com.philliphsu.clock2.BaseCursorAdapter; import com.philliphsu.clock2.OnListItemInteractionListener; import com.philliphsu.clock2.Timer; -import com.philliphsu.clock2.model.TimerDatabaseHelper; +import com.philliphsu.clock2.model.TimerCursor; /** * Created by Phillip Hsu on 7/29/2016. */ -public class TimersCursorAdapter extends BaseCursorAdapter { +public class TimersCursorAdapter extends BaseCursorAdapter { public TimersCursorAdapter(OnListItemInteractionListener listener) { super(listener); diff --git a/app/src/main/java/com/philliphsu/clock2/timers/TimersFragment.java b/app/src/main/java/com/philliphsu/clock2/timers/TimersFragment.java index df324b0..295deb6 100644 --- a/app/src/main/java/com/philliphsu/clock2/timers/TimersFragment.java +++ b/app/src/main/java/com/philliphsu/clock2/timers/TimersFragment.java @@ -8,13 +8,14 @@ import android.support.v4.content.Loader; import com.philliphsu.clock2.RecyclerViewFragment; import com.philliphsu.clock2.Timer; import com.philliphsu.clock2.edittimer.EditTimerActivity; +import com.philliphsu.clock2.model.TimerCursor; import com.philliphsu.clock2.model.TimerDatabaseHelper; import com.philliphsu.clock2.model.TimersListCursorLoader; public class TimersFragment extends RecyclerViewFragment< Timer, TimerViewHolder, - TimerDatabaseHelper.TimerCursor, + TimerCursor, TimersCursorAdapter> { public static final int REQUEST_CREATE_TIMER = 0; @@ -38,7 +39,7 @@ public class TimersFragment extends RecyclerViewFragment< } @Override - public Loader onCreateLoader(int id, Bundle args) { + public Loader onCreateLoader(int id, Bundle args) { return new TimersListCursorLoader(getActivity()); }