From 4eec10f86ad774475be21cdb76c81d7e33e72e7d Mon Sep 17 00:00:00 2001 From: Phillip Hsu Date: Sun, 3 Jul 2016 03:49:48 -0700 Subject: [PATCH] Wrote Loader for List of Alarms --- .../clock2/PendingAlarmScheduler.java | 1 + .../clock2/RecyclerViewItemChangeHandler.java | 40 ++++++++ .../clock2/alarms/AlarmsFragment.java | 35 ++++--- .../clock2/editalarm/EditAlarmActivity.java | 2 + .../clock2/model/AlarmListLoader.java | 43 ++++++++ .../clock2/model/DataListLoader.java | 97 +++++++++++++++++++ .../clock2/model/DatabaseManager.java | 12 ++- .../clock2/model/SQLiteCursorLoader.java | 19 +++- .../philliphsu/clock2/util/AlarmUtils.java | 1 + 9 files changed, 229 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/com/philliphsu/clock2/RecyclerViewItemChangeHandler.java create mode 100644 app/src/main/java/com/philliphsu/clock2/model/AlarmListLoader.java create mode 100644 app/src/main/java/com/philliphsu/clock2/model/DataListLoader.java diff --git a/app/src/main/java/com/philliphsu/clock2/PendingAlarmScheduler.java b/app/src/main/java/com/philliphsu/clock2/PendingAlarmScheduler.java index 8789462..5d0f129 100644 --- a/app/src/main/java/com/philliphsu/clock2/PendingAlarmScheduler.java +++ b/app/src/main/java/com/philliphsu/clock2/PendingAlarmScheduler.java @@ -26,6 +26,7 @@ public class PendingAlarmScheduler extends BroadcastReceiver { if (id < 0) { throw new IllegalStateException("No alarm id received"); } + // TODO: Do this in the background. AsyncTask? Alarm alarm = checkNotNull(DatabaseManager.getInstance(context).getAlarm(id)); AlarmUtils.scheduleAlarm(context, alarm, false); } diff --git a/app/src/main/java/com/philliphsu/clock2/RecyclerViewItemChangeHandler.java b/app/src/main/java/com/philliphsu/clock2/RecyclerViewItemChangeHandler.java new file mode 100644 index 0000000..0b13f4d --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/RecyclerViewItemChangeHandler.java @@ -0,0 +1,40 @@ +package com.philliphsu.clock2; + +import android.support.v7.widget.RecyclerView; +import android.view.View; + +/** + * Created by Phillip Hsu on 7/1/2016. + */ +public final class RecyclerViewItemChangeHandler { + + private final RecyclerView mRecyclerView; + private final View mSnackbarAnchor; + + /** + * @param recyclerView the RecyclerView for which we should handle item change events + * @param snackbarAnchor an optional anchor for a Snackbar to anchor to + */ + public RecyclerViewItemChangeHandler(RecyclerView recyclerView, View snackbarAnchor) { + mRecyclerView = recyclerView; + mSnackbarAnchor = snackbarAnchor; + } + + /** + * Dispatches an item change event to the item in the + * RecyclerView with the given stable id. + */ + // This won't work if the change on the item would cause it + // to be sorted in a different position! + public void notifyItemChanged(long id) { + if (id < 0) throw new IllegalArgumentException("id < 0"); + int position = mRecyclerView.findViewHolderForItemId(id).getAdapterPosition(); + mRecyclerView.getAdapter().notifyItemChanged(position); + } + + public void notifyItemRemoved(long id) { + if (id < 0) throw new IllegalArgumentException("id < 0"); + int position = mRecyclerView.findViewHolderForItemId(id).getAdapterPosition(); + mRecyclerView.getAdapter().notifyItemRemoved(position); + } +} 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 a36cdf3..f62d5c7 100644 --- a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java +++ b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java @@ -3,11 +3,11 @@ 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.support.design.widget.Snackbar; 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; @@ -19,23 +19,25 @@ import com.philliphsu.clock2.Alarm; import com.philliphsu.clock2.OnListItemInteractionListener; import com.philliphsu.clock2.R; import com.philliphsu.clock2.editalarm.EditAlarmActivity; -import com.philliphsu.clock2.model.AlarmsListCursorLoader; +import com.philliphsu.clock2.model.AlarmListLoader; import com.philliphsu.clock2.model.DatabaseManager; import com.philliphsu.clock2.util.AlarmUtils; +import java.util.Collections; +import java.util.List; + 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, +public class AlarmsFragment extends Fragment implements LoaderCallbacks>, OnListItemInteractionListener { private static final int REQUEST_EDIT_ALARM = 0; public static final int REQUEST_CREATE_ALARM = 1; private static final String TAG = "AlarmsFragment"; - private AlarmsCursorAdapter mCursorAdapter; - private DatabaseManager mDatabaseManager; + private AlarmsAdapter mAdapter; @Bind(R.id.list) RecyclerView mList; @@ -65,7 +67,6 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks, // Initialize the loader to load the list of runs getLoaderManager().initLoader(0, null, this); - mDatabaseManager = DatabaseManager.getInstance(getActivity()); } @Override @@ -76,8 +77,8 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks, // Set the adapter Context context = view.getContext(); mList.setLayoutManager(new LinearLayoutManager(context)); - mCursorAdapter = new AlarmsCursorAdapter(this); - mList.setAdapter(mCursorAdapter); + mAdapter = new AlarmsAdapter(Collections.emptyList(), this); + mList.setAdapter(mAdapter); return view; } @@ -100,20 +101,20 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks, } @Override - public android.support.v4.content.Loader onCreateLoader(int id, Bundle args) { - return new AlarmsListCursorLoader(getActivity()); + public Loader> onCreateLoader(int id, Bundle args) { + return new AlarmListLoader(getActivity()); } @Override - public void onLoadFinished(android.support.v4.content.Loader loader, Cursor data) { - // Called on the main thread after loading is complete - mCursorAdapter.swapCursor(data); + public void onLoadFinished(Loader> loader, List data) { + mAdapter.replaceData(data); } @Override - public void onLoaderReset(android.support.v4.content.Loader loader) { - // The adapter's current cursor should not be used anymore - mCursorAdapter.swapCursor(null); + public void onLoaderReset(Loader> loader) { + // Can't pass in null, because replaceData() will try to add all the elements + // from the given collection, so we would run into an NPE. + mAdapter.replaceData(Collections.emptyList()); } @Override @@ -125,6 +126,7 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks, switch (requestCode) { case REQUEST_CREATE_ALARM: + // TODO: notifyItemInserted? getLoaderManager().restartLoader(0, null, this); case REQUEST_EDIT_ALARM: Alarm deletedAlarm; @@ -132,6 +134,7 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks, EditAlarmActivity.EXTRA_DELETED_ALARM)) != null) { onListItemDeleted(deletedAlarm); } + // TODO: notifyItemRemoved? getLoaderManager().restartLoader(0, null, this); break; default: diff --git a/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmActivity.java b/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmActivity.java index 9fd0d47..0777283 100644 --- a/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmActivity.java +++ b/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmActivity.java @@ -241,8 +241,10 @@ public class EditAlarmActivity extends BaseActivity implements AlarmNumpad.KeyLi Log.d(TAG, "Cancelling old alarm first"); cancelAlarm(mOldAlarm, false); } + // TODO: Do this in the background. AsyncTask? mDatabaseManager.updateAlarm(mOldAlarm.id(), alarm); } else { + // TODO: Do this in the background. AsyncTask? mDatabaseManager.insertAlarm(alarm); } diff --git a/app/src/main/java/com/philliphsu/clock2/model/AlarmListLoader.java b/app/src/main/java/com/philliphsu/clock2/model/AlarmListLoader.java new file mode 100644 index 0000000..f9657cf --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/model/AlarmListLoader.java @@ -0,0 +1,43 @@ +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. + */ +public class AlarmListLoader extends DataListLoader { + + 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 loadItems(AlarmCursor cursor) { + ArrayList alarms = new ArrayList<>(); + if (cursor != null) { + while (cursor.moveToNext()) { + alarms.add(cursor.getAlarm()); + } + cursor.close(); + } + return alarms; + } +} diff --git a/app/src/main/java/com/philliphsu/clock2/model/DataListLoader.java b/app/src/main/java/com/philliphsu/clock2/model/DataListLoader.java new file mode 100644 index 0000000..0b05c7d --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/model/DataListLoader.java @@ -0,0 +1,97 @@ +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. + */ +// TODO: Consider C extends MyTypeBoundedCursorWrapper +public abstract class DataListLoader extends AsyncTaskLoader> { + + private C mCursor; + private List mItems; + + public DataListLoader(Context context) { + super(context); + } + + protected abstract C loadCursor(); + protected abstract List loadItems(C cursor); + + @Override + public List 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 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 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; + } +} 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 5e6ea19..7a4bd0a 100644 --- a/app/src/main/java/com/philliphsu/clock2/model/DatabaseManager.java +++ b/app/src/main/java/com/philliphsu/clock2/model/DatabaseManager.java @@ -63,10 +63,20 @@ public class DatabaseManager { } /** @deprecated Use {@link #queryAlarms()} */ + // TODO: Possible redundant. See AlarmListLoader. @Deprecated public ArrayList getAlarms() { + return getAlarms(mHelper.queryAlarms()); + } + + // TODO: Possible redundant. See AlarmListLoader. + public ArrayList getEnabledAlarms() { + return getAlarms(mHelper.queryEnabledAlarms()); + } + + // TODO: Possible redundant. See AlarmListLoader. + private ArrayList getAlarms(AlarmCursor cursor) { ArrayList alarms = new ArrayList<>(); - AlarmCursor cursor = mHelper.queryAlarms(); if (cursor != null) { while (cursor.moveToNext()) { alarms.add(cursor.getAlarm()); diff --git a/app/src/main/java/com/philliphsu/clock2/model/SQLiteCursorLoader.java b/app/src/main/java/com/philliphsu/clock2/model/SQLiteCursorLoader.java index 45c73d8..0f120c6 100644 --- a/app/src/main/java/com/philliphsu/clock2/model/SQLiteCursorLoader.java +++ b/app/src/main/java/com/philliphsu/clock2/model/SQLiteCursorLoader.java @@ -10,6 +10,8 @@ import android.support.v4.content.AsyncTaskLoader; * Efficiently loads and holds a Cursor. */ public abstract class SQLiteCursorLoader extends AsyncTaskLoader { + private static final String TAG = "SQLiteCursorLoader"; + private Cursor mCursor; public SQLiteCursorLoader(Context context) { @@ -18,6 +20,7 @@ public abstract class SQLiteCursorLoader extends AsyncTaskLoader { protected abstract Cursor loadCursor(); + /* Runs on a worker thread */ @Override public Cursor loadInBackground() { Cursor cursor = loadCursor(); @@ -30,20 +33,28 @@ public abstract class SQLiteCursorLoader extends AsyncTaskLoader { return cursor; } + /* Runs on the UI thread */ @Override - public void deliverResult(Cursor data) { + public void deliverResult(Cursor cursor) { + if (isReset()) { + // An async query came in while the loader is stopped + if (cursor != null) { + cursor.close(); + } + return; + } Cursor oldCursor = mCursor; - mCursor = data; + mCursor = cursor; if (isStarted()) { - super.deliverResult(data); + 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 != data && !oldCursor.isClosed()) { + if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) { oldCursor.close(); } } diff --git a/app/src/main/java/com/philliphsu/clock2/util/AlarmUtils.java b/app/src/main/java/com/philliphsu/clock2/util/AlarmUtils.java index ffe2fc7..059b0df 100644 --- a/app/src/main/java/com/philliphsu/clock2/util/AlarmUtils.java +++ b/app/src/main/java/com/philliphsu/clock2/util/AlarmUtils.java @@ -196,6 +196,7 @@ public final class AlarmUtils { private static void save(Context c, Alarm alarm) { // AlarmsRepository.getInstance(c).saveItems(); // Update the same alarm + // TODO: Do this in the background. AsyncTask? DatabaseManager.getInstance(c).updateAlarm(alarm.id(), alarm); } }