From 058d6c86b74f253ad0ba963bdd61b8c09b3e5fab Mon Sep 17 00:00:00 2001 From: Phillip Hsu Date: Mon, 11 Jul 2016 02:28:20 -0700 Subject: [PATCH] Created AlarmController class and moved relevant AlarmUtils code --- .../clock2/AsyncItemChangeHandler.java | 23 +- .../clock2/OnBootUpAlarmScheduler.java | 5 +- .../clock2/PendingAlarmScheduler.java | 12 +- .../clock2/UpcomingAlarmReceiver.java | 5 +- .../clock2/alarms/AlarmViewHolder.java | 21 +- .../clock2/alarms/AlarmsCursorAdapter.java | 8 +- .../clock2/alarms/AlarmsFragment.java | 52 +--- .../clock2/editalarm/EditAlarmActivity.java | 5 +- .../clock2/ringtone/RingtoneActivity.java | 11 +- .../clock2/ringtone/RingtoneService.java | 11 +- .../clock2/util/AlarmController.java | 225 ++++++++++++++++++ .../philliphsu/clock2/util/AlarmUtils.java | 40 +--- .../clock2/util/DelayedSnackbarHandler.java | 58 +++++ 13 files changed, 359 insertions(+), 117 deletions(-) create mode 100644 app/src/main/java/com/philliphsu/clock2/util/AlarmController.java create mode 100644 app/src/main/java/com/philliphsu/clock2/util/DelayedSnackbarHandler.java diff --git a/app/src/main/java/com/philliphsu/clock2/AsyncItemChangeHandler.java b/app/src/main/java/com/philliphsu/clock2/AsyncItemChangeHandler.java index 3a96542..35e2403 100644 --- a/app/src/main/java/com/philliphsu/clock2/AsyncItemChangeHandler.java +++ b/app/src/main/java/com/philliphsu/clock2/AsyncItemChangeHandler.java @@ -7,7 +7,7 @@ import android.view.View; import com.philliphsu.clock2.alarms.ScrollHandler; import com.philliphsu.clock2.model.DatabaseManager; -import com.philliphsu.clock2.util.AlarmUtils; +import com.philliphsu.clock2.util.AlarmController; /** * Created by Phillip Hsu on 7/1/2016. @@ -20,15 +20,19 @@ public final class AsyncItemChangeHandler { private final Context mContext; private final View mSnackbarAnchor; private final ScrollHandler mScrollHandler; + private final AlarmController mAlarmController; /** - * @param snackbarAnchor an optional anchor for a Snackbar to anchor to - * @param scrollHandler + * @param context the Context from which we get the application context + * @param snackbarAnchor */ - public AsyncItemChangeHandler(Context context, View snackbarAnchor, ScrollHandler scrollHandler) { + public AsyncItemChangeHandler(Context context, View snackbarAnchor, + ScrollHandler scrollHandler, + AlarmController alarmController) { mContext = context.getApplicationContext(); // to prevent memory leaks mSnackbarAnchor = snackbarAnchor; mScrollHandler = scrollHandler; + mAlarmController = alarmController; } public void asyncAddAlarm(final Alarm alarm) { @@ -54,6 +58,7 @@ public final class AsyncItemChangeHandler { @Override protected void onPostExecute(Integer integer) { + mAlarmController.cancelAlarm(alarm, false); if (mSnackbarAnchor != null) { // TODO: Consider adding delay to allow the alarm item animation // to finish first before we show the snackbar. Inbox app does this. @@ -87,17 +92,13 @@ public final class AsyncItemChangeHandler { @Override protected void onPostExecute(Long result) { - AlarmUtils.scheduleAlarm(mContext, mAlarm, true); + // TODO: Consider adding delay to allow the alarm item animation + // to finish first before we show the snackbar. Inbox app does this. + mAlarmController.scheduleAlarm(mAlarm, true); if (mScrollHandler != null) { // Prepare to scroll to this alarm mScrollHandler.setScrollToStableId(result); } - if (mSnackbarAnchor != null) { - // TODO: Consider adding delay to allow the alarm item animation - // to finish first before we show the snackbar. Inbox app does this. - String message = AlarmUtils.getRingsInText(mContext, mAlarm.ringsIn()); - AlarmUtils.showSnackbar(mSnackbarAnchor, message); - } } final Long insertAlarm() { diff --git a/app/src/main/java/com/philliphsu/clock2/OnBootUpAlarmScheduler.java b/app/src/main/java/com/philliphsu/clock2/OnBootUpAlarmScheduler.java index f9204f2..afe7b15 100644 --- a/app/src/main/java/com/philliphsu/clock2/OnBootUpAlarmScheduler.java +++ b/app/src/main/java/com/philliphsu/clock2/OnBootUpAlarmScheduler.java @@ -6,7 +6,7 @@ import android.content.Intent; import com.philliphsu.clock2.model.AlarmDatabaseHelper.AlarmCursor; import com.philliphsu.clock2.model.DatabaseManager; -import com.philliphsu.clock2.util.AlarmUtils; +import com.philliphsu.clock2.util.AlarmController; /** * An {@link IntentService} subclass for handling asynchronous task requests in @@ -59,6 +59,7 @@ public class OnBootUpAlarmScheduler extends IntentService { @Override protected void onHandleIntent(Intent intent) { if (intent != null) { + AlarmController controller = new AlarmController(this, null); // IntentService works in a background thread, so this won't hold us up. AlarmCursor cursor = DatabaseManager.getInstance(this).queryEnabledAlarms(); while (cursor.moveToNext()) { @@ -67,7 +68,7 @@ public class OnBootUpAlarmScheduler extends IntentService { throw new IllegalStateException( "queryEnabledAlarms() returned alarm(s) that aren't enabled"); } - AlarmUtils.scheduleAlarm(this, alarm, false); + controller.scheduleAlarm(alarm, false); } cursor.close(); diff --git a/app/src/main/java/com/philliphsu/clock2/PendingAlarmScheduler.java b/app/src/main/java/com/philliphsu/clock2/PendingAlarmScheduler.java index 340ff75..a754b49 100644 --- a/app/src/main/java/com/philliphsu/clock2/PendingAlarmScheduler.java +++ b/app/src/main/java/com/philliphsu/clock2/PendingAlarmScheduler.java @@ -5,7 +5,7 @@ import android.content.Context; import android.content.Intent; import com.philliphsu.clock2.model.DatabaseManager; -import com.philliphsu.clock2.util.AlarmUtils; +import com.philliphsu.clock2.util.AlarmController; import static com.philliphsu.clock2.util.Preconditions.checkNotNull; @@ -47,12 +47,10 @@ public class PendingAlarmScheduler extends BroadcastReceiver { throw new IllegalStateException("Alarm must be enabled!"); } alarm.ignoreUpcomingRingTime(false); // allow #ringsWithinHours() to behave normally - // Because showToast = false, we don't do any UI work. - // TODO: Since we're in a worker thread, verify that the - // UI related code within will not cause us to crash. - AlarmUtils.scheduleAlarm(context, alarm, false); - // Update the db - AlarmUtils.save(context, alarm); + // No UI work is done + AlarmController controller = new AlarmController(context, null); + controller.scheduleAlarm(alarm, false); + controller.save(alarm); } }).start(); } diff --git a/app/src/main/java/com/philliphsu/clock2/UpcomingAlarmReceiver.java b/app/src/main/java/com/philliphsu/clock2/UpcomingAlarmReceiver.java index c886226..99fb353 100644 --- a/app/src/main/java/com/philliphsu/clock2/UpcomingAlarmReceiver.java +++ b/app/src/main/java/com/philliphsu/clock2/UpcomingAlarmReceiver.java @@ -10,7 +10,7 @@ import android.os.AsyncTask; import android.support.v4.app.NotificationCompat; import com.philliphsu.clock2.model.DatabaseManager; -import com.philliphsu.clock2.util.AlarmUtils; +import com.philliphsu.clock2.util.AlarmController; import static android.app.PendingIntent.FLAG_ONE_SHOT; import static com.philliphsu.clock2.util.DateFormatUtils.formatTime; @@ -47,8 +47,7 @@ public class UpcomingAlarmReceiver extends BroadcastReceiver { @Override protected void onPostExecute(Alarm alarm) { if (ACTION_DISMISS_NOW.equals(intent.getAction())) { - // This MUST be done on the UI thread. - AlarmUtils.cancelAlarm(context, alarm, true); + new AlarmController(context, null).cancelAlarm(alarm, false); } else { // Prepare notification // http://stackoverflow.com/a/15803726/5055032 diff --git a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmViewHolder.java b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmViewHolder.java index 4ea2026..83de512 100644 --- a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmViewHolder.java +++ b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmViewHolder.java @@ -18,6 +18,7 @@ import com.philliphsu.clock2.DaysOfWeek; import com.philliphsu.clock2.OnListItemInteractionListener; import com.philliphsu.clock2.R; import com.philliphsu.clock2.model.AlarmsRepository; +import com.philliphsu.clock2.util.AlarmController; import com.philliphsu.clock2.util.AlarmUtils; import java.util.Date; @@ -38,6 +39,8 @@ import static com.philliphsu.clock2.util.DateFormatUtils.formatTime; public class AlarmViewHolder extends BaseViewHolder implements AlarmCountdown.OnTickListener { private static final RelativeSizeSpan AMPM_SIZE_SPAN = new RelativeSizeSpan(0.5f); + private final AlarmController mAlarmController; + @Bind(R.id.time) TextView mTime; @Bind(R.id.on_off_switch) SwitchCompat mSwitch; @Bind(R.id.label) TextView mLabel; @@ -45,8 +48,17 @@ public class AlarmViewHolder extends BaseViewHolder implements AlarmCount @Bind(R.id.recurring_days) TextView mDays; @Bind(R.id.dismiss) Button mDismissButton; + @Deprecated // TODO: Delete this, the only usage is from AlarmsAdapter (SortedList), which is not used anymore. public AlarmViewHolder(ViewGroup parent, OnListItemInteractionListener listener) { super(parent, R.layout.item_alarm, listener); + mAlarmController = null; + mCountdown.setOnTickListener(this); + } + + public AlarmViewHolder(ViewGroup parent, OnListItemInteractionListener listener, + AlarmController alarmController) { + super(parent, R.layout.item_alarm, listener); + mAlarmController = alarmController; mCountdown.setOnTickListener(this); } @@ -76,7 +88,7 @@ public class AlarmViewHolder extends BaseViewHolder implements AlarmCount } else { // Dismisses the current upcoming alarm and handles scheduling the next alarm for us. // Since changes are saved to the database, this prompts a UI refresh. - AlarmUtils.cancelAlarm(getContext(), alarm, true); + mAlarmController.cancelAlarm(alarm, true); } // TOneverDO: AlarmUtils.cancelAlarm() otherwise it will be called twice /* @@ -127,11 +139,10 @@ public class AlarmViewHolder extends BaseViewHolder implements AlarmCount alarm.setEnabled(checked); if (alarm.isEnabled()) { // TODO: On Moto X, upcoming notification doesn't post immediately - AlarmUtils.scheduleAlarm(getContext(), alarm, true); - AlarmUtils.sendShowSnackbarBroadcast(getContext(), AlarmUtils.getRingsInText(getContext(), alarm.ringsIn())); - AlarmUtils.save(getContext(), alarm); + mAlarmController.scheduleAlarm(alarm, true); + mAlarmController.save(alarm); } else { - AlarmUtils.cancelAlarm(getContext(), alarm, true); + mAlarmController.cancelAlarm(alarm, true); // cancelAlarm() already calls save() for you. } mSwitch.setPressed(false); // clear the pressed focus, esp. if setPressed(true) was called manually diff --git a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsCursorAdapter.java b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsCursorAdapter.java index 2046532..c93787a 100644 --- a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsCursorAdapter.java +++ b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsCursorAdapter.java @@ -8,6 +8,7 @@ import android.view.ViewGroup; import com.philliphsu.clock2.Alarm; import com.philliphsu.clock2.OnListItemInteractionListener; import com.philliphsu.clock2.model.AlarmDatabaseHelper.AlarmCursor; +import com.philliphsu.clock2.util.AlarmController; /** * Created by Phillip Hsu on 6/29/2016. @@ -18,10 +19,13 @@ public class AlarmsCursorAdapter extends RecyclerView.Adapter { private static final String TAG = "AlarmsCursorAdapter"; private final OnListItemInteractionListener mListener; + private final AlarmController mAlarmController; private AlarmCursor mCursor; - public AlarmsCursorAdapter(OnListItemInteractionListener listener) { + public AlarmsCursorAdapter(OnListItemInteractionListener listener, + AlarmController alarmController) { mListener = listener; + mAlarmController = alarmController; // Excerpt from docs of notifyDataSetChanged(): // "RecyclerView will attempt to synthesize [artificially create?] // visible structural change events [when items are inserted, removed or @@ -34,7 +38,7 @@ public class AlarmsCursorAdapter extends RecyclerView.Adapter { @Override public AlarmViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - return new AlarmViewHolder(parent, mListener); + return new AlarmViewHolder(parent, mListener, mAlarmController); } @Override diff --git a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java index dd3e8a7..775f427 100644 --- a/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java +++ b/app/src/main/java/com/philliphsu/clock2/alarms/AlarmsFragment.java @@ -1,7 +1,6 @@ package com.philliphsu.clock2.alarms; import android.app.Activity; -import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.database.Cursor; @@ -23,8 +22,8 @@ import com.philliphsu.clock2.OnListItemInteractionListener; import com.philliphsu.clock2.R; import com.philliphsu.clock2.editalarm.EditAlarmActivity; import com.philliphsu.clock2.model.AlarmsListCursorLoader; -import com.philliphsu.clock2.util.AlarmUtils; -import com.philliphsu.clock2.util.LocalBroadcastHelper; +import com.philliphsu.clock2.util.AlarmController; +import com.philliphsu.clock2.util.DelayedSnackbarHandler; import butterknife.Bind; import butterknife.ButterKnife; @@ -37,14 +36,10 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks, private static final int REQUEST_EDIT_ALARM = 0; // Public because MainActivity needs to use it. public static final int REQUEST_CREATE_ALARM = 1; - /** - * Local broadcast senders can tell us to show a snackbar with a message on their behalf. - */ - public static final String ACTION_SHOW_SNACKBAR_MSG = "com.philliphsu.clock2.alarms.action.SHOW_SNACKBAR_MSG"; - public static final String EXTRA_MSG = "com.philliphsu.clock2.alarms.extra.MSG"; private AlarmsCursorAdapter mAdapter; private AsyncItemChangeHandler mAsyncItemChangeHandler; + private AlarmController mAlarmController; private Handler mHandler = new Handler(); private View mSnackbarAnchor; private long mScrollToStableId = RecyclerView.NO_ID; @@ -78,6 +73,9 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks, // Will succeed because the activity is created at this point. // See the Fragment lifecycle. mSnackbarAnchor = getActivity().findViewById(R.id.main_content); + mAlarmController = new AlarmController(getActivity(), mSnackbarAnchor); + mAsyncItemChangeHandler = new AsyncItemChangeHandler(getActivity(), + mSnackbarAnchor, this, mAlarmController); getLoaderManager().initLoader(0, null, this); } @@ -87,33 +85,22 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_alarms, container, false); ButterKnife.bind(this, view); + // Set the adapter Context context = view.getContext(); mList.setLayoutManager(new LinearLayoutManager(context)); - mAdapter = new AlarmsCursorAdapter(this); + mAdapter = new AlarmsCursorAdapter(this, mAlarmController); mList.setAdapter(mAdapter); - mAsyncItemChangeHandler = new AsyncItemChangeHandler( - getActivity(), mSnackbarAnchor, this); return view; } @Override - public void onStart() { - super.onStart(); - LocalBroadcastHelper.registerReceiver(getActivity(), - mShowSnackbarReceiver, ACTION_SHOW_SNACKBAR_MSG); - } - - @Override - public void onStop() { - // This will always be called when we leave this screen, either by exiting the app or - // by navigating elsewhere. Since we unregister the receiver here, we will never receive - // a "show alarm snoozed" broadcast, because the snooze action is always made elsewhere - // in the app. - super.onStop(); - Log.e(TAG, "onStop()"); - LocalBroadcastHelper.unregisterReceiver(getActivity(), mShowSnackbarReceiver); + public void onResume() { + super.onResume(); + // Show the pending Snackbar, if any, that was prepared for us + // by another app component. + DelayedSnackbarHandler.makeAndShow(mSnackbarAnchor); } @Override @@ -221,19 +208,6 @@ public class AlarmsFragment extends Fragment implements LoaderCallbacks, // TODO: Delete this method. } - private final BroadcastReceiver mShowSnackbarReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - // See Intent#putExtras(Bundle): - // Putting a Bundle of extras into an intent will have its - // contents added to the intent's collection of extras, - // so we can individually retrieve the Bundle's extras - // directly from the intent. - String message = intent.getStringExtra(EXTRA_MSG); - AlarmUtils.showSnackbar(mSnackbarAnchor, message); - } - }; - private static abstract class BaseAsyncItemChangeRunnable { // TODO: Will holding onto this cause a memory leak? private final AsyncItemChangeHandler mAsyncItemChangeHandler; 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 eb06faf..c00b1ff 100644 --- a/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmActivity.java +++ b/app/src/main/java/com/philliphsu/clock2/editalarm/EditAlarmActivity.java @@ -33,6 +33,7 @@ import com.philliphsu.clock2.SharedPreferencesHelper; import com.philliphsu.clock2.model.AlarmLoader; import com.philliphsu.clock2.model.DatabaseManager; import com.philliphsu.clock2.ringtone.RingtoneActivity; +import com.philliphsu.clock2.util.AlarmController; import com.philliphsu.clock2.util.AlarmUtils; import com.philliphsu.clock2.util.LocalBroadcastHelper; @@ -520,7 +521,9 @@ public class EditAlarmActivity extends BaseActivity implements AlarmNumpad.KeyLi @Override public void cancelAlarm(Alarm alarm, boolean showToast) { - AlarmUtils.cancelAlarm(this, alarm, showToast); + // TODO: Rewrite XML layout to use CoordinatorLayout and + // pass in the snackbar anchor. + new AlarmController(this, null).cancelAlarm(alarm, true); if (RingtoneActivity.isAlive()) { LocalBroadcastHelper.sendBroadcast(this, RingtoneActivity.ACTION_FINISH); } 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 8edf171..96acecb 100644 --- a/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneActivity.java +++ b/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneActivity.java @@ -13,7 +13,7 @@ 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.AlarmUtils; +import com.philliphsu.clock2.util.AlarmController; import com.philliphsu.clock2.util.LocalBroadcastHelper; /** @@ -34,6 +34,7 @@ public class RingtoneActivity extends AppCompatActivity implements private long mAlarmId; private Alarm mAlarm; + private AlarmController mAlarmController; @Override protected void onCreate(Bundle savedInstanceState) { @@ -59,6 +60,8 @@ public class RingtoneActivity extends AppCompatActivity implements .putExtra(EXTRA_ITEM_ID, mAlarmId); startService(intent); + mAlarmController = new AlarmController(this, null); + // TODO: Butterknife binding Button snooze = (Button) findViewById(R.id.btn_snooze); snooze.setOnClickListener(new View.OnClickListener() { @@ -145,7 +148,7 @@ public class RingtoneActivity extends AppCompatActivity implements 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. - AlarmUtils.removeUpcomingAlarmNotification(this, mAlarm); + mAlarmController.removeUpcomingAlarmNotification(mAlarm); } } @@ -160,7 +163,7 @@ public class RingtoneActivity extends AppCompatActivity implements private void snooze() { if (mAlarm != null) { - AlarmUtils.snoozeAlarm(this, mAlarm); + 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. @@ -170,7 +173,7 @@ public class RingtoneActivity extends AppCompatActivity implements private void dismiss() { if (mAlarm != null) { // TODO do we really need to cancel the intent and alarm? - AlarmUtils.cancelAlarm(this, mAlarm, false); + mAlarmController.cancelAlarm(mAlarm, false); } stopAndFinish(); } 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 b269d69..9030004 100644 --- a/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneService.java +++ b/app/src/main/java/com/philliphsu/clock2/ringtone/RingtoneService.java @@ -22,6 +22,7 @@ import android.util.Log; import com.philliphsu.clock2.Alarm; import com.philliphsu.clock2.R; import com.philliphsu.clock2.model.DatabaseManager; +import com.philliphsu.clock2.util.AlarmController; import com.philliphsu.clock2.util.AlarmUtils; import com.philliphsu.clock2.util.LocalBroadcastHelper; @@ -54,6 +55,7 @@ public class RingtoneService extends Service { // TODO: abstract this, make subc private Ringtone mRingtone; private Alarm mAlarm; private String mNormalRingTime; + private AlarmController mAlarmController; private boolean mAutoSilenced = false; // TODO: Using Handler for this is ill-suited? Alarm ringing could outlast the // application's life. Use AlarmManager API instead. @@ -63,7 +65,7 @@ public class RingtoneService extends Service { // TODO: abstract this, make subc public void run() { mAutoSilenced = true; // TODO do we really need to cancel the alarm and intent? - AlarmUtils.cancelAlarm(RingtoneService.this, mAlarm, false); + mAlarmController.cancelAlarm(mAlarm, false); finishActivity(); stopSelf(); } @@ -72,7 +74,7 @@ public class RingtoneService extends Service { // TODO: abstract this, make subc @Override public void onReceive(Context context, Intent intent) { mAutoSilenced = true; - // TODO: Do we need to call AlarmUtils.cancelAlarm()? + // TODO: Do we need to call mAlarmController.cancelAlarm()? stopSelf(); // Activity finishes itself } @@ -101,9 +103,9 @@ public class RingtoneService extends Service { // TODO: abstract this, make subc }).start(); } else { if (ACTION_SNOOZE.equals(intent.getAction())) { - AlarmUtils.snoozeAlarm(this, mAlarm); + mAlarmController.snoozeAlarm(mAlarm); } else if (ACTION_DISMISS.equals(intent.getAction())) { - AlarmUtils.cancelAlarm(this, mAlarm, false); // 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? } else { throw new UnsupportedOperationException(); } @@ -119,6 +121,7 @@ public class RingtoneService extends Service { // TODO: abstract this, make subc public void onCreate() { super.onCreate(); LocalBroadcastHelper.registerReceiver(this, mNotifyMissedReceiver, ACTION_NOTIFY_MISSED); + mAlarmController = new AlarmController(this, null); } @Override diff --git a/app/src/main/java/com/philliphsu/clock2/util/AlarmController.java b/app/src/main/java/com/philliphsu/clock2/util/AlarmController.java new file mode 100644 index 0000000..e465a5f --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/util/AlarmController.java @@ -0,0 +1,225 @@ +package com.philliphsu.clock2.util; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.support.design.widget.Snackbar; +import android.util.Log; +import android.view.View; + +import com.philliphsu.clock2.Alarm; +import com.philliphsu.clock2.PendingAlarmScheduler; +import com.philliphsu.clock2.R; +import com.philliphsu.clock2.UpcomingAlarmReceiver; +import com.philliphsu.clock2.model.DatabaseManager; +import com.philliphsu.clock2.ringtone.RingtoneActivity; +import com.philliphsu.clock2.ringtone.RingtoneService; + +import static android.app.PendingIntent.FLAG_CANCEL_CURRENT; +import static android.app.PendingIntent.FLAG_NO_CREATE; +import static android.app.PendingIntent.getActivity; +import static com.philliphsu.clock2.util.DateFormatUtils.formatTime; +import static java.util.concurrent.TimeUnit.HOURS; + +/** + * Created by Phillip Hsu on 7/10/2016. + * + * API to control alarm states and update the UI. + * TODO: Move this out of the .utils package when done. + * TODO: Rename to AlarmStateHandler? AlarmStateController? + */ +public final class AlarmController { + private static final String TAG = "AlarmController"; + + private final Context mAppContext; + private final View mSnackbarAnchor; + + /** + * + * @param context the Context from which the application context will be requested + * @param snackbarAnchor an optional anchor for a Snackbar to anchor to + */ + public AlarmController(Context context, View snackbarAnchor) { + mAppContext = context.getApplicationContext(); + mSnackbarAnchor = snackbarAnchor; + } + + /** + * Schedules the alarm with the {@link AlarmManager}. + * If {@code alarm.}{@link Alarm#isEnabled() isEnabled()} + * returns false, this does nothing and returns immediately. + */ + public void scheduleAlarm(Alarm alarm, boolean showSnackbar) { + if (!alarm.isEnabled()) { + Log.i(TAG, "Skipped scheduling an alarm because it was not enabled"); + return; + } + + // TODO: Consider doing this in a new thread. + Log.d(TAG, "Scheduling alarm " + alarm); + AlarmManager am = (AlarmManager) mAppContext.getSystemService(Context.ALARM_SERVICE); + // If there is already an alarm for this Intent scheduled (with the equality of two + // intents being defined by filterEquals(Intent)), then it will be removed and replaced + // by this one. For most of our uses, the relevant criteria for equality will be the + // action, the data, and the class (component). Although not documented, the request code + // of a PendingIntent is also considered to determine equality of two intents. + + // WAKEUP alarm types wake the CPU up, but NOT the screen. If that is what you want, you need + // to handle that yourself by using a wakelock, etc.. + // We use a WAKEUP alarm to send the upcoming alarm notification so it goes off even if the + // device is asleep. Otherwise, it will not go off until the device is turned back on. + long ringAt = alarm.isSnoozed() ? alarm.snoozingUntil() : alarm.ringsAt(); + int hoursToNotifyInAdvance = AlarmUtils.hoursBeforeUpcoming(mAppContext); + long upcomingAt = ringAt - HOURS.toMillis(hoursToNotifyInAdvance); + // If snoozed, upcoming note posted immediately. + am.set(AlarmManager.RTC_WAKEUP, upcomingAt, notifyUpcomingAlarmIntent(alarm, false)); + am.setExact(AlarmManager.RTC_WAKEUP, ringAt, alarmIntent(alarm, false)); + + if (showSnackbar) { + String message = mAppContext.getString(R.string.alarm_set_for, + DurationUtils.toString(mAppContext, alarm.ringsIn(), false /*abbreviate?*/)); + // TODO: Consider adding delay to allow the alarm item animation + // to finish first before we show the snackbar. Inbox app does this. + showSnackbar(message); + } + } + + /** + * Cancel the alarm. This does NOT check if you previously scheduled the alarm. + */ + public void cancelAlarm(Alarm alarm, boolean showSnackbar) { + // TODO: Consider doing this in a new thread. + Log.d(TAG, "Cancelling alarm " + alarm); + AlarmManager am = (AlarmManager) mAppContext.getSystemService(Context.ALARM_SERVICE); + + PendingIntent pi = alarmIntent(alarm, true); + if (pi != null) { + am.cancel(pi); + pi.cancel(); + } + + pi = notifyUpcomingAlarmIntent(alarm, true); + if (pi != null) { + am.cancel(pi); + pi.cancel(); + } + + // Does nothing if it's not posted. + removeUpcomingAlarmNotification(alarm); + + int hoursToNotifyInAdvance = AlarmUtils.hoursBeforeUpcoming(mAppContext); + // TOneverDO: Place block after making value changes to the alarm. + if (showSnackbar + // TODO: Consider showing the snackbar for non-upcoming alarms too; + // then, we can remove these checks. + && alarm.ringsWithinHours(hoursToNotifyInAdvance) || alarm.isSnoozed()) { + long time = alarm.isSnoozed() ? alarm.snoozingUntil() : alarm.ringsAt(); + String msg = mAppContext.getString(R.string.upcoming_alarm_dismissed, + formatTime(mAppContext, time)); + showSnackbar(msg); + } + + if (alarm.isSnoozed()) { + alarm.stopSnoozing(); + } + + if (!alarm.hasRecurrence()) { + alarm.setEnabled(false); + } else if (alarm.isEnabled()) { + if (alarm.ringsWithinHours(hoursToNotifyInAdvance)) { + // Still upcoming today, so wait until the normal ring time + // passes before rescheduling the alarm. + alarm.ignoreUpcomingRingTime(true); // Useful only for VH binding + Intent intent = new Intent(mAppContext, PendingAlarmScheduler.class) + .putExtra(PendingAlarmScheduler.EXTRA_ALARM_ID, alarm.id()); + pi = PendingIntent.getBroadcast(mAppContext, alarm.intId(), + intent, PendingIntent.FLAG_ONE_SHOT); + am.set(AlarmManager.RTC_WAKEUP, alarm.ringsAt(), pi); + } else { + scheduleAlarm(alarm, false); + } + } + + save(alarm); + + // If service is not running, nothing happens + mAppContext.stopService(new Intent(mAppContext, RingtoneService.class)); + } + + public void snoozeAlarm(Alarm alarm) { + int minutesToSnooze = AlarmUtils.snoozeDuration(mAppContext); + alarm.snooze(minutesToSnooze); + scheduleAlarm(alarm, false); + String message = mAppContext.getString(R.string.title_snoozing_until, + formatTime(mAppContext, alarm.snoozingUntil())); + // Since snoozing is always done by an app component away from + // the list screen, the Snackbar will never be shown. In fact, this + // controller has a null mSnackbarAnchor if we're using it for snoozing + // an alarm. We solve this by preparing the message, and waiting until + // the list screen is resumed so that it can display the Snackbar for us. + DelayedSnackbarHandler.prepareMessage(message); + save(alarm); + } + + public void removeUpcomingAlarmNotification(Alarm a) { + Intent intent = new Intent(mAppContext, UpcomingAlarmReceiver.class) + .setAction(UpcomingAlarmReceiver.ACTION_CANCEL_NOTIFICATION) + .putExtra(UpcomingAlarmReceiver.EXTRA_ALARM_ID, a.id()); + mAppContext.sendBroadcast(intent); + } + + public void save(final Alarm alarm) { + // TODO: Will using the Runnable like this cause a memory leak? + new Thread(new Runnable() { + @Override + public void run() { + DatabaseManager.getInstance(mAppContext).updateAlarm(alarm.id(), alarm); + } + }).start(); + } + + 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()); + 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, + // null can be returned for some reason. +/* + if (retrievePrevious) { + checkNotNull(pi); + } +*/ + return pi; + } + + private PendingIntent notifyUpcomingAlarmIntent(Alarm alarm, boolean retrievePrevious) { + Intent intent = new Intent(mAppContext, UpcomingAlarmReceiver.class) + .putExtra(UpcomingAlarmReceiver.EXTRA_ALARM_ID, alarm.id()); + if (alarm.isSnoozed()) { + // TODO: Will this affect retrieving a previous instance? Say if the previous instance + // didn't have this action set initially, but at a later time we made a new instance + // with it set. + intent.setAction(UpcomingAlarmReceiver.ACTION_SHOW_SNOOZING); + } + int flag = retrievePrevious ? FLAG_NO_CREATE : FLAG_CANCEL_CURRENT; + PendingIntent pi = PendingIntent.getBroadcast(mAppContext, alarm.intId(), intent, flag); + // Even when we try to retrieve a previous instance that actually did exist, + // null can be returned for some reason. +/* + if (retrievePrevious) { + checkNotNull(pi); + } +*/ + return pi; + } + + private void showSnackbar(String message) { + // Is the window containing this anchor currently focused? + if (mSnackbarAnchor != null && mSnackbarAnchor.hasWindowFocus()) { + Snackbar.make(mSnackbarAnchor, message, Snackbar.LENGTH_LONG).show(); + } + } +} 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 219d48b..32da1fb 100644 --- a/app/src/main/java/com/philliphsu/clock2/util/AlarmUtils.java +++ b/app/src/main/java/com/philliphsu/clock2/util/AlarmUtils.java @@ -4,19 +4,15 @@ import android.app.AlarmManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; -import android.os.Bundle; import android.preference.PreferenceManager; import android.support.annotation.StringRes; -import android.support.design.widget.Snackbar; import android.util.Log; -import android.view.View; import android.widget.Toast; 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.AlarmsFragment; import com.philliphsu.clock2.model.DatabaseManager; import com.philliphsu.clock2.ringtone.RingtoneActivity; import com.philliphsu.clock2.ringtone.RingtoneService; @@ -48,7 +44,7 @@ public final class AlarmUtils { * @deprecated {@code showToast} is no longer working. Callers must * handle popup confirmations on their own. */ - // TODO: Delete showToast param + // TODO: Consider moving usages to the background public static void scheduleAlarm(Context context, Alarm alarm, boolean showToast) { if (!alarm.isEnabled()) { Log.i(TAG, "Skipped scheduling an alarm because it was not enabled"); @@ -149,14 +145,6 @@ public final class AlarmUtils { public static void snoozeAlarm(Context c, Alarm a) { a.snooze(snoozeDuration(c)); scheduleAlarm(c, a, true); - // TODO: Based on the current lifecycle methods pair where we register/unregister the - // receiver in AlarmsFragment, the snackbar won't be shown. - // We have no reference to the snackbar anchor, so let AlarmsFragment - // handle showing the snackbar for us. AlarmsFragment has no knowledge - // of which alarm is snoozed (and actually doesn't need to know); we can build - // the message for it. This is why we don't have a showAlarmSnoozedSnackbar(Alarm) - // utility method. - sendShowSnackbarBroadcast(c, getSnoozingUntilText(c, a.snoozingUntil())); save(c, a); } @@ -235,30 +223,4 @@ public final class AlarmUtils { } }).start(); } - - public static String getRingsInText(Context context, long ringsIn) { - return context.getString(R.string.alarm_set_for, - DurationUtils.toString(context, ringsIn, false /*abbreviate?*/)); - } - - public static String getSnoozingUntilText(Context context, long snoozingUntil) { - return context.getString(R.string.title_snoozing_until, - formatTime(context, snoozingUntil)); - } - - public static void sendShowSnackbarBroadcast(Context c, String message) { - Bundle extra = new Bundle(1); - extra.putString(AlarmsFragment.EXTRA_MSG, message); - LocalBroadcastHelper.sendBroadcast(c, AlarmsFragment.ACTION_SHOW_SNACKBAR_MSG, extra); - } - - /** - * Show a snackbar confirmation about an event related to an alarm. - * Used for showing an alarm has been snoozed. - */ - public static void showSnackbar(View snackbarAnchor, String message) { - if (snackbarAnchor != null) { - Snackbar.make(snackbarAnchor, message, Snackbar.LENGTH_LONG).show(); - } - } } diff --git a/app/src/main/java/com/philliphsu/clock2/util/DelayedSnackbarHandler.java b/app/src/main/java/com/philliphsu/clock2/util/DelayedSnackbarHandler.java new file mode 100644 index 0000000..0b90d8f --- /dev/null +++ b/app/src/main/java/com/philliphsu/clock2/util/DelayedSnackbarHandler.java @@ -0,0 +1,58 @@ +package com.philliphsu.clock2.util; + +import android.support.design.widget.Snackbar; +import android.view.View; + +/** + * Created by Phillip Hsu on 7/10/2016. + * + * Handler to prepare a Snackbar to be shown only when requested to. + * Useful when the Snackbar is created in an app component that + * is not where it should be shown. + */ +public final class DelayedSnackbarHandler { + // TODO: Consider wrapping this in a WeakReference, so that you + // don't prevent this from being GCed if you never call #show(). + private static Snackbar snackbar; + private static String message; + + private DelayedSnackbarHandler() {} + + /** + * Saves a reference to the given Snackbar, so that you can + * call {@link #show()} at a later time. + */ + public static void prepareSnackbar(Snackbar sb) { + snackbar = sb; + } + + /** + * Shows the Snackbar previously prepared with + * {@link #prepareSnackbar(Snackbar)} + */ + public static void show() { + if (snackbar != null) { + snackbar.show(); + snackbar = null; + } + } + + /** + * Saves a static reference to the message, so that you can + * call {@link #makeAndShow(View)} at a later time. + */ + public static void prepareMessage(String msg) { + message = msg; + } + + /** + * Makes a Snackbar with the message previously prepared with + * {@link #prepareMessage(String)} and shows it. + */ + public static void makeAndShow(View snackbarAnchor) { + if (snackbarAnchor != null && message != null) { + Snackbar.make(snackbarAnchor, message, Snackbar.LENGTH_LONG).show(); + message = null; + } + } +}