RecyclerViewFragment and other abstract classes in use for alarms and timers

This commit is contained in:
Phillip Hsu 2016-07-30 14:36:33 -07:00
parent b43ff03662
commit 066ac67325
21 changed files with 605 additions and 133 deletions

View File

@ -6,6 +6,7 @@ import android.support.annotation.NonNull;
import com.google.auto.value.AutoValue; import com.google.auto.value.AutoValue;
import com.philliphsu.clock2.model.JsonSerializable; import com.philliphsu.clock2.model.JsonSerializable;
import com.philliphsu.clock2.model.ObjectWithId;
import org.json.JSONObject; import org.json.JSONObject;
@ -21,11 +22,10 @@ import static com.philliphsu.clock2.DaysOfWeek.SUNDAY;
* Created by Phillip Hsu on 5/26/2016. * Created by Phillip Hsu on 5/26/2016.
*/ */
@AutoValue @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; private static final int MAX_MINUTES_CAN_SNOOZE = 30;
// =================== MUTABLE ======================= // =================== MUTABLE =======================
private long id;
private long snoozingUntilMillis; private long snoozingUntilMillis;
private boolean enabled; private boolean enabled;
private final boolean[] recurringDays = new boolean[NUM_DAYS]; private final boolean[] recurringDays = new boolean[NUM_DAYS];
@ -204,18 +204,15 @@ public abstract class Alarm implements JsonSerializable, Parcelable {
} }
public int intId() { public int intId() {
return (int) id; return (int) getId();
}
public void setId(long id) {
this.id = id;
} }
// TODO: Remove method signature from JsonSerializable interface. // TODO: Remove method signature from JsonSerializable interface.
// TODO: Remove final modifier. // TODO: Remove final modifier.
// TODO: Rename to getId() so usages refer to ObjectWithId#getId(), then delete this method.
@Override @Override
public final long id() { public final long id() {
return id; return getId();
} }
@Deprecated @Deprecated
@ -246,7 +243,7 @@ public abstract class Alarm implements JsonSerializable, Parcelable {
// because when we recreate the object, we can't initialize // because when we recreate the object, we can't initialize
// those mutable fields until after we call build(). Values // those mutable fields until after we call build(). Values
// in the parcel are read in the order they were written. // in the parcel are read in the order they were written.
dest.writeLong(id); dest.writeLong(getId());
dest.writeLong(snoozingUntilMillis); dest.writeLong(snoozingUntilMillis);
dest.writeInt(enabled ? 1 : 0); dest.writeInt(enabled ? 1 : 0);
dest.writeBooleanArray(recurringDays); dest.writeBooleanArray(recurringDays);
@ -261,7 +258,7 @@ public abstract class Alarm implements JsonSerializable, Parcelable {
.ringtone(in.readString()) .ringtone(in.readString())
.vibrates(in.readInt() != 0) .vibrates(in.readInt() != 0)
.build(); .build();
alarm.id = in.readLong(); alarm.setId(in.readLong());
alarm.snoozingUntilMillis = in.readLong(); alarm.snoozingUntilMillis = in.readLong();
alarm.enabled = in.readInt() != 0; alarm.enabled = in.readInt() != 0;
in.readBooleanArray(alarm.recurringDays); in.readBooleanArray(alarm.recurringDays);

View File

@ -1,13 +1,10 @@
package com.philliphsu.clock2.alarms; package com.philliphsu.clock2.alarms;
import android.database.Cursor;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.philliphsu.clock2.Alarm; import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.BaseCursorAdapter;
import com.philliphsu.clock2.OnListItemInteractionListener; import com.philliphsu.clock2.OnListItemInteractionListener;
import com.philliphsu.clock2.model.AlarmDatabaseHelper.AlarmCursor;
import com.philliphsu.clock2.util.AlarmController; import com.philliphsu.clock2.util.AlarmController;
/** /**
@ -15,63 +12,19 @@ import com.philliphsu.clock2.util.AlarmController;
* *
* TODO: Extend from BaseCursorAdapter * TODO: Extend from BaseCursorAdapter
*/ */
public class AlarmsCursorAdapter extends RecyclerView.Adapter<AlarmViewHolder> { public class AlarmsCursorAdapter extends BaseCursorAdapter<Alarm, AlarmViewHolder, com.philliphsu.clock2.model.AlarmCursor> {
private static final String TAG = "AlarmsCursorAdapter"; private static final String TAG = "AlarmsCursorAdapter";
private final OnListItemInteractionListener<Alarm> mListener;
private final AlarmController mAlarmController; private final AlarmController mAlarmController;
private AlarmCursor mCursor;
public AlarmsCursorAdapter(OnListItemInteractionListener<Alarm> listener, public AlarmsCursorAdapter(OnListItemInteractionListener<Alarm> listener,
AlarmController alarmController) { AlarmController alarmController) {
mListener = listener; super(listener);
mAlarmController = alarmController; 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 @Override
public AlarmViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { protected AlarmViewHolder onCreateViewHolder(ViewGroup parent, OnListItemInteractionListener<Alarm> listener) {
return new AlarmViewHolder(parent, mListener, mAlarmController); return new AlarmViewHolder(parent, listener, 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();
} }
} }

View File

@ -1,26 +1,20 @@
package com.philliphsu.clock2.alarms; package com.philliphsu.clock2.alarms;
import android.app.Activity; import android.app.Activity;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; 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.v4.content.Loader;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import com.philliphsu.clock2.Alarm; import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.AsyncItemChangeHandler; import com.philliphsu.clock2.AsyncItemChangeHandler;
import com.philliphsu.clock2.OnListItemInteractionListener;
import com.philliphsu.clock2.R; import com.philliphsu.clock2.R;
import com.philliphsu.clock2.RecyclerViewFragment;
import com.philliphsu.clock2.editalarm.EditAlarmActivity; import com.philliphsu.clock2.editalarm.EditAlarmActivity;
import com.philliphsu.clock2.model.AlarmCursor;
import com.philliphsu.clock2.model.AlarmsListCursorLoader; import com.philliphsu.clock2.model.AlarmsListCursorLoader;
import com.philliphsu.clock2.util.AlarmController; import com.philliphsu.clock2.util.AlarmController;
import com.philliphsu.clock2.util.DelayedSnackbarHandler; import com.philliphsu.clock2.util.DelayedSnackbarHandler;
@ -28,10 +22,11 @@ import com.philliphsu.clock2.util.DelayedSnackbarHandler;
import butterknife.Bind; import butterknife.Bind;
import butterknife.ButterKnife; import butterknife.ButterKnife;
// TODO: Use native fragments since we're targeting API >=19? public class AlarmsFragment extends RecyclerViewFragment<
// TODO: Use native LoaderCallbacks. Alarm,
public class AlarmsFragment extends Fragment implements LoaderCallbacks<Cursor>, AlarmViewHolder,
OnListItemInteractionListener<Alarm>, ScrollHandler { AlarmCursor,
AlarmsCursorAdapter> implements ScrollHandler {
private static final String TAG = "AlarmsFragment"; private static final String TAG = "AlarmsFragment";
private static final int REQUEST_EDIT_ALARM = 0; private static final int REQUEST_EDIT_ALARM = 0;
// Public because MainActivity needs to use it. // Public because MainActivity needs to use it.
@ -76,23 +71,6 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks<Cursor>,
mAlarmController = new AlarmController(getActivity(), mSnackbarAnchor); mAlarmController = new AlarmController(getActivity(), mSnackbarAnchor);
mAsyncItemChangeHandler = new AsyncItemChangeHandler(getActivity(), mAsyncItemChangeHandler = new AsyncItemChangeHandler(getActivity(),
mSnackbarAnchor, this, mAlarmController); 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 @Override
@ -110,20 +88,29 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks<Cursor>,
} }
@Override @Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) { public Loader<AlarmCursor> onCreateLoader(int id, Bundle args) {
return new AlarmsListCursorLoader(getActivity()); return new AlarmsListCursorLoader(getActivity());
} }
@Override @Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) { public void onLoadFinished(Loader<AlarmCursor> loader, AlarmCursor data) {
mAdapter.swapCursor(data); super.onLoadFinished(loader, data);
// Scroll to the last modified alarm // Scroll to the last modified alarm
performScrollToStableId(); performScrollToStableId();
} }
@Override @Override
public void onLoaderReset(Loader<Cursor> loader) { public void onFabClick() {
mAdapter.swapCursor(null); 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 @Override

View File

@ -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<Alarm> {
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;
}
}

View File

@ -24,6 +24,7 @@ import static com.philliphsu.clock2.DaysOfWeek.WEDNESDAY;
* *
* TODO: We can generalize this class to all data models, not just Alarms. * TODO: We can generalize this class to all data models, not just Alarms.
*/ */
@Deprecated
public class AlarmDatabaseHelper extends SQLiteOpenHelper { public class AlarmDatabaseHelper extends SQLiteOpenHelper {
private static final String TAG = "AlarmDatabaseHelper"; private static final String TAG = "AlarmDatabaseHelper";
private static final String DB_NAME = "alarms.db"; private static final String DB_NAME = "alarms.db";

View File

@ -1,19 +1,27 @@
package com.philliphsu.clock2.model; package com.philliphsu.clock2.model;
import android.content.Context; import android.content.Context;
import android.database.Cursor;
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 SQLiteCursorLoader { public class AlarmsListCursorLoader extends NewSQLiteCursorLoader<Alarm, AlarmCursor> {
public static final String ACTION_CHANGE_CONTENT
= "com.philliphsu.clock2.model.AlarmsListCursorLoader.action.CHANGE_CONTENT";
public AlarmsListCursorLoader(Context context) { public AlarmsListCursorLoader(Context context) {
super(context); super(context);
} }
@Override @Override
protected Cursor loadCursor() { protected AlarmCursor loadCursor() {
return DatabaseManager.getInstance(getContext()).queryAlarms(); return new AlarmsTableManager(getContext()).queryItems();
}
@Override
protected String getOnContentChangeAction() {
return ACTION_CHANGE_CONTENT;
} }
} }

View File

@ -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);
}
}

View File

@ -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<Alarm> {
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);
}
}

View File

@ -11,6 +11,7 @@ import com.philliphsu.clock2.util.LocalBroadcastHelper;
/** /**
* Created by Phillip Hsu on 7/29/2016. * Created by Phillip Hsu on 7/29/2016.
*/ */
@Deprecated
public abstract class BaseDatabaseHelper<T extends ObjectWithId> extends SQLiteOpenHelper { public abstract class BaseDatabaseHelper<T extends ObjectWithId> extends SQLiteOpenHelper {
public static final String COLUMN_ID = "_id"; public static final String COLUMN_ID = "_id";

View File

@ -25,7 +25,7 @@ public abstract class BaseItemCursor<T extends ObjectWithId> extends CursorWrapp
Log.e(TAG, "Failed to retrieve id, cursor out of range"); Log.e(TAG, "Failed to retrieve id, cursor out of range");
return -1; return -1;
} }
return getLong(getColumnIndexOrThrow(BaseDatabaseHelper.COLUMN_ID)); return getLong(getColumnIndexOrThrow("_id")); // TODO: Refer to a constant instead of a hardcoded value
} }
/** /**

View File

@ -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);
}
}

View File

@ -10,6 +10,7 @@ import java.util.ArrayList;
/** /**
* Created by Phillip Hsu on 6/25/2016. * Created by Phillip Hsu on 6/25/2016.
*/ */
@Deprecated
public class DatabaseManager { public class DatabaseManager {
private static DatabaseManager sDatabaseManager; private static DatabaseManager sDatabaseManager;

View File

@ -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<T extends ObjectWithId> {
// 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());
}
}

View File

@ -17,10 +17,9 @@ import com.philliphsu.clock2.util.LocalBroadcastHelper;
public abstract class NewSQLiteCursorLoader< public abstract class NewSQLiteCursorLoader<
T extends ObjectWithId, T extends ObjectWithId,
C extends BaseItemCursor<T>> C extends BaseItemCursor<T>>
extends AsyncTaskLoader<C> { extends AsyncTaskLoader<C> {
private static final String TAG = "SQLiteCursorLoader";
public static final String ACTION_CHANGE_CONTENT = "com.philliphsu.clock2.model.action.CHANGE_CONTENT"; private static final String TAG = "SQLiteCursorLoader";
private C mCursor; private C mCursor;
private OnContentChangeReceiver mOnContentChangeReceiver; private OnContentChangeReceiver mOnContentChangeReceiver;
@ -31,6 +30,13 @@ public abstract class NewSQLiteCursorLoader<
protected abstract C 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 C loadInBackground() { public C loadInBackground() {
@ -81,7 +87,7 @@ public abstract class NewSQLiteCursorLoader<
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) {

View File

@ -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<Timer> {
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;
}
}

View File

@ -10,6 +10,7 @@ import com.philliphsu.clock2.Timer;
/** /**
* Created by Phillip Hsu on 7/29/2016. * Created by Phillip Hsu on 7/29/2016.
*/ */
@Deprecated
public class TimerDatabaseHelper extends BaseDatabaseHelper<Timer> { public class TimerDatabaseHelper extends BaseDatabaseHelper<Timer> {
private static final String TAG = "TimerDatabaseHelper"; private static final String TAG = "TimerDatabaseHelper";
private static final String DB_NAME = "timers.db"; private static final String DB_NAME = "timers.db";
@ -108,25 +109,4 @@ public class TimerDatabaseHelper extends BaseDatabaseHelper<Timer> {
return new TimerCursor(c); return new TimerCursor(c);
} }
public static class TimerCursor extends BaseItemCursor<Timer> {
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;
}
}
} }

View File

@ -7,14 +7,21 @@ 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, TimerDatabaseHelper.TimerCursor> { public class TimersListCursorLoader extends NewSQLiteCursorLoader<Timer, TimerCursor> {
public static final String ACTION_CHANGE_CONTENT
= "com.philliphsu.clock2.model.TimersListCursorLoader.action.CHANGE_CONTENT";
public TimersListCursorLoader(Context context) { public TimersListCursorLoader(Context context) {
super(context); super(context);
} }
@Override @Override
protected TimerDatabaseHelper.TimerCursor loadCursor() { protected TimerCursor loadCursor() {
return new TimerDatabaseHelper(getContext()).queryItems(); return new TimersTableManager(getContext()).queryItems();
}
@Override
protected String getOnContentChangeAction() {
return ACTION_CHANGE_CONTENT;
} }
} }

View File

@ -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);
}
}

