Scheduling alarm via AlarmUtils no longer shows Toast confirmation for you

This commit is contained in:
Phillip Hsu 2016-07-09 23:46:05 -07:00
parent 4436d5852a
commit 08e12cd14f
7 changed files with 173 additions and 54 deletions

View File

@ -32,23 +32,7 @@ public final class AsyncItemChangeHandler {
} }
public void asyncAddAlarm(final Alarm alarm) { public void asyncAddAlarm(final Alarm alarm) {
new AsyncTask<Void, Void, Long>() { new InsertAlarmAsyncTask(alarm).execute();
@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();
} }
/** /**
@ -56,37 +40,9 @@ public final class AsyncItemChangeHandler {
* when we were in the edit activity. * when we were in the edit activity.
* TODO: Consider changing the signature of updateAlarm() in DatabaseManager and * TODO: Consider changing the signature of updateAlarm() in DatabaseManager and
* AlarmDatabaseHelper to only require one Alarm param. * 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) { public void asyncUpdateAlarm(final Alarm newAlarm) {
new AsyncTask<Void, Void, Integer>() { new UpdateAlarmAsyncTask(newAlarm).execute();
@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();
} }
public void asyncRemoveAlarm(final Alarm alarm) { public void asyncRemoveAlarm(final Alarm alarm) {
@ -99,6 +55,8 @@ public final class AsyncItemChangeHandler {
@Override @Override
protected void onPostExecute(Integer integer) { protected void onPostExecute(Integer integer) {
if (mSnackbarAnchor != null) { 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, String message = mContext.getString(R.string.snackbar_item_deleted,
mContext.getString(R.string.alarm)); mContext.getString(R.string.alarm));
Snackbar.make(mSnackbarAnchor, message, Snackbar.LENGTH_LONG) Snackbar.make(mSnackbarAnchor, message, Snackbar.LENGTH_LONG)
@ -112,4 +70,66 @@ public final class AsyncItemChangeHandler {
} }
}.execute(); }.execute();
} }
////////////////////////////////////////////////////////////
// Insert and update AsyncTasks
////////////////////////////////////////////////////////////
/**
* Created because the code in insert and update AsyncTasks are exactly the same.
*/
private abstract class BaseAsyncTask extends AsyncTask<Void, Void, Long> {
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();
}
}
} }

View File

@ -128,6 +128,7 @@ public class AlarmViewHolder extends BaseViewHolder<Alarm> implements AlarmCount
if (alarm.isEnabled()) { if (alarm.isEnabled()) {
// TODO: On Moto X, upcoming notification doesn't post immediately // TODO: On Moto X, upcoming notification doesn't post immediately
AlarmUtils.scheduleAlarm(getContext(), alarm, true); AlarmUtils.scheduleAlarm(getContext(), alarm, true);
AlarmUtils.sendShowSnackbarBroadcast(getContext(), AlarmUtils.getRingsInText(getContext(), alarm.ringsIn()));
AlarmUtils.save(getContext(), alarm); AlarmUtils.save(getContext(), alarm);
} else { } else {
AlarmUtils.cancelAlarm(getContext(), alarm, true); AlarmUtils.cancelAlarm(getContext(), alarm, true);

View File

@ -1,6 +1,7 @@
package com.philliphsu.clock2.alarms; package com.philliphsu.clock2.alarms;
import android.app.Activity; import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
@ -22,6 +23,8 @@ 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.AlarmsListCursorLoader; import com.philliphsu.clock2.model.AlarmsListCursorLoader;
import com.philliphsu.clock2.util.AlarmUtils;
import com.philliphsu.clock2.util.LocalBroadcastHelper;
import butterknife.Bind; import butterknife.Bind;
import butterknife.ButterKnife; import butterknife.ButterKnife;
@ -34,10 +37,16 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks<Cursor>,
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.
public static final int REQUEST_CREATE_ALARM = 1; 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 AlarmsCursorAdapter mAdapter;
private AsyncItemChangeHandler mAsyncItemChangeHandler; private AsyncItemChangeHandler mAsyncItemChangeHandler;
private Handler mHandler = new Handler(); private Handler mHandler = new Handler();
private View mSnackbarAnchor;
private long mScrollToStableId = RecyclerView.NO_ID; private long mScrollToStableId = RecyclerView.NO_ID;
@Bind(R.id.list) RecyclerView mList; @Bind(R.id.list) RecyclerView mList;
@ -66,6 +75,10 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks<Cursor>,
// TODO Read arguments // 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); getLoaderManager().initLoader(0, null, this);
} }
@ -80,14 +93,27 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks<Cursor>,
mAdapter = new AlarmsCursorAdapter(this); mAdapter = new AlarmsCursorAdapter(this);
mList.setAdapter(mAdapter); mList.setAdapter(mAdapter);
mAsyncItemChangeHandler = new AsyncItemChangeHandler(getActivity(), mAsyncItemChangeHandler = new AsyncItemChangeHandler(
getActivity().findViewById(R.id.main_content), this); getActivity(), mSnackbarAnchor, this);
return view; return view;
} }
@Override @Override
public void onResume() { public void onStart() {
super.onResume(); 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 @Override
@ -195,6 +221,19 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks<Cursor>,
// TODO: Delete this method. // 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 { private static abstract class BaseAsyncItemChangeRunnable {
// TODO: Will holding onto this cause a memory leak? // TODO: Will holding onto this cause a memory leak?
private final AsyncItemChangeHandler mAsyncItemChangeHandler; private final AsyncItemChangeHandler mAsyncItemChangeHandler;

View File

@ -511,9 +511,11 @@ public class EditAlarmActivity extends BaseActivity implements AlarmNumpad.KeyLi
} }
} }
// TODO: Delete this
@Deprecated
@Override @Override
public void scheduleAlarm(Alarm alarm) { public void scheduleAlarm(Alarm alarm) {
AlarmUtils.scheduleAlarm(this, alarm, true); //AlarmUtils.scheduleAlarm(this, alarm, true);
} }
@Override @Override

