From 9e4369282d4c9d4824b0fe629a677666c3047580 Mon Sep 17 00:00:00 2001 From: Phillip Hsu Date: Wed, 3 Aug 2016 03:26:53 -0700 Subject: [PATCH] Created TimerNotificationService, TimesUpActivity. Schedule alarms with AlarmManager for timers. --- app/src/main/AndroidManifest.xml | 31 +++- .../java/com/philliphsu/clock2/Alarm.java | 3 +- .../clock2/AsyncTimersTableUpdateHandler.java | 53 +++++- .../clock2/alarms/AlarmActivity.java | 78 ++++++++ .../clock2/editalarm/EditAlarmActivity.java | 6 +- .../philliphsu/clock2/model/AlarmLoader.java | 2 + .../philliphsu/clock2/model/DataLoader.java | 3 + .../philliphsu/clock2/model/ObjectWithId.java | 4 + .../philliphsu/clock2/model/TimerLoader.java | 25 +++ .../clock2/ringtone/RingtoneActivity.java | 95 ++++------ .../clock2/ringtone/RingtoneService.java | 4 + .../clock2/timers/TimerController.java | 39 ++-- .../timers/TimerNotificationService.java | 171 ++++++++++++++++++ .../clock2/timers/TimerViewHolder.java | 13 +- .../clock2/timers/TimesUpActivity.java | 22 +++ .../clock2/util/AlarmController.java | 6 +- .../philliphsu/clock2/util/AlarmUtils.java | 6 +- app/src/main/res/values/strings.xml | 2 + 18 files changed, 449 insertions(+), 114 deletions(-) create mode 100644 app/src/main/java/com/philliphsu/clock2/alarms/AlarmActivity.java create mode 100644 app/src/main/java/com/philliphsu/clock2/model/TimerLoader.java create mode 100644 app/src/main/java/com/philliphsu/clock2/timers/TimerNotificationService.java create mode 100644 app/src/main/java/com/philliphsu/clock2/timers/TimesUpActivity.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ab5cf5f..8a02120 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,13 +21,13 @@ - - + --> - + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/philliphsu/clock2/Alarm.java b/app/src/main/java/com/philliphsu/clock2/Alarm.java index c4c430a..c1f2f09 100644 --- a/app/src/main/java/com/philliphsu/clock2/Alarm.java +++ b/app/src/main/java/com/philliphsu/clock2/Alarm.java @@ -203,8 +203,9 @@ public abstract class Alarm extends ObjectWithId implements JsonSerializable, Pa return !ignoreUpcomingRingTime && ringsIn() <= TimeUnit.HOURS.toMillis(hours); } + // TODO: Rename to getIntId() so usages refer to ObjectWithId#getIntId(), then delete this method. public int intId() { - return (int) getId(); + return getIntId(); } // TODO: Remove method signature from JsonSerializable interface. diff --git a/app/src/main/java/com/philliphsu/clock2/AsyncTimersTableUpdateHandler.java b/app/src/main/java/com/philliphsu/clock2/AsyncTimersTableUpdateHandler.java index 295b35c..2e598de 100644 --- a/app/src/main/java/com/philliphsu/clock2/AsyncTimersTableUpdateHandler.java +++ b/app/src/main/java/com/philliphsu/clock2/AsyncTimersTableUpdateHandler.java @@ -1,14 +1,22 @@ package com.philliphsu.clock2; +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; +import com.philliphsu.clock2.timers.TimerNotificationService; +import com.philliphsu.clock2.timers.TimesUpActivity; /** * Created by Phillip Hsu on 8/2/2016. */ public final class AsyncTimersTableUpdateHandler extends AsyncDatabaseTableUpdateHandler { + private static final String TAG = "TimersTableUpdater"; // TAG max 23 chars public AsyncTimersTableUpdateHandler(Context context, ScrollHandler scrollHandler) { super(context, scrollHandler); @@ -21,16 +29,55 @@ public final class AsyncTimersTableUpdateHandler extends AsyncDatabaseTableUpdat @Override protected void onPostAsyncDelete(Integer result, Timer timer) { - // TODO: Cancel the alarm scheduled for this timer + cancelAlarm(timer); } @Override protected void onPostAsyncInsert(Long result, Timer timer) { - // TODO: if running, schedule alarm + Log.d(TAG, "onPostAsyncInsert()"); + if (timer.isRunning()) { + Log.d(TAG, "Scheduling alarm for timer launch"); + scheduleAlarm(timer); + } } @Override protected void onPostAsyncUpdate(Long result, Timer timer) { - // TODO: cancel and reschedule + if (timer.isRunning()) { + // We don't need to cancel the previous alarm, because this one + // will remove and replace it. + scheduleAlarm(timer); + } else { + cancelAlarm(timer); + } + } + + // TODO: Consider changing to just a long id param + private PendingIntent createTimesUpIntent(Timer timer) { + Intent intent = new Intent(getContext(), TimesUpActivity.class); +// intent.putExtra(TimesUpActivity.EXTRA_ITEM_ID, timer.getId()); + // 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 + // needed to be recreated, and you reassigning the reference each time you were done with + // one of them, which leaves the one before unreferenced and hence eligible for GC. + return PendingIntent.getActivity(getContext(), timer.getIntId(), intent, PendingIntent.FLAG_CANCEL_CURRENT); + } + + 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) { + // 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()); } } diff --git a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmActivity.java b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmActivity.java new file mode 100644 index 0000000..39ce01f --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmActivity.java @@ -0,0 +1,78 @@ +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.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); + mAlarmController = new AlarmController(this, null); + // TODO: Butterknife binding + Button snooze = (Button) findViewById(R.id.btn_snooze); + snooze.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + snooze(); + } + }); + Button dismiss = (Button) findViewById(R.id.btn_dismiss); + dismiss.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + dismiss(); + } + }); + } + + @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; + } + + private void snooze() { + if (mAlarm != null) { + mAlarmController.snoozeAlarm(mAlarm); + } + // 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); + } + stopAndFinish(); + } +} diff --git a/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmActivity.java b/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmActivity.java index 1049d56..53b80b3 100644 --- a/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmActivity.java +++ b/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmActivity.java @@ -28,8 +28,8 @@ import com.philliphsu.clock2.BaseActivity; import com.philliphsu.clock2.DaysOfWeek; import com.philliphsu.clock2.R; import com.philliphsu.clock2.SharedPreferencesHelper; +import com.philliphsu.clock2.alarms.AlarmActivity; import com.philliphsu.clock2.model.AlarmLoader; -import com.philliphsu.clock2.ringtone.RingtoneActivity; import com.philliphsu.clock2.util.AlarmController; import com.philliphsu.clock2.util.AlarmUtils; import com.philliphsu.clock2.util.DateFormatUtils; @@ -543,8 +543,8 @@ public class EditAlarmActivity extends BaseActivity implements @Override public void cancelAlarm(Alarm alarm, boolean showToast) { new AlarmController(this, mMainContent).cancelAlarm(alarm, true); - if (RingtoneActivity.isAlive()) { - LocalBroadcastHelper.sendBroadcast(this, RingtoneActivity.ACTION_FINISH); + if (AlarmActivity.isAlive()) { + LocalBroadcastHelper.sendBroadcast(this, AlarmActivity.ACTION_FINISH); } } diff --git a/app/src/main/java/com/philliphsu/clock2/model/AlarmLoader.java b/app/src/main/java/com/philliphsu/clock2/model/AlarmLoader.java index 0066ae8..4c22470 100644 --- a/app/src/main/java/com/philliphsu/clock2/model/AlarmLoader.java +++ b/app/src/main/java/com/philliphsu/clock2/model/AlarmLoader.java @@ -11,6 +11,8 @@ public class AlarmLoader extends DataLoader { private long mAlarmId; + // TODO: Consider writing a super ctor that has the id param, so + // subclasses don't need to write their own. public AlarmLoader(Context context, long alarmId) { super(context); mAlarmId = alarmId; diff --git a/app/src/main/java/com/philliphsu/clock2/model/DataLoader.java b/app/src/main/java/com/philliphsu/clock2/model/DataLoader.java index baaea25..bd33bb4 100644 --- a/app/src/main/java/com/philliphsu/clock2/model/DataLoader.java +++ b/app/src/main/java/com/philliphsu/clock2/model/DataLoader.java @@ -6,6 +6,9 @@ import android.support.v4.content.AsyncTaskLoader; /** * Created by Phillip Hsu on 6/30/2016. */ +// TODO: Consider adding a DatabaseTableManager type param, so we can then +// implement loadInBackground for subclasses. You would, however, need to write +// an abstract method getTableManager() that subclasses implement for us. public abstract class DataLoader extends AsyncTaskLoader { private D mData; diff --git a/app/src/main/java/com/philliphsu/clock2/model/ObjectWithId.java b/app/src/main/java/com/philliphsu/clock2/model/ObjectWithId.java index 1bfbc54..09c9020 100644 --- a/app/src/main/java/com/philliphsu/clock2/model/ObjectWithId.java +++ b/app/src/main/java/com/philliphsu/clock2/model/ObjectWithId.java @@ -15,4 +15,8 @@ public abstract class ObjectWithId { public final void setId(long id) { this.id = id; } + + public final int getIntId() { + return (int) id; + } } diff --git a/app/src/main/java/com/philliphsu/clock2/model/TimerLoader.java b/app/src/main/java/com/philliphsu/clock2/model/TimerLoader.java new file mode 100644 index 0000000..ba598ac --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/model/TimerLoader.java @@ -0,0 +1,25 @@ +package com.philliphsu.clock2.model; + +import android.content.Context; + +import com.philliphsu.clock2.Timer; + +/** + * Created by Phillip Hsu on 8/3/2016. + */ +public class TimerLoader extends DataLoader { + + private long mTimerId; + + // TODO: Consider writing a super ctor that has the id param, so + // subclasses don't need to write their own. + public TimerLoader(Context context, long timerId) { + super(context); + mTimerId = timerId; + } + + @Override + public Timer loadInBackground() { + return new TimersTableManager(getContext()).queryItem(mTimerId).getItem(); + } +} 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 96acecb..d32d4ca 100644 --- a/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneActivity.java +++ b/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneActivity.java @@ -4,26 +4,20 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.os.Bundle; +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; -import android.widget.Button; -import com.philliphsu.clock2.Alarm; -import com.philliphsu.clock2.R; -import com.philliphsu.clock2.model.AlarmLoader; -import com.philliphsu.clock2.util.AlarmController; 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. - * - * TODO: Make this abstract and make appropriate subclasses for Alarms and Timers. */ -public class RingtoneActivity extends AppCompatActivity implements - android.support.v4.app.LoaderManager.LoaderCallbacks { +public abstract class RingtoneActivity extends AppCompatActivity implements LoaderCallbacks { private static final String TAG = "RingtoneActivity"; // Shared with RingtoneService @@ -32,14 +26,19 @@ public class RingtoneActivity extends AppCompatActivity implements private static boolean sIsAlive = false; - private long mAlarmId; - private Alarm mAlarm; - private AlarmController mAlarmController; + private long mItemId; + private T mItem; + + public abstract Loader onCreateLoader(long itemId); + + // TODO: Should we extend from BaseActivity instead? + @LayoutRes + public abstract int layoutResource(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_ringtone); + setContentView(layoutResource()); sIsAlive = true; getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); @@ -47,8 +46,8 @@ public class RingtoneActivity extends AppCompatActivity implements getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD); getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); - mAlarmId = getIntent().getLongExtra(EXTRA_ITEM_ID, -1); - if (mAlarmId < 0) { + 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 @@ -57,43 +56,31 @@ public class RingtoneActivity extends AppCompatActivity implements getSupportLoaderManager().initLoader(0, null, this); Intent intent = new Intent(this, RingtoneService.class) - .putExtra(EXTRA_ITEM_ID, mAlarmId); + .putExtra(EXTRA_ITEM_ID, mItemId); startService(intent); - - mAlarmController = new AlarmController(this, null); - - // TODO: Butterknife binding - Button snooze = (Button) findViewById(R.id.btn_snooze); - snooze.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - snooze(); - } - }); - Button dismiss = (Button) findViewById(R.id.btn_dismiss); - dismiss.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - dismiss(); - } - }); } @Override protected void onResume() { super.onResume(); + // TODO: Do we need this anymore? I think this broadcast was only sent from + // EditAlarmActivity? LocalBroadcastHelper.registerReceiver(this, mFinishReceiver, ACTION_FINISH); } @Override protected void onPause() { super.onPause(); + // TODO: Do we need this anymore? I think this broadcast was only sent from + // EditAlarmActivity? LocalBroadcastHelper.unregisterReceiver(this, mFinishReceiver); } @Override protected void onNewIntent(Intent intent) { //super.onNewIntent(intent); // Not needed since no fragments hosted? + // TODO: Do we need this anymore? I think the broadcast that calls through to + // this was only sent from EditAlarmActivity? // Notifies alarm missed and stops the service LocalBroadcastHelper.sendBroadcast(this, RingtoneService.ACTION_NOTIFY_MISSED); @@ -138,22 +125,17 @@ public class RingtoneActivity extends AppCompatActivity implements } @Override - public Loader onCreateLoader(int id, Bundle args) { - return new AlarmLoader(this, mAlarmId); + public Loader onCreateLoader(int id, Bundle args) { + return onCreateLoader(mItemId); } @Override - public void onLoadFinished(Loader loader, Alarm data) { - mAlarm = data; - if (mAlarm != 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); - } + public void onLoadFinished(Loader loader, T data) { + mItem = data; } @Override - public void onLoaderReset(Loader loader) { + public void onLoaderReset(Loader loader) { // Do nothing } @@ -161,28 +143,17 @@ public class RingtoneActivity extends AppCompatActivity implements return sIsAlive; } - private void snooze() { - if (mAlarm != null) { - mAlarmController.snoozeAlarm(mAlarm); - } - // 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); - } - stopAndFinish(); - } - - private void stopAndFinish() { + /** + * Exposed to subclasses so they can force us to stop the + * ringtone and finish us. + */ + protected final void stopAndFinish() { stopService(new Intent(this, RingtoneService.class)); finish(); } + // TODO: Do we need this anymore? I think this broadcast was only sent from + // EditAlarmActivity? private final BroadcastReceiver mFinishReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { 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 a236628..8f85939 100644 --- a/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneService.java +++ b/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneService.java @@ -220,6 +220,10 @@ public class RingtoneService extends Service { // TODO: abstract this, make subc } private 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. LocalBroadcastHelper.sendBroadcast(this, RingtoneActivity.ACTION_FINISH); } 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 6f5bfeb..3364cd4 100644 --- a/app/src/main/java/com/philliphsu/clock2/timers/TimerController.java +++ b/app/src/main/java/com/philliphsu/clock2/timers/TimerController.java @@ -1,7 +1,5 @@ package com.philliphsu.clock2.timers; -import android.widget.ImageButton; - import com.philliphsu.clock2.Timer; /** @@ -9,27 +7,26 @@ import com.philliphsu.clock2.Timer; */ public class TimerController { private final Timer mTimer; - private final CountdownChronometer mChronometer; - private final ImageButton mAddOneMinute; - private final ImageButton mStartPause; - private final ImageButton mStop; - public TimerController(Timer timer, CountdownChronometer chronometer, ImageButton addOneMinute, - ImageButton startPause, ImageButton stop) { - mTimer = timer; - mChronometer = chronometer; - mAddOneMinute = addOneMinute; - mStartPause = startPause; - mStop = stop; - -// init(); + /** + * Calls the appropriate state on the given Timer, based on + * its current state. + */ + public static void startPause(Timer timer) { + if (timer.hasStarted()) { + if (timer.isRunning()) { + timer.pause(); + } else { + timer.resume(); + } + } else { + timer.start(); + } + } + + public TimerController(Timer timer) { + mTimer = timer; } - -// private void init() { -// mChronometer.setBase(SystemClock.elapsedRealtime() + mTimer.duration()); -// updateStartPauseIcon(); -// setSecondaryButtonsVisible(false); -// } public void start() { mTimer.start(); diff --git a/app/src/main/java/com/philliphsu/clock2/timers/TimerNotificationService.java b/app/src/main/java/com/philliphsu/clock2/timers/TimerNotificationService.java new file mode 100644 index 0000000..4318cc1 --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/timers/TimerNotificationService.java @@ -0,0 +1,171 @@ +package com.philliphsu.clock2.timers; + +import android.app.IntentService; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.support.annotation.DrawableRes; +import android.support.v4.app.NotificationCompat; + +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. + */ +public class TimerNotificationService extends IntentService { + 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"; + + private TimersTableManager mTableManager; + + public TimerNotificationService() { + super("TimerNotificationService"); + } + + /** + * 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) { + Intent intent = new Intent(context, TimerNotificationService.class); + intent.putExtra(EXTRA_TIMER_ID, timerId); + 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)}. + * @param timerId the id of the Timer associated with the notification + * you want to cancel + */ + public static void cancelNotification(Context context, long timerId) { + NotificationManager nm = (NotificationManager) + context.getSystemService(Context.NOTIFICATION_SERVICE); + nm.cancel(TAG, (int) timerId); + } + + @Override + public void onCreate() { + super.onCreate(); + mTableManager = new TimersTableManager(this); + } + + @Override + protected void onHandleIntent(Intent intent) { + 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); + } else if (ACTION_ADD_ONE_MINUTE.equals(action)) { + handleAddOneMinute(timerId); + } else if (ACTION_START_PAUSE.equals(action)) { + handleStartPause(timerId); + } else if (ACTION_STOP.equals(action)) { + handleStop(timerId); + } + } + } + + private void showNotification(long timerId) { + Timer timer = getTimer(timerId); + + // Base note + NotificationCompat.Builder builder = new NotificationCompat.Builder(this) + // TODO: correct icon + .setSmallIcon(R.drawable.ic_half_day_1_black_24dp) + .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); + 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.. + contentIntent, + 0/*Shouldn't need a flag..*/)); + // TODO: Use a handler to continually update the countdown text + + String title = timer.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*/); + addAction(builder, ACTION_START_PAUSE, + timer.getId(), R.drawable.ic_add_circle_24dp/*TODO: correct icon*/); + addAction(builder, ACTION_STOP, + timer.getId(), R.drawable.ic_add_circle_24dp/*TODO: correct icon*/); + + NotificationManager nm = (NotificationManager) + getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(TAG, timer.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) { + Intent intent = new Intent(this, TimerNotificationService.class) + .setAction(action) + .putExtra(EXTRA_TIMER_ID, timerId); + PendingIntent pi = PendingIntent.getService(this, + (int) timerId, 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(); + } +} 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 4948193..902d660 100644 --- a/app/src/main/java/com/philliphsu/clock2/timers/TimerViewHolder.java +++ b/app/src/main/java/com/philliphsu/clock2/timers/TimerViewHolder.java @@ -1,6 +1,5 @@ package com.philliphsu.clock2.timers; -import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; @@ -52,17 +51,7 @@ public class TimerViewHolder extends BaseViewHolder { @OnClick(R.id.start_pause) void startPause() { - Timer t = getItem(); - if (t.isRunning()) { -// mController.pause(); - t.pause(); - } else { - if (t.hasStarted()) { - t.resume(); - } else { - t.start(); - } - } + TimerController.startPause(getItem()); // Persist value changes update(); } diff --git a/app/src/main/java/com/philliphsu/clock2/timers/TimesUpActivity.java b/app/src/main/java/com/philliphsu/clock2/timers/TimesUpActivity.java new file mode 100644 index 0000000..6549cfd --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/timers/TimesUpActivity.java @@ -0,0 +1,22 @@ +package com.philliphsu.clock2.timers; + +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; + +public class TimesUpActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + +// @Override +// public Loader onCreateLoader(long itemId) { +// return new TimerLoader(this, itemId); +// } + +// @Override +// public int layoutResource() { +// return R.layout.activity_ringtone; +// } +} 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 468ec1c..9cba57e 100644 --- a/app/src/main/java/com/philliphsu/clock2/util/AlarmController.java +++ b/app/src/main/java/com/philliphsu/clock2/util/AlarmController.java @@ -12,8 +12,8 @@ import com.philliphsu.clock2.Alarm; 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.model.AlarmsTableManager; -import com.philliphsu.clock2.ringtone.RingtoneActivity; import com.philliphsu.clock2.ringtone.RingtoneService; import static android.app.PendingIntent.FLAG_CANCEL_CURRENT; @@ -183,8 +183,8 @@ public final class AlarmController { private PendingIntent alarmIntent(Alarm alarm, boolean retrievePrevious) { // TODO: Use appropriate subclass instead - Intent intent = new Intent(mAppContext, RingtoneActivity.class) - .putExtra(RingtoneActivity.EXTRA_ITEM_ID, alarm.id()); + Intent intent = new Intent(mAppContext, AlarmActivity.class) + .putExtra(AlarmActivity.EXTRA_ITEM_ID, alarm.id()); 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 3a51b3c..980cdb1 100644 --- a/app/src/main/java/com/philliphsu/clock2/util/AlarmUtils.java +++ b/app/src/main/java/com/philliphsu/clock2/util/AlarmUtils.java @@ -13,8 +13,8 @@ import com.philliphsu.clock2.Alarm; 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.model.AlarmsTableManager; -import com.philliphsu.clock2.ringtone.RingtoneActivity; import com.philliphsu.clock2.ringtone.RingtoneService; import static android.app.PendingIntent.FLAG_CANCEL_CURRENT; @@ -180,8 +180,8 @@ public final class AlarmUtils { private static PendingIntent alarmIntent(Context context, Alarm alarm, boolean retrievePrevious) { // TODO: Use appropriate subclass instead - Intent intent = new Intent(context, RingtoneActivity.class) - .putExtra(RingtoneActivity.EXTRA_ITEM_ID, alarm.id()); + Intent intent = new Intent(context, AlarmActivity.class) + .putExtra(AlarmActivity.EXTRA_ITEM_ID, alarm.id()); 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 bdf5af4..47c2014 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -191,4 +191,6 @@ CreateTimerActivity + + Timer