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