From 49b7d801858544e78b75ce27deb4f1dc826f4aa2 Mon Sep 17 00:00:00 2001 From: Phillip Hsu Date: Fri, 3 Jun 2016 14:40:27 -0700 Subject: [PATCH] Refactored alarm management classes --- .../java/com/philliphsu/clock2/Alarm.java | 4 + .../com/philliphsu/clock2/MainActivity.java | 62 ------------- .../clock2/UpcomingAlarmReceiver.java | 68 ++++++++++---- .../clock2/editalarm/AlarmUtils.java | 89 +++++++++++++++++++ .../clock2/editalarm/EditAlarmPresenter.java | 5 ++ .../clock2/ringtone/RingtoneActivity.java | 26 ++++-- .../clock2/ringtone/RingtoneService.java | 31 +++++-- .../clock2/util/DateFormatUtils.java | 19 ++++ app/src/main/res/values/strings.xml | 12 ++- 9 files changed, 221 insertions(+), 95 deletions(-) create mode 100644 app/src/main/java/com/philliphsu/clock2/editalarm/AlarmUtils.java create mode 100644 app/src/main/java/com/philliphsu/clock2/util/DateFormatUtils.java diff --git a/app/src/main/java/com/philliphsu/clock2/Alarm.java b/app/src/main/java/com/philliphsu/clock2/Alarm.java index 851ba29..bc9c262 100644 --- a/app/src/main/java/com/philliphsu/clock2/Alarm.java +++ b/app/src/main/java/com/philliphsu/clock2/Alarm.java @@ -172,6 +172,10 @@ public abstract class Alarm implements JsonSerializable { return ringsIn() <= hours * 3600000; } + public int intId() { + return (int) id(); + } + @Override @NonNull public JSONObject toJsonObject() { diff --git a/app/src/main/java/com/philliphsu/clock2/MainActivity.java b/app/src/main/java/com/philliphsu/clock2/MainActivity.java index 766fad7..a2817e0 100644 --- a/app/src/main/java/com/philliphsu/clock2/MainActivity.java +++ b/app/src/main/java/com/philliphsu/clock2/MainActivity.java @@ -1,9 +1,6 @@ package com.philliphsu.clock2; -import android.app.AlarmManager; -import android.app.PendingIntent; import android.content.Intent; -import android.media.RingtoneManager; import android.os.Bundle; import android.support.design.widget.FloatingActionButton; import android.support.design.widget.Snackbar; @@ -21,7 +18,6 @@ import android.widget.TextView; import com.philliphsu.clock2.alarms.AlarmsFragment; import com.philliphsu.clock2.editalarm.EditAlarmActivity; -import com.philliphsu.clock2.ringtone.RingtoneActivity; public class MainActivity extends BaseActivity implements AlarmsFragment.OnAlarmInteractionListener { private static final String TAG = "MainActivity"; @@ -61,22 +57,6 @@ public class MainActivity extends BaseActivity implements AlarmsFragment.OnAlarm @Override public void onClick(View view) { 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); 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); - } } diff --git a/app/src/main/java/com/philliphsu/clock2/UpcomingAlarmReceiver.java b/app/src/main/java/com/philliphsu/clock2/UpcomingAlarmReceiver.java index 4331542..a7a176a 100644 --- a/app/src/main/java/com/philliphsu/clock2/UpcomingAlarmReceiver.java +++ b/app/src/main/java/com/philliphsu/clock2/UpcomingAlarmReceiver.java @@ -6,38 +6,72 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; 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 { + private static final String TAG = "UpcomingAlarmReceiver"; + public static final String ACTION_CANCEL_NOTIFICATION = "com.philliphsu.clock2.action.CANCEL_NOTIFICATION"; public static final String ACTION_SHOW_SNOOZING - = "com.philliphsu.clock2.action.CANCEL_NOTIFICATION"; - - private static int count = -1; + = "com.philliphsu.clock2.action.SHOW_SNOOZING"; + public static final String EXTRA_ALARM_ID + = "com.philliphsu.clock2.extra.ALARM_ID"; @Override 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); - if (ACTION_CANCEL_NOTIFICATION.equals(intent.getAction())) { - nm.cancel(count); - } else if (ACTION_SHOW_SNOOZING.equals(intent.getAction())) { - Notification note = new NotificationCompat.Builder(context) - .setSmallIcon(R.mipmap.ic_launcher) - .setContentTitle("Snoozing") - .setContentText("New ring time here") - .setOngoing(true) - .build(); - // todo actions - nm.notify(count, note); + if (intent.getAction() != null) { + // TODO: Verify that no java/project configuration is needed for strings to work in switch + 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) + .setSmallIcon(R.mipmap.ic_launcher) // TODO: alarm icon + .setContentTitle(title) + .setContentText(text) + .setOngoing(true) + .build(); + // todo actions + nm.notify(getClass().getName(), alarm.intId(), note); + break; + default: + break; + } } 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) .setSmallIcon(R.mipmap.ic_launcher) - .setContentTitle("Upcoming alarm") - .setContentText("Ring time here") + .setContentTitle(context.getString(R.string.upcoming_alarm)) + .setContentText(text) .setOngoing(true) .build(); // todo actions - nm.notify(++count, note); + nm.notify(getClass().getName(), alarm.intId(), note); } } } diff --git a/app/src/main/java/com/philliphsu/clock2/editalarm/AlarmUtils.java b/app/src/main/java/com/philliphsu/clock2/editalarm/AlarmUtils.java new file mode 100644 index 0000000..5c459fe --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/editalarm/AlarmUtils.java @@ -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; + } +} diff --git a/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmPresenter.java b/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmPresenter.java index c22ee6f..8651db6 100644 --- a/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmPresenter.java +++ b/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmPresenter.java @@ -31,6 +31,11 @@ public class EditAlarmPresenter implements EditAlarmContract.Presenter { @Override 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; showDetails(); } diff --git a/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneActivity.java b/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneActivity.java index 9797e52..1820ea1 100644 --- a/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneActivity.java +++ b/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneActivity.java @@ -4,14 +4,16 @@ import android.app.AlarmManager; import android.app.PendingIntent; import android.content.Intent; import android.media.RingtoneManager; -import android.net.Uri; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.view.View; import android.widget.Button; +import com.philliphsu.clock2.Alarm; import com.philliphsu.clock2.R; 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; @@ -24,19 +26,27 @@ import static com.philliphsu.clock2.util.Preconditions.checkNotNull; */ 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 protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - 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 - Uri ringtone = checkNotNull(getIntent().getData()); - Intent intent = new Intent(this, RingtoneService.class).setData(ringtone); + Intent intent = new Intent(this, RingtoneService.class) + .putExtra(EXTRA_ITEM_ID, mAlarm.id()); startService(intent); - // Cancel the upcoming alarm notification - Intent intent2 = new Intent(this, UpcomingAlarmReceiver.class) - .setAction(UpcomingAlarmReceiver.ACTION_CANCEL_NOTIFICATION); - sendBroadcast(intent2); + + AlarmUtils.removeUpcomingAlarmNotification(this, mAlarm); Button snooze = (Button) findViewById(R.id.btn_snooze); snooze.setOnClickListener(new View.OnClickListener() { diff --git a/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneService.java b/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneService.java index 9de5578..c3b242a 100644 --- a/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneService.java +++ b/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneService.java @@ -14,8 +14,11 @@ import android.os.IBinder; import android.support.v4.app.NotificationCompat; import android.util.Log; +import com.philliphsu.clock2.Alarm; 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; /** @@ -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, * 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 AudioManager mAudioManager; private Ringtone mRingtone; + private Alarm mAlarm; + private String mNormalRingTime; 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 Runnable mSilenceRunnable = new Runnable() { @Override @@ -50,16 +57,21 @@ public class RingtoneService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { 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 // 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 // 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) // Required contents .setSmallIcon(R.mipmap.ic_launcher) // TODO: alarm icon - .setContentTitle("Foreground RingtoneService") - .setContentText("Ringtone is playing in the foreground.") + .setContentTitle(title) + .setContentText(mNormalRingTime) .build(); 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 AudioManager.AUDIOFOCUS_GAIN); if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + Uri ringtone = Uri.parse(mAlarm.ringtone()); mRingtone = RingtoneManager.getRingtone(this, ringtone); // Deprecated, but the alternative AudioAttributes requires API 21 mRingtone.setStreamType(AudioManager.STREAM_ALARM); @@ -94,11 +107,15 @@ public class RingtoneService extends Service { // TODO: You should probably do this in the appropriate subclass. NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); Notification note = new NotificationCompat.Builder(this) - .setContentTitle("Missed alarm") - .setContentText("Regular alarm time here") + .setContentTitle(getString(R.string.missed_alarm)) + .setContentText(mNormalRingTime) .setSmallIcon(R.mipmap.ic_launcher) + //.setShowWhen(true) // TODO: Is it shown by default? .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); } diff --git a/app/src/main/java/com/philliphsu/clock2/util/DateFormatUtils.java b/app/src/main/java/com/philliphsu/clock2/util/DateFormatUtils.java new file mode 100644 index 0000000..488e105 --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/util/DateFormatUtils.java @@ -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)); + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a1672ef..7994cc1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,6 +7,7 @@ Dummy Button DUMMY\nCONTENT + EditAlarmActivity Save Delete @@ -15,9 +16,18 @@ Snoozing until %1$s Dismiss now Done snoozing - + + + + Upcoming alarm + Alarm + Missed alarm + + + %1$s deleted Undo + Sun Mon