Refactored alarm management classes

This commit is contained in:
Phillip Hsu 2016-06-03 14:40:27 -07:00
parent b798a8e2b0
commit 49b7d80185
9 changed files with 221 additions and 95 deletions

View File

@ -172,6 +172,10 @@ public abstract class Alarm implements JsonSerializable {
return ringsIn() <= hours * 3600000; return ringsIn() <= hours * 3600000;
} }
public int intId() {
return (int) id();
}
@Override @Override
@NonNull @NonNull
public JSONObject toJsonObject() { public JSONObject toJsonObject() {

View File

@ -1,9 +1,6 @@
package com.philliphsu.clock2; package com.philliphsu.clock2;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Intent; import android.content.Intent;
import android.media.RingtoneManager;
import android.os.Bundle; import android.os.Bundle;
import android.support.design.widget.FloatingActionButton; import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar; import android.support.design.widget.Snackbar;
@ -21,7 +18,6 @@ import android.widget.TextView;
import com.philliphsu.clock2.alarms.AlarmsFragment; import com.philliphsu.clock2.alarms.AlarmsFragment;
import com.philliphsu.clock2.editalarm.EditAlarmActivity; import com.philliphsu.clock2.editalarm.EditAlarmActivity;
import com.philliphsu.clock2.ringtone.RingtoneActivity;
public class MainActivity extends BaseActivity implements AlarmsFragment.OnAlarmInteractionListener { public class MainActivity extends BaseActivity implements AlarmsFragment.OnAlarmInteractionListener {
private static final String TAG = "MainActivity"; private static final String TAG = "MainActivity";
@ -61,22 +57,6 @@ public class MainActivity extends BaseActivity implements AlarmsFragment.OnAlarm
@Override @Override
public void onClick(View view) { public void onClick(View view) {
startEditAlarmActivity(-1); startEditAlarmActivity(-1);
/*
scheduleAlarm();
Snackbar.make(view, "Alarm set for 1 minute from now", Snackbar.LENGTH_INDEFINITE)
.setAction("Dismiss", new View.OnClickListener() {
@Override
public void onClick(View v) {
AlarmManager am = (AlarmManager) getSystemService(ALARM_SERVICE);
PendingIntent pi = alarmIntent(true);
am.cancel(pi);
pi.cancel();
Intent intent = new Intent(MainActivity.this, UpcomingAlarmReceiver.class)
.setAction(UpcomingAlarmReceiver.ACTION_CANCEL_NOTIFICATION);
sendBroadcast(intent);
}
}).show();
*/
} }
}); });
} }
@ -208,46 +188,4 @@ public class MainActivity extends BaseActivity implements AlarmsFragment.OnAlarm
intent.putExtra(EditAlarmActivity.EXTRA_ALARM_ID, alarmId); intent.putExtra(EditAlarmActivity.EXTRA_ALARM_ID, alarmId);
startActivity(intent); startActivity(intent);
} }
private void scheduleAlarm() {
AlarmManager am = (AlarmManager) getSystemService(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.
// todo: use alarm's ring time - (number of hours to be notified in advance, converted to millis)
am.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), notifyUpcomingAlarmIntent());
// todo: get alarm's ring time
am.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 10000, alarmIntent(false));
}
private static int alarmCount;
private PendingIntent alarmIntent(boolean retrievePrevious) {
// TODO: Use appropriate subclass instead
Intent intent = new Intent(this, RingtoneActivity.class)
.setData(RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM));
// TODO: Pass in the id of the alarm to the intent. Alternatively, if the upcoming alarm note
// only needs to show the alarm's ring time, just pass in the alarm's ringsAt().
// TODO: Use unique request codes per alarm.
// If a PendingIntent with this request code already exists, then we are likely modifying
// an alarm, so we should cancel the existing intent.
int requestCode = retrievePrevious ? alarmCount - 1 : alarmCount++;
int flag = retrievePrevious
? PendingIntent.FLAG_NO_CREATE
: PendingIntent.FLAG_CANCEL_CURRENT;
return PendingIntent.getActivity(this, requestCode, intent, flag);
}
private PendingIntent notifyUpcomingAlarmIntent() {
Intent intent = new Intent(this, UpcomingAlarmReceiver.class);
// TODO: Use unique request codes per alarm.
return PendingIntent.getBroadcast(this, alarmCount, intent, PendingIntent.FLAG_CANCEL_CURRENT);
}
} }

