From 8dea6301aa354ee72d5d7c82daef501c2ad49ef7 Mon Sep 17 00:00:00 2001 From: Phillip Hsu Date: Fri, 5 Aug 2016 01:06:26 -0700 Subject: [PATCH] Implement notification actions for timers --- app/src/main/AndroidManifest.xml | 12 +- .../clock2/AsyncTimersTableUpdateHandler.java | 3 +- .../java/com/philliphsu/clock2/Timer.java | 54 +++++-- .../clock2/alarms/AlarmActivity.java | 33 +---- .../clock2/alarms/AlarmRingtoneService.java | 60 ++------ .../clock2/ringtone/RingtoneActivity.java | 44 ++---- .../clock2/ringtone/RingtoneService.java | 42 ++++-- .../clock2/timers/TimerController.java | 67 +++------ .../timers/TimerNotificationService.java | 137 +++++++----------- .../clock2/timers/TimerRingtoneService.java | 94 ++++++++++++ .../clock2/timers/TimerViewHolder.java | 28 +--- .../clock2/timers/TimersFragment.java | 2 + .../clock2/timers/TimesUpActivity.java | 27 ++-- .../clock2/util/AlarmController.java | 3 +- .../philliphsu/clock2/util/AlarmUtils.java | 2 +- app/src/main/res/values/strings.xml | 3 + 16 files changed, 312 insertions(+), 299 deletions(-) create mode 100644 app/src/main/java/com/philliphsu/clock2/timers/TimerRingtoneService.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 652a6e1..a8d8d08 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -31,11 +31,14 @@ --> - + + --> + + \ 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 f105405..bf4f704 100644 --- a/app/src/main/java/com/philliphsu/clock2/AsyncTimersTableUpdateHandler.java +++ b/app/src/main/java/com/philliphsu/clock2/AsyncTimersTableUpdateHandler.java @@ -52,6 +52,7 @@ public final class AsyncTimersTableUpdateHandler extends AsyncDatabaseTableUpdat private PendingIntent createTimesUpIntent(Timer timer) { Intent intent = new Intent(getContext(), TimesUpActivity.class); // intent.putExtra(TimesUpActivity.EXTRA_ITEM_ID, timer.getId()); + intent.putExtra(TimesUpActivity.EXTRA_RINGING_OBJECT, timer); // There's no point to determining whether to retrieve a previous instance, because // we chose to ignore it since we had issues with NPEs. TODO: Perhaps these issues // were caused by you using the same reference variable for every Intent/PI that @@ -63,7 +64,7 @@ public final class AsyncTimersTableUpdateHandler extends AsyncDatabaseTableUpdat private void scheduleAlarm(Timer timer) { AlarmManager am = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE); am.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, timer.endTime(), createTimesUpIntent(timer)); - TimerNotificationService.showNotification(getContext(), timer.getId()); + TimerNotificationService.showNotification(getContext(), timer); } private void cancelAlarm(Timer timer, boolean removeNotification) { diff --git a/app/src/main/java/com/philliphsu/clock2/Timer.java b/app/src/main/java/com/philliphsu/clock2/Timer.java index 0282baa..7c671a2 100644 --- a/app/src/main/java/com/philliphsu/clock2/Timer.java +++ b/app/src/main/java/com/philliphsu/clock2/Timer.java @@ -1,5 +1,7 @@ package com.philliphsu.clock2; +import android.os.Parcel; +import android.os.Parcelable; import android.os.SystemClock; import com.google.auto.value.AutoValue; @@ -11,7 +13,7 @@ import java.util.concurrent.TimeUnit; * Created by Phillip Hsu on 7/25/2016. */ @AutoValue -public abstract class Timer extends ObjectWithId /*implements Parcelable*/ { +public abstract class Timer extends ObjectWithId implements Parcelable { private static final long MINUTE = TimeUnit.MINUTES.toMillis(1); private long endTime; @@ -129,17 +131,41 @@ public abstract class Timer extends ObjectWithId /*implements Parcelable*/ { return pauseTime; } -// @Override -// public int describeContents() { -// return 0; -// } -// -// @Override -// public void writeToParcel(Parcel dest, int flags) { -// dest.writeInt(hour()); -// dest.writeInt(minute()); -// dest.writeInt(second()); -// dest.writeString(group()); -// dest.writeString(label()); -// } + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(hour()); + dest.writeInt(minute()); + dest.writeInt(second()); + dest.writeString(group()); + dest.writeString(label()); + dest.writeLong(getId()); + dest.writeLong(endTime); + dest.writeLong(pauseTime); + } + + public static final Creator CREATOR = new Creator() { + @Override + public Timer createFromParcel(Parcel source) { + return Timer.create(source); + } + + @Override + public Timer[] newArray(int size) { + return new Timer[size]; + } + }; + + private static Timer create(Parcel source) { + Timer t = Timer.create(source.readInt(), source.readInt(), source.readInt(), + source.readString(), source.readString()); + t.setId(source.readLong()); + t.endTime = source.readLong(); + t.pauseTime = source.readLong(); + return t; + } } 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 2484bf6..32cb18e 100644 --- a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmActivity.java +++ b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmActivity.java @@ -13,19 +13,14 @@ import com.philliphsu.clock2.util.AlarmController; public class AlarmActivity extends RingtoneActivity { private AlarmController mAlarmController; - // TODO: Write a getter method instead in the base class? - private Alarm mAlarm; @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); + mAlarmController.removeUpcomingAlarmNotification(getRingingObject()); // TODO: Butterknife binding Button snooze = (Button) findViewById(R.id.btn_snooze); snooze.setOnClickListener(new View.OnClickListener() { @@ -43,22 +38,6 @@ 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 int layoutResource() { return R.layout.activity_ringtone; @@ -70,19 +49,15 @@ public class AlarmActivity extends RingtoneActivity { } private void snooze() { - if (mAlarm != null) { - mAlarmController.snoozeAlarm(mAlarm); - } + mAlarmController.snoozeAlarm(getRingingObject()); // Can't call dismiss() because we don't want to also call cancelAlarm()! Why? For example, // we don't want the alarm, if it has no recurrence, to be turned off right now. stopAndFinish(); } private void dismiss() { - if (mAlarm != null) { - // TODO do we really need to cancel the intent and alarm? - mAlarmController.cancelAlarm(mAlarm, false); - } + // TODO do we really need to cancel the intent and alarm? + mAlarmController.cancelAlarm(getRingingObject(), false); stopAndFinish(); } } diff --git a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmRingtoneService.java b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmRingtoneService.java index c156d27..964d74e 100644 --- a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmRingtoneService.java +++ b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmRingtoneService.java @@ -3,8 +3,6 @@ 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; @@ -24,41 +22,16 @@ public class AlarmRingtoneService extends RingtoneService { 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 { + // We can have this before super because this will only call through + // WHILE this Service has already been alive. + if (intent.getAction() != null) { if (ACTION_SNOOZE.equals(intent.getAction())) { - mAlarmController.snoozeAlarm(mAlarm); + mAlarmController.snoozeAlarm(getRingingObject()); } else if (ACTION_DISMISS.equals(intent.getAction())) { - mAlarmController.cancelAlarm(mAlarm, false); // TODO do we really need to cancel the intent and alarm? + mAlarmController.cancelAlarm(getRingingObject(), false); // TODO do we really need to cancel the intent and alarm? } else { throw new UnsupportedOperationException(); } @@ -78,7 +51,7 @@ public class AlarmRingtoneService extends RingtoneService { @Override protected void onAutoSilenced() { // TODO do we really need to cancel the alarm and intent? - mAlarmController.cancelAlarm(mAlarm, false); + mAlarmController.cancelAlarm(getRingingObject(), false); // Post notification that alarm was missed NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); Notification note = new NotificationCompat.Builder(this) @@ -86,38 +59,37 @@ public class AlarmRingtoneService extends RingtoneService { .setContentText(mNormalRingTime) .setSmallIcon(R.mipmap.ic_launcher) .build(); - nm.notify(TAG, mAlarm.intId(), note); + nm.notify(TAG, getRingingObject().intId(), note); } @Override - protected Ringtone getRingtone() { - Uri ringtone = Uri.parse(mAlarm.ringtone()); - return RingtoneManager.getRingtone(this, ringtone); + protected Uri getRingtoneUri() { + return Uri.parse(getRingingObject().ringtone()); } @Override protected Notification getForegroundNotification() { - String title = mAlarm.label().isEmpty() + String title = getRingingObject().label().isEmpty() ? getString(R.string.alarm) - : mAlarm.label(); + : getRingingObject().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, + .addAction(R.mipmap.ic_launcher, // TODO: correct icon getString(R.string.snooze), - getPendingIntent(ACTION_SNOOZE, mAlarm)) - .addAction(R.mipmap.ic_launcher, + getPendingIntent(ACTION_SNOOZE, getRingingObject().getIntId())) + .addAction(R.mipmap.ic_launcher, // TODO: correct icon getString(R.string.dismiss), - getPendingIntent(ACTION_DISMISS, mAlarm)) + getPendingIntent(ACTION_DISMISS, getRingingObject().getIntId())) .build(); } @Override protected boolean doesVibrate() { - return mAlarm.vibrates(); + return getRingingObject().vibrates(); } @Override 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 4314a6e..c09905f 100644 --- a/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneActivity.java +++ b/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneActivity.java @@ -16,20 +16,15 @@ 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 { 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.FINISH"; - public static final String EXTRA_ITEM = "com.philliphsu.clock2.ringtone.extra.ITEM"; + public static final String EXTRA_RINGING_OBJECT = "com.philliphsu.clock2.ringtone.extra.RINGING_OBJECT"; private static boolean sIsAlive = false; - -// private long mItemId; -// private T mItem; - -// public abstract Loader onCreateLoader(long itemId); + private T mRingingObject; // TODO: Should we extend from BaseActivity instead? @LayoutRes @@ -48,17 +43,11 @@ public abstract class RingtoneActivity extends AppCompatAc 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"); -// } - // 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); - + if ((mRingingObject = getIntent().getParcelableExtra(EXTRA_RINGING_OBJECT)) == null) { + throw new IllegalStateException("Cannot start RingtoneActivity without a ringing object"); + } Intent intent = new Intent(this, getRingtoneServiceClass()) - .putExtra(EXTRA_ITEM, getIntent().getParcelableExtra(EXTRA_ITEM)); + .putExtra(EXTRA_RINGING_OBJECT, mRingingObject); startService(intent); } @@ -126,21 +115,6 @@ public abstract class RingtoneActivity extends AppCompatAc 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 -// } - public static boolean isAlive() { return sIsAlive; } @@ -154,6 +128,10 @@ public abstract class RingtoneActivity extends AppCompatAc finish(); } + protected final T getRingingObject() { + return mRingingObject; + } + // TODO: Do we need this anymore? I think this broadcast was only sent from // EditAlarmActivity? private final BroadcastReceiver mFinishReceiver = new BroadcastReceiver() { 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 eb282ef..41bbe3a 100644 --- a/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneService.java +++ b/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneService.java @@ -6,14 +6,16 @@ import android.app.Service; 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.Parcelable; import android.os.Vibrator; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; -import com.philliphsu.clock2.Alarm; import com.philliphsu.clock2.R; import com.philliphsu.clock2.util.LocalBroadcastHelper; @@ -29,18 +31,18 @@ import java.util.concurrent.TimeUnit; * * TOneverDO: Change this to not be a started service! */ -// TODO: Remove this from manifest, keep only the subclasses. -public abstract class RingtoneService extends Service { +public abstract class RingtoneService extends Service { private static final String TAG = "RingtoneService"; // public okay public static final String ACTION_NOTIFY_MISSED = "com.philliphsu.clock2.ringtone.action.NOTIFY_MISSED"; // public static final String EXTRA_ITEM_ID = RingtoneActivity.EXTRA_ITEM_ID; - public static final String EXTRA_ITEM = RingtoneActivity.EXTRA_ITEM; + public static final String EXTRA_RINGING_OBJECT = RingtoneActivity.EXTRA_RINGING_OBJECT; private AudioManager mAudioManager; private Ringtone mRingtone; @Nullable private Vibrator mVibrator; + private T mRingingObject; // TODO: Using Handler for this is ill-suited? Alarm ringing could outlast the // application's life. Use AlarmManager API instead. @@ -57,7 +59,8 @@ public abstract class RingtoneService extends Service { } }; - // Pretty sure we don't need this anymore... + // Pretty sure this won't ever get called anymore... b/c EditAlarmActivity, the only component + // that sends such a broadcast, is deprecated. // private final BroadcastReceiver mNotifyMissedReceiver = new BroadcastReceiver() { // @Override // public void onReceive(Context context, Intent intent) { @@ -68,9 +71,13 @@ public abstract class RingtoneService extends Service { // } // }; + /** + * Callback invoked when this Service is stopping and the corresponding + * {@link RingtoneActivity} is finishing. + */ protected abstract void onAutoSilenced(); - protected abstract Ringtone getRingtone(); + protected abstract Uri getRingtoneUri(); /** * @return the notification to show when this Service starts in the foreground @@ -86,6 +93,11 @@ public abstract class RingtoneService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { + if (mRingingObject == null) { + if ((mRingingObject = intent.getParcelableExtra(EXTRA_RINGING_OBJECT)) == null) { + throw new IllegalStateException("Cannot start RingtoneService without a ringing object"); + } + } // Play ringtone, if not already playing if (mAudioManager == null && mRingtone == null) { // TOneverDO: Pass 0 as the first argument @@ -100,7 +112,7 @@ public abstract class RingtoneService extends Service { // Request permanent focus, as ringing could last several minutes AudioManager.AUDIOFOCUS_GAIN); if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { - mRingtone = getRingtone(); + mRingtone = RingtoneManager.getRingtone(this, getRingtoneUri()); // Deprecated, but the alternative AudioAttributes requires API 21 mRingtone.setStreamType(AudioManager.STREAM_ALARM); mRingtone.play(); @@ -125,7 +137,8 @@ public abstract class RingtoneService extends Service { @Override public void onCreate() { super.onCreate(); - // Pretty sure this won't ever get called anymore... + // Pretty sure this won't ever get called anymore... b/c EditAlarmActivity, the only component + // that sends such a broadcast, is deprecated. // LocalBroadcastHelper.registerReceiver(this, mNotifyMissedReceiver, ACTION_NOTIFY_MISSED); } @@ -139,6 +152,8 @@ public abstract class RingtoneService extends Service { } mSilenceHandler.removeCallbacks(mSilenceRunnable); stopForeground(true); + // Pretty sure this won't ever get called anymore... b/c EditAlarmActivity, the only component + // that sends such a broadcast, is deprecated. // LocalBroadcastHelper.unregisterReceiver(this, mNotifyMissedReceiver); } @@ -166,16 +181,17 @@ public abstract class RingtoneService extends Service { /** * 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) { + protected final PendingIntent getPendingIntent(@NonNull String action, int requestCode) { Intent intent = new Intent(this, getClass()) .setAction(action); - // TODO: Why do we need this? -// .putExtra(EXTRA_ITEM_ID, alarm.id()); return PendingIntent.getService( this, - alarm.intId(), + requestCode, intent, PendingIntent.FLAG_ONE_SHOT); } + + protected final T getRingingObject() { + return mRingingObject; + } } diff --git a/app/src/main/java/com/philliphsu/clock2/timers/TimerController.java b/app/src/main/java/com/philliphsu/clock2/timers/TimerController.java index 3364cd4..a10262e 100644 --- a/app/src/main/java/com/philliphsu/clock2/timers/TimerController.java +++ b/app/src/main/java/com/philliphsu/clock2/timers/TimerController.java @@ -1,5 +1,6 @@ package com.philliphsu.clock2.timers; +import com.philliphsu.clock2.AsyncTimersTableUpdateHandler; import com.philliphsu.clock2.Timer; /** @@ -7,68 +8,40 @@ import com.philliphsu.clock2.Timer; */ public class TimerController { private final Timer mTimer; + private final AsyncTimersTableUpdateHandler mUpdateHandler; + + public TimerController(Timer timer, AsyncTimersTableUpdateHandler updateHandler) { + mTimer = timer; + mUpdateHandler = updateHandler; + } /** - * Calls the appropriate state on the given Timer, based on - * its current state. + * Start/resume or pause the timer. */ - public static void startPause(Timer timer) { - if (timer.hasStarted()) { - if (timer.isRunning()) { - timer.pause(); + public void startPause() { + if (mTimer.hasStarted()) { + if (mTimer.isRunning()) { + mTimer.pause(); } else { - timer.resume(); + mTimer.resume(); } } else { - timer.start(); + mTimer.start(); } - } - - public TimerController(Timer timer) { - mTimer = timer; - } - - public void start() { - mTimer.start(); -// mChronometer.setBase(mTimer.endTime()); -// mChronometer.start(); -// updateStartPauseIcon(); -// setSecondaryButtonsVisible(true); - - } - - public void pause() { - mTimer.pause(); -// mChronometer.stop(); -// updateStartPauseIcon(); - } - - public void resume() { - mTimer.resume(); -// mChronometer.setBase(mTimer.endTime()); -// mChronometer.start(); -// updateStartPauseIcon(); + update(); } public void stop() { mTimer.stop(); -// mChronometer.stop(); -// init(); + update(); } public void addOneMinute() { mTimer.addOneMinute(); -// mChronometer.setBase(mTimer.endTime()); + update(); } -// public void updateStartPauseIcon() { -// // TODO: Pause and start icons, resp. -// mStartPause.setImageResource(mTimer.isRunning() ? 0 : 0); -// } - -// public void setSecondaryButtonsVisible(boolean visible) { -// int visibility = visible ? View.VISIBLE : View.INVISIBLE; -// mAddOneMinute.setVisibility(visibility); -// mStop.setVisibility(visibility); -// } + private void update() { + mUpdateHandler.asyncUpdate(mTimer.getId(), mTimer); + } } diff --git a/app/src/main/java/com/philliphsu/clock2/timers/TimerNotificationService.java b/app/src/main/java/com/philliphsu/clock2/timers/TimerNotificationService.java index 4318cc1..2f0d60f 100644 --- a/app/src/main/java/com/philliphsu/clock2/timers/TimerNotificationService.java +++ b/app/src/main/java/com/philliphsu/clock2/timers/TimerNotificationService.java @@ -1,54 +1,54 @@ package com.philliphsu.clock2.timers; -import android.app.IntentService; import android.app.NotificationManager; import android.app.PendingIntent; +import android.app.Service; import android.content.Context; import android.content.Intent; +import android.os.IBinder; import android.support.annotation.DrawableRes; +import android.support.annotation.Nullable; import android.support.v4.app.NotificationCompat; +import com.philliphsu.clock2.AsyncTimersTableUpdateHandler; import com.philliphsu.clock2.MainActivity; import com.philliphsu.clock2.R; import com.philliphsu.clock2.Timer; -import com.philliphsu.clock2.model.TimersTableManager; /** - * An {@link IntentService} subclass for handling asynchronous task requests in - * a service on a separate handler thread. - *

- * TODO: Customize class - update intent actions, extra parameters and static - * helper methods. + * Handles the notification for an active Timer. + * TOneverDO: extend IntentService, it is ill-suited for our requirement that + * this remains alive until we explicitly stop it. Otherwise, it would finish + * a single task and immediately destroy itself, which means we lose all of + * our instance state. */ -public class TimerNotificationService extends IntentService { +public class TimerNotificationService extends Service { private static final String TAG = "TimerNotificationService"; public static final String ACTION_ADD_ONE_MINUTE = "com.philliphsu.clock2.timers.action.ADD_ONE_MINUTE"; public static final String ACTION_START_PAUSE = "com.philliphsu.clock2.timers.action.START_PAUSE"; public static final String ACTION_STOP = "com.philliphsu.clock2.timers.action.STOP"; - public static final String EXTRA_TIMER_ID = "com.philliphsu.clock2.timers.extra.TIMER_ID"; + public static final String EXTRA_TIMER = "com.philliphsu.clock2.timers.extra.TIMER"; - private TimersTableManager mTableManager; - - public TimerNotificationService() { - super("TimerNotificationService"); - } + private Timer mTimer; + private TimerController mController; /** * Helper method to start this Service for its default action: to show * the notification for the Timer with the given id. */ - public static void showNotification(Context context, long timerId) { + public static void showNotification(Context context, Timer timer) { Intent intent = new Intent(context, TimerNotificationService.class); - intent.putExtra(EXTRA_TIMER_ID, timerId); + intent.putExtra(EXTRA_TIMER, timer); context.startService(intent); } /** * Helper method to cancel the notification previously shown from calling - * {@link #showNotification(Context, long)}. This does NOT start the Service - * and call through to {@link #onHandleIntent(Intent)}. + * {@link #showNotification(Context, Timer)}. This does NOT start the Service + * and call through to {@link #onStartCommand(Intent, int, int)}, because + * the work does not require so. * @param timerId the id of the Timer associated with the notification * you want to cancel */ @@ -59,113 +59,86 @@ public class TimerNotificationService extends IntentService { } @Override - public void onCreate() { - super.onCreate(); - mTableManager = new TimersTableManager(this); - } - - @Override - protected void onHandleIntent(Intent intent) { + public int onStartCommand(Intent intent, int flags, int startId) { if (intent != null) { - final long timerId = intent.getLongExtra(EXTRA_TIMER_ID, -1); - if (timerId == -1) { - throw new IllegalStateException("Did not pass in timer id"); - } final String action = intent.getAction(); if (action == null) { - showNotification(timerId); + if ((mTimer = intent.getParcelableExtra(EXTRA_TIMER)) == null) { + throw new IllegalStateException("Cannot start TimerNotificationService without a Timer"); + } + mController = new TimerController(mTimer, new AsyncTimersTableUpdateHandler(this, null)); + // TODO: Spawn your own thread to update the countdown text + showNotification(); } else if (ACTION_ADD_ONE_MINUTE.equals(action)) { - handleAddOneMinute(timerId); + mController.addOneMinute(); + // TODO: Verify the notification countdown is extended by one minute. } else if (ACTION_START_PAUSE.equals(action)) { - handleStartPause(timerId); + mController.startPause(); } else if (ACTION_STOP.equals(action)) { - handleStop(timerId); + mController.stop(); + stopSelf(); + // We leave removing the notification up to AsyncTimersTableUpdateHandler + // when it calls cancelAlarm() from onPostAsyncUpdate(). } } + return super.onStartCommand(intent, flags, startId); } - private void showNotification(long timerId) { - Timer timer = getTimer(timerId); + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + private void showNotification() { // Base note NotificationCompat.Builder builder = new NotificationCompat.Builder(this) - // TODO: correct icon - .setSmallIcon(R.drawable.ic_half_day_1_black_24dp) + .setSmallIcon(R.drawable.ic_half_day_1_black_24dp) // TODO: correct icon .setShowWhen(false) .setOngoing(true); // TODO: Set content intent so that when clicked, we launch // TimersFragment and scroll to the given timer id. The following // is merely pseudocode. Intent contentIntent = new Intent(this, MainActivity.class); - contentIntent.putExtra(null/*TODO:MainActivity.EXTRA_SHOW_PAGE*/, - 1/*TODO:The tab index of the timers page*/); - contentIntent.putExtra(null/*TODO:MainActivity.EXTRA_SCROLL_TO_ID*/, - timerId); + contentIntent.putExtra(null/*TODO:MainActivity.EXTRA_SHOW_PAGE*/, 1/*TODO:The tab index of the timers page*/); + contentIntent.putExtra(null/*TODO:MainActivity.EXTRA_SCROLL_TO_ID*/, mTimer.getId()); builder.setContentIntent(PendingIntent.getActivity( this, 0, // TODO: Request code not needed? Since any multiple notifications - // should be able to use the same PendingIntent for this action.... - // unless the underlying *Intent* and its id extra are overwritten - // per notification when retrieving the PendingIntent.. + // should be able to use the same PendingIntent for this action.... + // unless the underlying *Intent* and its id extra are overwritten + // per notification when retrieving the PendingIntent.. contentIntent, 0/*Shouldn't need a flag..*/)); // TODO: Use a handler to continually update the countdown text - String title = timer.label(); + String title = mTimer.label(); if (title.isEmpty()) { title = getString(R.string.timer); } builder.setContentTitle(title); addAction(builder, ACTION_ADD_ONE_MINUTE, - timer.getId(), R.drawable.ic_add_circle_24dp/*TODO: correct icon*/); + R.drawable.ic_add_circle_24dp/*TODO: correct icon*/); addAction(builder, ACTION_START_PAUSE, - timer.getId(), R.drawable.ic_add_circle_24dp/*TODO: correct icon*/); + R.drawable.ic_add_circle_24dp/*TODO: correct icon*/); addAction(builder, ACTION_STOP, - timer.getId(), R.drawable.ic_add_circle_24dp/*TODO: correct icon*/); + R.drawable.ic_add_circle_24dp/*TODO: correct icon*/); NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(TAG, timer.getIntId(), builder.build()); + nm.notify(TAG, mTimer.getIntId(), builder.build()); } /** * Builds and adds the specified action to the notification's builder. */ - private void addAction(NotificationCompat.Builder noteBuilder, String action, - long timerId, @DrawableRes int icon) { + private void addAction(NotificationCompat.Builder noteBuilder, String action, @DrawableRes int icon) { Intent intent = new Intent(this, TimerNotificationService.class) - .setAction(action) - .putExtra(EXTRA_TIMER_ID, timerId); + .setAction(action); +// .putExtra(EXTRA_TIMER, mTimer); PendingIntent pi = PendingIntent.getService(this, - (int) timerId, intent, 0/*no flags*/); + mTimer.getIntId(), intent, 0/*no flags*/); noteBuilder.addAction(icon, ""/*no action title*/, pi); } - - private void handleAddOneMinute(long timerId) { - Timer timer = getTimer(timerId); - timer.addOneMinute(); - updateTimer(timer); - // TODO: Verify the notification countdown is extended by one minute. - } - - private void handleStartPause(long timerId) { - Timer t = getTimer(timerId); - TimerController.startPause(t); - updateTimer(t); - } - - private void handleStop(long timerId) { - Timer t = getTimer(timerId); - t.stop(); - updateTimer(t); - } - - private void updateTimer(Timer timer) { - mTableManager.updateItem(timer.getId(), timer); - } - - private Timer getTimer(long timerId) { - return mTableManager.queryItem(timerId).getItem(); - } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/philliphsu/clock2/timers/TimerRingtoneService.java b/app/src/main/java/com/philliphsu/clock2/timers/TimerRingtoneService.java new file mode 100644 index 0000000..c6eddd6 --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/timers/TimerRingtoneService.java @@ -0,0 +1,94 @@ +package com.philliphsu.clock2.timers; + +import android.app.Notification; +import android.content.Intent; +import android.net.Uri; +import android.provider.Settings; +import android.support.v4.app.NotificationCompat; + +import com.philliphsu.clock2.AsyncTimersTableUpdateHandler; +import com.philliphsu.clock2.R; +import com.philliphsu.clock2.Timer; +import com.philliphsu.clock2.ringtone.RingtoneService; + +public class TimerRingtoneService extends RingtoneService { + + // private because they refer to our foreground notification's actions. + // we reuse these from TimerNotificationService because they're just constants, the values + // don't actually matter. + private static final String ACTION_ADD_ONE_MINUTE = TimerNotificationService.ACTION_ADD_ONE_MINUTE; + private static final String ACTION_STOP = TimerNotificationService.ACTION_STOP; + + private TimerController mController; + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + // This has to be first so our Timer is initialized + int value = super.onStartCommand(intent, flags, startId); + if (mController == null) { + mController = new TimerController(getRingingObject(), + new AsyncTimersTableUpdateHandler(this, null)); + } + if (intent.getAction() != null) { + switch (intent.getAction()) { + case ACTION_ADD_ONE_MINUTE: + mController.addOneMinute(); + break; + case ACTION_STOP: + mController.stop(); + break; + default: + throw new UnsupportedOperationException(); + } + stopSelf(startId); + finishActivity(); + } + return value; + } + + @Override + protected void onAutoSilenced() { + // TODO: We probably have relevant code to copy over from the old project. + // TODO: Stop the Timer and update the table + } + + @Override + protected Uri getRingtoneUri() { + // TODO: Read Timer ringtone preference + return Settings.System.DEFAULT_ALARM_ALERT_URI; + } + + @Override + protected Notification getForegroundNotification() { + String title = getRingingObject().label(); + if (title.isEmpty()) { + title = getString(R.string.timer); + } + return new NotificationCompat.Builder(this) + .setContentTitle(title) + .setContentText(getString(R.string.times_up)) + .setSmallIcon(R.drawable.ic_half_day_1_black_24dp) // TODO: correct icon + .setShowWhen(false) // TODO: Should we show this? +// .setOngoing(true) // foreground notes are ongoing by default + .addAction(R.drawable.ic_add_circle_24dp, // TODO: correct icon + getString(R.string.add_one_minute), + getPendingIntent(ACTION_ADD_ONE_MINUTE, getRingingObject().getIntId())) + .addAction(R.drawable.ic_add_circle_24dp, // TODO: correct icon + getString(R.string.stop), + getPendingIntent(ACTION_STOP, getRingingObject().getIntId())) + .build(); +// TODO: .setContentIntent(getPendingIntent(timer.requestCode(), intent, true)); + } + + @Override + protected boolean doesVibrate() { + // TODO: Create new preference. + return false; + } + + @Override + protected int minutesToAutoSilence() { + // TODO: Use same value as for Alarms, or create new preference. + return 1; + } +} diff --git a/app/src/main/java/com/philliphsu/clock2/timers/TimerViewHolder.java b/app/src/main/java/com/philliphsu/clock2/timers/TimerViewHolder.java index 902d660..8d54ca1 100644 --- a/app/src/main/java/com/philliphsu/clock2/timers/TimerViewHolder.java +++ b/app/src/main/java/com/philliphsu/clock2/timers/TimerViewHolder.java @@ -21,8 +21,8 @@ import butterknife.OnClick; public class TimerViewHolder extends BaseViewHolder { private static final String TAG = "TimerViewHolder"; -// private TimerController mController; private final AsyncTimersTableUpdateHandler mAsyncTimersTableUpdateHandler; + private TimerController mController; @Bind(R.id.label) TextView mLabel; @Bind(R.id.duration) CountdownChronometer mChronometer; @@ -31,7 +31,6 @@ public class TimerViewHolder extends BaseViewHolder { @Bind(R.id.start_pause) ImageButton mStartPause; @Bind(R.id.stop) ImageButton mStop; - // TODO: Controller param public TimerViewHolder(ViewGroup parent, OnListItemInteractionListener listener, AsyncTimersTableUpdateHandler asyncTimersTableUpdateHandler) { super(parent, R.layout.item_timer, listener); @@ -41,32 +40,26 @@ public class TimerViewHolder extends BaseViewHolder { @Override public void onBind(Timer timer) { super.onBind(timer); + // TOneverDO: create before super + mController = new TimerController(timer, mAsyncTimersTableUpdateHandler); bindLabel(timer.label()); -// // We can't create the controller until this VH binds, because -// // the widgets only exist after this point. -// mController = new TimerController(timer, mChronometer, mAddOneMinute, mStartPause, mStop); bindChronometer(timer); bindButtonControls(timer); } @OnClick(R.id.start_pause) void startPause() { - TimerController.startPause(getItem()); - // Persist value changes - update(); + mController.startPause(); } @OnClick(R.id.add_one_minute) void addOneMinute() { - getItem().addOneMinute(); - // Persist end time increase - update(); + mController.addOneMinute(); } @OnClick(R.id.stop) void stop() { - getItem().stop(); - update(); + mController.stop(); } private void bindLabel(String label) { @@ -113,13 +106,4 @@ public class TimerViewHolder extends BaseViewHolder { mAddOneMinute.setVisibility(visibility); mStop.setVisibility(visibility); } - - private void update() { - Timer t = getItem(); - mAsyncTimersTableUpdateHandler.asyncUpdate( - // Alternatively, use ViewHolder#getItemId() because we can forget - // to set the id on the object in BaseItemCursor#getItem(). We - // luckily remembered to this time! - t.getId(), t); - } } diff --git a/app/src/main/java/com/philliphsu/clock2/timers/TimersFragment.java b/app/src/main/java/com/philliphsu/clock2/timers/TimersFragment.java index b7420f6..73e46ae 100644 --- a/app/src/main/java/com/philliphsu/clock2/timers/TimersFragment.java +++ b/app/src/main/java/com/philliphsu/clock2/timers/TimersFragment.java @@ -33,6 +33,8 @@ public class TimersFragment extends RecyclerViewFragment< public void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode != Activity.RESULT_OK || data == null) return; + // TODO: From EditTimerActivity, pass back the Timer as a parcelable and + // retrieve it here directly. int hour = data.getIntExtra(EditTimerActivity.EXTRA_HOUR, -1); int minute = data.getIntExtra(EditTimerActivity.EXTRA_MINUTE, -1); int second = data.getIntExtra(EditTimerActivity.EXTRA_SECOND, -1); diff --git a/app/src/main/java/com/philliphsu/clock2/timers/TimesUpActivity.java b/app/src/main/java/com/philliphsu/clock2/timers/TimesUpActivity.java index 6549cfd..ec0182c 100644 --- a/app/src/main/java/com/philliphsu/clock2/timers/TimesUpActivity.java +++ b/app/src/main/java/com/philliphsu/clock2/timers/TimesUpActivity.java @@ -1,22 +1,29 @@ package com.philliphsu.clock2.timers; +import android.content.Intent; import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; -public class TimesUpActivity extends AppCompatActivity { +import com.philliphsu.clock2.R; +import com.philliphsu.clock2.Timer; +import com.philliphsu.clock2.ringtone.RingtoneActivity; +import com.philliphsu.clock2.ringtone.RingtoneService; + +public class TimesUpActivity extends RingtoneActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + stopService(new Intent(this, TimerNotificationService.class)); + TimerNotificationService.cancelNotification(this, getRingingObject().getId()); } -// @Override -// public Loader onCreateLoader(long itemId) { -// return new TimerLoader(this, itemId); -// } + @Override + public int layoutResource() { + return R.layout.activity_ringtone; + } -// @Override -// public int layoutResource() { -// return R.layout.activity_ringtone; -// } + @Override + protected Class getRingtoneServiceClass() { + return TimerRingtoneService.class; + } } 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 ba89b1c..b80db3b 100644 --- a/app/src/main/java/com/philliphsu/clock2/util/AlarmController.java +++ b/app/src/main/java/com/philliphsu/clock2/util/AlarmController.java @@ -34,6 +34,7 @@ public final class AlarmController { private final Context mAppContext; private final View mSnackbarAnchor; + // TODO: Why aren't we using AsyncAlarmsTableUpdateHandler? private final AlarmsTableManager mTableManager; /** @@ -184,7 +185,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, alarm); + .putExtra(AlarmActivity.EXTRA_RINGING_OBJECT, 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 7db5970..f31a25d 100644 --- a/app/src/main/java/com/philliphsu/clock2/util/AlarmUtils.java +++ b/app/src/main/java/com/philliphsu/clock2/util/AlarmUtils.java @@ -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, alarm); + .putExtra(AlarmActivity.EXTRA_RINGING_OBJECT, 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, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 47c2014..3fa9c8f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -193,4 +193,7 @@ CreateTimerActivity Timer + Time\'s up + Add 1 minute + Stop