View File

@ -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<Timer> {
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);
}
}

View File

@ -5,12 +5,12 @@ import android.view.ViewGroup;
import com.philliphsu.clock2.BaseCursorAdapter; import com.philliphsu.clock2.BaseCursorAdapter;
import com.philliphsu.clock2.OnListItemInteractionListener; import com.philliphsu.clock2.OnListItemInteractionListener;
import com.philliphsu.clock2.Timer; 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. * Created by Phillip Hsu on 7/29/2016.
*/ */
public class TimersCursorAdapter extends BaseCursorAdapter<Timer, TimerViewHolder, TimerDatabaseHelper.TimerCursor> { public class TimersCursorAdapter extends BaseCursorAdapter<Timer, TimerViewHolder, TimerCursor> {
public TimersCursorAdapter(OnListItemInteractionListener<Timer> listener) { public TimersCursorAdapter(OnListItemInteractionListener<Timer> listener) {
super(listener); super(listener);

View File

@ -8,13 +8,14 @@ import android.support.v4.content.Loader;
import com.philliphsu.clock2.RecyclerViewFragment; import com.philliphsu.clock2.RecyclerViewFragment;
import com.philliphsu.clock2.Timer; import com.philliphsu.clock2.Timer;
import com.philliphsu.clock2.edittimer.EditTimerActivity; import com.philliphsu.clock2.edittimer.EditTimerActivity;
import com.philliphsu.clock2.model.TimerCursor;
import com.philliphsu.clock2.model.TimerDatabaseHelper; import com.philliphsu.clock2.model.TimerDatabaseHelper;
import com.philliphsu.clock2.model.TimersListCursorLoader; import com.philliphsu.clock2.model.TimersListCursorLoader;
public class TimersFragment extends RecyclerViewFragment< public class TimersFragment extends RecyclerViewFragment<
Timer, Timer,
TimerViewHolder, TimerViewHolder,
TimerDatabaseHelper.TimerCursor, TimerCursor,
TimersCursorAdapter> { TimersCursorAdapter> {
public static final int REQUEST_CREATE_TIMER = 0; public static final int REQUEST_CREATE_TIMER = 0;
@ -38,7 +39,7 @@ public class TimersFragment extends RecyclerViewFragment<
} }
@Override @Override
public Loader<TimerDatabaseHelper.TimerCursor> onCreateLoader(int id, Bundle args) { public Loader<TimerCursor> onCreateLoader(int id, Bundle args) {
return new TimersListCursorLoader(getActivity()); return new TimersListCursorLoader(getActivity());
} }