View File

@ -6,38 +6,72 @@ import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
import android.util.Log;
import com.philliphsu.clock2.model.AlarmsRepository;
import static com.philliphsu.clock2.util.DateFormatUtils.formatTime;
import static com.philliphsu.clock2.util.Preconditions.checkNotNull;
public class UpcomingAlarmReceiver extends BroadcastReceiver { public class UpcomingAlarmReceiver extends BroadcastReceiver {
private static final String TAG = "UpcomingAlarmReceiver";
public static final String ACTION_CANCEL_NOTIFICATION public static final String ACTION_CANCEL_NOTIFICATION
= "com.philliphsu.clock2.action.CANCEL_NOTIFICATION"; = "com.philliphsu.clock2.action.CANCEL_NOTIFICATION";
public static final String ACTION_SHOW_SNOOZING public static final String ACTION_SHOW_SNOOZING
= "com.philliphsu.clock2.action.CANCEL_NOTIFICATION"; = "com.philliphsu.clock2.action.SHOW_SNOOZING";
public static final String EXTRA_ALARM_ID
private static int count = -1; = "com.philliphsu.clock2.extra.ALARM_ID";
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
long id = intent.getLongExtra(EXTRA_ALARM_ID, -1);
if (id < 0) {
Log.e(TAG, "No alarm id received");
}
Alarm alarm = checkNotNull(AlarmsRepository.getInstance(context).getItem(id));
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
if (ACTION_CANCEL_NOTIFICATION.equals(intent.getAction())) { if (intent.getAction() != null) {
nm.cancel(count); // TODO: Verify that no java/project configuration is needed for strings to work in switch
} else if (ACTION_SHOW_SNOOZING.equals(intent.getAction())) { switch (intent.getAction()) {
case ACTION_CANCEL_NOTIFICATION:
nm.cancel(getClass().getName(), alarm.intId());
break;
case ACTION_SHOW_SNOOZING:
if (!alarm.isSnoozed()) {
throw new IllegalStateException("Can't show snoozing notif. if alarm not snoozed!");
}
String title = alarm.label().isEmpty()
? context.getString(R.string.alarm)
: alarm.label();
String text = context.getString(R.string.title_snoozing_until,
formatTime(context, alarm.snoozingUntil()));
Notification note = new NotificationCompat.Builder(context) Notification note = new NotificationCompat.Builder(context)
.setSmallIcon(R.mipmap.ic_launcher) .setSmallIcon(R.mipmap.ic_launcher) // TODO: alarm icon
.setContentTitle("Snoozing") .setContentTitle(title)
.setContentText("New ring time here") .setContentText(text)
.setOngoing(true) .setOngoing(true)
.build(); .build();
// todo actions // todo actions
nm.notify(count, note); nm.notify(getClass().getName(), alarm.intId(), note);
break;
default:
break;
}
} else { } else {
// No intent action required for default behavior
String text = formatTime(context, alarm.ringsAt());
if (!alarm.label().isEmpty()) {
text = alarm.label() + ", " + text;
}
Notification note = new NotificationCompat.Builder(context) Notification note = new NotificationCompat.Builder(context)
.setSmallIcon(R.mipmap.ic_launcher) .setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle("Upcoming alarm") .setContentTitle(context.getString(R.string.upcoming_alarm))
.setContentText("Ring time here") .setContentText(text)
.setOngoing(true) .setOngoing(true)
.build(); .build();
// todo actions // todo actions
nm.notify(++count, note); nm.notify(getClass().getName(), alarm.intId(), note);
} }
} }
} }

View File

