Show alarm set icon in status bar

This commit is contained in:
Phillip Hsu 2016-09-27 18:56:12 -07:00
parent d171ac9536
commit c6cfe57e5d
4 changed files with 51 additions and 209 deletions

View File

@ -267,7 +267,7 @@ public class MainActivity extends BaseActivity {
public Fragment getItem(int position) {
switch (position) {
case PAGE_ALARMS:
return AlarmsFragment.newInstance(1);
return new AlarmsFragment();
case PAGE_TIMERS:
return new TimersFragment();
case PAGE_STOPWATCH:

View File

@ -4,12 +4,15 @@ import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.support.design.widget.Snackbar;
import android.util.Log;
import android.view.View;
import com.philliphsu.clock2.MainActivity;
import com.philliphsu.clock2.R;
import com.philliphsu.clock2.alarms.Alarm;
import com.philliphsu.clock2.alarms.ui.AlarmsFragment;
import com.philliphsu.clock2.ringtone.AlarmActivity;
import com.philliphsu.clock2.ringtone.playback.AlarmRingtoneService;
import com.philliphsu.clock2.alarms.background.PendingAlarmScheduler;
@ -53,46 +56,56 @@ public final class AlarmController {
* Schedules the alarm with the {@link AlarmManager}.
* If {@code alarm.}{@link Alarm#isEnabled() isEnabled()}
* returns false, this does nothing and returns immediately.
*
* 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.
*/
public void scheduleAlarm(Alarm alarm, boolean showSnackbar) {
if (!alarm.isEnabled()) {
Log.i(TAG, "Skipped scheduling an alarm because it was not enabled");
return;
}
// Does nothing if it's not posted. This is primarily here for when alarms
// are updated, instead of newly created, so that we don't leave behind
// stray upcoming alarm notifications. This occurs e.g. when a single-use
// alarm is updated to recur on a weekday later than the current day.
removeUpcomingAlarmNotification(alarm);
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.
final long ringAt = alarm.isSnoozed() ? alarm.snoozingUntil() : alarm.ringsAt();
am.setExact(AlarmManager.RTC_WAKEUP, ringAt, alarmIntent(alarm, false));
final PendingIntent alarmIntent = alarmIntent(alarm, false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Intent viewAlarm = new Intent(mAppContext, MainActivity.class);
viewAlarm.putExtra(AlarmsFragment.EXTRA_SCROLL_TO_ALARM_ID, alarm.getId());
PendingIntent showIntent = PendingIntent.getActivity(mAppContext,
alarm.getIntId(), viewAlarm, FLAG_CANCEL_CURRENT);
AlarmManager.AlarmClockInfo info = new AlarmManager.AlarmClockInfo(ringAt, showIntent);
am.setAlarmClock(info, alarmIntent);
} else {
// WAKEUP alarm types wake the CPU up, but NOT the screen;
// you would handle that yourself by using a wakelock, etc..
am.setExact(AlarmManager.RTC_WAKEUP, ringAt, alarmIntent);
// Show alarm in the status bar
Intent alarmChanged = new Intent("android.intent.action.ALARM_CHANGED");
alarmChanged.putExtra("alarmSet", true/*enabled*/);
mAppContext.sendBroadcast(alarmChanged);
}
final int hoursToNotifyInAdvance = AlarmPreferences.hoursBeforeUpcoming(mAppContext);
if (hoursToNotifyInAdvance > 0 || alarm.isSnoozed()) {
// If snoozed, upcoming note posted immediately.
long upcomingAt = ringAt - HOURS.toMillis(hoursToNotifyInAdvance);
// 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.
am.set(AlarmManager.RTC_WAKEUP, upcomingAt, notifyUpcomingAlarmIntent(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.
DurationUtils.toString(mAppContext, alarm.ringsIn(), false/*abbreviate*/));
showSnackbar(message);
}
}
@ -104,7 +117,6 @@ public final class AlarmController {
* and is enabled.
*/
public void cancelAlarm(Alarm alarm, boolean showSnackbar, boolean rescheduleIfRecurring) {
// TODO: Consider doing this in a new thread.
Log.d(TAG, "Cancelling alarm " + alarm);
AlarmManager am = (AlarmManager) mAppContext.getSystemService(Context.ALARM_SERVICE);
@ -112,6 +124,12 @@ public final class AlarmController {
if (pi != null) {
am.cancel(pi);
pi.cancel();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
// Remove alarm in the status bar
Intent alarmChanged = new Intent("android.intent.action.ALARM_CHANGED");
alarmChanged.putExtra("alarmSet", false/*enabled*/);
mAppContext.sendBroadcast(alarmChanged);
}
}
pi = notifyUpcomingAlarmIntent(alarm, true);
@ -124,6 +142,7 @@ public final class AlarmController {
removeUpcomingAlarmNotification(alarm);
final int hoursToNotifyInAdvance = AlarmPreferences.hoursBeforeUpcoming(mAppContext);
// ------------------------------------------------------------------------------------
// TOneverDO: Place block after making value changes to the alarm.
if ((hoursToNotifyInAdvance > 0 && showSnackbar
// TODO: Consider showing the snackbar for non-upcoming alarms too;
@ -134,6 +153,7 @@ public final class AlarmController {
formatTime(mAppContext, time));
showSnackbar(msg);
}
// ------------------------------------------------------------------------------------
if (alarm.isSnoozed()) {
alarm.stopSnoozing();
@ -195,40 +215,24 @@ 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_RINGING_OBJECT, alarm);
int flag = retrievePrevious ? FLAG_NO_CREATE : FLAG_CANCEL_CURRENT;
PendingIntent pi = getActivity(mAppContext, alarm.getIntId(), 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;
// null can be returned for some reason. Thus, we don't checkNotNull().
return getActivity(mAppContext, alarm.getIntId(), intent, flag);
}
private PendingIntent notifyUpcomingAlarmIntent(Alarm alarm, boolean retrievePrevious) {
Intent intent = new Intent(mAppContext, UpcomingAlarmReceiver.class)
.putExtra(UpcomingAlarmReceiver.EXTRA_ALARM, alarm);
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.getIntId(), 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;
// null can be returned for some reason. Thus, we don't checkNotNull().
return PendingIntent.getBroadcast(mAppContext, alarm.getIntId(), intent, flag);
}
private void showSnackbar(final String message) {

View File

@ -1,28 +1,22 @@
package com.philliphsu.clock2.alarms.ui;
import android.app.Activity;
import android.content.Intent;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.support.v4.content.Loader;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.philliphsu.clock2.R;
import com.philliphsu.clock2.list.RecyclerViewFragment;
import com.philliphsu.clock2.dialogs.TimePickerDialogController;
import com.philliphsu.clock2.alarms.Alarm;
import com.philliphsu.clock2.alarms.data.AlarmCursor;
import com.philliphsu.clock2.alarms.data.AlarmsListCursorLoader;
import com.philliphsu.clock2.alarms.data.AsyncAlarmsTableUpdateHandler;
import com.philliphsu.clock2.alarms.misc.AlarmController;
import com.philliphsu.clock2.dialogs.TimePickerDialogController;
import com.philliphsu.clock2.list.RecyclerViewFragment;
import com.philliphsu.clock2.timepickers.BaseTimePickerDialog;
import com.philliphsu.clock2.alarms.data.AlarmCursor;
import com.philliphsu.clock2.util.DelayedSnackbarHandler;
import static com.philliphsu.clock2.util.FragmentTagUtils.makeTag;
@ -30,54 +24,19 @@ import static com.philliphsu.clock2.util.FragmentTagUtils.makeTag;
public class AlarmsFragment extends RecyclerViewFragment<Alarm, BaseAlarmViewHolder, AlarmCursor,
AlarmsCursorAdapter> implements BaseTimePickerDialog.OnTimeSetListener {
private static final String TAG = "AlarmsFragment";
private static final String KEY_EXPANDED_POSITION = "expanded_position";
// TODO: Delete these constants. We no longer use EditAlarmActivity.
// @Deprecated
// private static final int REQUEST_EDIT_ALARM = 0;
// // Public because MainActivity needs to use it.
// // TODO: private because we handle fab clicks in the fragment now
// @Deprecated
// public static final int REQUEST_CREATE_ALARM = 1;
// TODO: Delete this. We no longer use the system's ringtone picker.
public static final int REQUEST_PICK_RINGTONE = 1;
public static final String EXTRA_SCROLL_TO_ALARM_ID = "com.philliphsu.clock2.alarms.extra.SCROLL_TO_ALARM_ID";
private AsyncAlarmsTableUpdateHandler mAsyncUpdateHandler;
private AlarmController mAlarmController;
// TODO: Delete this. If I recall correctly, this was just used for delaying item animations.
private Handler mHandler = new Handler();
private View mSnackbarAnchor;
private TimePickerDialogController mTimePickerDialogController;
private int mExpandedPosition = RecyclerView.NO_POSITION;
/**
* Mandatory empty constructor for the fragment manager to instantiate the
* fragment (e.g. upon screen orientation changes).
*/
public AlarmsFragment() {}
// TODO: Customize parameter initialization
@SuppressWarnings("unused")
public static AlarmsFragment newInstance(int columnCount) {
AlarmsFragment fragment = new AlarmsFragment();
Bundle args = new Bundle();
// TODO Put any arguments in bundle
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
// TODO Read arguments
}
if (savedInstanceState != null) {
// Restore the value of the last expanded position here.
// We cannot tell the adapter to expand this item until onLoadFinished()
@ -144,61 +103,8 @@ public class AlarmsFragment extends RecyclerViewFragment<Alarm, BaseAlarmViewHol
return R.string.empty_alarms_container;
}
// TODO: We're not using EditAlarmActivity anymore, so move this logic somewhere else.
// We also don't need to delay the change to get animations working.
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
Log.d(TAG, "onActivityResult()");
if (resultCode != Activity.RESULT_OK || data == null)
return;
if (requestCode == REQUEST_PICK_RINGTONE) {
Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
Log.d(TAG, "Retrieved ringtone URI: " + uri);
// TODO: We'll have to create a new Alarm instance with this ringtone value
// because we don't have a setter method. Alternatively, write an independent
// SQL update statement updating COLUMN_RINGTONE.
}
// final Alarm alarm = data.getParcelableExtra(EditAlarmActivity.EXTRA_MODIFIED_ALARM);
// if (alarm == null)
// return;
//
// // http://stackoverflow.com/a/27055512/5055032
// // "RecyclerView does not run animations in the first layout
// // pass after being attached." A workaround is to postpone
// // the CRUD operation to the next frame. A delay of 300ms is
// // short enough to not be noticeable, and long enough to
// // give us the animation *most of the time*.
// switch (requestCode) {
// case REQUEST_CREATE_ALARM:
// mHandler.postDelayed(
// new AsyncAddItemRunnable(mAsyncUpdateHandler, alarm),
// 300);
// break;
// case REQUEST_EDIT_ALARM:
// if (data.getBooleanExtra(EditAlarmActivity.EXTRA_IS_DELETING, false)) {
// // TODO: Should we delay this too? It seems animations run
// // some of the time.
// mAsyncUpdateHandler.asyncDelete(alarm);
// } else {
// // TODO: Increase the delay, because update animation is
// // more elusive than insert.
// mHandler.postDelayed(
// new AsyncUpdateItemRunnable(mAsyncUpdateHandler, alarm),
// 300);
// }
// break;
// default:
// Log.i(TAG, "Could not handle request code " + requestCode);
// break;
// }
}
@Override
public void onListItemClick(Alarm item, int position) {
// Intent intent = new Intent(getActivity(), EditAlarmActivity.class);
// intent.putExtra(EditAlarmActivity.EXTRA_ALARM_ID, item.id());
// startActivityForResult(intent, REQUEST_EDIT_ALARM);
boolean expanded = getAdapter().expand(position);
if (!expanded) {
getAdapter().collapse(position);
@ -210,7 +116,6 @@ public class AlarmsFragment extends RecyclerViewFragment<Alarm, BaseAlarmViewHol
// to the AlarmsCursorAdapter and call these on the save and delete button click bindings.
@Override
// TODO: Rename to onListItem***Delete*** because the item hasn't been deleted from our db yet
public void onListItemDeleted(final Alarm item) {
// The corresponding VH will be automatically removed from view following
// the requery, so we don't have to do anything to it.
@ -223,9 +128,6 @@ public class AlarmsFragment extends RecyclerViewFragment<Alarm, BaseAlarmViewHol
// be in view. While the requery will probably update the values displayed
// by the VH, the VH remains in its expanded state from before we were
// called. Tell the adapter reset its expanded position.
// TODO: Implement editing in the expanded VH. Then verify that changes
// while in that VH are saved and updated after the requery.
// getAdapter().collapse(position);
mAsyncUpdateHandler.asyncUpdate(item.getId(), item);
}
@ -233,8 +135,6 @@ public class AlarmsFragment extends RecyclerViewFragment<Alarm, BaseAlarmViewHol
@Override
protected void onScrolledToStableId(long id, int position) {
// We were called because of a requery. If it was due to an insertion,
// expand the newly added alarm.
boolean expanded = getAdapter().expand(position);
if (!expanded) {
// Otherwise, it was due to an item update. The VH is expanded
@ -300,65 +200,4 @@ public class AlarmsFragment extends RecyclerViewFragment<Alarm, BaseAlarmViewHol
private static String makeTimePickerDialogTag() {
return makeTag(AlarmsFragment.class, R.id.fab);
}
/////////////////////////////////////////////////////////////////////////////////////
// TODO: We won't need these anymore, since we won't handle the db
// update in onActivityResult() anymore.
@Deprecated
private static abstract class BaseAsyncItemChangeRunnable {
// TODO: Will holding onto this cause a memory leak?
private final AsyncAlarmsTableUpdateHandler mAsyncAlarmsTableUpdateHandler;
private final Alarm mAlarm;
BaseAsyncItemChangeRunnable(AsyncAlarmsTableUpdateHandler asyncAlarmsTableUpdateHandler, Alarm alarm) {
mAsyncAlarmsTableUpdateHandler = asyncAlarmsTableUpdateHandler;
mAlarm = alarm;
}
void asyncAddAlarm() {
mAsyncAlarmsTableUpdateHandler.asyncInsert(mAlarm);
}
void asyncUpdateAlarm() {
mAsyncAlarmsTableUpdateHandler.asyncUpdate(mAlarm.getId(), mAlarm);
}
void asyncRemoveAlarm() {
mAsyncAlarmsTableUpdateHandler.asyncDelete(mAlarm);
}
}
private static class AsyncAddItemRunnable extends BaseAsyncItemChangeRunnable implements Runnable {
AsyncAddItemRunnable(AsyncAlarmsTableUpdateHandler asyncAlarmsTableUpdateHandler, Alarm alarm) {
super(asyncAlarmsTableUpdateHandler, alarm);
}
@Override
public void run() {
asyncAddAlarm();
}
}
private static class AsyncUpdateItemRunnable extends BaseAsyncItemChangeRunnable implements Runnable {
AsyncUpdateItemRunnable(AsyncAlarmsTableUpdateHandler asyncAlarmsTableUpdateHandler, Alarm alarm) {
super(asyncAlarmsTableUpdateHandler, alarm);
}
@Override
public void run() {
asyncUpdateAlarm();
}
}
private static class AsyncRemoveItemRunnable extends BaseAsyncItemChangeRunnable implements Runnable {
AsyncRemoveItemRunnable(AsyncAlarmsTableUpdateHandler asyncAlarmsTableUpdateHandler, Alarm alarm) {
super(asyncAlarmsTableUpdateHandler, alarm);
}
@Override
public void run() {
asyncRemoveAlarm();
}
}
}

View File

@ -140,7 +140,8 @@ public abstract class RecyclerViewFragment<
}
// This may have been a requery due to content change. If the change
// was an insertion, scroll to the last modified alarm.
performScrollToStableId();
performScrollToStableId(mScrollToStableId);
mScrollToStableId = RecyclerView.NO_ID;
}
@Override
@ -167,21 +168,19 @@ public abstract class RecyclerViewFragment<
mList.smoothScrollToPosition(position);
}
private void performScrollToStableId() {
if (mScrollToStableId != RecyclerView.NO_ID) {
protected final void performScrollToStableId(long stableId) {
if (stableId != RecyclerView.NO_ID) {
int position = -1;
for (int i = 0; i < mAdapter.getItemCount(); i++) {
if (mAdapter.getItemId(i) == mScrollToStableId) {
if (mAdapter.getItemId(i) == stableId) {
position = i;
break;
}
}
if (position >= 0) {
scrollToPosition(position);
onScrolledToStableId(mScrollToStableId, position);
onScrolledToStableId(stableId, position);
}
}
// Reset
mScrollToStableId = RecyclerView.NO_ID;
}
}