Created AlarmController class and moved relevant AlarmUtils code

This commit is contained in:
Phillip Hsu 2016-07-11 02:28:20 -07:00
parent 08e12cd14f
commit 058d6c86b7
13 changed files with 359 additions and 117 deletions

View File

@ -7,7 +7,7 @@ import android.view.View;
import com.philliphsu.clock2.alarms.ScrollHandler;
import com.philliphsu.clock2.model.DatabaseManager;
import com.philliphsu.clock2.util.AlarmUtils;
import com.philliphsu.clock2.util.AlarmController;
/**
* Created by Phillip Hsu on 7/1/2016.
@ -20,15 +20,19 @@ public final class AsyncItemChangeHandler {
private final Context mContext;
private final View mSnackbarAnchor;
private final ScrollHandler mScrollHandler;
private final AlarmController mAlarmController;
/**
* @param snackbarAnchor an optional anchor for a Snackbar to anchor to
* @param scrollHandler
* @param context the Context from which we get the application context
* @param snackbarAnchor
*/
public AsyncItemChangeHandler(Context context, View snackbarAnchor, ScrollHandler scrollHandler) {
public AsyncItemChangeHandler(Context context, View snackbarAnchor,
ScrollHandler scrollHandler,
AlarmController alarmController) {
mContext = context.getApplicationContext(); // to prevent memory leaks
mSnackbarAnchor = snackbarAnchor;
mScrollHandler = scrollHandler;
mAlarmController = alarmController;
}
public void asyncAddAlarm(final Alarm alarm) {
@ -54,6 +58,7 @@ public final class AsyncItemChangeHandler {
@Override
protected void onPostExecute(Integer integer) {
mAlarmController.cancelAlarm(alarm, false);
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.
@ -87,17 +92,13 @@ public final class AsyncItemChangeHandler {
@Override
protected void onPostExecute(Long result) {
AlarmUtils.scheduleAlarm(mContext, mAlarm, true);
// TODO: Consider adding delay to allow the alarm item animation
// to finish first before we show the snackbar. Inbox app does this.
mAlarmController.scheduleAlarm(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() {

View File

@ -6,7 +6,7 @@ import android.content.Intent;
import com.philliphsu.clock2.model.AlarmDatabaseHelper.AlarmCursor;
import com.philliphsu.clock2.model.DatabaseManager;
import com.philliphsu.clock2.util.AlarmUtils;
import com.philliphsu.clock2.util.AlarmController;
/**
* An {@link IntentService} subclass for handling asynchronous task requests in
@ -59,6 +59,7 @@ public class OnBootUpAlarmScheduler extends IntentService {
@Override
protected void onHandleIntent(Intent intent) {
if (intent != null) {
AlarmController controller = new AlarmController(this, null);
// IntentService works in a background thread, so this won't hold us up.
AlarmCursor cursor = DatabaseManager.getInstance(this).queryEnabledAlarms();
while (cursor.moveToNext()) {
@ -67,7 +68,7 @@ public class OnBootUpAlarmScheduler extends IntentService {
throw new IllegalStateException(
"queryEnabledAlarms() returned alarm(s) that aren't enabled");
}
AlarmUtils.scheduleAlarm(this, alarm, false);
controller.scheduleAlarm(alarm, false);
}
cursor.close();

View File

@ -5,7 +5,7 @@ import android.content.Context;
import android.content.Intent;
import com.philliphsu.clock2.model.DatabaseManager;
import com.philliphsu.clock2.util.AlarmUtils;
import com.philliphsu.clock2.util.AlarmController;
import static com.philliphsu.clock2.util.Preconditions.checkNotNull;
@ -47,12 +47,10 @@ public class PendingAlarmScheduler extends BroadcastReceiver {
throw new IllegalStateException("Alarm must be enabled!");
}
alarm.ignoreUpcomingRingTime(false); // allow #ringsWithinHours() to behave normally
// Because showToast = false, we don't do any UI work.
// TODO: Since we're in a worker thread, verify that the
// UI related code within will not cause us to crash.
AlarmUtils.scheduleAlarm(context, alarm, false);
// Update the db
AlarmUtils.save(context, alarm);
// No UI work is done
AlarmController controller = new AlarmController(context, null);
controller.scheduleAlarm(alarm, false);
controller.save(alarm);
}
}).start();
}

View File

@ -10,7 +10,7 @@ import android.os.AsyncTask;
import android.support.v4.app.NotificationCompat;
import com.philliphsu.clock2.model.DatabaseManager;
import com.philliphsu.clock2.util.AlarmUtils;
import com.philliphsu.clock2.util.AlarmController;
import static android.app.PendingIntent.FLAG_ONE_SHOT;
import static com.philliphsu.clock2.util.DateFormatUtils.formatTime;
@ -47,8 +47,7 @@ public class UpcomingAlarmReceiver extends BroadcastReceiver {
@Override
protected void onPostExecute(Alarm alarm) {
if (ACTION_DISMISS_NOW.equals(intent.getAction())) {
// This MUST be done on the UI thread.
AlarmUtils.cancelAlarm(context, alarm, true);
new AlarmController(context, null).cancelAlarm(alarm, false);
} else {
// Prepare notification
// http://stackoverflow.com/a/15803726/5055032

View File

@ -18,6 +18,7 @@ import com.philliphsu.clock2.DaysOfWeek;
import com.philliphsu.clock2.OnListItemInteractionListener;
import com.philliphsu.clock2.R;
import com.philliphsu.clock2.model.AlarmsRepository;
import com.philliphsu.clock2.util.AlarmController;
import com.philliphsu.clock2.util.AlarmUtils;
import java.util.Date;
@ -38,6 +39,8 @@ import static com.philliphsu.clock2.util.DateFormatUtils.formatTime;
public class AlarmViewHolder extends BaseViewHolder<Alarm> implements AlarmCountdown.OnTickListener {
private static final RelativeSizeSpan AMPM_SIZE_SPAN = new RelativeSizeSpan(0.5f);
private final AlarmController mAlarmController;
@Bind(R.id.time) TextView mTime;
@Bind(R.id.on_off_switch) SwitchCompat mSwitch;
@Bind(R.id.label) TextView mLabel;
@ -45,8 +48,17 @@ public class AlarmViewHolder extends BaseViewHolder<Alarm> implements AlarmCount
@Bind(R.id.recurring_days) TextView mDays;
@Bind(R.id.dismiss) Button mDismissButton;
@Deprecated // TODO: Delete this, the only usage is from AlarmsAdapter (SortedList), which is not used anymore.
public AlarmViewHolder(ViewGroup parent, OnListItemInteractionListener<Alarm> listener) {
super(parent, R.layout.item_alarm, listener);
mAlarmController = null;
mCountdown.setOnTickListener(this);
}
public AlarmViewHolder(ViewGroup parent, OnListItemInteractionListener<Alarm> listener,
AlarmController alarmController) {
super(parent, R.layout.item_alarm, listener);
mAlarmController = alarmController;
mCountdown.setOnTickListener(this);
}
@ -76,7 +88,7 @@ public class AlarmViewHolder extends BaseViewHolder<Alarm> implements AlarmCount
} else {
// Dismisses the current upcoming alarm and handles scheduling the next alarm for us.
// Since changes are saved to the database, this prompts a UI refresh.
AlarmUtils.cancelAlarm(getContext(), alarm, true);
mAlarmController.cancelAlarm(alarm, true);
}
// TOneverDO: AlarmUtils.cancelAlarm() otherwise it will be called twice
/*
@ -127,11 +139,10 @@ public class AlarmViewHolder extends BaseViewHolder<Alarm> implements AlarmCount
alarm.setEnabled(checked);
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);
mAlarmController.scheduleAlarm(alarm, true);
mAlarmController.save(alarm);
} else {
AlarmUtils.cancelAlarm(getContext(), alarm, true);
mAlarmController.cancelAlarm(alarm, true);
// cancelAlarm() already calls save() for you.
}
mSwitch.setPressed(false); // clear the pressed focus, esp. if setPressed(true) was called manually

View File

@ -8,6 +8,7 @@ import android.view.ViewGroup;
import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.OnListItemInteractionListener;
import com.philliphsu.clock2.model.AlarmDatabaseHelper.AlarmCursor;
import com.philliphsu.clock2.util.AlarmController;
/**
* Created by Phillip Hsu on 6/29/2016.
@ -18,10 +19,13 @@ public class AlarmsCursorAdapter extends RecyclerView.Adapter<AlarmViewHolder> {
private static final String TAG = "AlarmsCursorAdapter";
private final OnListItemInteractionListener<Alarm> mListener;
private final AlarmController mAlarmController;
private AlarmCursor mCursor;
public AlarmsCursorAdapter(OnListItemInteractionListener<Alarm> listener) {
public AlarmsCursorAdapter(OnListItemInteractionListener<Alarm> listener,
AlarmController alarmController) {
mListener = listener;
mAlarmController = alarmController;
// Excerpt from docs of notifyDataSetChanged():
// "RecyclerView will attempt to synthesize [artificially create?]
// visible structural change events [when items are inserted, removed or
@ -34,7 +38,7 @@ public class AlarmsCursorAdapter extends RecyclerView.Adapter<AlarmViewHolder> {
@Override
public AlarmViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new AlarmViewHolder(parent, mListener);
return new AlarmViewHolder(parent, mListener, mAlarmController);
}
@Override

View File

@ -1,7 +1,6 @@
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;
@ -23,8 +22,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 com.philliphsu.clock2.util.AlarmController;
import com.philliphsu.clock2.util.DelayedSnackbarHandler;
import butterknife.Bind;
import butterknife.ButterKnife;
@ -37,14 +36,10 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks<Cursor>,
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 AlarmController mAlarmController;
private Handler mHandler = new Handler();
private View mSnackbarAnchor;
private long mScrollToStableId = RecyclerView.NO_ID;
@ -78,6 +73,9 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks<Cursor>,
// Will succeed because the activity is created at this point.
// See the Fragment lifecycle.
mSnackbarAnchor = getActivity().findViewById(R.id.main_content);
mAlarmController = new AlarmController(getActivity(), mSnackbarAnchor);
mAsyncItemChangeHandler = new AsyncItemChangeHandler(getActivity(),
mSnackbarAnchor, this, mAlarmController);
getLoaderManager().initLoader(0, null, this);
}
@ -87,33 +85,22 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks<Cursor>,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_alarms, container, false);
ButterKnife.bind(this, view);
// Set the adapter
Context context = view.getContext();
mList.setLayoutManager(new LinearLayoutManager(context));
mAdapter = new AlarmsCursorAdapter(this);
mAdapter = new AlarmsCursorAdapter(this, mAlarmController);
mList.setAdapter(mAdapter);
mAsyncItemChangeHandler = new AsyncItemChangeHandler(
getActivity(), mSnackbarAnchor, this);
return view;
}
@Override
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);
public void onResume() {
super.onResume();
// Show the pending Snackbar, if any, that was prepared for us
// by another app component.
DelayedSnackbarHandler.makeAndShow(mSnackbarAnchor);
}
@Override
@ -221,19 +208,6 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks<Cursor>,
// 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;

View File

@ -33,6 +33,7 @@ import com.philliphsu.clock2.SharedPreferencesHelper;
import com.philliphsu.clock2.model.AlarmLoader;
import com.philliphsu.clock2.model.DatabaseManager;
import com.philliphsu.clock2.ringtone.RingtoneActivity;
import com.philliphsu.clock2.util.AlarmController;
import com.philliphsu.clock2.util.AlarmUtils;
import com.philliphsu.clock2.util.LocalBroadcastHelper;
@ -520,7 +521,9 @@ public class EditAlarmActivity extends BaseActivity implements AlarmNumpad.KeyLi
@Override
public void cancelAlarm(Alarm alarm, boolean showToast) {
AlarmUtils.cancelAlarm(this, alarm, showToast);
// TODO: Rewrite XML layout to use CoordinatorLayout and
// pass in the snackbar anchor.
new AlarmController(this, null).cancelAlarm(alarm, true);
if (RingtoneActivity.isAlive()) {
LocalBroadcastHelper.sendBroadcast(this, RingtoneActivity.ACTION_FINISH);
}

View File

@ -13,7 +13,7 @@ import android.widget.Button;
import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.R;
import com.philliphsu.clock2.model.AlarmLoader;
import com.philliphsu.clock2.util.AlarmUtils;
import com.philliphsu.clock2.util.AlarmController;
import com.philliphsu.clock2.util.LocalBroadcastHelper;
/**
@ -34,6 +34,7 @@ public class RingtoneActivity extends AppCompatActivity implements
private long mAlarmId;
private Alarm mAlarm;
private AlarmController mAlarmController;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -59,6 +60,8 @@ public class RingtoneActivity extends AppCompatActivity implements
.putExtra(EXTRA_ITEM_ID, mAlarmId);
startService(intent);
mAlarmController = new AlarmController(this, null);
// TODO: Butterknife binding
Button snooze = (Button) findViewById(R.id.btn_snooze);
snooze.setOnClickListener(new View.OnClickListener() {
@ -145,7 +148,7 @@ public class RingtoneActivity extends AppCompatActivity implements
if (mAlarm != null) {
// TODO: If the upcoming alarm notification isn't present, verify other notifications aren't affected.
// This could be the case if we're starting a new instance of this activity after leaving the first launch.
AlarmUtils.removeUpcomingAlarmNotification(this, mAlarm);
mAlarmController.removeUpcomingAlarmNotification(mAlarm);
}
}
@ -160,7 +163,7 @@ public class RingtoneActivity extends AppCompatActivity implements
private void snooze() {
if (mAlarm != null) {
AlarmUtils.snoozeAlarm(this, mAlarm);
mAlarmController.snoozeAlarm(mAlarm);
}
// Can't call dismiss() because we don't want to also call cancelAlarm()! Why? For example,
// we don't want the alarm, if it has no recurrence, to be turned off right now.
@ -170,7 +173,7 @@ public class RingtoneActivity extends AppCompatActivity implements
private void dismiss() {
if (mAlarm != null) {
// TODO do we really need to cancel the intent and alarm?
AlarmUtils.cancelAlarm(this, mAlarm, false);
mAlarmController.cancelAlarm(mAlarm, false);
}
stopAndFinish();
}

View File

@ -22,6 +22,7 @@ import android.util.Log;
import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.R;
import com.philliphsu.clock2.model.DatabaseManager;
import com.philliphsu.clock2.util.AlarmController;
import com.philliphsu.clock2.util.AlarmUtils;
import com.philliphsu.clock2.util.LocalBroadcastHelper;
@ -54,6 +55,7 @@ public class RingtoneService extends Service { // TODO: abstract this, make subc
private Ringtone mRingtone;
private Alarm mAlarm;
private String mNormalRingTime;
private AlarmController mAlarmController;
private boolean mAutoSilenced = false;
// TODO: Using Handler for this is ill-suited? Alarm ringing could outlast the
// application's life. Use AlarmManager API instead.
@ -63,7 +65,7 @@ public class RingtoneService extends Service { // TODO: abstract this, make subc
public void run() {
mAutoSilenced = true;
// TODO do we really need to cancel the alarm and intent?
AlarmUtils.cancelAlarm(RingtoneService.this, mAlarm, false);
mAlarmController.cancelAlarm(mAlarm, false);
finishActivity();
stopSelf();
}
@ -72,7 +74,7 @@ public class RingtoneService extends Service { // TODO: abstract this, make subc
@Override
public void onReceive(Context context, Intent intent) {
mAutoSilenced = true;
// TODO: Do we need to call AlarmUtils.cancelAlarm()?
// TODO: Do we need to call mAlarmController.cancelAlarm()?
stopSelf();
// Activity finishes itself
}
@ -101,9 +103,9 @@ public class RingtoneService extends Service { // TODO: abstract this, make subc
}).start();
} else {
if (ACTION_SNOOZE.equals(intent.getAction())) {
AlarmUtils.snoozeAlarm(this, mAlarm);
mAlarmController.snoozeAlarm(mAlarm);
} else if (ACTION_DISMISS.equals(intent.getAction())) {
AlarmUtils.cancelAlarm(this, mAlarm, false); // TODO do we really need to cancel the intent and alarm?
mAlarmController.cancelAlarm(mAlarm, false); // TODO do we really need to cancel the intent and alarm?
} else {
throw new UnsupportedOperationException();
}
@ -119,6 +121,7 @@ public class RingtoneService extends Service { // TODO: abstract this, make subc
public void onCreate() {
super.onCreate();
LocalBroadcastHelper.registerReceiver(this, mNotifyMissedReceiver, ACTION_NOTIFY_MISSED);
mAlarmController = new AlarmController(this, null);
}
@Override

View File

@ -0,0 +1,225 @@
package com.philliphsu.clock2.util;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.support.design.widget.Snackbar;
import android.util.Log;
import android.view.View;
import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.PendingAlarmScheduler;
import com.philliphsu.clock2.R;
import com.philliphsu.clock2.UpcomingAlarmReceiver;
import com.philliphsu.clock2.model.DatabaseManager;
import com.philliphsu.clock2.ringtone.RingtoneActivity;
import com.philliphsu.clock2.ringtone.RingtoneService;
import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
import static android.app.PendingIntent.FLAG_NO_CREATE;
import static android.app.PendingIntent.getActivity;
import static com.philliphsu.clock2.util.DateFormatUtils.formatTime;
import static java.util.concurrent.TimeUnit.HOURS;
/**
* Created by Phillip Hsu on 7/10/2016.
*
* API to control alarm states and update the UI.
* TODO: Move this out of the .utils package when done.
* TODO: Rename to AlarmStateHandler? AlarmStateController?
*/
public final class AlarmController {
private static final String TAG = "AlarmController";
private final Context mAppContext;
private final View mSnackbarAnchor;
/**
*
* @param context the Context from which the application context will be requested
* @param snackbarAnchor an optional anchor for a Snackbar to anchor to
*/
public AlarmController(Context context, View snackbarAnchor) {
mAppContext = context.getApplicationContext();
mSnackbarAnchor = snackbarAnchor;
}
/**
* Schedules the alarm with the {@link AlarmManager}.
* If {@code alarm.}{@link Alarm#isEnabled() isEnabled()}
* returns false, this does nothing and returns immediately.
*/
public void scheduleAlarm(Alarm alarm, boolean showSnackbar) {
if (!alarm.isEnabled()) {
Log.i(TAG, "Skipped scheduling an alarm because it was not enabled");
return;
}
// TODO: Consider doing this in a new thread.
Log.d(TAG, "Scheduling alarm " + alarm);
AlarmManager am = (AlarmManager) mAppContext.getSystemService(Context.ALARM_SERVICE);
// If there is already an alarm for this Intent scheduled (with the equality of two
// intents being defined by filterEquals(Intent)), then it will be removed and replaced
// by this one. For most of our uses, the relevant criteria for equality will be the
// action, the data, and the class (component). Although not documented, the request code
// of a PendingIntent is also considered to determine equality of two intents.
// WAKEUP alarm types wake the CPU up, but NOT the screen. If that is what you want, you need
// to handle that yourself by using a wakelock, etc..
// We use a WAKEUP alarm to send the upcoming alarm notification so it goes off even if the
// device is asleep. Otherwise, it will not go off until the device is turned back on.
long ringAt = alarm.isSnoozed() ? alarm.snoozingUntil() : alarm.ringsAt();
int hoursToNotifyInAdvance = AlarmUtils.hoursBeforeUpcoming(mAppContext);
long upcomingAt = ringAt - HOURS.toMillis(hoursToNotifyInAdvance);
// If snoozed, upcoming note posted immediately.
am.set(AlarmManager.RTC_WAKEUP, upcomingAt, notifyUpcomingAlarmIntent(alarm, false));
am.setExact(AlarmManager.RTC_WAKEUP, ringAt, alarmIntent(alarm, false));
if (showSnackbar) {
String message = mAppContext.getString(R.string.alarm_set_for,
DurationUtils.toString(mAppContext, alarm.ringsIn(), false /*abbreviate?*/));
// TODO: Consider adding delay to allow the alarm item animation
// to finish first before we show the snackbar. Inbox app does this.
showSnackbar(message);
}
}
/**
* Cancel the alarm. This does NOT check if you previously scheduled the alarm.
*/
public void cancelAlarm(Alarm alarm, boolean showSnackbar) {
// TODO: Consider doing this in a new thread.
Log.d(TAG, "Cancelling alarm " + alarm);
AlarmManager am = (AlarmManager) mAppContext.getSystemService(Context.ALARM_SERVICE);
PendingIntent pi = alarmIntent(alarm, true);
if (pi != null) {
am.cancel(pi);
pi.cancel();
}
pi = notifyUpcomingAlarmIntent(alarm, true);
if (pi != null) {
am.cancel(pi);
pi.cancel();
}
// Does nothing if it's not posted.
removeUpcomingAlarmNotification(alarm);
int hoursToNotifyInAdvance = AlarmUtils.hoursBeforeUpcoming(mAppContext);
// TOneverDO: Place block after making value changes to the alarm.
if (showSnackbar
// TODO: Consider showing the snackbar for non-upcoming alarms too;
// then, we can remove these checks.
&& alarm.ringsWithinHours(hoursToNotifyInAdvance) || alarm.isSnoozed()) {
long time = alarm.isSnoozed() ? alarm.snoozingUntil() : alarm.ringsAt();
String msg = mAppContext.getString(R.string.upcoming_alarm_dismissed,
formatTime(mAppContext, time));
showSnackbar(msg);
}
if (alarm.isSnoozed()) {
alarm.stopSnoozing();
}
if (!alarm.hasRecurrence()) {
alarm.setEnabled(false);
} else if (alarm.isEnabled()) {
if (alarm.ringsWithinHours(hoursToNotifyInAdvance)) {
// Still upcoming today, so wait until the normal ring time
// passes before rescheduling the alarm.
alarm.ignoreUpcomingRingTime(true); // Useful only for VH binding
Intent intent = new Intent(mAppContext, PendingAlarmScheduler.class)
.putExtra(PendingAlarmScheduler.EXTRA_ALARM_ID, alarm.id());
pi = PendingIntent.getBroadcast(mAppContext, alarm.intId(),
intent, PendingIntent.FLAG_ONE_SHOT);
am.set(AlarmManager.RTC_WAKEUP, alarm.ringsAt(), pi);
} else {
scheduleAlarm(alarm, false);
}
}
save(alarm);
// If service is not running, nothing happens
mAppContext.stopService(new Intent(mAppContext, RingtoneService.class));
}
public void snoozeAlarm(Alarm alarm) {
int minutesToSnooze = AlarmUtils.snoozeDuration(mAppContext);
alarm.snooze(minutesToSnooze);
scheduleAlarm(alarm, false);
String message = mAppContext.getString(R.string.title_snoozing_until,
formatTime(mAppContext, alarm.snoozingUntil()));
// Since snoozing is always done by an app component away from
// the list screen, the Snackbar will never be shown. In fact, this
// controller has a null mSnackbarAnchor if we're using it for snoozing
// an alarm. We solve this by preparing the message, and waiting until
// the list screen is resumed so that it can display the Snackbar for us.
DelayedSnackbarHandler.prepareMessage(message);
save(alarm);
}
public void removeUpcomingAlarmNotification(Alarm a) {
Intent intent = new Intent(mAppContext, UpcomingAlarmReceiver.class)
.setAction(UpcomingAlarmReceiver.ACTION_CANCEL_NOTIFICATION)
.putExtra(UpcomingAlarmReceiver.EXTRA_ALARM_ID, a.id());
mAppContext.sendBroadcast(intent);
}
public void save(final Alarm alarm) {
// TODO: Will using the Runnable like this cause a memory leak?
new Thread(new Runnable() {
@Override
public void run() {
DatabaseManager.getInstance(mAppContext).updateAlarm(alarm.id(), alarm);
}
}).start();
}
private PendingIntent alarmIntent(Alarm alarm, boolean retrievePrevious) {
// TODO: Use appropriate subclass instead
Intent intent = new Intent(mAppContext, RingtoneActivity.class)
.putExtra(RingtoneActivity.EXTRA_ITEM_ID, alarm.id());
int flag = retrievePrevious ? FLAG_NO_CREATE : FLAG_CANCEL_CURRENT;
PendingIntent pi = getActivity(mAppContext, alarm.intId(), intent, flag);
// Even when we try to retrieve a previous instance that actually did exist,
// null can be returned for some reason.
/*
if (retrievePrevious) {
checkNotNull(pi);
}
*/
return pi;
}
private PendingIntent notifyUpcomingAlarmIntent(Alarm alarm, boolean retrievePrevious) {
Intent intent = new Intent(mAppContext, UpcomingAlarmReceiver.class)
.putExtra(UpcomingAlarmReceiver.EXTRA_ALARM_ID, alarm.id());
if (alarm.isSnoozed()) {
// TODO: Will this affect retrieving a previous instance? Say if the previous instance
// didn't have this action set initially, but at a later time we made a new instance
// with it set.
intent.setAction(UpcomingAlarmReceiver.ACTION_SHOW_SNOOZING);
}
int flag = retrievePrevious ? FLAG_NO_CREATE : FLAG_CANCEL_CURRENT;
PendingIntent pi = PendingIntent.getBroadcast(mAppContext, alarm.intId(), intent, flag);
// Even when we try to retrieve a previous instance that actually did exist,
// null can be returned for some reason.
/*
if (retrievePrevious) {
checkNotNull(pi);
}
*/
return pi;
}
private void showSnackbar(String message) {
// Is the window containing this anchor currently focused?
if (mSnackbarAnchor != null && mSnackbarAnchor.hasWindowFocus()) {
Snackbar.make(mSnackbarAnchor, message, Snackbar.LENGTH_LONG).show();
}
}
}

View File

@ -4,19 +4,15 @@ 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;
@ -48,7 +44,7 @@ public final class AlarmUtils {
* @deprecated {@code showToast} is no longer working. Callers must
* handle popup confirmations on their own.
*/
// TODO: Delete showToast param
// TODO: Consider moving usages to the background
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");
@ -149,14 +145,6 @@ 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);
}
@ -235,30 +223,4 @@ 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();
}
}
}

View File

@ -0,0 +1,58 @@
package com.philliphsu.clock2.util;
import android.support.design.widget.Snackbar;
import android.view.View;
/**
* Created by Phillip Hsu on 7/10/2016.
*
* Handler to prepare a Snackbar to be shown only when requested to.
* Useful when the Snackbar is created in an app component that
* is not where it should be shown.
*/
public final class DelayedSnackbarHandler {
// TODO: Consider wrapping this in a WeakReference, so that you
// don't prevent this from being GCed if you never call #show().
private static Snackbar snackbar;
private static String message;
private DelayedSnackbarHandler() {}
/**
* Saves a reference to the given Snackbar, so that you can
* call {@link #show()} at a later time.
*/
public static void prepareSnackbar(Snackbar sb) {
snackbar = sb;
}
/**
* Shows the Snackbar previously prepared with
* {@link #prepareSnackbar(Snackbar)}
*/
public static void show() {
if (snackbar != null) {
snackbar.show();
snackbar = null;
}
}
/**
* Saves a static reference to the message, so that you can
* call {@link #makeAndShow(View)} at a later time.
*/
public static void prepareMessage(String msg) {
message = msg;
}
/**
* Makes a Snackbar with the message previously prepared with
* {@link #prepareMessage(String)} and shows it.
*/
public static void makeAndShow(View snackbarAnchor) {
if (snackbarAnchor != null && message != null) {
Snackbar.make(snackbarAnchor, message, Snackbar.LENGTH_LONG).show();
message = null;
}
}
}