@ -0,0 +1,89 @@
package com.philliphsu.clock2.editalarm;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.UpcomingAlarmReceiver;
import com.philliphsu.clock2.ringtone.RingtoneActivity;
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.Preconditions.checkNotNull;
/**
* Created by Phillip Hsu on 6/3/2016.
*
* Utilities for scheduling and unscheduling alarms with the {@link AlarmManager}, as well as
* managing the upcoming alarm notification.
*
* TODO: Adapt this to Timers too...
*/
public final class AlarmUtils {
private AlarmUtils() {}
public static void scheduleAlarm(Context context, Alarm alarm) {
AlarmManager am = (AlarmManager) context.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.
// todo: read shared prefs for number of hours to be notified in advance
am.set(AlarmManager.RTC_WAKEUP, alarm.ringsAt() - 2*3600000, notifyUpcomingAlarmIntent(context, alarm, false));
am.setExact(AlarmManager.RTC_WAKEUP, alarm.ringsAt(), alarmIntent(context, alarm, false));
}
public static void unscheduleAlarm(Context c, Alarm a) {
AlarmManager am = (AlarmManager) c.getSystemService(Context.ALARM_SERVICE);
PendingIntent pi = alarmIntent(c, a, true);
am.cancel(pi);
pi.cancel();
pi = notifyUpcomingAlarmIntent(c, a, true);
am.cancel(pi);
pi.cancel();
removeUpcomingAlarmNotification(c, a);
}
public static void removeUpcomingAlarmNotification(Context c, Alarm a) {
Intent intent = new Intent(c, UpcomingAlarmReceiver.class)
.setAction(UpcomingAlarmReceiver.ACTION_CANCEL_NOTIFICATION)
.putExtra(UpcomingAlarmReceiver.EXTRA_ALARM_ID, a.id());
c.sendBroadcast(intent);
}
private static PendingIntent alarmIntent(Context context, Alarm alarm, boolean retrievePrevious) {
// TODO: Use appropriate subclass instead
Intent intent = new Intent(context, RingtoneActivity.class)
.putExtra(RingtoneActivity.EXTRA_ITEM_ID, alarm.id());
int flag = retrievePrevious ? FLAG_NO_CREATE : FLAG_CANCEL_CURRENT;
PendingIntent pi = getActivity(context, alarm.intId(), intent, flag);
if (retrievePrevious) {
checkNotNull(pi);
}
return pi;
}
private static PendingIntent notifyUpcomingAlarmIntent(Context context, Alarm alarm, boolean retrievePrevious) {
Intent intent = new Intent(context, UpcomingAlarmReceiver.class)
.putExtra(UpcomingAlarmReceiver.EXTRA_ALARM_ID, alarm.id());
int flag = retrievePrevious ? FLAG_NO_CREATE : FLAG_CANCEL_CURRENT;
PendingIntent pi = PendingIntent.getBroadcast(context, alarm.intId(), intent, flag);
if (retrievePrevious) {
checkNotNull(pi);
}
return pi;
}
}

View File

