diff --git a/app/src/main/java/com/philliphsu/clock2/AsyncItemChangeHandler.java b/app/src/main/java/com/philliphsu/clock2/AsyncItemChangeHandler.java index e412045..3a96542 100644 --- a/app/src/main/java/com/philliphsu/clock2/AsyncItemChangeHandler.java +++ b/app/src/main/java/com/philliphsu/clock2/AsyncItemChangeHandler.java @@ -32,23 +32,7 @@ public final class AsyncItemChangeHandler { } public void asyncAddAlarm(final Alarm alarm) { - new AsyncTask() { - @Override - protected Long doInBackground(Void... params) { - return DatabaseManager.getInstance(mContext).insertAlarm(alarm); - } - - @Override - protected void onPostExecute(Long aLong) { - // TODO: Snackbar/Toast here? If so, remove the code in AlarmUtils.scheduleAlarm() that does it. - // Then, consider scheduling the alarm in the background. - AlarmUtils.scheduleAlarm(mContext, alarm, true); - if (mScrollHandler != null) { - // Prepare to scroll to the newly added alarm - mScrollHandler.setScrollToStableId(aLong); - } - } - }.execute(); + new InsertAlarmAsyncTask(alarm).execute(); } /** @@ -56,37 +40,9 @@ public final class AsyncItemChangeHandler { * when we were in the edit activity. * TODO: Consider changing the signature of updateAlarm() in DatabaseManager and * AlarmDatabaseHelper to only require one Alarm param. - * TODO: The AsyncTask employed here is very similar to the one employed in - * asyncAddAlarm(). Figure out a way to refactor the code in common. Possible - * starts are to: - * * Change the Result type to Long, and then the onPostExecute() can be - * expressed the same between the two methods. - * * Similar to what you did in AlarmsFragment with the static - * inner Runnables, write a static inner abstract class that extends - * AsyncTask that takes in an Alarm; leave doInBackground() unimplemented - * in this base class. Then, define methods in this base class that subclasses - * can call to do their desired CRUD task in their doInBackground(). */ public void asyncUpdateAlarm(final Alarm newAlarm) { - new AsyncTask() { - @Override - protected Integer doInBackground(Void... params) { - 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); - if (mScrollHandler != null) { - // 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(); + new UpdateAlarmAsyncTask(newAlarm).execute(); } public void asyncRemoveAlarm(final Alarm alarm) { @@ -99,6 +55,8 @@ public final class AsyncItemChangeHandler { @Override protected void onPostExecute(Integer integer) { if (mSnackbarAnchor != null) { + // TODO: Consider adding delay to allow the alarm item animation + // to finish first before we show the snackbar. Inbox app does this. String message = mContext.getString(R.string.snackbar_item_deleted, mContext.getString(R.string.alarm)); Snackbar.make(mSnackbarAnchor, message, Snackbar.LENGTH_LONG) @@ -112,4 +70,66 @@ public final class AsyncItemChangeHandler { } }.execute(); } + + //////////////////////////////////////////////////////////// + // Insert and update AsyncTasks + //////////////////////////////////////////////////////////// + + /** + * Created because the code in insert and update AsyncTasks are exactly the same. + */ + private abstract class BaseAsyncTask extends AsyncTask { + private final Alarm mAlarm; + + BaseAsyncTask(Alarm alarm) { + mAlarm = alarm; + } + + @Override + protected void onPostExecute(Long result) { + AlarmUtils.scheduleAlarm(mContext, mAlarm, true); + if (mScrollHandler != null) { + // Prepare to scroll to this alarm + mScrollHandler.setScrollToStableId(result); + } + if (mSnackbarAnchor != null) { + // TODO: Consider adding delay to allow the alarm item animation + // to finish first before we show the snackbar. Inbox app does this. + String message = AlarmUtils.getRingsInText(mContext, mAlarm.ringsIn()); + AlarmUtils.showSnackbar(mSnackbarAnchor, message); + } + } + + final Long insertAlarm() { + return DatabaseManager.getInstance(mContext).insertAlarm(mAlarm); + } + + final Long updateAlarm() { + long id = mAlarm.id(); + DatabaseManager.getInstance(mContext).updateAlarm(id, mAlarm); + return id; + } + } + + private class InsertAlarmAsyncTask extends BaseAsyncTask { + InsertAlarmAsyncTask(Alarm alarm) { + super(alarm); + } + + @Override + protected Long doInBackground(Void... params) { + return insertAlarm(); + } + } + + private class UpdateAlarmAsyncTask extends BaseAsyncTask { + UpdateAlarmAsyncTask(Alarm alarm) { + super(alarm); + } + + @Override + protected Long doInBackground(Void... params) { + return updateAlarm(); + } + } } diff --git a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmViewHolder.java b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmViewHolder.java index d177782..4ea2026 100644 --- a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmViewHolder.java +++ b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmViewHolder.java @@ -128,6 +128,7 @@ public class AlarmViewHolder extends BaseViewHolder implements AlarmCount if (alarm.isEnabled()) { // TODO: On Moto X, upcoming notification doesn't post immediately AlarmUtils.scheduleAlarm(getContext(), alarm, true); + AlarmUtils.sendShowSnackbarBroadcast(getContext(), AlarmUtils.getRingsInText(getContext(), alarm.ringsIn())); AlarmUtils.save(getContext(), alarm); } else { AlarmUtils.cancelAlarm(getContext(), alarm, true); 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 917cbcd..dd3e8a7 100644 --- a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java +++ b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java @@ -1,6 +1,7 @@ package com.philliphsu.clock2.alarms; import android.app.Activity; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.database.Cursor; @@ -22,6 +23,8 @@ 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.util.AlarmUtils; +import com.philliphsu.clock2.util.LocalBroadcastHelper; import butterknife.Bind; import butterknife.ButterKnife; @@ -34,10 +37,16 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks, private static final int REQUEST_EDIT_ALARM = 0; // Public because MainActivity needs to use it. public static final int REQUEST_CREATE_ALARM = 1; + /** + * Local broadcast senders can tell us to show a snackbar with a message on their behalf. + */ + public static final String ACTION_SHOW_SNACKBAR_MSG = "com.philliphsu.clock2.alarms.action.SHOW_SNACKBAR_MSG"; + public static final String EXTRA_MSG = "com.philliphsu.clock2.alarms.extra.MSG"; private AlarmsCursorAdapter mAdapter; private AsyncItemChangeHandler mAsyncItemChangeHandler; private Handler mHandler = new Handler(); + private View mSnackbarAnchor; private long mScrollToStableId = RecyclerView.NO_ID; @Bind(R.id.list) RecyclerView mList; @@ -66,6 +75,10 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks, // TODO Read arguments } + // Will succeed because the activity is created at this point. + // See the Fragment lifecycle. + mSnackbarAnchor = getActivity().findViewById(R.id.main_content); + getLoaderManager().initLoader(0, null, this); } @@ -80,14 +93,27 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks, mAdapter = new AlarmsCursorAdapter(this); mList.setAdapter(mAdapter); - mAsyncItemChangeHandler = new AsyncItemChangeHandler(getActivity(), - getActivity().findViewById(R.id.main_content), this); + mAsyncItemChangeHandler = new AsyncItemChangeHandler( + getActivity(), mSnackbarAnchor, this); return view; } @Override - public void onResume() { - super.onResume(); + public void onStart() { + super.onStart(); + LocalBroadcastHelper.registerReceiver(getActivity(), + mShowSnackbarReceiver, ACTION_SHOW_SNACKBAR_MSG); + } + + @Override + public void onStop() { + // This will always be called when we leave this screen, either by exiting the app or + // by navigating elsewhere. Since we unregister the receiver here, we will never receive + // a "show alarm snoozed" broadcast, because the snooze action is always made elsewhere + // in the app. + super.onStop(); + Log.e(TAG, "onStop()"); + LocalBroadcastHelper.unregisterReceiver(getActivity(), mShowSnackbarReceiver); } @Override @@ -195,6 +221,19 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks, // TODO: Delete this method. } + private final BroadcastReceiver mShowSnackbarReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // See Intent#putExtras(Bundle): + // Putting a Bundle of extras into an intent will have its + // contents added to the intent's collection of extras, + // so we can individually retrieve the Bundle's extras + // directly from the intent. + String message = intent.getStringExtra(EXTRA_MSG); + AlarmUtils.showSnackbar(mSnackbarAnchor, message); + } + }; + private static abstract class BaseAsyncItemChangeRunnable { // TODO: Will holding onto this cause a memory leak? private final AsyncItemChangeHandler mAsyncItemChangeHandler; 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 9a1559f..eb06faf 100644 --- a/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmActivity.java +++ b/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmActivity.java @@ -511,9 +511,11 @@ public class EditAlarmActivity extends BaseActivity implements AlarmNumpad.KeyLi } } + // TODO: Delete this + @Deprecated @Override public void scheduleAlarm(Alarm alarm) { - AlarmUtils.scheduleAlarm(this, alarm, true); + //AlarmUtils.scheduleAlarm(this, alarm, true); } @Override 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 0cb23ac..c1dc4f4 100644 --- a/app/src/main/java/com/philliphsu/clock2/model/AlarmDatabaseHelper.java +++ b/app/src/main/java/com/philliphsu/clock2/model/AlarmDatabaseHelper.java @@ -64,6 +64,11 @@ public class AlarmDatabaseHelper extends SQLiteOpenHelper { // First sort by ring time in ascending order (smaller values first), // then break ties by sorting by id in ascending order. + // TODO: Consider changing the sort order to hour ASC, minutes ASC, enabled DESC. Then, we can + // delete the COLUMN_RING_TIME_MILLIS. + // As defined now, the ordering can be confusing; some examples are: + // * If there are multiple single-use alarms in the list, and one of them is snoozed, then on the + // next cursor load, this alarm will be reordered to the very bottom private static final String SORT_ORDER = COLUMN_RING_TIME_MILLIS + " ASC, " + COLUMN_ID + " ASC"; 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 bf28b9e..219d48b 100644 --- a/app/src/main/java/com/philliphsu/clock2/util/AlarmUtils.java +++ b/app/src/main/java/com/philliphsu/clock2/util/AlarmUtils.java @@ -4,15 +4,19 @@ import android.app.AlarmManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; +import android.os.Bundle; import android.preference.PreferenceManager; import android.support.annotation.StringRes; +import android.support.design.widget.Snackbar; import android.util.Log; +import android.view.View; import android.widget.Toast; import com.philliphsu.clock2.Alarm; import com.philliphsu.clock2.PendingAlarmScheduler; import com.philliphsu.clock2.R; import com.philliphsu.clock2.UpcomingAlarmReceiver; +import com.philliphsu.clock2.alarms.AlarmsFragment; import com.philliphsu.clock2.model.DatabaseManager; import com.philliphsu.clock2.ringtone.RingtoneActivity; import com.philliphsu.clock2.ringtone.RingtoneService; @@ -40,7 +44,11 @@ public final class AlarmUtils { * Schedules the alarm with the {@link AlarmManager}. If * {@code alarm.}{@link Alarm#isEnabled() isEnabled()} returns false, * this does nothing and returns immediately. + * + * @deprecated {@code showToast} is no longer working. Callers must + * handle popup confirmations on their own. */ + // TODO: Delete showToast param public static void scheduleAlarm(Context context, Alarm alarm, boolean showToast) { if (!alarm.isEnabled()) { Log.i(TAG, "Skipped scheduling an alarm because it was not enabled"); @@ -65,9 +73,9 @@ public final class AlarmUtils { notifyUpcomingAlarmIntent(context, alarm, false)); am.setExact(AlarmManager.RTC_WAKEUP, ringAt, alarmIntent(context, alarm, false)); - // TODO: Consider removing this and letting callers handle Toasts, because + // TODO: Consider removing this and letting callers handle this, because // it could be beneficial for callers to schedule the alarm in a worker thread. - if (showToast) { + if (false && showToast) { String message; if (alarm.isSnoozed()) { message = context.getString(R.string.title_snoozing_until, @@ -141,6 +149,14 @@ public final class AlarmUtils { public static void snoozeAlarm(Context c, Alarm a) { a.snooze(snoozeDuration(c)); scheduleAlarm(c, a, true); + // TODO: Based on the current lifecycle methods pair where we register/unregister the + // receiver in AlarmsFragment, the snackbar won't be shown. + // We have no reference to the snackbar anchor, so let AlarmsFragment + // handle showing the snackbar for us. AlarmsFragment has no knowledge + // of which alarm is snoozed (and actually doesn't need to know); we can build + // the message for it. This is why we don't have a showAlarmSnoozedSnackbar(Alarm) + // utility method. + sendShowSnackbarBroadcast(c, getSnoozingUntilText(c, a.snoozingUntil())); save(c, a); } @@ -219,4 +235,30 @@ public final class AlarmUtils { } }).start(); } + + public static String getRingsInText(Context context, long ringsIn) { + return context.getString(R.string.alarm_set_for, + DurationUtils.toString(context, ringsIn, false /*abbreviate?*/)); + } + + public static String getSnoozingUntilText(Context context, long snoozingUntil) { + return context.getString(R.string.title_snoozing_until, + formatTime(context, snoozingUntil)); + } + + public static void sendShowSnackbarBroadcast(Context c, String message) { + Bundle extra = new Bundle(1); + extra.putString(AlarmsFragment.EXTRA_MSG, message); + LocalBroadcastHelper.sendBroadcast(c, AlarmsFragment.ACTION_SHOW_SNACKBAR_MSG, extra); + } + + /** + * Show a snackbar confirmation about an event related to an alarm. + * Used for showing an alarm has been snoozed. + */ + public static void showSnackbar(View snackbarAnchor, String message) { + if (snackbarAnchor != null) { + Snackbar.make(snackbarAnchor, message, Snackbar.LENGTH_LONG).show(); + } + } } diff --git a/app/src/main/java/com/philliphsu/clock2/util/LocalBroadcastHelper.java b/app/src/main/java/com/philliphsu/clock2/util/LocalBroadcastHelper.java index e98d7e6..8a534b6 100644 --- a/app/src/main/java/com/philliphsu/clock2/util/LocalBroadcastHelper.java +++ b/app/src/main/java/com/philliphsu/clock2/util/LocalBroadcastHelper.java @@ -4,6 +4,7 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.Bundle; import android.support.v4.content.LocalBroadcastManager; /** @@ -13,7 +14,16 @@ public final class LocalBroadcastHelper { /** Sends a local broadcast using an intent with the action specified */ public static void sendBroadcast(Context context, String action) { - LocalBroadcastManager.getInstance(context).sendBroadcast(new Intent(action)); + sendBroadcast(context, action, null); + } + + /** Sends a local broadcast using an intent with the action and the extras specified */ + public static void sendBroadcast(Context context, String action, Bundle extras) { + Intent intent = new Intent(action); + if (extras != null) { + intent.putExtras(extras); + } + LocalBroadcastManager.getInstance(context).sendBroadcast(intent); } /** Registers a BroadcastReceiver that filters intents by the actions specified */