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
@@ -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();
}
}
}
@@ -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();
}
}
}
@@ -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;
}
}
}