@ -31,6 +31,11 @@ public class EditAlarmPresenter implements EditAlarmContract.Presenter {
@Override @Override
public void loadAlarm(long alarmId) { public void loadAlarm(long alarmId) {
// Can't load alarm in ctor because showDetails() calls
// showTime(), which calls setTime() on the numpad, which
// fires onNumberInput() events, which routes to the presenter,
// which would not be initialized yet because we still haven't
// returned from the ctor.
mAlarm = alarmId > -1 ? mRepository.getItem(alarmId) : null; mAlarm = alarmId > -1 ? mRepository.getItem(alarmId) : null;
showDetails(); showDetails();
} }

View File

@ -4,14 +4,16 @@ import android.app.AlarmManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Intent; import android.content.Intent;
import android.media.RingtoneManager; import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.R; import com.philliphsu.clock2.R;
import com.philliphsu.clock2.UpcomingAlarmReceiver; import com.philliphsu.clock2.UpcomingAlarmReceiver;
import com.philliphsu.clock2.editalarm.AlarmUtils;
import com.philliphsu.clock2.model.AlarmsRepository;
import static com.philliphsu.clock2.util.Preconditions.checkNotNull; import static com.philliphsu.clock2.util.Preconditions.checkNotNull;
@ -24,19 +26,27 @@ import static com.philliphsu.clock2.util.Preconditions.checkNotNull;
*/ */
public class RingtoneActivity extends AppCompatActivity { public class RingtoneActivity extends AppCompatActivity {
// Shared with RingtoneService
public static final String EXTRA_ITEM_ID = "com.philliphsu.clock2.ringtone.extra.ITEM_ID";
private Alarm mAlarm;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_ringtone); setContentView(R.layout.activity_ringtone);
long id = getIntent().getLongExtra(EXTRA_ITEM_ID, -1);
if (id < 0) {
throw new IllegalStateException("Cannot start RingtoneActivity without item's id");
}
mAlarm = checkNotNull(AlarmsRepository.getInstance(this).getItem(id));
// Play the ringtone // Play the ringtone
Uri ringtone = checkNotNull(getIntent().getData()); Intent intent = new Intent(this, RingtoneService.class)
Intent intent = new Intent(this, RingtoneService.class).setData(ringtone); .putExtra(EXTRA_ITEM_ID, mAlarm.id());
startService(intent); startService(intent);
// Cancel the upcoming alarm notification
Intent intent2 = new Intent(this, UpcomingAlarmReceiver.class) AlarmUtils.removeUpcomingAlarmNotification(this, mAlarm);
.setAction(UpcomingAlarmReceiver.ACTION_CANCEL_NOTIFICATION);
sendBroadcast(intent2);
Button snooze = (Button) findViewById(R.id.btn_snooze); Button snooze = (Button) findViewById(R.id.btn_snooze);
snooze.setOnClickListener(new View.OnClickListener() { snooze.setOnClickListener(new View.OnClickListener() {

View File

@ -14,8 +14,11 @@ import android.os.IBinder;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
import android.util.Log; import android.util.Log;
import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.R; import com.philliphsu.clock2.R;
import com.philliphsu.clock2.model.AlarmsRepository;
import static com.philliphsu.clock2.util.DateFormatUtils.formatTime;
import static com.philliphsu.clock2.util.Preconditions.checkNotNull; import static com.philliphsu.clock2.util.Preconditions.checkNotNull;
/** /**
@ -26,12 +29,16 @@ import static com.philliphsu.clock2.util.Preconditions.checkNotNull;
* navigate away from the Activity without making an action. But if they do accidentally navigate away, * navigate away from the Activity without making an action. But if they do accidentally navigate away,
* they have plenty of time to make the desired action via the notification. * they have plenty of time to make the desired action via the notification.
*/ */
public class RingtoneService extends Service { public class RingtoneService extends Service { // TODO: abstract this, make subclasses
private static final String TAG = "RingtoneService"; private static final String TAG = "RingtoneService";
private AudioManager mAudioManager; private AudioManager mAudioManager;
private Ringtone mRingtone; private Ringtone mRingtone;
private Alarm mAlarm;
private String mNormalRingTime;
private boolean mAutoSilenced = false; private boolean mAutoSilenced = false;
// TODO: Using Handler for this is ill-suited? Alarm ringing could outlast the
// application's life. Use AlarmManager API instead.
private final Handler mSilenceHandler = new Handler(); private final Handler mSilenceHandler = new Handler();
private final Runnable mSilenceRunnable = new Runnable() { private final Runnable mSilenceRunnable = new Runnable() {
@Override @Override
@ -50,16 +57,21 @@ public class RingtoneService extends Service {
@Override @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
if (mAudioManager == null && mRingtone == null) { if (mAudioManager == null && mRingtone == null) {
Uri ringtone = checkNotNull(intent.getData()); long id = intent.getLongExtra(RingtoneActivity.EXTRA_ITEM_ID, -1);
mAlarm = checkNotNull(AlarmsRepository.getInstance(this).getItem(id));
// TODO: The below call requires a notification, and there is no way to provide one suitable // TODO: The below call requires a notification, and there is no way to provide one suitable
// for both Alarms and Timers. Consider making this class abstract, and have subclasses // for both Alarms and Timers. Consider making this class abstract, and have subclasses
// implement an abstract method that calls startForeground(). You would then call that // implement an abstract method that calls startForeground(). You would then call that
// method here instead. // method here instead.
String title = mAlarm.label().isEmpty()
? getString(R.string.alarm)
: mAlarm.label();
mNormalRingTime = formatTime(this, System.currentTimeMillis()); // now
Notification note = new NotificationCompat.Builder(this) Notification note = new NotificationCompat.Builder(this)
// Required contents // Required contents
.setSmallIcon(R.mipmap.ic_launcher) // TODO: alarm icon .setSmallIcon(R.mipmap.ic_launcher) // TODO: alarm icon
.setContentTitle("Foreground RingtoneService") .setContentTitle(title)
.setContentText("Ringtone is playing in the foreground.") .setContentText(mNormalRingTime)
.build(); .build();
startForeground(R.id.ringtone_service_notification, note); // TOneverDO: Pass 0 as the first argument startForeground(R.id.ringtone_service_notification, note); // TOneverDO: Pass 0 as the first argument
@ -72,6 +84,7 @@ public class RingtoneService extends Service {
// Request permanent focus, as ringing could last several minutes // Request permanent focus, as ringing could last several minutes
AudioManager.AUDIOFOCUS_GAIN); AudioManager.AUDIOFOCUS_GAIN);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Uri ringtone = Uri.parse(mAlarm.ringtone());
mRingtone = RingtoneManager.getRingtone(this, ringtone); mRingtone = RingtoneManager.getRingtone(this, ringtone);
// Deprecated, but the alternative AudioAttributes requires API 21 // Deprecated, but the alternative AudioAttributes requires API 21
mRingtone.setStreamType(AudioManager.STREAM_ALARM); mRingtone.setStreamType(AudioManager.STREAM_ALARM);
@ -94,11 +107,15 @@ public class RingtoneService extends Service {
// TODO: You should probably do this in the appropriate subclass. // TODO: You should probably do this in the appropriate subclass.
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
Notification note = new NotificationCompat.Builder(this) Notification note = new NotificationCompat.Builder(this)
.setContentTitle("Missed alarm") .setContentTitle(getString(R.string.missed_alarm))
.setContentText("Regular alarm time here") .setContentText(mNormalRingTime)
.setSmallIcon(R.mipmap.ic_launcher) .setSmallIcon(R.mipmap.ic_launcher)
//.setShowWhen(true) // TODO: Is it shown by default?
.build(); .build();
nm.notify("tag", 0, note); // A tag with the name of the subclass is used in addition to the item's id to prevent
// conflicting notifications for items of different class types. Items of any class type
// have ids starting from 0.
nm.notify(getClass().getName(), mAlarm.intId(), note);
} }
stopForeground(true); stopForeground(true);
} }

View File

@ -0,0 +1,19 @@
package com.philliphsu.clock2.util;
import android.content.Context;
import java.util.Date;
import static android.text.format.DateFormat.getTimeFormat;
/**
* Created by Phillip Hsu on 6/3/2016.
*/
public final class DateFormatUtils {
private DateFormatUtils() {}
public static String formatTime(Context context, long millis) {
return getTimeFormat(context).format(new Date(millis));
}
}

View File

@ -7,6 +7,7 @@
<string name="dummy_button">Dummy Button</string> <string name="dummy_button">Dummy Button</string>
<string name="dummy_content">DUMMY\nCONTENT</string> <string name="dummy_content">DUMMY\nCONTENT</string>
<!-- ================================= EDIT ALARM ACTIVITY ==================================-->
<string name="title_activity_edit_alarm">EditAlarmActivity</string> <string name="title_activity_edit_alarm">EditAlarmActivity</string>
<string name="save">Save</string> <string name="save">Save</string>
<string name="delete">Delete</string> <string name="delete">Delete</string>
@ -15,9 +16,18 @@
<string name="title_snoozing_until">Snoozing until %1$s</string> <string name="title_snoozing_until">Snoozing until %1$s</string>
<string name="dismiss_now">Dismiss now</string> <string name="dismiss_now">Dismiss now</string>
<string name="done_snoozing">Done snoozing</string> <string name="done_snoozing">Done snoozing</string>
<!-- ======================================================================================= -->
<!-- ==================================== NOTIFICATIONS ==================================== -->
<string name="upcoming_alarm">Upcoming alarm</string>
<string name="alarm">Alarm</string>
<string name="missed_alarm">Missed alarm</string>
<!-- ======================================================================================= -->
<!-- ==================================== MAIN ACTIVITY ==================================== -->
<string name="snackbar_item_deleted">%1$s deleted</string> <string name="snackbar_item_deleted">%1$s deleted</string>
<string name="snackbar_undo_item_deleted">Undo</string> <string name="snackbar_undo_item_deleted">Undo</string>
<!-- ======================================================================================= -->
<string name="sun">Sun</string> <string name="sun">Sun</string>
<string name="mon">Mon</string> <string name="mon">Mon</string>