diff --git a/app/src/main/java/com/philliphsu/clock2/AsyncItemChangeHandler.java b/app/src/main/java/com/philliphsu/clock2/AsyncItemChangeHandler.java index a48e286..cbe9de0 100644 --- a/app/src/main/java/com/philliphsu/clock2/AsyncItemChangeHandler.java +++ b/app/src/main/java/com/philliphsu/clock2/AsyncItemChangeHandler.java @@ -49,16 +49,28 @@ public final class AsyncItemChangeHandler { }.execute(); } - public void asyncUpdateAlarm(final Alarm oldAlarm, final Alarm newAlarm) { + /** + * We only need one Alarm param because we called newAlarm.setId(oldAlarm.id()) + * when we were in the edit activity. + * TODO: Consider changing the signature of updateAlarm() in DatabaseManager and + * AlarmDatabaseHelper to only require one Alarm param. + */ + public void asyncUpdateAlarm(final Alarm newAlarm) { new AsyncTask() { @Override protected Integer doInBackground(Void... params) { - return DatabaseManager.getInstance(mContext).updateAlarm(oldAlarm.id(), newAlarm); + return DatabaseManager.getInstance(mContext).updateAlarm(newAlarm.id(), newAlarm); } @Override protected void onPostExecute(Integer integer) { // TODO: Snackbar/Toast here? If so, remove the code in AlarmUtils.scheduleAlarm() that does it. + AlarmUtils.scheduleAlarm(mContext, newAlarm, true); + // The new alarm could have a different sort order from the old alarm. + // TODO: Sometimes this won't scrolls to the new alarm if the old alarm is + // towards the bottom and the new alarm is ordered towards the top. This + // may have something to do with us breaking the stable id guarantee? + mScrollHandler.setScrollToStableId(newAlarm.id()); } }.execute(); } 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 3e72330..dad752f 100644 --- a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java +++ b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java @@ -6,7 +6,6 @@ import android.content.Intent; import android.database.Cursor; import android.os.Bundle; import android.os.Handler; -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; @@ -23,8 +22,6 @@ 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.DatabaseManager; -import com.philliphsu.clock2.util.AlarmUtils; import butterknife.Bind; import butterknife.ButterKnife; @@ -39,6 +36,7 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks, private AlarmsCursorAdapter mAdapter; private AsyncItemChangeHandler mAsyncItemChangeHandler; + private Handler mHandler = new Handler(); private long mScrollToStableId = RecyclerView.NO_ID; @Bind(R.id.list) RecyclerView mList; @@ -67,7 +65,6 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks, // TODO Read arguments } - // Initialize the loader to load the list of runs getLoaderManager().initLoader(0, null, this); } @@ -125,32 +122,36 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks, @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { Log.d(TAG, "onActivityResult()"); - if (resultCode != Activity.RESULT_OK) { + if (resultCode != Activity.RESULT_OK || data == null) + return; + final Alarm alarm = data.getParcelableExtra(EditAlarmActivity.EXTRA_MODIFIED_ALARM); + if (alarm == null) return; - } + // http://stackoverflow.com/a/27055512/5055032 + // "RecyclerView does not run animations in the first layout + // pass after being attached." A workaround is to postpone + // the CRUD operation to the next frame. A delay of 300ms is + // short enough to not be noticeable, and long enough to + // give us the animation *most of the time*. switch (requestCode) { case REQUEST_CREATE_ALARM: - if (data != null) { - final Alarm createdAlarm = data.getParcelableExtra(EditAlarmActivity.EXTRA_MODIFIED_ALARM); - if (createdAlarm != null) { - new Handler().postDelayed(new Runnable() { - @Override - public void run() { - mAsyncItemChangeHandler.asyncAddAlarm(createdAlarm); - } - }, 300); - } - } + mHandler.postDelayed( + new AsyncAddItemRunnable(mAsyncItemChangeHandler, alarm), + 300); break; case REQUEST_EDIT_ALARM: - Alarm deletedAlarm; - if (data != null && (deletedAlarm = data.getParcelableExtra( - EditAlarmActivity.EXTRA_MODIFIED_ALARM)) != null) { - onListItemDeleted(deletedAlarm); + if (data.getBooleanExtra(EditAlarmActivity.EXTRA_IS_DELETING, false)) { + // TODO: Should we delay this too? It seems animations run + // some of the time. + mAsyncItemChangeHandler.asyncRemoveAlarm(alarm); + } else { + // TODO: Increase the delay, because update animation is + // more elusive than insert. + mHandler.postDelayed( + new AsyncUpdateItemRunnable(mAsyncItemChangeHandler, alarm), + 300); } - // TODO: notifyItemRemoved? - getLoaderManager().restartLoader(0, null, this); break; default: Log.i(TAG, "Could not handle request code " + requestCode); @@ -192,25 +193,67 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks, mScrollToStableId = RecyclerView.NO_ID; } - // TODO: This doesn't need to be defined in the interface. - // TODO: Rename to showDeletedSnackbar() or something - // TODO: This needs to prompt a reload of the list. - @Deprecated // TODO: Delete this method. + + @Deprecated @Override public void onListItemDeleted(final Alarm item) { - Snackbar.make(getActivity().findViewById(R.id.main_content), - getString(R.string.snackbar_item_deleted, "Alarm"), - Snackbar.LENGTH_LONG) // TODO: not long enough? - .setAction(R.string.snackbar_undo_item_deleted, new View.OnClickListener() { - @Override - public void onClick(View v) { - DatabaseManager.getInstance(getActivity()).insertAlarm(item); - getLoaderManager().restartLoader(0, null, AlarmsFragment.this); - if (item.isEnabled()) { - AlarmUtils.scheduleAlarm(getActivity(), item, true); - } - } - }) - .show(); + // TODO: This doesn't need to be defined in the interface. + // TODO: Delete this method. + } + + private static abstract class BaseAsyncItemChangeRunnable { + // TODO: Will holding onto this cause a memory leak? + private final AsyncItemChangeHandler mAsyncItemChangeHandler; + private final Alarm mAlarm; + + BaseAsyncItemChangeRunnable(AsyncItemChangeHandler asyncItemChangeHandler, Alarm alarm) { + mAsyncItemChangeHandler = asyncItemChangeHandler; + mAlarm = alarm; + } + + void asyncAddAlarm() { + mAsyncItemChangeHandler.asyncAddAlarm(mAlarm); + } + + void asyncUpdateAlarm() { + mAsyncItemChangeHandler.asyncUpdateAlarm(mAlarm); + } + + void asyncRemoveAlarm() { + mAsyncItemChangeHandler.asyncRemoveAlarm(mAlarm); + } + } + + private static class AsyncAddItemRunnable extends BaseAsyncItemChangeRunnable implements Runnable { + AsyncAddItemRunnable(AsyncItemChangeHandler asyncItemChangeHandler, Alarm alarm) { + super(asyncItemChangeHandler, alarm); + } + + @Override + public void run() { + asyncAddAlarm(); + } + } + + private static class AsyncUpdateItemRunnable extends BaseAsyncItemChangeRunnable implements Runnable { + AsyncUpdateItemRunnable(AsyncItemChangeHandler asyncItemChangeHandler, Alarm alarm) { + super(asyncItemChangeHandler, alarm); + } + + @Override + public void run() { + asyncUpdateAlarm(); + } + } + + private static class AsyncRemoveItemRunnable extends BaseAsyncItemChangeRunnable implements Runnable { + AsyncRemoveItemRunnable(AsyncItemChangeHandler asyncItemChangeHandler, Alarm alarm) { + super(asyncItemChangeHandler, alarm); + } + + @Override + public void run() { + asyncRemoveAlarm(); + } } } 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 1427bf1..fda5f12 100644 --- a/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmActivity.java +++ b/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmActivity.java @@ -59,6 +59,7 @@ public class EditAlarmActivity extends BaseActivity implements AlarmNumpad.KeyLi private static final String TAG = "EditAlarmActivity"; public static final String EXTRA_ALARM_ID = "com.philliphsu.clock2.editalarm.extra.ALARM_ID"; public static final String EXTRA_MODIFIED_ALARM = "com.philliphsu.clock2.editalarm.extra.MODIFIED_ALARM"; + public static final String EXTRA_IS_DELETING = "com.philliphsu.clock2.editalarm.extra.IS_DELETING"; private static final RelativeSizeSpan AMPM_SIZE_SPAN = new RelativeSizeSpan(0.5f); private static final int REQUEST_PICK_RINGTONE = 0; @@ -211,12 +212,12 @@ public class EditAlarmActivity extends BaseActivity implements AlarmNumpad.KeyLi showRingtonePickerDialog(); } - // TODO: Privatize accessor methods @OnClick(R.id.save) void save() { int hour; int minutes; try { + // TODO: Privatize accessor methods hour = getHour(); minutes = getMinutes(); } catch (IllegalStateException e) { @@ -242,16 +243,15 @@ 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 { - //mDatabaseManager.insertAlarm(alarm); - intent.putExtra(EXTRA_MODIFIED_ALARM, alarm); + alarm.setId(mOldAlarm.id()); + intent.putExtra(EXTRA_IS_DELETING, false); } + intent.putExtra(EXTRA_MODIFIED_ALARM, alarm); - if (alarm.isEnabled()) { - //scheduleAlarm(alarm); - } + // The reason we don't schedule the alarm here is AlarmUtils + // will attempt to retrieve the specified alarm + // from the database; however, the alarm hasn't yet + // been added to the database at this point. setResult(RESULT_OK, intent); showEditorClosed(); @@ -269,6 +269,7 @@ public class EditAlarmActivity extends BaseActivity implements AlarmNumpad.KeyLi } mDatabaseManager.deleteAlarm(mOldAlarm); Intent intent = new Intent(); + intent.putExtra(EXTRA_IS_DELETING, true); intent.putExtra(EXTRA_MODIFIED_ALARM, mOldAlarm); setResult(RESULT_OK, intent); }