View File

@ -64,6 +64,11 @@ public class AlarmDatabaseHelper extends SQLiteOpenHelper {
// First sort by ring time in ascending order (smaller values first), // First sort by ring time in ascending order (smaller values first),
// then break ties by sorting by id in ascending order. // 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 = private static final String SORT_ORDER =
COLUMN_RING_TIME_MILLIS + " ASC, " + COLUMN_ID + " ASC"; COLUMN_RING_TIME_MILLIS + " ASC, " + COLUMN_ID + " ASC";

View File

@ -4,15 +4,19 @@ import android.app.AlarmManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.StringRes; import android.support.annotation.StringRes;
import android.support.design.widget.Snackbar;
import android.util.Log; import android.util.Log;
import android.view.View;
import android.widget.Toast; import android.widget.Toast;
import com.philliphsu.clock2.Alarm; import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.PendingAlarmScheduler; import com.philliphsu.clock2.PendingAlarmScheduler;
import com.philliphsu.clock2.R; import com.philliphsu.clock2.R;
import com.philliphsu.clock2.UpcomingAlarmReceiver; import com.philliphsu.clock2.UpcomingAlarmReceiver;
import com.philliphsu.clock2.alarms.AlarmsFragment;
import com.philliphsu.clock2.model.DatabaseManager; import com.philliphsu.clock2.model.DatabaseManager;
import com.philliphsu.clock2.ringtone.RingtoneActivity; import com.philliphsu.clock2.ringtone.RingtoneActivity;
import com.philliphsu.clock2.ringtone.RingtoneService; import com.philliphsu.clock2.ringtone.RingtoneService;
@ -40,7 +44,11 @@ public final class AlarmUtils {
* Schedules the alarm with the {@link AlarmManager}. If * Schedules the alarm with the {@link AlarmManager}. If
* {@code alarm.}{@link Alarm#isEnabled() isEnabled()} returns false, * {@code alarm.}{@link Alarm#isEnabled() isEnabled()} returns false,
* this does nothing and returns immediately. * 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) { public static void scheduleAlarm(Context context, Alarm alarm, boolean showToast) {
if (!alarm.isEnabled()) { if (!alarm.isEnabled()) {
Log.i(TAG, "Skipped scheduling an alarm because it was not enabled"); Log.i(TAG, "Skipped scheduling an alarm because it was not enabled");
@ -65,9 +73,9 @@ public final class AlarmUtils {
notifyUpcomingAlarmIntent(context, alarm, false)); notifyUpcomingAlarmIntent(context, alarm, false));
am.setExact(AlarmManager.RTC_WAKEUP, ringAt, alarmIntent(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. // it could be beneficial for callers to schedule the alarm in a worker thread.
if (showToast) { if (false && showToast) {
String message; String message;
if (alarm.isSnoozed()) { if (alarm.isSnoozed()) {
message = context.getString(R.string.title_snoozing_until, message = context.getString(R.string.title_snoozing_until,
@ -141,6 +149,14 @@ public final class AlarmUtils {
public static void snoozeAlarm(Context c, Alarm a) { public static void snoozeAlarm(Context c, Alarm a) {
a.snooze(snoozeDuration(c)); a.snooze(snoozeDuration(c));
scheduleAlarm(c, a, true); 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); save(c, a);
} }
@ -219,4 +235,30 @@ public final class AlarmUtils {
} }
}).start(); }).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();
}
}
} }

View File

@ -4,6 +4,7 @@ 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.content.IntentFilter;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager; 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 */ /** Sends a local broadcast using an intent with the action specified */
public static void sendBroadcast(Context context, String action) { 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 */ /** Registers a BroadcastReceiver that filters intents by the actions specified */