From 25c544df43fdf114e34c09c64e461a8d8a0b241c Mon Sep 17 00:00:00 2001 From: Phillip Hsu Date: Thu, 4 Aug 2016 02:51:50 -0700 Subject: [PATCH] Pass alarm directly to AlarmActivity and AlarmRingtoneService --- app/src/main/AndroidManifest.xml | 38 ++- .../clock2/AsyncTimersTableUpdateHandler.java | 15 +- .../clock2/alarms/AlarmActivity.java | 44 ++-- .../clock2/alarms/AlarmRingtoneService.java | 127 +++++++++ .../clock2/model/DatabaseTableManager.java | 6 +- .../clock2/ringtone/RingtoneActivity.java | 62 ++--- .../clock2/ringtone/RingtoneService.java | 243 +++++++----------- .../clock2/util/AlarmController.java | 6 +- .../philliphsu/clock2/util/AlarmUtils.java | 6 +- 9 files changed, 317 insertions(+), 230 deletions(-) create mode 100644 app/src/main/java/com/philliphsu/clock2/alarms/AlarmRingtoneService.java 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 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 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,