diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8a02120..652a6e1 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -21,19 +21,21 @@
-
+
+ -->
-
-
+ -->
-
+
-
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/philliphsu/clock2/AsyncTimersTableUpdateHandler.java b/app/src/main/java/com/philliphsu/clock2/AsyncTimersTableUpdateHandler.java
index 2e598de..f105405 100644
--- a/app/src/main/java/com/philliphsu/clock2/AsyncTimersTableUpdateHandler.java
+++ b/app/src/main/java/com/philliphsu/clock2/AsyncTimersTableUpdateHandler.java
@@ -4,8 +4,6 @@ import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
-import android.os.SystemClock;
-import android.util.Log;
import com.philliphsu.clock2.alarms.ScrollHandler;
import com.philliphsu.clock2.model.TimersTableManager;
@@ -29,14 +27,12 @@ public final class AsyncTimersTableUpdateHandler extends AsyncDatabaseTableUpdat
@Override
protected void onPostAsyncDelete(Integer result, Timer timer) {
- cancelAlarm(timer);
+ cancelAlarm(timer, true);
}
@Override
protected void onPostAsyncInsert(Long result, Timer timer) {
- Log.d(TAG, "onPostAsyncInsert()");
if (timer.isRunning()) {
- Log.d(TAG, "Scheduling alarm for timer launch");
scheduleAlarm(timer);
}
}
@@ -48,7 +44,7 @@ public final class AsyncTimersTableUpdateHandler extends AsyncDatabaseTableUpdat
// will remove and replace it.
scheduleAlarm(timer);
} else {
- cancelAlarm(timer);
+ cancelAlarm(timer, !timer.hasStarted());
}
}
@@ -65,19 +61,20 @@ public final class AsyncTimersTableUpdateHandler extends AsyncDatabaseTableUpdat
}
private void scheduleAlarm(Timer timer) {
- Log.d(TAG, String.format("now = %d, endTime = %d", SystemClock.elapsedRealtime(), timer.endTime()));
AlarmManager am = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
am.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, timer.endTime(), createTimesUpIntent(timer));
TimerNotificationService.showNotification(getContext(), timer.getId());
}
- private void cancelAlarm(Timer timer) {
+ private void cancelAlarm(Timer timer, boolean removeNotification) {
// Cancel the alarm scheduled. If one was never scheduled, does nothing.
AlarmManager am = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
PendingIntent pi = createTimesUpIntent(timer);
// Now can't be null
am.cancel(pi);
pi.cancel();
- TimerNotificationService.cancelNotification(getContext(), timer.getId());
+ if (removeNotification) {
+ TimerNotificationService.cancelNotification(getContext(), timer.getId());
+ }
}
}
diff --git a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmActivity.java b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmActivity.java
index 39ce01f..2484bf6 100644
--- a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmActivity.java
+++ b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmActivity.java
@@ -1,14 +1,13 @@
package com.philliphsu.clock2.alarms;
import android.os.Bundle;
-import android.support.v4.content.Loader;
import android.view.View;
import android.widget.Button;
import com.philliphsu.clock2.Alarm;
import com.philliphsu.clock2.R;
-import com.philliphsu.clock2.model.AlarmLoader;
import com.philliphsu.clock2.ringtone.RingtoneActivity;
+import com.philliphsu.clock2.ringtone.RingtoneService;
import com.philliphsu.clock2.util.AlarmController;
public class AlarmActivity extends RingtoneActivity {
@@ -20,7 +19,13 @@ public class AlarmActivity extends RingtoneActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ if ((mAlarm = getIntent().getParcelableExtra(EXTRA_ITEM)) == null) {
+ throw new IllegalStateException("Cannot start AlarmActivity without an Alarm");
+ }
mAlarmController = new AlarmController(this, null);
+ // TODO: If the upcoming alarm notification isn't present, verify other notifications aren't affected.
+ // This could be the case if we're starting a new instance of this activity after leaving the first launch.
+ mAlarmController.removeUpcomingAlarmNotification(mAlarm);
// TODO: Butterknife binding
Button snooze = (Button) findViewById(R.id.btn_snooze);
snooze.setOnClickListener(new View.OnClickListener() {
@@ -38,27 +43,32 @@ public class AlarmActivity extends RingtoneActivity {
});
}
- @Override
- public Loader onCreateLoader(long id) {
- return new AlarmLoader(this, id);
- }
-
- @Override
- public void onLoadFinished(Loader loader, Alarm data) {
- super.onLoadFinished(loader, data);
- mAlarm = data;
- if (data != null) {
- // TODO: If the upcoming alarm notification isn't present, verify other notifications aren't affected.
- // This could be the case if we're starting a new instance of this activity after leaving the first launch.
- mAlarmController.removeUpcomingAlarmNotification(data);
- }
- }
+// @Override
+// public Loader onCreateLoader(long id) {
+// return new AlarmLoader(this, id);
+// }
+//
+// @Override
+// public void onLoadFinished(Loader loader, Alarm data) {
+// super.onLoadFinished(loader, data);
+// mAlarm = data;
+// if (data != null) {
+// // TODO: If the upcoming alarm notification isn't present, verify other notifications aren't affected.
+// // This could be the case if we're starting a new instance of this activity after leaving the first launch.
+// mAlarmController.removeUpcomingAlarmNotification(data);
+// }
+// }
@Override
public int layoutResource() {
return R.layout.activity_ringtone;
}
+ @Override
+ protected Class extends RingtoneService> getRingtoneServiceClass() {
+ return AlarmRingtoneService.class;
+ }
+
private void snooze() {
if (mAlarm != null) {
mAlarmController.snoozeAlarm(mAlarm);
diff --git a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmRingtoneService.java b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmRingtoneService.java
new file mode 100644
index 0000000..c156d27
--- /dev/null
+++ b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmRingtoneService.java
@@ -0,0 +1,127 @@
+package com.philliphsu.clock2.alarms;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Intent;
+import android.media.Ringtone;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.support.v4.app.NotificationCompat;
+
+import com.philliphsu.clock2.Alarm;
+import com.philliphsu.clock2.R;
+import com.philliphsu.clock2.ringtone.RingtoneService;
+import com.philliphsu.clock2.util.AlarmController;
+import com.philliphsu.clock2.util.AlarmUtils;
+
+import static com.philliphsu.clock2.util.DateFormatUtils.formatTime;
+
+public class AlarmRingtoneService extends RingtoneService {
+ private static final String TAG = "AlarmRingtoneService";
+ /* TOneverDO: not private */
+ private static final String ACTION_SNOOZE = "com.philliphsu.clock2.ringtone.action.SNOOZE";
+ private static final String ACTION_DISMISS = "com.philliphsu.clock2.ringtone.action.DISMISS";
+
+ private String mNormalRingTime;
+ private AlarmController mAlarmController;
+ private Alarm mAlarm;
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ // TOneverDO: Call super before our custom logic
+ if (intent.getAction() == null) {
+// final long id = intent.getLongExtra(EXTRA_ITEM_ID, -1);
+// if (id < 0)
+// throw new IllegalStateException("No item id set");
+ // http://stackoverflow.com/q/8696146/5055032
+ // Start our own thread to load the alarm instead of using a loader,
+ // because Services do not have a built-in LoaderManager (because they have no need for one since
+ // their lifecycle is not complex like in Activities/Fragments) and our
+ // work is simple enough that getting loaders to work here is not
+ // worth the effort.
+// // TODO: Will using the Runnable like this cause a memory leak?
+// new Thread(new Runnable() {
+// @Override
+// public void run() {
+// // TODO: We don't actually need the exact same Alarm instance as the
+// // one from our calling component, because we won't mutate any of its
+// // fields. Since we only read values, we could just pass in the Alarm
+// // to the intent as a Parcelable.
+// AlarmCursor cursor = new AlarmsTableManager(AlarmRingtoneService.this).queryItem(id);
+// mAlarm = checkNotNull(cursor.getItem());
+// }
+// }).start();
+ if ((mAlarm = intent.getParcelableExtra(EXTRA_ITEM)) == null) {
+ throw new IllegalStateException("Cannot start AlarmRingtoneService without an Alarm");
+ }
+ } else {
+ if (ACTION_SNOOZE.equals(intent.getAction())) {
+ mAlarmController.snoozeAlarm(mAlarm);
+ } else if (ACTION_DISMISS.equals(intent.getAction())) {
+ mAlarmController.cancelAlarm(mAlarm, false); // TODO do we really need to cancel the intent and alarm?
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ // ==========================================================================
+ stopSelf(startId);
+ finishActivity();
+ }
+ return super.onStartCommand(intent, flags, startId);
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mAlarmController = new AlarmController(this, null);
+ }
+
+ @Override
+ protected void onAutoSilenced() {
+ // TODO do we really need to cancel the alarm and intent?
+ mAlarmController.cancelAlarm(mAlarm, false);
+ // Post notification that alarm was missed
+ NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ Notification note = new NotificationCompat.Builder(this)
+ .setContentTitle(getString(R.string.missed_alarm))
+ .setContentText(mNormalRingTime)
+ .setSmallIcon(R.mipmap.ic_launcher)
+ .build();
+ nm.notify(TAG, mAlarm.intId(), note);
+ }
+
+ @Override
+ protected Ringtone getRingtone() {
+ Uri ringtone = Uri.parse(mAlarm.ringtone());
+ return RingtoneManager.getRingtone(this, ringtone);
+ }
+
+ @Override
+ protected Notification getForegroundNotification() {
+ String title = mAlarm.label().isEmpty()
+ ? getString(R.string.alarm)
+ : mAlarm.label();
+ mNormalRingTime = formatTime(this, System.currentTimeMillis()); // now
+ return new NotificationCompat.Builder(this)
+ // Required contents
+ .setSmallIcon(R.mipmap.ic_launcher) // TODO: alarm icon
+ .setContentTitle(title)
+ .setContentText(mNormalRingTime)
+ .addAction(R.mipmap.ic_launcher,
+ getString(R.string.snooze),
+ getPendingIntent(ACTION_SNOOZE, mAlarm))
+ .addAction(R.mipmap.ic_launcher,
+ getString(R.string.dismiss),
+ getPendingIntent(ACTION_DISMISS, mAlarm))
+ .build();
+ }
+
+ @Override
+ protected boolean doesVibrate() {
+ return mAlarm.vibrates();
+ }
+
+ @Override
+ protected int minutesToAutoSilence() {
+ return AlarmUtils.minutesToSilenceAfter(this);
+ }
+}
diff --git a/app/src/main/java/com/philliphsu/clock2/model/DatabaseTableManager.java b/app/src/main/java/com/philliphsu/clock2/model/DatabaseTableManager.java
index 9517fe6..6e574ea 100644
--- a/app/src/main/java/com/philliphsu/clock2/model/DatabaseTableManager.java
+++ b/app/src/main/java/com/philliphsu/clock2/model/DatabaseTableManager.java
@@ -73,9 +73,9 @@ public abstract class DatabaseTableManager {
toContentValues(newItem),
COLUMN_ID + " = " + id,
null);
- if (rowsUpdated == 0) {
- throw new IllegalStateException("wtf?");
- }
+// if (rowsUpdated == 0) {
+// throw new IllegalStateException("wtf?");
+// }
notifyContentChanged();
return rowsUpdated;
}
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 d32d4ca..4314a6e 100644
--- a/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneActivity.java
+++ b/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneActivity.java
@@ -4,9 +4,8 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
+import android.os.Parcelable;
import android.support.annotation.LayoutRes;
-import android.support.v4.app.LoaderManager.LoaderCallbacks;
-import android.support.v4.content.Loader;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.view.WindowManager;
@@ -17,24 +16,27 @@ import com.philliphsu.clock2.util.LocalBroadcastHelper;
* An example full-screen activity that shows and hides the system UI (i.e.
* status bar and navigation/system bar) with user interaction.
*/
-public abstract class RingtoneActivity extends AppCompatActivity implements LoaderCallbacks {
+public abstract class RingtoneActivity extends AppCompatActivity /*implements LoaderCallbacks*/ {
private static final String TAG = "RingtoneActivity";
// Shared with RingtoneService
- public static final String EXTRA_ITEM_ID = "com.philliphsu.clock2.ringtone.extra.ITEM_ID";
- public static final String ACTION_FINISH = "com.philliphsu.clock2.ringtone.action.UNBIND";
+// public static final String EXTRA_ITEM_ID = "com.philliphsu.clock2.ringtone.extra.ITEM_ID";
+ public static final String ACTION_FINISH = "com.philliphsu.clock2.ringtone.action.FINISH";
+ public static final String EXTRA_ITEM = "com.philliphsu.clock2.ringtone.extra.ITEM";
private static boolean sIsAlive = false;
- private long mItemId;
- private T mItem;
+// private long mItemId;
+// private T mItem;
- public abstract Loader onCreateLoader(long itemId);
+// public abstract Loader onCreateLoader(long itemId);
// TODO: Should we extend from BaseActivity instead?
@LayoutRes
public abstract int layoutResource();
+ protected abstract Class extends RingtoneService> getRingtoneServiceClass();
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -46,17 +48,17 @@ public abstract class RingtoneActivity extends AppCompatActivity implements L
getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
- mItemId = getIntent().getLongExtra(EXTRA_ITEM_ID, -1);
- if (mItemId < 0) {
- throw new IllegalStateException("Cannot start RingtoneActivity without item's id");
- }
+// mItemId = getIntent().getLongExtra(EXTRA_ITEM_ID, -1);
+// if (mItemId < 0) {
+// throw new IllegalStateException("Cannot start RingtoneActivity without item's id");
+// }
// The reason we don't use a thread to load the alarm is because this is an
// Activity, which has complex lifecycle. LoaderManager is designed to help
// us through the vagaries of the lifecycle that could affect loading data.
- getSupportLoaderManager().initLoader(0, null, this);
+// getSupportLoaderManager().initLoader(0, null, this);
- Intent intent = new Intent(this, RingtoneService.class)
- .putExtra(EXTRA_ITEM_ID, mItemId);
+ Intent intent = new Intent(this, getRingtoneServiceClass())
+ .putExtra(EXTRA_ITEM, getIntent().getParcelableExtra(EXTRA_ITEM));
startService(intent);
}
@@ -124,20 +126,20 @@ public abstract class RingtoneActivity extends AppCompatActivity implements L
sIsAlive = false;
}
- @Override
- public Loader onCreateLoader(int id, Bundle args) {
- return onCreateLoader(mItemId);
- }
-
- @Override
- public void onLoadFinished(Loader loader, T data) {
- mItem = data;
- }
-
- @Override
- public void onLoaderReset(Loader loader) {
- // Do nothing
- }
+// @Override
+// public Loader onCreateLoader(int id, Bundle args) {
+// return onCreateLoader(mItemId);
+// }
+//
+// @Override
+// public void onLoadFinished(Loader loader, T data) {
+// mItem = data;
+// }
+//
+// @Override
+// public void onLoaderReset(Loader loader) {
+// // Do nothing
+// }
public static boolean isAlive() {
return sIsAlive;
@@ -148,7 +150,7 @@ public abstract class RingtoneActivity extends AppCompatActivity implements L
* ringtone and finish us.
*/
protected final void stopAndFinish() {
- stopService(new Intent(this, RingtoneService.class));
+ stopService(new Intent(this, getRingtoneServiceClass()));
finish();
}
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 8f85939..eb282ef 100644
--- a/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneService.java
+++ b/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneService.java
@@ -1,34 +1,23 @@
package com.philliphsu.clock2.ringtone;
import android.app.Notification;
-import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
-import android.content.BroadcastReceiver;
-import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.media.Ringtone;
-import android.media.RingtoneManager;
-import android.net.Uri;
import android.os.Handler;
import android.os.IBinder;
import android.os.Vibrator;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-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.AlarmCursor;
-import com.philliphsu.clock2.model.AlarmsTableManager;
-import com.philliphsu.clock2.util.AlarmController;
-import com.philliphsu.clock2.util.AlarmUtils;
import com.philliphsu.clock2.util.LocalBroadcastHelper;
-import static com.philliphsu.clock2.util.DateFormatUtils.formatTime;
-import static com.philliphsu.clock2.util.Preconditions.checkNotNull;
+import java.util.concurrent.TimeUnit;
/**
* Runs in the foreground. While it can still be killed by the system, it stays alive significantly
@@ -40,89 +29,104 @@ import static com.philliphsu.clock2.util.Preconditions.checkNotNull;
*
* TOneverDO: Change this to not be a started service!
*/
-public class RingtoneService extends Service { // TODO: abstract this, make subclasses
+// TODO: Remove this from manifest, keep only the subclasses.
+public abstract class RingtoneService extends Service {
private static final String TAG = "RingtoneService";
- /* TOneverDO: not private */
- private static final String ACTION_SNOOZE = "com.philliphsu.clock2.ringtone.action.SNOOZE";
- private static final String ACTION_DISMISS = "com.philliphsu.clock2.ringtone.action.DISMISS";
// public okay
public static final String ACTION_NOTIFY_MISSED = "com.philliphsu.clock2.ringtone.action.NOTIFY_MISSED";
- // TODO: Same value as RingtoneActivity.EXTRA_ITEM_ID. Is it important enough to define a different constant?
- private static final String EXTRA_ITEM_ID = "com.philliphsu.clock2.ringtone.extra.ITEM_ID";
+// public static final String EXTRA_ITEM_ID = RingtoneActivity.EXTRA_ITEM_ID;
+ public static final String EXTRA_ITEM = RingtoneActivity.EXTRA_ITEM;
private AudioManager mAudioManager;
- @Nullable private Vibrator mVibrator;
private Ringtone mRingtone;
- private Alarm mAlarm;
- private String mNormalRingTime;
- private AlarmController mAlarmController;
- private boolean mAutoSilenced = false;
+ @Nullable private Vibrator mVibrator;
+
// 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
public void run() {
- mAutoSilenced = true;
- // TODO do we really need to cancel the alarm and intent?
- mAlarmController.cancelAlarm(mAlarm, false);
+ onAutoSilenced();
+ // TODO: Consider not finishing the activity, but update
+ // its view to display that this ringing was missed?
finishActivity();
stopSelf();
}
};
- private final BroadcastReceiver mNotifyMissedReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- mAutoSilenced = true;
- // TODO: Do we need to call mAlarmController.cancelAlarm()?
- stopSelf();
- // Activity finishes itself
- }
- };
+
+ // Pretty sure we don't need this anymore...
+// private final BroadcastReceiver mNotifyMissedReceiver = new BroadcastReceiver() {
+// @Override
+// public void onReceive(Context context, Intent intent) {
+// // TODO: Do we need to call mAlarmController.cancelAlarm()?
+// onAutoSilenced();
+// stopSelf();
+// // Activity finishes itself
+// }
+// };
+
+ protected abstract void onAutoSilenced();
+
+ protected abstract Ringtone getRingtone();
+
+ /**
+ * @return the notification to show when this Service starts in the foreground
+ */
+ protected abstract Notification getForegroundNotification();
+
+ protected abstract boolean doesVibrate();
+
+ /**
+ * @return the number of minutes to keep ringing before auto silence
+ */
+ protected abstract int minutesToAutoSilence();
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
- final long id = intent.getLongExtra(EXTRA_ITEM_ID, -1);
- if (id < 0)
- throw new IllegalStateException("No item id set");
- if (intent.getAction() == null) {
- // http://stackoverflow.com/q/8696146/5055032
- // Start our own thread to load the alarm instead of using a loader,
- // because Services do not have a built-in LoaderManager (because they have no need for one since
- // their lifecycle is not complex like in Activities/Fragments) and our
- // work is simple enough that getting loaders to work here is not
- // worth the effort.
- // TODO: Will using the Runnable like this cause a memory leak?
- new Thread(new Runnable() {
- @Override
- public void run() {
- AlarmCursor cursor = new AlarmsTableManager(RingtoneService.this).queryItem(id);
- mAlarm = checkNotNull(cursor.getItem());
- playRingtone();
- }
- }).start();
- } else {
- if (ACTION_SNOOZE.equals(intent.getAction())) {
- mAlarmController.snoozeAlarm(mAlarm);
- } else if (ACTION_DISMISS.equals(intent.getAction())) {
- mAlarmController.cancelAlarm(mAlarm, false); // TODO do we really need to cancel the intent and alarm?
- } else {
- throw new UnsupportedOperationException();
- }
- // ==========================================================================
- stopSelf(startId);
- finishActivity();
- }
+ // Play ringtone, if not already playing
+ if (mAudioManager == null && mRingtone == null) {
+ // TOneverDO: Pass 0 as the first argument
+ startForeground(R.id.ringtone_service_notification, getForegroundNotification());
- return START_NOT_STICKY; // If killed while started, don't recreate. Should be sufficient.
+ mAudioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
+ // Request audio focus first, so we don't play our ringtone on top of any
+ // other apps that currently have playback.
+ int result = mAudioManager.requestAudioFocus(
+ null, // Playback will likely be short, so don't worry about listening for focus changes
+ AudioManager.STREAM_ALARM,
+ // Request permanent focus, as ringing could last several minutes
+ AudioManager.AUDIOFOCUS_GAIN);
+ if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+ mRingtone = getRingtone();
+ // Deprecated, but the alternative AudioAttributes requires API 21
+ mRingtone.setStreamType(AudioManager.STREAM_ALARM);
+ mRingtone.play();
+ if (doesVibrate()) {
+ mVibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
+ mVibrator.vibrate(new long[] { // apply pattern
+ 0, // millis to wait before turning vibrator on
+ 500, // millis to keep vibrator on before turning off
+ 500, // millis to wait before turning back on
+ 500 // millis to keep on before turning off
+ }, 2 /* start repeating at this index of the array, after one cycle */);
+ }
+ // Schedule auto silence
+ mSilenceHandler.postDelayed(mSilenceRunnable,
+ TimeUnit.MINUTES.toMillis(minutesToAutoSilence()));
+ }
+ }
+ // If killed while started, don't recreate. Should be sufficient.
+ return START_NOT_STICKY;
}
@Override
public void onCreate() {
super.onCreate();
- LocalBroadcastHelper.registerReceiver(this, mNotifyMissedReceiver, ACTION_NOTIFY_MISSED);
- mAlarmController = new AlarmController(this, null);
+ // Pretty sure this won't ever get called anymore...
+// LocalBroadcastHelper.registerReceiver(this, mNotifyMissedReceiver, ACTION_NOTIFY_MISSED);
}
@Override
@@ -134,103 +138,40 @@ public class RingtoneService extends Service { // TODO: abstract this, make subc
mVibrator.cancel();
}
mSilenceHandler.removeCallbacks(mSilenceRunnable);
- if (mAutoSilenced) {
- // Post notification that alarm was missed, or timer expired.
- // TODO: You should probably do this in the appropriate subclass.
- NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
- Notification note = new NotificationCompat.Builder(this)
- .setContentTitle(getString(R.string.missed_alarm))
- .setContentText(mNormalRingTime)
- .setSmallIcon(R.mipmap.ic_launcher)
- .build();
- // 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);
- LocalBroadcastHelper.unregisterReceiver(this, mNotifyMissedReceiver);
+// LocalBroadcastHelper.unregisterReceiver(this, mNotifyMissedReceiver);
}
@Override
- public IBinder onBind(Intent intent) {
+ public final IBinder onBind(Intent intent) {
return null;
}
- private void playRingtone() {
- if (mAudioManager == null && mRingtone == null) {
- // 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(title)
- .setContentText(mNormalRingTime)
- .addAction(R.mipmap.ic_launcher,
- getString(R.string.snooze),
- getPendingIntent(ACTION_SNOOZE, mAlarm))
- .addAction(R.mipmap.ic_launcher,
- getString(R.string.dismiss),
- getPendingIntent(ACTION_DISMISS, mAlarm))
- .build();
- startForeground(R.id.ringtone_service_notification, note); // TOneverDO: Pass 0 as the first argument
-
- mAudioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
- if (mAlarm.vibrates()) {
- mVibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
- }
- // Request audio focus first, so we don't play our ringtone on top of any
- // other apps that currently have playback.
- int result = mAudioManager.requestAudioFocus(
- null, // Playback will likely be short, so don't worry about listening for focus changes
- AudioManager.STREAM_ALARM,
- // 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);
- mRingtone.play();
- if (mVibrator != null) {
- mVibrator.vibrate(new long[] { // apply pattern
- 0, // millis to wait before turning vibrator on
- 500, // millis to keep vibrator on before turning off
- 500, // millis to wait before turning back on
- 500 // millis to keep on before turning off
- }, 2 /* start repeating at this index of the array, after one cycle */);
- }
- scheduleAutoSilence();
- }
- }
- }
-
- // TODO: For Timers, update the foreground notification to say "timer expired". Also,
- // if Alarms and Timers will have distinct settings for the minutes to silence after, then consider
- // doing this in the respective subclass of this service.
- private void scheduleAutoSilence() {
- int minutes = AlarmUtils.minutesToSilenceAfter(this);
- mSilenceHandler.postDelayed(mSilenceRunnable, /*minutes * 60000*/70000); // TODO: uncomment
- }
-
- private void finishActivity() {
+ /**
+ * Exposed to let subclasses finish their designated activity from, e.g. a
+ * notification action.
+ */
+ protected void finishActivity() {
// I think this will be received by all instances of RingtoneActivity
// subclasses in memory.. but since we realistically expect only one
// instance alive at any given time, we don't need to worry about having
// to restrict the broadcast to only the subclass that's alive.
+ // TODO: If we cared, we could write an abstract method called getFinishAction()
+ // that subclasses implement, and call that here instead. The subclass of
+ // RingtoneActivity would define their own ACTION_FINISH constants, and
+ // the RingtoneService subclass retrieves that constant and returns it to us.
LocalBroadcastHelper.sendBroadcast(this, RingtoneActivity.ACTION_FINISH);
}
- private PendingIntent getPendingIntent(@NonNull String action, Alarm alarm) {
+ /**
+ * Exposed so subclasses can create their notification actions.
+ */
+ // TODO: Consider changing Alarm param to int requestCode param.
+ protected final PendingIntent getPendingIntent(@NonNull String action, Alarm alarm) {
Intent intent = new Intent(this, getClass())
- .setAction(action)
- .putExtra(EXTRA_ITEM_ID, alarm.id());
+ .setAction(action);
+ // TODO: Why do we need this?
+// .putExtra(EXTRA_ITEM_ID, alarm.id());
return PendingIntent.getService(
this,
alarm.intId(),
diff --git a/app/src/main/java/com/philliphsu/clock2/util/AlarmController.java b/app/src/main/java/com/philliphsu/clock2/util/AlarmController.java
index 9cba57e..ba89b1c 100644
--- a/app/src/main/java/com/philliphsu/clock2/util/AlarmController.java
+++ b/app/src/main/java/com/philliphsu/clock2/util/AlarmController.java
@@ -13,8 +13,8 @@ import com.philliphsu.clock2.PendingAlarmScheduler;
import com.philliphsu.clock2.R;
import com.philliphsu.clock2.UpcomingAlarmReceiver;
import com.philliphsu.clock2.alarms.AlarmActivity;
+import com.philliphsu.clock2.alarms.AlarmRingtoneService;
import com.philliphsu.clock2.model.AlarmsTableManager;
-import com.philliphsu.clock2.ringtone.RingtoneService;
import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
import static android.app.PendingIntent.FLAG_NO_CREATE;
@@ -146,7 +146,7 @@ public final class AlarmController {
save(alarm);
// If service is not running, nothing happens
- mAppContext.stopService(new Intent(mAppContext, RingtoneService.class));
+ mAppContext.stopService(new Intent(mAppContext, AlarmRingtoneService.class));
}
public void snoozeAlarm(Alarm alarm) {
@@ -184,7 +184,7 @@ public final class AlarmController {
private PendingIntent alarmIntent(Alarm alarm, boolean retrievePrevious) {
// TODO: Use appropriate subclass instead
Intent intent = new Intent(mAppContext, AlarmActivity.class)
- .putExtra(AlarmActivity.EXTRA_ITEM_ID, alarm.id());
+ .putExtra(AlarmActivity.EXTRA_ITEM, alarm);
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,
diff --git a/app/src/main/java/com/philliphsu/clock2/util/AlarmUtils.java b/app/src/main/java/com/philliphsu/clock2/util/AlarmUtils.java
index 980cdb1..7db5970 100644
--- a/app/src/main/java/com/philliphsu/clock2/util/AlarmUtils.java
+++ b/app/src/main/java/com/philliphsu/clock2/util/AlarmUtils.java
@@ -14,8 +14,8 @@ import com.philliphsu.clock2.PendingAlarmScheduler;
import com.philliphsu.clock2.R;
import com.philliphsu.clock2.UpcomingAlarmReceiver;
import com.philliphsu.clock2.alarms.AlarmActivity;
+import com.philliphsu.clock2.alarms.AlarmRingtoneService;
import com.philliphsu.clock2.model.AlarmsTableManager;
-import com.philliphsu.clock2.ringtone.RingtoneService;
import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
import static android.app.PendingIntent.FLAG_NO_CREATE;
@@ -140,7 +140,7 @@ public final class AlarmUtils {
save(c, a);
// If service is not running, nothing happens
- c.stopService(new Intent(c, RingtoneService.class));
+ c.stopService(new Intent(c, AlarmRingtoneService.class));
}
public static void snoozeAlarm(Context c, Alarm a) {
@@ -181,7 +181,7 @@ public final class AlarmUtils {
private static PendingIntent alarmIntent(Context context, Alarm alarm, boolean retrievePrevious) {
// TODO: Use appropriate subclass instead
Intent intent = new Intent(context, AlarmActivity.class)
- .putExtra(AlarmActivity.EXTRA_ITEM_ID, alarm.id());
+ .putExtra(AlarmActivity.EXTRA_ITEM, alarm);
int flag = retrievePrevious ? FLAG_NO_CREATE : FLAG_CANCEL_CURRENT;
PendingIntent pi = getActivity(context, alarm.intId(), intent, flag);
// Even when we try to retrieve a previous instance that actually did exist,