Created AlarmController class and moved relevant AlarmUtils code
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user