Local broadcast receiver used as observer in loader

This commit is contained in:
Phillip Hsu 2016-07-06 00:40:52 -07:00
parent 9fb4727051
commit cf46160b1e
6 changed files with 92 additions and 45 deletions

View File

@ -5,7 +5,6 @@ import android.os.AsyncTask;
import android.support.design.widget.Snackbar; import android.support.design.widget.Snackbar;
import android.view.View; import android.view.View;
import com.philliphsu.clock2.alarms.AlarmsAdapter;
import com.philliphsu.clock2.model.DatabaseManager; import com.philliphsu.clock2.model.DatabaseManager;
import com.philliphsu.clock2.util.AlarmUtils; import com.philliphsu.clock2.util.AlarmUtils;
@ -14,18 +13,16 @@ import com.philliphsu.clock2.util.AlarmUtils;
* *
* TODO: Generify this class for any item. * TODO: Generify this class for any item.
*/ */
public final class AsyncRecyclerViewItemChangeHandler { public final class AsyncItemChangeHandler {
private final Context mContext; private final Context mContext;
private final AlarmsAdapter mAdapter;
private final View mSnackbarAnchor; private final View mSnackbarAnchor;
/** /**
* @param snackbarAnchor an optional anchor for a Snackbar to anchor to * @param snackbarAnchor an optional anchor for a Snackbar to anchor to
*/ */
public AsyncRecyclerViewItemChangeHandler(AlarmsAdapter adapter, View snackbarAnchor) { public AsyncItemChangeHandler(Context context, View snackbarAnchor) {
mContext = snackbarAnchor.getContext(); mContext = context.getApplicationContext(); // to prevent memory leaks
mAdapter = adapter;
mSnackbarAnchor = snackbarAnchor; mSnackbarAnchor = snackbarAnchor;
} }
@ -39,7 +36,7 @@ public final class AsyncRecyclerViewItemChangeHandler {
@Override @Override
protected void onPostExecute(Long aLong) { protected void onPostExecute(Long aLong) {
// TODO: Snackbar/Toast here? If so, remove the code in AlarmUtils.scheduleAlarm() that does it. // TODO: Snackbar/Toast here? If so, remove the code in AlarmUtils.scheduleAlarm() that does it.
mAdapter.addItem(alarm); // Then, consider scheduling the alarm in the background.
AlarmUtils.scheduleAlarm(mContext, alarm, true); AlarmUtils.scheduleAlarm(mContext, alarm, true);
} }
}.execute(); }.execute();
@ -49,8 +46,6 @@ public final class AsyncRecyclerViewItemChangeHandler {
new AsyncTask<Void, Void, Integer>() { new AsyncTask<Void, Void, Integer>() {
@Override @Override
protected Integer doInBackground(Void... params) { protected Integer doInBackground(Void... params) {
mAdapter.updateItem(oldAlarm, newAlarm);
// Save the item to the db
return DatabaseManager.getInstance(mContext).updateAlarm(oldAlarm.id(), newAlarm); return DatabaseManager.getInstance(mContext).updateAlarm(oldAlarm.id(), newAlarm);
} }
@ -65,14 +60,11 @@ public final class AsyncRecyclerViewItemChangeHandler {
new AsyncTask<Void, Void, Integer>() { new AsyncTask<Void, Void, Integer>() {
@Override @Override
protected Integer doInBackground(Void... params) { protected Integer doInBackground(Void... params) {
mAdapter.removeItem(alarm);
// Save the item to the db
return DatabaseManager.getInstance(mContext).deleteAlarm(alarm); return DatabaseManager.getInstance(mContext).deleteAlarm(alarm);
} }
@Override @Override
protected void onPostExecute(Integer integer) { protected void onPostExecute(Integer integer) {
// TODO: Snackbar
if (mSnackbarAnchor != null) { if (mSnackbarAnchor != null) {
String message = mContext.getString(R.string.snackbar_item_deleted, String message = mContext.getString(R.string.snackbar_item_deleted,
mContext.getString(R.string.alarm)); mContext.getString(R.string.alarm));

View File

@ -3,8 +3,8 @@ package com.philliphsu.clock2.alarms;
import android.app.Activity; import android.app.Activity;
import android.content.Context; 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.support.design.widget.Snackbar; import android.support.design.widget.Snackbar;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.app.LoaderManager.LoaderCallbacks;
@ -17,30 +17,25 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.philliphsu.clock2.Alarm; import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.AsyncRecyclerViewItemChangeHandler;
import com.philliphsu.clock2.OnListItemInteractionListener; import com.philliphsu.clock2.OnListItemInteractionListener;
import com.philliphsu.clock2.R; import com.philliphsu.clock2.R;
import com.philliphsu.clock2.editalarm.EditAlarmActivity; import com.philliphsu.clock2.editalarm.EditAlarmActivity;
import com.philliphsu.clock2.model.AlarmListLoader; import com.philliphsu.clock2.model.AlarmsListCursorLoader;
import com.philliphsu.clock2.model.DatabaseManager; import com.philliphsu.clock2.model.DatabaseManager;
import com.philliphsu.clock2.util.AlarmUtils; import com.philliphsu.clock2.util.AlarmUtils;
import java.util.Collections;
import java.util.List;
import butterknife.Bind; import butterknife.Bind;
import butterknife.ButterKnife; import butterknife.ButterKnife;
// TODO: Use native fragments since we're targeting API >=19? // TODO: Use native fragments since we're targeting API >=19?
// TODO: Use native LoaderCallbacks. // TODO: Use native LoaderCallbacks.
public class AlarmsFragment extends Fragment implements LoaderCallbacks<List<Alarm>>, public class AlarmsFragment extends Fragment implements LoaderCallbacks<Cursor>,
OnListItemInteractionListener<Alarm> { OnListItemInteractionListener<Alarm> {
private static final int REQUEST_EDIT_ALARM = 0; private static final int REQUEST_EDIT_ALARM = 0;
public static final int REQUEST_CREATE_ALARM = 1; public static final int REQUEST_CREATE_ALARM = 1;
private static final String TAG = "AlarmsFragment"; private static final String TAG = "AlarmsFragment";
private AsyncRecyclerViewItemChangeHandler mAsyncRecyclerViewItemChangeHandler; private AlarmsCursorAdapter mAdapter;
private AlarmsAdapter mAdapter;
@Bind(R.id.list) RecyclerView mList; @Bind(R.id.list) RecyclerView mList;
@ -80,11 +75,8 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks<List<Ala
// Set the adapter // Set the adapter
Context context = view.getContext(); Context context = view.getContext();
mList.setLayoutManager(new LinearLayoutManager(context)); mList.setLayoutManager(new LinearLayoutManager(context));
mAdapter = new AlarmsAdapter(Collections.<Alarm>emptyList(), this); mAdapter = new AlarmsCursorAdapter(this);
mList.setAdapter(mAdapter); mList.setAdapter(mAdapter);
mAsyncRecyclerViewItemChangeHandler = new AsyncRecyclerViewItemChangeHandler(
mAdapter, getActivity().findViewById(R.id.main_content));
return view; return view;
} }
@ -107,20 +99,18 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks<List<Ala
} }
@Override @Override
public Loader<List<Alarm>> onCreateLoader(int id, Bundle args) { public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new AlarmListLoader(getActivity()); return new AlarmsListCursorLoader(getActivity());
} }
@Override @Override
public void onLoadFinished(Loader<List<Alarm>> loader, List<Alarm> data) { public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
mAdapter.replaceData(data); mAdapter.swapCursor(data);
} }
@Override @Override
public void onLoaderReset(Loader<List<Alarm>> loader) { public void onLoaderReset(Loader<Cursor> loader) {
// Can't pass in null, because replaceData() will try to add all the elements mAdapter.swapCursor(null);
// from the given collection, so we would run into an NPE.
mAdapter.replaceData(Collections.<Alarm>emptyList());
} }
@Override @Override
@ -132,14 +122,9 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks<List<Ala
switch (requestCode) { switch (requestCode) {
case REQUEST_CREATE_ALARM: case REQUEST_CREATE_ALARM:
Log.d(TAG, "Async add alarm"); // TODO: Should we still do the async add here?
final Alarm createdAlarm = data.getParcelableExtra(EditAlarmActivity.EXTRA_MODIFIED_ALARM); // We must if we want the async handler to post the toast/snackbar
new Handler().postDelayed(new Runnable() { // for us.
@Override
public void run() {
mAsyncRecyclerViewItemChangeHandler.asyncAddAlarm(createdAlarm);
}
}, 1000);
break; break;
case REQUEST_EDIT_ALARM: case REQUEST_EDIT_ALARM:
Alarm deletedAlarm; Alarm deletedAlarm;

View File

@ -26,6 +26,7 @@ import android.widget.Toast;
import android.widget.ToggleButton; import android.widget.ToggleButton;
import com.philliphsu.clock2.Alarm; import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.AsyncItemChangeHandler;
import com.philliphsu.clock2.BaseActivity; import com.philliphsu.clock2.BaseActivity;
import com.philliphsu.clock2.DaysOfWeek; import com.philliphsu.clock2.DaysOfWeek;
import com.philliphsu.clock2.R; import com.philliphsu.clock2.R;
@ -68,6 +69,7 @@ public class EditAlarmActivity extends BaseActivity implements AlarmNumpad.KeyLi
private Uri mSelectedRingtoneUri; private Uri mSelectedRingtoneUri;
private Alarm mOldAlarm; private Alarm mOldAlarm;
private DatabaseManager mDatabaseManager; private DatabaseManager mDatabaseManager;
private AsyncItemChangeHandler mAsyncItemChangeHandler;
@Bind(R.id.save) Button mSave; @Bind(R.id.save) Button mSave;
@Bind(R.id.delete) Button mDelete; @Bind(R.id.delete) Button mDelete;
@ -98,6 +100,8 @@ public class EditAlarmActivity extends BaseActivity implements AlarmNumpad.KeyLi
showDetails(); showDetails();
} }
setTimeTextHint(); // TODO: private access setTimeTextHint(); // TODO: private access
mAsyncItemChangeHandler = new AsyncItemChangeHandler(this, null);
} }
@Override @Override
@ -246,13 +250,15 @@ public class EditAlarmActivity extends BaseActivity implements AlarmNumpad.KeyLi
mDatabaseManager.updateAlarm(mOldAlarm.id(), alarm); mDatabaseManager.updateAlarm(mOldAlarm.id(), alarm);
} else { } else {
//mDatabaseManager.insertAlarm(alarm); //mDatabaseManager.insertAlarm(alarm);
intent.putExtra(EXTRA_MODIFIED_ALARM, alarm); mAsyncItemChangeHandler.asyncAddAlarm(alarm);
//intent.putExtra(EXTRA_MODIFIED_ALARM, alarm);
} }
if (alarm.isEnabled()) { if (alarm.isEnabled()) {
//scheduleAlarm(alarm); //scheduleAlarm(alarm);
} }
// TODO: Remove intent
setResult(RESULT_OK, intent); setResult(RESULT_OK, intent);
showEditorClosed(); showEditorClosed();
} }

View File

@ -9,6 +9,7 @@ import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log; import android.util.Log;
import com.philliphsu.clock2.Alarm; import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.util.LocalBroadcastHelper;
import static com.philliphsu.clock2.DaysOfWeek.FRIDAY; import static com.philliphsu.clock2.DaysOfWeek.FRIDAY;
import static com.philliphsu.clock2.DaysOfWeek.MONDAY; import static com.philliphsu.clock2.DaysOfWeek.MONDAY;
@ -65,8 +66,13 @@ public class AlarmDatabaseHelper extends SQLiteOpenHelper {
private static final String SORT_ORDER = private static final String SORT_ORDER =
COLUMN_RING_TIME_MILLIS + " ASC, " + COLUMN_ID + " ASC"; COLUMN_RING_TIME_MILLIS + " ASC, " + COLUMN_ID + " ASC";
private final Context mAppContext;
public AlarmDatabaseHelper(Context context) { public AlarmDatabaseHelper(Context context) {
super(context, DB_NAME, null, VERSION_1); super(context, DB_NAME, null, VERSION_1);
// Since DatabaseManager calls this with the application
// context, we can safely hold onto this context.
mAppContext = context;
} }
@Override @Override
@ -105,6 +111,7 @@ public class AlarmDatabaseHelper extends SQLiteOpenHelper {
long id = getWritableDatabase().insert(TABLE_ALARMS, long id = getWritableDatabase().insert(TABLE_ALARMS,
null, toContentValues(alarm)); null, toContentValues(alarm));
alarm.setId(id); alarm.setId(id);
notifyContentChanged();
return id; return id;
} }
@ -115,26 +122,32 @@ public class AlarmDatabaseHelper extends SQLiteOpenHelper {
public int updateAlarm(Alarm oldAlarm, Alarm newAlarm) { public int updateAlarm(Alarm oldAlarm, Alarm newAlarm) {
newAlarm.setId(oldAlarm.id()); newAlarm.setId(oldAlarm.id());
SQLiteDatabase db = getWritableDatabase(); SQLiteDatabase db = getWritableDatabase();
return db.update(TABLE_ALARMS, int rowsUpdated = db.update(TABLE_ALARMS,
toContentValues(newAlarm), toContentValues(newAlarm),
COLUMN_ID + " = " + newAlarm.id(), COLUMN_ID + " = " + newAlarm.id(),
null); null);
notifyContentChanged();
return rowsUpdated;
} }
public int updateAlarm(long id, Alarm newAlarm) { public int updateAlarm(long id, Alarm newAlarm) {
newAlarm.setId(id); newAlarm.setId(id);
SQLiteDatabase db = getWritableDatabase(); SQLiteDatabase db = getWritableDatabase();
return db.update(TABLE_ALARMS, int rowsUpdated = db.update(TABLE_ALARMS,
toContentValues(newAlarm), toContentValues(newAlarm),
COLUMN_ID + " = " + id, COLUMN_ID + " = " + id,
null); null);
notifyContentChanged();
return rowsUpdated;
} }
public int deleteAlarm(Alarm alarm) { public int deleteAlarm(Alarm alarm) {
SQLiteDatabase db = getWritableDatabase(); SQLiteDatabase db = getWritableDatabase();
return db.delete(TABLE_ALARMS, int rowsDeleted = db.delete(TABLE_ALARMS,
COLUMN_ID + " = " + alarm.id(), COLUMN_ID + " = " + alarm.id(),
null); null);
notifyContentChanged();
return rowsDeleted;
} }
public AlarmCursor queryAlarm(long id) { public AlarmCursor queryAlarm(long id) {
@ -184,6 +197,12 @@ public class AlarmDatabaseHelper extends SQLiteOpenHelper {
return values; return values;
} }
private void notifyContentChanged() {
Log.d(TAG, "notifyContentChanged()");
LocalBroadcastHelper.sendBroadcast(mAppContext,
SQLiteCursorLoader.ACTION_CHANGE_CONTENT);
}
// An alternative method to creating an Alarm from a cursor is to // An alternative method to creating an Alarm from a cursor is to
// make an Alarm constructor that takes an Cursor param. However, // make an Alarm constructor that takes an Cursor param. However,
// this method has the advantage of keeping all the constants // this method has the advantage of keeping all the constants

View File

@ -1,8 +1,13 @@
package com.philliphsu.clock2.model; package com.philliphsu.clock2.model;
import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.support.v4.content.AsyncTaskLoader; import android.support.v4.content.AsyncTaskLoader;
import android.util.Log;
import com.philliphsu.clock2.util.LocalBroadcastHelper;
/** /**
* Created by Phillip Hsu on 6/28/2016. * Created by Phillip Hsu on 6/28/2016.
@ -12,7 +17,10 @@ import android.support.v4.content.AsyncTaskLoader;
public abstract class SQLiteCursorLoader extends AsyncTaskLoader<Cursor> { public abstract class SQLiteCursorLoader extends AsyncTaskLoader<Cursor> {
private static final String TAG = "SQLiteCursorLoader"; private static final String TAG = "SQLiteCursorLoader";
public static final String ACTION_CHANGE_CONTENT = "com.philliphsu.clock2.model.action.CHANGE_CONTENT";
private Cursor mCursor; private Cursor mCursor;
private OnContentChangeReceiver mOnContentChangeReceiver;
public SQLiteCursorLoader(Context context) { public SQLiteCursorLoader(Context context) {
super(context); super(context);
@ -66,30 +74,54 @@ public abstract class SQLiteCursorLoader extends AsyncTaskLoader<Cursor> {
if (mCursor != null) { if (mCursor != null) {
deliverResult(mCursor); deliverResult(mCursor);
} }
if (mOnContentChangeReceiver == null) {
mOnContentChangeReceiver = new OnContentChangeReceiver();
LocalBroadcastHelper.registerReceiver(getContext(),
mOnContentChangeReceiver, ACTION_CHANGE_CONTENT);
}
if (takeContentChanged() || mCursor == null) { if (takeContentChanged() || mCursor == null) {
forceLoad(); forceLoad();
} }
} }
@Override @Override
protected void onStopLoading() { protected void onStopLoading() {
// Attempt to cancel the current load task if possible. // Attempt to cancel the current load task if possible.
cancelLoad(); cancelLoad();
} }
@Override @Override
public void onCanceled(Cursor cursor) { public void onCanceled(Cursor cursor) {
if (cursor != null && !cursor.isClosed()) { if (cursor != null && !cursor.isClosed()) {
cursor.close(); cursor.close();
} }
} }
@Override @Override
protected void onReset() { protected void onReset() {
super.onReset(); super.onReset();
// Ensure the loader is stopped // Ensure the loader is stopped
onStopLoading(); onStopLoading();
if (mCursor != null && !mCursor.isClosed()) { if (mCursor != null && !mCursor.isClosed()) {
mCursor.close(); mCursor.close();
} }
mCursor = null; mCursor = null;
if (mOnContentChangeReceiver != null) {
LocalBroadcastHelper.unregisterReceiver(getContext(),
mOnContentChangeReceiver);
mOnContentChangeReceiver = null;
}
} }
private final class OnContentChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "Received content change event");
onContentChanged();
}
}
} }

View File

@ -1,7 +1,9 @@
package com.philliphsu.clock2.util; package com.philliphsu.clock2.util;
import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter;
import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.LocalBroadcastManager;
/** /**
@ -14,5 +16,16 @@ public final class LocalBroadcastHelper {
LocalBroadcastManager.getInstance(context).sendBroadcast(new Intent(action)); LocalBroadcastManager.getInstance(context).sendBroadcast(new Intent(action));
} }
/** Registers a BroadcastReceiver that filters intents by the action specified */
// TODO: Refactor any local registrations to use this utility method.
public static void registerReceiver(Context context, BroadcastReceiver receiver, String action) {
LocalBroadcastManager.getInstance(context).registerReceiver(receiver, new IntentFilter(action));
}
// TODO: Refactor any local unregistrations to use this utility method.
public static void unregisterReceiver(Context context, BroadcastReceiver receiver) {
LocalBroadcastManager.getInstance(context).unregisterReceiver(receiver);
}
private LocalBroadcastHelper() {} private LocalBroadcastHelper() {}
} }