Type: INTEGER
+ */ + public static final String HOUR = "hour"; + + /** + * Minutes in localtime 0 - 59 + *Type: INTEGER
+ */ + public static final String MINUTES = "minutes"; + + /** + * Days of week coded as integer + *Type: INTEGER
+ */ + public static final String DAYS_OF_WEEK = "daysofweek"; + + /** + * Alarm time in UTC milliseconds from the epoch. + *Type: INTEGER
+ */ + public static final String ALARM_TIME = "alarmtime"; + + /** + * True if alarm is active + *Type: BOOLEAN
+ */ + public static final String ENABLED = "enabled"; + + /** + * True if alarm should vibrate + *Type: BOOLEAN
+ */ + public static final String VIBRATE = "vibrate"; + + /** + * Message to show when alarm triggers + * Note: not currently used + *Type: STRING
+ */ + public static final String MESSAGE = "message"; + + /** + * Audio alert to play when alarm triggers + *Type: STRING
+ */ + public static final String ALERT = "alert"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = _ID + " ASC"; + + // Used when filtering enabled alarms. + public static final String WHERE_ENABLED = ENABLED + "=1"; + + static final String[] ALARM_QUERY_COLUMNS = { + _ID, HOUR, MINUTES, DAYS_OF_WEEK, ALARM_TIME, + ENABLED, VIBRATE, MESSAGE, ALERT }; + + /** + * These save calls to cursor.getColumnIndexOrThrow() + * THEY MUST BE KEPT IN SYNC WITH ABOVE QUERY COLUMNS + */ + public static final int ALARM_ID_INDEX = 0; + public static final int ALARM_HOUR_INDEX = 1; + public static final int ALARM_MINUTES_INDEX = 2; + public static final int ALARM_DAYS_OF_WEEK_INDEX = 3; + public static final int ALARM_TIME_INDEX = 4; + public static final int ALARM_ENABLED_INDEX = 5; + public static final int ALARM_VIBRATE_INDEX = 6; + public static final int ALARM_MESSAGE_INDEX = 7; + public static final int ALARM_ALERT_INDEX = 8; + } + ////////////////////////////// + // End column definitions + ////////////////////////////// + + // Public fields + public int id; + public boolean enabled; + public int hour; + public int minutes; + public DaysOfWeek daysOfWeek; + public long time; + public boolean vibrate; + public String label; + public Uri alert; + public boolean silent; + + public Alarm(Cursor c) { + id = c.getInt(Columns.ALARM_ID_INDEX); + enabled = c.getInt(Columns.ALARM_ENABLED_INDEX) == 1; + hour = c.getInt(Columns.ALARM_HOUR_INDEX); + minutes = c.getInt(Columns.ALARM_MINUTES_INDEX); + daysOfWeek = new DaysOfWeek(c.getInt(Columns.ALARM_DAYS_OF_WEEK_INDEX)); + time = c.getLong(Columns.ALARM_TIME_INDEX); + vibrate = c.getInt(Columns.ALARM_VIBRATE_INDEX) == 1; + label = c.getString(Columns.ALARM_MESSAGE_INDEX); + String alertString = c.getString(Columns.ALARM_ALERT_INDEX); + if (Alarms.ALARM_ALERT_SILENT.equals(alertString)) { + if (Log.LOGV) { + Log.v("Alarm is marked as silent"); + } + silent = true; + } else { + if (alertString != null && alertString.length() != 0) { + alert = Uri.parse(alertString); + } + + // If the database alert is null or it failed to parse, use the + // default alert. + if (alert == null) { + alert = RingtoneManager.getDefaultUri( + RingtoneManager.TYPE_ALARM); + } + } + } + + public Alarm(Parcel p) { + id = p.readInt(); + enabled = p.readInt() == 1; + hour = p.readInt(); + minutes = p.readInt(); + daysOfWeek = new DaysOfWeek(p.readInt()); + time = p.readLong(); + vibrate = p.readInt() == 1; + label = p.readString(); + alert = (Uri) p.readParcelable(null); + silent = p.readInt() == 1; + } + + public String getLabelOrDefault(Context context) { + if (label == null || label.length() == 0) { + return context.getString(R.string.default_label); + } + return label; + } + + /* + * Days of week code as a single int. + * 0x00: no day + * 0x01: Monday + * 0x02: Tuesday + * 0x04: Wednesday + * 0x08: Thursday + * 0x10: Friday + * 0x20: Saturday + * 0x40: Sunday + */ + static final class DaysOfWeek { + + private static int[] DAY_MAP = new int[] { + Calendar.MONDAY, + Calendar.TUESDAY, + Calendar.WEDNESDAY, + Calendar.THURSDAY, + Calendar.FRIDAY, + Calendar.SATURDAY, + Calendar.SUNDAY, + }; + + // Bitmask of all repeating days + private int mDays; + + DaysOfWeek(int days) { + mDays = days; + } + + public String toString(Context context, boolean showNever) { + StringBuilder ret = new StringBuilder(); + + // no days + if (mDays == 0) { + return showNever ? + context.getText(R.string.never).toString() : ""; + } + + // every day + if (mDays == 0x7f) { + return context.getText(R.string.every_day).toString(); + } + + // count selected days + int dayCount = 0, days = mDays; + while (days > 0) { + if ((days & 1) == 1) dayCount++; + days >>= 1; + } + + // short or long form? + DateFormatSymbols dfs = new DateFormatSymbols(); + String[] dayList = (dayCount > 1) ? + dfs.getShortWeekdays() : + dfs.getWeekdays(); + + // selected days + for (int i = 0; i < 7; i++) { + if ((mDays & (1 << i)) != 0) { + ret.append(dayList[DAY_MAP[i]]); + dayCount -= 1; + if (dayCount > 0) ret.append( + context.getText(R.string.day_concat)); + } + } + return ret.toString(); + } + + private boolean isSet(int day) { + return ((mDays & (1 << day)) > 0); + } + + public void set(int day, boolean set) { + if (set) { + mDays |= (1 << day); + } else { + mDays &= ~(1 << day); + } + } + + public void set(DaysOfWeek dow) { + mDays = dow.mDays; + } + + public int getCoded() { + return mDays; + } + + // Returns days of week encoded in an array of booleans. + public boolean[] getBooleanArray() { + boolean[] ret = new boolean[7]; + for (int i = 0; i < 7; i++) { + ret[i] = isSet(i); + } + return ret; + } + + public boolean isRepeatSet() { + return mDays != 0; + } + + /** + * returns number of days from today until next alarm + * @param c must be set to today + */ + public int getNextAlarm(Calendar c) { + if (mDays == 0) { + return -1; + } + + int today = (c.get(Calendar.DAY_OF_WEEK) + 5) % 7; + + int day = 0; + int dayCount = 0; + for (; dayCount < 7; dayCount++) { + day = (today + dayCount) % 7; + if (isSet(day)) { + break; + } + } + return dayCount; + } + } +} diff --git a/src/com/android/alarmclock/AlarmAlert.java b/src/com/android/alarmclock/AlarmAlert.java index a309618..89a866a 100644 --- a/src/com/android/alarmclock/AlarmAlert.java +++ b/src/com/android/alarmclock/AlarmAlert.java @@ -17,10 +17,17 @@ package com.android.alarmclock; import android.app.Activity; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.BroadcastReceiver; import android.content.Intent; +import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.res.Configuration; import android.os.Bundle; +import android.preference.PreferenceManager; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; @@ -39,78 +46,52 @@ */ public class AlarmAlert extends Activity { - private static final int SNOOZE_MINUTES = 10; - private static final int UNKNOWN = 0; - private static final int SNOOZE = 1; - private static final int DISMISS = 2; - private static final int KILLED = 3; - private Button mSnoozeButton; - private int mState = UNKNOWN; + // These defaults must match the values in res/xml/settings.xml + private static final String DEFAULT_SNOOZE = "10"; + private static final String DEFAULT_VOLUME_BEHAVIOR = "2"; - private AlarmKlaxon mKlaxon; - private int mAlarmId; - private String mLabel; + private Alarm mAlarm; + private int mVolumeBehavior; + + // Receives the ALARM_KILLED action from the AlarmKlaxon. + private BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Alarm alarm = intent.getParcelableExtra(Alarms.ALARM_INTENT_EXTRA); + if (mAlarm.id == alarm.id) { + dismiss(true); + } + } + }; @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); - // Maintain a lock during the playback of the alarm. This lock may have - // already been acquired in AlarmReceiver. If the process was killed, - // the global wake lock is gone. Acquire again just to be sure. - AlarmAlertWakeLock.acquireCpuWakeLock(this); - - /* FIXME Intentionally verbose: always log this until we've - fully debugged the app failing to start up */ - Log.v("AlarmAlert.onCreate()"); - - Intent i = getIntent(); - mAlarmId = i.getIntExtra(Alarms.ID, -1); - - mKlaxon = new AlarmKlaxon(); - mKlaxon.postPlay(this, mAlarmId); + mAlarm = getIntent().getParcelableExtra(Alarms.ALARM_INTENT_EXTRA); - /* allow next alarm to trigger while this activity is - active */ - Alarms.disableSnoozeAlert(AlarmAlert.this); - Alarms.disableAlert(AlarmAlert.this, mAlarmId); - Alarms.setNextAlert(this); - - mKlaxon.setKillerCallback(new AlarmKlaxon.KillerCallback() { - public void onKilled() { - if (Log.LOGV) Log.v("onKilled()"); - updateSilencedText(); - - /* don't allow snooze */ - mSnoozeButton.setEnabled(false); - - // Dismiss the alarm but mark the state as killed so if the - // config changes, we show the silenced message and disable - // snooze. - dismiss(); - mState = KILLED; - } - }); + // Get the volume/camera button behavior setting + final String vol = + PreferenceManager.getDefaultSharedPreferences(this) + .getString(SettingsActivity.KEY_VOLUME_BEHAVIOR, + DEFAULT_VOLUME_BEHAVIOR); + mVolumeBehavior = Integer.parseInt(vol); requestWindowFeature(android.view.Window.FEATURE_NO_TITLE); - getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD + | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); updateLayout(); - } - private void setTitleFromIntent(Intent i) { - mLabel = i.getStringExtra(Alarms.LABEL); - if (mLabel == null || mLabel.length() == 0) { - mLabel = getString(R.string.default_label); - } - TextView title = (TextView) findViewById(R.id.alertTitle); - title.setText(mLabel); + // Register to get the alarm killed intent. + registerReceiver(mReceiver, new IntentFilter(Alarms.ALARM_KILLED)); } - private void updateSilencedText() { - TextView silenced = (TextView) findViewById(R.id.silencedText); - silenced.setText(getString(R.string.alarm_alert_alert_silenced, - AlarmKlaxon.ALARM_TIMEOUT_SECONDS / 60)); - silenced.setVisibility(View.VISIBLE); + private void setTitle() { + String label = mAlarm.getLabelOrDefault(this); + TextView title = (TextView) findViewById(R.id.alertTitle); + title.setText(label); } // This method is overwritten in AlarmAlertFullScreen in order to show a @@ -140,77 +121,87 @@ private void updateLayout() { /* snooze behavior: pop a snooze confirmation view, kick alarm manager. */ - mSnoozeButton = (Button) findViewById(R.id.snooze); - mSnoozeButton.requestFocus(); - // If this was a configuration change, keep the silenced text if the - // alarm was killed. - if (mState == KILLED) { - updateSilencedText(); - mSnoozeButton.setEnabled(false); - } else { - mSnoozeButton.setOnClickListener(new Button.OnClickListener() { - public void onClick(View v) { - snooze(); - finish(); - } - }); - } + Button snooze = (Button) findViewById(R.id.snooze); + snooze.requestFocus(); + snooze.setOnClickListener(new Button.OnClickListener() { + public void onClick(View v) { + snooze(); + } + }); /* dismiss button: close notification */ findViewById(R.id.dismiss).setOnClickListener( new Button.OnClickListener() { public void onClick(View v) { - dismiss(); - finish(); + dismiss(false); } }); - /* Set the title from the passed in label */ - setTitleFromIntent(getIntent()); + /* Set the title from the passed in alarm */ + setTitle(); } // Attempt to snooze this alert. private void snooze() { - if (mState != UNKNOWN) { - return; - } - // If the next alarm is set for sooner than the snooze interval, don't - // snooze. Instead, toast the user that the snooze will not be set. + final String snooze = + PreferenceManager.getDefaultSharedPreferences(this) + .getString(SettingsActivity.KEY_ALARM_SNOOZE, DEFAULT_SNOOZE); + int snoozeMinutes = Integer.parseInt(snooze); + final long snoozeTime = System.currentTimeMillis() - + (1000 * 60 * SNOOZE_MINUTES); - final long nextAlarm = - Alarms.calculateNextAlert(AlarmAlert.this).getAlert(); - String displayTime = null; - if (nextAlarm < snoozeTime) { - final Calendar c = Calendar.getInstance(); - c.setTimeInMillis(nextAlarm); - displayTime = getString(R.string.alarm_alert_snooze_not_set, - Alarms.formatTime(AlarmAlert.this, c)); - mState = DISMISS; - } else { - Alarms.saveSnoozeAlert(AlarmAlert.this, mAlarmId, snoozeTime, - mLabel); - Alarms.setNextAlert(AlarmAlert.this); - displayTime = getString(R.string.alarm_alert_snooze_set, - SNOOZE_MINUTES); - mState = SNOOZE; - } + + (1000 * 60 * snoozeMinutes); + Alarms.saveSnoozeAlert(AlarmAlert.this, mAlarm.id, snoozeTime); + + // Get the display time for the snooze and update the notification. + final Calendar c = Calendar.getInstance(); + c.setTimeInMillis(snoozeTime); + + // Append (snoozed) to the label. + String label = mAlarm.getLabelOrDefault(this); + label = getString(R.string.alarm_notify_snooze_label, label); + + // Notify the user that the alarm has been snoozed. + Intent cancelSnooze = new Intent(this, AlarmReceiver.class); + cancelSnooze.setAction(Alarms.CANCEL_SNOOZE); + cancelSnooze.putExtra(Alarms.ALARM_ID, mAlarm.id); + PendingIntent broadcast = + PendingIntent.getBroadcast(this, mAlarm.id, cancelSnooze, 0); + NotificationManager nm = getNotificationManager(); + Notification n = new Notification(R.drawable.stat_notify_alarm, + label, 0); + n.setLatestEventInfo(this, label, + getString(R.string.alarm_notify_snooze_text, + Alarms.formatTime(this, c)), broadcast); + n.deleteIntent = broadcast; + n.flags |= Notification.FLAG_AUTO_CANCEL; + nm.notify(mAlarm.id, n); + + String displayTime = getString(R.string.alarm_alert_snooze_set, + snoozeMinutes); // Intentionally log the snooze time for debugging. Log.v(displayTime); + // Display the snooze minutes in a toast. Toast.makeText(AlarmAlert.this, displayTime, Toast.LENGTH_LONG).show(); - mKlaxon.stop(this, mState == SNOOZE); - releaseLocks(); + stopService(new Intent(Alarms.ALARM_ALERT_ACTION)); + finish(); + } + + private NotificationManager getNotificationManager() { + return (NotificationManager) getSystemService(NOTIFICATION_SERVICE); } // Dismiss the alarm. - private void dismiss() { - if (mState != UNKNOWN) { - return; + private void dismiss(boolean killed) { + // The service told us that the alarm has been killed, do not modify + // the notification or stop the service. + if (!killed) { + // Cancel the notification and stop playing the alarm + NotificationManager nm = getNotificationManager(); + nm.cancel(mAlarm.id); + stopService(new Intent(Alarms.ALARM_ALERT_ACTION)); } - mState = DISMISS; - mKlaxon.stop(this, false); - releaseLocks(); + finish(); } /** @@ -220,87 +211,57 @@ private void dismiss() { @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); - if (Log.LOGV) Log.v("AlarmAlert.OnNewIntent()"); - mState = UNKNOWN; - mSnoozeButton.setEnabled(true); - mAlarmId = intent.getIntExtra(Alarms.ID, -1); - // Play the new alarm sound. - mKlaxon.postPlay(this, mAlarmId); - - setTitleFromIntent(intent); + if (Log.LOGV) Log.v("AlarmAlert.OnNewIntent()"); - /* unset silenced message */ - TextView silenced = (TextView)findViewById(R.id.silencedText); - silenced.setVisibility(View.GONE); + mAlarm = intent.getParcelableExtra(Alarms.ALARM_INTENT_EXTRA); - Alarms.setNextAlert(this); - setIntent(intent); - } - - @Override - protected void onResume() { - super.onResume(); - if (Log.LOGV) Log.v("AlarmAlert.onResume()"); - // Acquire a separate lock for the screen to stay on. This is necessary - // to avoid flashing the keyguard when the screen is locked. - AlarmAlertWakeLock.acquireScreenWakeLock(this); + setTitle(); } @Override protected void onStop() { super.onStop(); - if (Log.LOGV) Log.v("AlarmAlert.onStop()"); - // As a last resort, try to snooze if this activity is stopped. - snooze(); - // We might have been killed by the KillerCallback so always release - // the lock and keyguard. - releaseLocks(); + // Don't hang around. + finish(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (Log.LOGV) Log.v("AlarmAlert.onDestroy()"); + // No longer care about the alarm being killed. + unregisterReceiver(mReceiver); } @Override public boolean dispatchKeyEvent(KeyEvent event) { - // Do this on key down to handle a few of the system keys. Only handle - // the snooze and dismiss this alert if the state is unknown. + // Do this on key down to handle a few of the system keys. boolean up = event.getAction() == KeyEvent.ACTION_UP; - boolean dismiss = false; switch (event.getKeyCode()) { - case KeyEvent.KEYCODE_DPAD_UP: - case KeyEvent.KEYCODE_DPAD_DOWN: - case KeyEvent.KEYCODE_DPAD_LEFT: - case KeyEvent.KEYCODE_DPAD_RIGHT: - case KeyEvent.KEYCODE_DPAD_CENTER: - // Ignore ENDCALL because we do not receive the event if the screen - // is on. However, we do receive the key up for ENDCALL if the - // screen was off. - case KeyEvent.KEYCODE_ENDCALL: - break; - // Volume keys dismiss the alarm + // Volume keys and camera keys dismiss the alarm case KeyEvent.KEYCODE_VOLUME_UP: case KeyEvent.KEYCODE_VOLUME_DOWN: - dismiss = true; - // All other keys will snooze the alarm - default: - // Check for UNKNOWN here so that we intercept both key events - // and prevent the volume keys from triggering their default - // behavior. - if (mState == UNKNOWN && up) { - if (dismiss) { - dismiss(); - } else { - snooze(); + case KeyEvent.KEYCODE_CAMERA: + case KeyEvent.KEYCODE_FOCUS: + if (up) { + switch (mVolumeBehavior) { + case 1: + snooze(); + break; + + case 2: + dismiss(false); + break; + + default: + break; } - finish(); } return true; + default: + break; } return super.dispatchKeyEvent(event); } - - /** - * release wake and keyguard locks - */ - private synchronized void releaseLocks() { - AlarmAlertWakeLock.release(); - } } diff --git a/src/com/android/alarmclock/AlarmAlertFullScreen.java b/src/com/android/alarmclock/AlarmAlertFullScreen.java index 07d0d9c..c00ffce 100644 --- a/src/com/android/alarmclock/AlarmAlertFullScreen.java +++ b/src/com/android/alarmclock/AlarmAlertFullScreen.java @@ -16,10 +16,8 @@ package com.android.alarmclock; -import android.graphics.drawable.BitmapDrawable; -import android.view.View; -import android.view.Gravity; -import android.view.LayoutInflater; +import android.os.Bundle; +import android.view.WindowManager; /** * Full screen alarm alert: pops visible indicator and plays alarm tone. This @@ -27,17 +25,14 @@ * background is the current wallpaper. */ public class AlarmAlertFullScreen extends AlarmAlert { - @Override - final protected View inflateView(LayoutInflater inflater) { - View v = inflater.inflate(R.layout.alarm_alert, null); - - // Display the wallpaper as the background. - BitmapDrawable wallpaper = (BitmapDrawable) getWallpaper(); - wallpaper.setGravity(Gravity.CENTER); - v.setBackgroundDrawable(wallpaper); - - return v; + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + } + + @Override + public void onBackPressed() { + // Don't allow back to dismiss. + return; } - } diff --git a/src/com/android/alarmclock/AlarmAlertWakeLock.java b/src/com/android/alarmclock/AlarmAlertWakeLock.java index 0291df8..8cbcd94 100644 --- a/src/com/android/alarmclock/AlarmAlertWakeLock.java +++ b/src/com/android/alarmclock/AlarmAlertWakeLock.java @@ -25,7 +25,6 @@ */ class AlarmAlertWakeLock { - private static PowerManager.WakeLock sScreenWakeLock; private static PowerManager.WakeLock sCpuWakeLock; static void acquireCpuWakeLock(Context context) { @@ -44,31 +43,11 @@ static void acquireCpuWakeLock(Context context) { sCpuWakeLock.acquire(); } - static void acquireScreenWakeLock(Context context) { - Log.v("Acquiring screen wake lock"); - if (sScreenWakeLock != null) { - return; - } - - PowerManager pm = - (PowerManager) context.getSystemService(Context.POWER_SERVICE); - - sScreenWakeLock = pm.newWakeLock( - PowerManager.FULL_WAKE_LOCK | - PowerManager.ACQUIRE_CAUSES_WAKEUP | - PowerManager.ON_AFTER_RELEASE, Log.LOGTAG); - sScreenWakeLock.acquire(); - } - - static void release() { - Log.v("Releasing wake lock"); + static void releaseCpuLock() { + Log.v("Releasing cpu wake lock"); if (sCpuWakeLock != null) { sCpuWakeLock.release(); sCpuWakeLock = null; } - if (sScreenWakeLock != null) { - sScreenWakeLock.release(); - sScreenWakeLock = null; - } } } diff --git a/src/com/android/alarmclock/AlarmClock.java b/src/com/android/alarmclock/AlarmClock.java index 9804994..75477fd 100644 --- a/src/com/android/alarmclock/AlarmClock.java +++ b/src/com/android/alarmclock/AlarmClock.java @@ -22,6 +22,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; +import android.content.res.Configuration; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; @@ -34,7 +35,11 @@ import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; +import android.view.View.OnCreateContextMenuListener; import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.AdapterView.AdapterContextMenuInfo; +import android.widget.AdapterView.OnItemClickListener; import android.widget.CursorAdapter; import android.widget.ListView; import android.widget.TextView; @@ -46,7 +51,7 @@ /** * AlarmClock application. */ -public class AlarmClock extends Activity { +public class AlarmClock extends Activity implements OnItemClickListener { final static String PREFERENCES = "AlarmClock"; final static String PREF_CLOCK_FACE = "face"; @@ -63,8 +68,6 @@ public class AlarmClock extends Activity { private LayoutInflater mFactory; private ViewGroup mClockLayout; private View mClock = null; - private MenuItem mAddAlarmItem; - private MenuItem mToggleClockItem; private ListView mAlarmsList; private Cursor mCursor; @@ -104,58 +107,36 @@ public View newView(Context context, Cursor cursor, ViewGroup parent) { } public void bindView(View view, Context context, Cursor cursor) { - final int id = cursor.getInt(Alarms.AlarmColumns.ALARM_ID_INDEX); - final int hour = cursor.getInt(Alarms.AlarmColumns.ALARM_HOUR_INDEX); - final int minutes = cursor.getInt(Alarms.AlarmColumns.ALARM_MINUTES_INDEX); - final Alarms.DaysOfWeek daysOfWeek = new Alarms.DaysOfWeek( - cursor.getInt(Alarms.AlarmColumns.ALARM_DAYS_OF_WEEK_INDEX)); - final boolean enabled = cursor.getInt(Alarms.AlarmColumns.ALARM_ENABLED_INDEX) == 1; - final String label = - cursor.getString(Alarms.AlarmColumns.ALARM_MESSAGE_INDEX); + final Alarm alarm = new Alarm(cursor); CheckBox onButton = (CheckBox)view.findViewById(R.id.alarmButton); - onButton.setChecked(enabled); + onButton.setChecked(alarm.enabled); onButton.setOnClickListener(new OnClickListener() { public void onClick(View v) { boolean isChecked = ((CheckBox) v).isChecked(); - Alarms.enableAlarm(AlarmClock.this, id, isChecked); + Alarms.enableAlarm(AlarmClock.this, alarm.id, + isChecked); if (isChecked) { - SetAlarm.popAlarmSetToast( - AlarmClock.this, hour, minutes, daysOfWeek); + SetAlarm.popAlarmSetToast(AlarmClock.this, + alarm.hour, alarm.minutes, alarm.daysOfWeek); } } }); - DigitalClock digitalClock = (DigitalClock)view.findViewById(R.id.digitalClock); - if (Log.LOGV) Log.v("bindView " + cursor.getPosition() + " " + id + " " + hour + - ":" + minutes + " " + daysOfWeek.toString(context, true) + " dc " + digitalClock); - - digitalClock.setOnClickListener(new OnClickListener() { - public void onClick(View v) { - if (true) { - Intent intent = new Intent(AlarmClock.this, SetAlarm.class); - intent.putExtra(Alarms.ID, id); - startActivity(intent); - } else { - // TESTING: immediately pop alarm - Intent fireAlarm = new Intent(AlarmClock.this, AlarmAlert.class); - fireAlarm.putExtra(Alarms.ID, id); - fireAlarm.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(fireAlarm); - } - } - }); + DigitalClock digitalClock = + (DigitalClock) view.findViewById(R.id.digitalClock); // set the alarm text final Calendar c = Calendar.getInstance(); - c.set(Calendar.HOUR_OF_DAY, hour); - c.set(Calendar.MINUTE, minutes); + c.set(Calendar.HOUR_OF_DAY, alarm.hour); + c.set(Calendar.MINUTE, alarm.minutes); digitalClock.updateTime(c); // Set the repeat text or leave it blank if it does not repeat. - TextView daysOfWeekView = (TextView) digitalClock.findViewById(R.id.daysOfWeek); + TextView daysOfWeekView = + (TextView) digitalClock.findViewById(R.id.daysOfWeek); final String daysOfWeekStr = - daysOfWeek.toString(AlarmClock.this, false); + alarm.daysOfWeek.toString(AlarmClock.this, false); if (daysOfWeekStr != null && daysOfWeekStr.length() != 0) { daysOfWeekView.setText(daysOfWeekStr); daysOfWeekView.setVisibility(View.VISIBLE); @@ -166,39 +147,52 @@ public void onClick(View v) { // Display the label TextView labelView = (TextView) digitalClock.findViewById(R.id.label); - if (label != null && label.length() != 0) { - labelView.setText(label); + if (alarm.label != null && alarm.label.length() != 0) { + labelView.setText(alarm.label); + labelView.setVisibility(View.VISIBLE); } else { - labelView.setText(R.string.default_label); + labelView.setVisibility(View.GONE); } - - // Build context menu - digitalClock.setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() { - public void onCreateContextMenu(ContextMenu menu, View view, - ContextMenuInfo menuInfo) { - menu.setHeaderTitle(Alarms.formatTime(AlarmClock.this, c)); - MenuItem deleteAlarmItem = menu.add(0, id, 0, R.string.delete_alarm); - } - }); } }; @Override public boolean onContextItemSelected(final MenuItem item) { - // Confirm that the alarm will be deleted. - new AlertDialog.Builder(this) - .setTitle(getString(R.string.delete_alarm)) - .setMessage(getString(R.string.delete_alarm_confirm)) - .setPositiveButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface d, int w) { - Alarms.deleteAlarm(AlarmClock.this, - item.getItemId()); - } - }) - .setNegativeButton(android.R.string.cancel, null) - .show(); - return true; + final AdapterContextMenuInfo info = + (AdapterContextMenuInfo) item.getMenuInfo(); + final int id = (int) info.id; + switch (item.getItemId()) { + case R.id.delete_alarm: + // Confirm that the alarm will be deleted. + new AlertDialog.Builder(this) + .setTitle(getString(R.string.delete_alarm)) + .setMessage(getString(R.string.delete_alarm_confirm)) + .setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface d, + int w) { + Alarms.deleteAlarm(AlarmClock.this, id); + } + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + return true; + + case R.id.enable_alarm: + final Cursor c = (Cursor) mAlarmsList.getAdapter() + .getItem(info.position); + final Alarm alarm = new Alarm(c); + Alarms.enableAlarm(this, alarm.id, !alarm.enabled); + if (!alarm.enabled) { + SetAlarm.popAlarmSetToast(this, alarm.hour, alarm.minutes, + alarm.daysOfWeek); + } + return true; + + default: + break; + } + return super.onContextItemSelected(item); } @Override @@ -209,48 +203,38 @@ protected void onCreate(Bundle icicle) { mAm = ampm[0]; mPm = ampm[1]; - // sanity check -- no database, no clock - if (getContentResolver() == null) { - new AlertDialog.Builder(this) - .setTitle(getString(R.string.error)) - .setMessage(getString(R.string.dberror)) - .setPositiveButton( - android.R.string.ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - finish(); - } - }) - .setOnCancelListener( - new DialogInterface.OnCancelListener() { - public void onCancel(DialogInterface dialog) { - finish(); - }}) - .setIcon(android.R.drawable.ic_dialog_alert) - .create().show(); - return; - } - - setContentView(R.layout.alarm_clock); mFactory = LayoutInflater.from(this); mPrefs = getSharedPreferences(PREFERENCES, 0); - mCursor = Alarms.getAlarmsCursor(getContentResolver()); + + updateLayout(); + setClockVisibility(mPrefs.getBoolean(PREF_SHOW_CLOCK, true)); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + updateLayout(); + inflateClock(); + } + + private void updateLayout() { + setContentView(R.layout.alarm_clock); mAlarmsList = (ListView) findViewById(R.id.alarms_list); mAlarmsList.setAdapter(new AlarmTimeAdapter(this, mCursor)); mAlarmsList.setVerticalScrollBarEnabled(true); - mAlarmsList.setItemsCanFocus(true); + mAlarmsList.setOnItemClickListener(this); + mAlarmsList.setOnCreateContextMenuListener(this); mClockLayout = (ViewGroup) findViewById(R.id.clock_layout); mClockLayout.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { - final Intent intent = new Intent(AlarmClock.this, ClockPicker.class); + final Intent intent = + new Intent(AlarmClock.this, ClockPicker.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } }); - - setClockVisibility(mPrefs.getBoolean(PREF_SHOW_CLOCK, true)); } @Override @@ -259,10 +243,11 @@ protected void onResume() { int face = mPrefs.getInt(PREF_CLOCK_FACE, 0); if (mFace != face) { - if (face < 0 || face >= AlarmClock.CLOCKS.length) + if (face < 0 || face >= AlarmClock.CLOCKS.length) { mFace = 0; - else + } else { mFace = face; + } inflateClock(); } } @@ -301,6 +286,45 @@ public boolean onCreateOptionsMenu(Menu menu) { return super.onCreateOptionsMenu(menu); } + @Override + public void onCreateContextMenu(ContextMenu menu, View view, + ContextMenuInfo menuInfo) { + // Inflate the menu from xml. + getMenuInflater().inflate(R.menu.context_menu, menu); + + // Use the current item to create a custom view for the header. + final AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; + final Cursor c = + (Cursor) mAlarmsList.getAdapter().getItem((int) info.position); + final Alarm alarm = new Alarm(c); + + // Construct the Calendar to compute the time. + final Calendar cal = Calendar.getInstance(); + cal.set(Calendar.HOUR_OF_DAY, alarm.hour); + cal.set(Calendar.MINUTE, alarm.minutes); + final String time = Alarms.formatTime(this, cal); + + // Inflate the custom view and set each TextView's text. + final View v = mFactory.inflate(R.layout.context_menu_header, null); + TextView textView = (TextView) v.findViewById(R.id.header_time); + textView.setText(time); + textView = (TextView) v.findViewById(R.id.header_label); + textView.setText(alarm.label); + + // Set the custom view on the menu. + menu.setHeaderView(v); + // Change the text to "disable" if the alarm is already enabled. + if (alarm.enabled) { + menu.findItem(R.id.enable_alarm).setTitle(R.string.disable_alarm); + } + } + + public void onItemClick(AdapterView parent, View v, int pos, long id) { + Intent intent = new Intent(this, SetAlarm.class); + intent.putExtra(Alarms.ALARM_ID, (int) id); + startActivity(intent); + } + /** * Only allow user to add a new alarm if there are fewer than * MAX_ALARM_COUNT @@ -327,7 +351,7 @@ public boolean onOptionsItemSelected(MenuItem item) { Log.v("In AlarmClock, new alarm id = " + newId); } Intent intent = new Intent(this, SetAlarm.class); - intent.putExtra(Alarms.ID, newId); + intent.putExtra(Alarms.ALARM_ID, newId); startActivity(intent); return true; diff --git a/src/com/android/alarmclock/AlarmInitReceiver.java b/src/com/android/alarmclock/AlarmInitReceiver.java index 77549b0..8657e03 100644 --- a/src/com/android/alarmclock/AlarmInitReceiver.java +++ b/src/com/android/alarmclock/AlarmInitReceiver.java @@ -36,7 +36,7 @@ public void onReceive(Context context, Intent intent) { return; } if (action.equals(Intent.ACTION_BOOT_COMPLETED)) { - Alarms.disableSnoozeAlert(context); + Alarms.saveSnoozeAlert(context, -1, -1); Alarms.disableExpiredAlarms(context); } Alarms.setNextAlert(context); diff --git a/src/com/android/alarmclock/AlarmKlaxon.java b/src/com/android/alarmclock/AlarmKlaxon.java index 95bb0e2..7205db6 100644 --- a/src/com/android/alarmclock/AlarmKlaxon.java +++ b/src/com/android/alarmclock/AlarmKlaxon.java @@ -16,47 +16,45 @@ package com.android.alarmclock; -import android.content.ContentResolver; +import android.app.Service; import android.content.Context; +import android.content.Intent; import android.content.res.AssetFileDescriptor; import android.content.res.Resources; import android.media.AudioManager; import android.media.MediaPlayer; import android.media.MediaPlayer.OnErrorListener; +import android.media.RingtoneManager; import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.os.IBinder; import android.os.Message; import android.os.Vibrator; +import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; /** - * Manages alarms and vibe. Singleton, so it can be initiated in - * AlarmReceiver and shut down in the AlarmAlert activity + * Manages alarms and vibe. Runs as a service so that it can continue to play + * if another activity overrides the AlarmAlert dialog. */ -class AlarmKlaxon implements Alarms.AlarmSettings { - - interface KillerCallback { - public void onKilled(); - } +public class AlarmKlaxon extends Service { /** Play alarm up to 10 minutes before silencing */ - final static int ALARM_TIMEOUT_SECONDS = 10 * 60; + private static final int ALARM_TIMEOUT_SECONDS = 10 * 60; private static final long[] sVibratePattern = new long[] { 500, 500 }; - private int mAlarmId; - private String mAlert; - private Alarms.DaysOfWeek mDaysOfWeek; - private boolean mVibrate; private boolean mPlaying = false; private Vibrator mVibrator; private MediaPlayer mMediaPlayer; - private KillerCallback mKillerCallback; + private Alarm mCurrentAlarm; + private long mStartTime; + private TelephonyManager mTelephonyManager; + private int mInitialCallState; // Internal messages private static final int KILLER = 1000; - private static final int PLAY = 1001; private Handler mHandler = new Handler() { public void handleMessage(Message msg) { switch (msg.what) { @@ -64,103 +62,167 @@ public void handleMessage(Message msg) { if (Log.LOGV) { Log.v("*********** Alarm killer triggered ***********"); } - if (mKillerCallback != null) { - mKillerCallback.onKilled(); - } - break; - case PLAY: - play((Context) msg.obj, msg.arg1); + sendKillBroadcast((Alarm) msg.obj); + stopSelf(); break; } } }; - AlarmKlaxon() { + private PhoneStateListener mPhoneStateListener = new PhoneStateListener() { + @Override + public void onCallStateChanged(int state, String ignored) { + // The user might already be in a call when the alarm fires. When + // we register onCallStateChanged, we get the initial in-call state + // which kills the alarm. Check against the initial call state so + // we don't kill the alarm during a call. + if (state != TelephonyManager.CALL_STATE_IDLE + && state != mInitialCallState) { + sendKillBroadcast(mCurrentAlarm); + stopSelf(); + } + } + }; + + @Override + public void onCreate() { mVibrator = new Vibrator(); + // Listen for incoming calls to kill the alarm. + mTelephonyManager = + (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); + mTelephonyManager.listen( + mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); + AlarmAlertWakeLock.acquireCpuWakeLock(this); } - public void reportAlarm( - int idx, boolean enabled, int hour, int minutes, - Alarms.DaysOfWeek daysOfWeek, boolean vibrate, String message, - String alert) { - if (Log.LOGV) Log.v("AlarmKlaxon.reportAlarm: " + idx + " " + hour + - " " + minutes + " dow " + daysOfWeek); - mAlert = alert; - mDaysOfWeek = daysOfWeek; - mVibrate = vibrate; + @Override + public void onDestroy() { + stop(); + // Stop listening for incoming calls. + mTelephonyManager.listen(mPhoneStateListener, 0); + AlarmAlertWakeLock.releaseCpuLock(); } - public void postPlay(final Context context, final int alarmId) { - mHandler.sendMessage(mHandler.obtainMessage(PLAY, alarmId, 0, context)); + @Override + public IBinder onBind(Intent intent) { + return null; } - // Volume suggested by media team for in-call alarms. - private static final float IN_CALL_VOLUME = 0.125f; + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + // No intent, tell the system not to restart us. + if (intent == null) { + stopSelf(); + return START_NOT_STICKY; + } - private void play(Context context, int alarmId) { - ContentResolver contentResolver = context.getContentResolver(); + final Alarm alarm = intent.getParcelableExtra( + Alarms.ALARM_INTENT_EXTRA); - if (mPlaying) stop(context, false); + if (alarm == null) { + Log.v("AlarmKlaxon failed to parse the alarm from the intent"); + stopSelf(); + return START_NOT_STICKY; + } - mAlarmId = alarmId; + if (mCurrentAlarm != null) { + sendKillBroadcast(mCurrentAlarm); + } - /* this will call reportAlarm() callback */ - Alarms.getAlarm(contentResolver, this, mAlarmId); + play(alarm); + mCurrentAlarm = alarm; + // Record the initial call state here so that the new alarm has the + // newest state. + mInitialCallState = mTelephonyManager.getCallState(); - if (Log.LOGV) Log.v("AlarmKlaxon.play() " + mAlarmId + " alert " + mAlert); + return START_STICKY; + } - // TODO: Reuse mMediaPlayer instead of creating a new one and/or use - // RingtoneManager. - mMediaPlayer = new MediaPlayer(); - mMediaPlayer.setOnErrorListener(new OnErrorListener() { - public boolean onError(MediaPlayer mp, int what, int extra) { - Log.e("Error occurred while playing audio."); - mp.stop(); - mp.release(); - mMediaPlayer = null; - return true; - } - }); - - try { - TelephonyManager tm = (TelephonyManager) context.getSystemService( - Context.TELEPHONY_SERVICE); - // Check if we are in a call. If we are, use the in-call alarm - // resource at a low volume to not disrupt the call. - if (tm.getCallState() != TelephonyManager.CALL_STATE_IDLE) { - Log.v("Using the in-call alarm"); - mMediaPlayer.setVolume(IN_CALL_VOLUME, IN_CALL_VOLUME); - setDataSourceFromResource(context.getResources(), - mMediaPlayer, R.raw.in_call_alarm); - } else { - mMediaPlayer.setDataSource(context, Uri.parse(mAlert)); + private void sendKillBroadcast(Alarm alarm) { + long millis = System.currentTimeMillis() - mStartTime; + int minutes = (int) Math.round(millis / 60000.0); + Intent alarmKilled = new Intent(Alarms.ALARM_KILLED); + alarmKilled.putExtra(Alarms.ALARM_INTENT_EXTRA, alarm); + alarmKilled.putExtra(Alarms.ALARM_KILLED_TIMEOUT, minutes); + sendBroadcast(alarmKilled); + } + + // Volume suggested by media team for in-call alarms. + private static final float IN_CALL_VOLUME = 0.125f; + + private void play(Alarm alarm) { + // stop() checks to see if we are already playing. + stop(); + + if (Log.LOGV) { + Log.v("AlarmKlaxon.play() " + alarm.id + " alert " + alarm.alert); + } + + if (!alarm.silent) { + Uri alert = alarm.alert; + // Fall back on the default alarm if the database does not have an + // alarm stored. + if (alert == null) { + alert = RingtoneManager.getDefaultUri( + RingtoneManager.TYPE_ALARM); + if (Log.LOGV) { + Log.v("Using default alarm: " + alert.toString()); + } } - startAlarm(mMediaPlayer); - } catch (Exception ex) { - Log.v("Using the fallback ringtone"); - // The alert may be on the sd card which could be busy right now. - // Use the fallback ringtone. + + // TODO: Reuse mMediaPlayer instead of creating a new one and/or use + // RingtoneManager. + mMediaPlayer = new MediaPlayer(); + mMediaPlayer.setOnErrorListener(new OnErrorListener() { + public boolean onError(MediaPlayer mp, int what, int extra) { + Log.e("Error occurred while playing audio."); + mp.stop(); + mp.release(); + mMediaPlayer = null; + return true; + } + }); + try { - // Must reset the media player to clear the error state. - mMediaPlayer.reset(); - setDataSourceFromResource(context.getResources(), mMediaPlayer, - com.android.internal.R.raw.fallbackring); + // Check if we are in a call. If we are, use the in-call alarm + // resource at a low volume to not disrupt the call. + if (mTelephonyManager.getCallState() + != TelephonyManager.CALL_STATE_IDLE) { + Log.v("Using the in-call alarm"); + mMediaPlayer.setVolume(IN_CALL_VOLUME, IN_CALL_VOLUME); + setDataSourceFromResource(getResources(), mMediaPlayer, + R.raw.in_call_alarm); + } else { + mMediaPlayer.setDataSource(this, alert); + } startAlarm(mMediaPlayer); - } catch (Exception ex2) { - // At this point we just don't play anything. - Log.e("Failed to play fallback ringtone", ex2); + } catch (Exception ex) { + Log.v("Using the fallback ringtone"); + // The alert may be on the sd card which could be busy right + // now. Use the fallback ringtone. + try { + // Must reset the media player to clear the error state. + mMediaPlayer.reset(); + setDataSourceFromResource(getResources(), mMediaPlayer, + com.android.internal.R.raw.fallbackring); + startAlarm(mMediaPlayer); + } catch (Exception ex2) { + // At this point we just don't play anything. + Log.e("Failed to play fallback ringtone", ex2); + } } } /* Start the vibrator after everything is ok with the media player */ - if (mVibrate) { + if (alarm.vibrate) { mVibrator.vibrate(sVibratePattern, 0); } else { mVibrator.cancel(); } - enableKiller(); + enableKiller(alarm); mPlaying = true; + mStartTime = System.currentTimeMillis(); } // Do the common stuff when starting the alarm. @@ -187,8 +249,8 @@ private void setDataSourceFromResource(Resources resources, * Stops alarm audio and disables alarm if it not snoozed and not * repeating */ - public void stop(Context context, boolean snoozed) { - if (Log.LOGV) Log.v("AlarmKlaxon.stop() " + mAlarmId); + public void stop() { + if (Log.LOGV) Log.v("AlarmKlaxon.stop()"); if (mPlaying) { mPlaying = false; @@ -201,23 +263,10 @@ public void stop(Context context, boolean snoozed) { // Stop vibrator mVibrator.cancel(); - - /* disable alarm only if it is not set to repeat */ - if (!snoozed && ((mDaysOfWeek == null || !mDaysOfWeek.isRepeatSet()))) { - Alarms.enableAlarm(context, mAlarmId, false); - } } disableKiller(); } - /** - * This callback called when alarm killer times out unattended - * alarm - */ - public void setKillerCallback(KillerCallback killerCallback) { - mKillerCallback = killerCallback; - } - /** * Kills alarm audio after ALARM_TIMEOUT_SECONDS, so the alarm * won't run all day. @@ -225,8 +274,8 @@ public void setKillerCallback(KillerCallback killerCallback) { * This just cancels the audio, but leaves the notification * popped, so the user will know that the alarm tripped. */ - private void enableKiller() { - mHandler.sendMessageDelayed(mHandler.obtainMessage(KILLER), + private void enableKiller(Alarm alarm) { + mHandler.sendMessageDelayed(mHandler.obtainMessage(KILLER, alarm), 1000 * ALARM_TIMEOUT_SECONDS); } diff --git a/src/com/android/alarmclock/AlarmPreference.java b/src/com/android/alarmclock/AlarmPreference.java index 0fd4f89..cb0e3f5 100644 --- a/src/com/android/alarmclock/AlarmPreference.java +++ b/src/com/android/alarmclock/AlarmPreference.java @@ -17,38 +17,49 @@ package com.android.alarmclock; import android.content.Context; +import android.media.Ringtone; +import android.media.RingtoneManager; import android.net.Uri; import android.preference.RingtonePreference; import android.util.AttributeSet; +/** + * The RingtonePreference does not have a way to get/set the current ringtone so + * we override onSaveRingtone and onRestoreRingtone to get the same behavior. + */ public class AlarmPreference extends RingtonePreference { - public Uri mAlert; - private IRingtoneChangedListener mRingtoneChangedListener; - - public interface IRingtoneChangedListener { - public void onRingtoneChanged(Uri ringtoneUri); - }; + private Uri mAlert; public AlarmPreference(Context context, AttributeSet attrs) { super(context, attrs); } - public void setRingtoneChangedListener(IRingtoneChangedListener listener) { - mRingtoneChangedListener = listener; - } - @Override protected void onSaveRingtone(Uri ringtoneUri) { - if (ringtoneUri != null) { - mAlert = ringtoneUri; - if (mRingtoneChangedListener != null) { - mRingtoneChangedListener.onRingtoneChanged(ringtoneUri); - } - } + setAlert(ringtoneUri); } @Override protected Uri onRestoreRingtone() { return mAlert; } + + public void setAlert(Uri alert) { + mAlert = alert; + if (alert != null) { + final Ringtone r = RingtoneManager.getRingtone(getContext(), alert); + if (r != null) { + setSummary(r.getTitle(getContext())); + } + } else { + setSummary(R.string.silent_alarm_summary); + } + } + + public String getAlertString() { + if (mAlert != null) { + return mAlert.toString(); + } + return Alarms.ALARM_ALERT_SILENT; + } } diff --git a/src/com/android/alarmclock/AlarmProvider.java b/src/com/android/alarmclock/AlarmProvider.java index 74fdd2e..5849a38 100644 --- a/src/com/android/alarmclock/AlarmProvider.java +++ b/src/com/android/alarmclock/AlarmProvider.java @@ -173,38 +173,38 @@ public Uri insert(Uri url, ContentValues initialValues) { else values = new ContentValues(); - if (!values.containsKey(Alarms.AlarmColumns.HOUR)) - values.put(Alarms.AlarmColumns.HOUR, 0); + if (!values.containsKey(Alarm.Columns.HOUR)) + values.put(Alarm.Columns.HOUR, 0); - if (!values.containsKey(Alarms.AlarmColumns.MINUTES)) - values.put(Alarms.AlarmColumns.MINUTES, 0); + if (!values.containsKey(Alarm.Columns.MINUTES)) + values.put(Alarm.Columns.MINUTES, 0); - if (!values.containsKey(Alarms.AlarmColumns.DAYS_OF_WEEK)) - values.put(Alarms.AlarmColumns.DAYS_OF_WEEK, 0); + if (!values.containsKey(Alarm.Columns.DAYS_OF_WEEK)) + values.put(Alarm.Columns.DAYS_OF_WEEK, 0); - if (!values.containsKey(Alarms.AlarmColumns.ALARM_TIME)) - values.put(Alarms.AlarmColumns.ALARM_TIME, 0); + if (!values.containsKey(Alarm.Columns.ALARM_TIME)) + values.put(Alarm.Columns.ALARM_TIME, 0); - if (!values.containsKey(Alarms.AlarmColumns.ENABLED)) - values.put(Alarms.AlarmColumns.ENABLED, 0); + if (!values.containsKey(Alarm.Columns.ENABLED)) + values.put(Alarm.Columns.ENABLED, 0); - if (!values.containsKey(Alarms.AlarmColumns.VIBRATE)) - values.put(Alarms.AlarmColumns.VIBRATE, 1); + if (!values.containsKey(Alarm.Columns.VIBRATE)) + values.put(Alarm.Columns.VIBRATE, 1); - if (!values.containsKey(Alarms.AlarmColumns.MESSAGE)) - values.put(Alarms.AlarmColumns.MESSAGE, ""); + if (!values.containsKey(Alarm.Columns.MESSAGE)) + values.put(Alarm.Columns.MESSAGE, ""); - if (!values.containsKey(Alarms.AlarmColumns.ALERT)) - values.put(Alarms.AlarmColumns.ALERT, ""); + if (!values.containsKey(Alarm.Columns.ALERT)) + values.put(Alarm.Columns.ALERT, ""); SQLiteDatabase db = mOpenHelper.getWritableDatabase(); - long rowId = db.insert("alarms", Alarms.AlarmColumns.MESSAGE, values); + long rowId = db.insert("alarms", Alarm.Columns.MESSAGE, values); if (rowId < 0) { throw new SQLException("Failed to insert row into " + url); } if (Log.LOGV) Log.v("Added alarm rowId = " + rowId); - Uri newUrl = ContentUris.withAppendedId(Alarms.AlarmColumns.CONTENT_URI, rowId); + Uri newUrl = ContentUris.withAppendedId(Alarm.Columns.CONTENT_URI, rowId); getContext().getContentResolver().notifyChange(newUrl, null); return newUrl; } diff --git a/src/com/android/alarmclock/AlarmReceiver.java b/src/com/android/alarmclock/AlarmReceiver.java index 218db98..97374ef 100644 --- a/src/com/android/alarmclock/AlarmReceiver.java +++ b/src/com/android/alarmclock/AlarmReceiver.java @@ -17,9 +17,18 @@ package com.android.alarmclock; import android.app.KeyguardManager; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.BroadcastReceiver; +import android.database.Cursor; +import android.os.Parcel; + +import java.text.SimpleDateFormat; +import java.util.Date; /** * Glue class: connects AlarmAlert IntentReceiver to AlarmAlert @@ -33,30 +42,63 @@ public class AlarmReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - long now = System.currentTimeMillis(); - int id = intent.getIntExtra(Alarms.ID, 0); - long setFor = intent.getLongExtra(Alarms.TIME, 0); + // Take care of the easy intents first. + if (Alarms.CLEAR_NOTIFICATION.equals(intent.getAction())) { + // If this is the "Clear All Notifications" intent, stop the alarm + // service and return. + context.stopService(new Intent(Alarms.ALARM_ALERT_ACTION)); + return; + } else if (Alarms.ALARM_KILLED.equals(intent.getAction())) { + // The alarm has been killed, update the notification + updateNotification(context, (Alarm) + intent.getParcelableExtra(Alarms.ALARM_INTENT_EXTRA), + intent.getIntExtra(Alarms.ALARM_KILLED_TIMEOUT, -1)); + return; + } else if (Alarms.CANCEL_SNOOZE.equals(intent.getAction())) { + Alarms.saveSnoozeAlert(context, -1, -1); + return; + } - /* FIXME Intentionally verbose: always log this until we've - fully debugged the app failing to start up */ - Log.v("AlarmReceiver.onReceive() id " + id + " setFor " + setFor + - " now " + now); + Alarm alarm = null; + // Grab the alarm from the intent. Since the remote AlarmManagerService + // fills in the Intent to add some extra data, it must unparcel the + // Alarm object. It throws a ClassNotFoundException when unparcelling. + // To avoid this, do the marshalling ourselves. + final byte[] data = intent.getByteArrayExtra(Alarms.ALARM_RAW_DATA); + if (data != null) { + Parcel in = Parcel.obtain(); + in.unmarshall(data, 0, data.length); + in.setDataPosition(0); + alarm = Alarm.CREATOR.createFromParcel(in); + } - if (now > setFor + STALE_WINDOW * 1000) { - if (Log.LOGV) Log.v("AlarmReceiver ignoring stale alarm intent id" - + id + " setFor " + setFor + " now " + now); + if (alarm == null) { + Log.v("AlarmReceiver failed to parse the alarm from the intent"); return; } - // Wake the device and stay awake until the AlarmAlert intent is - // handled. Also acquire the screen lock so that if the AlarmAlert - // activity is paused, it will be resumed. + // Intentionally verbose: always log the alarm time to provide useful + // information in bug reports. + long now = System.currentTimeMillis(); + SimpleDateFormat format = + new SimpleDateFormat("HH:mm:ss.SSS aaa"); + Log.v("AlarmReceiver.onReceive() id " + alarm.id + " setFor " + + format.format(new Date(alarm.time))); + + if (now > alarm.time + STALE_WINDOW * 1000) { + if (Log.LOGV) { + Log.v("AlarmReceiver ignoring stale alarm"); + } + return; + } + + // Maintain a cpu wake lock until the AlarmAlert and AlarmKlaxon can + // pick it up. AlarmAlertWakeLock.acquireCpuWakeLock(context); - AlarmAlertWakeLock.acquireScreenWakeLock(context); /* Close dialogs and window shade */ - Intent i = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); - context.sendBroadcast(i); + Intent closeDialogs = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); + context.sendBroadcast(closeDialogs); // Decide which activity to start based on the state of the keyguard. Class c = AlarmAlert.class; @@ -69,10 +111,92 @@ public void onReceive(Context context, Intent intent) { /* launch UI, explicitly stating that this is not due to user action * so that the current app's notification management is not disturbed */ - Intent fireAlarm = new Intent(context, c); - fireAlarm.putExtra(Alarms.ID, id); - fireAlarm.putExtra(Alarms.LABEL, intent.getStringExtra(Alarms.LABEL)); - fireAlarm.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NO_USER_ACTION); - context.startActivity(fireAlarm); - } + Intent alarmAlert = new Intent(context, c); + alarmAlert.putExtra(Alarms.ALARM_INTENT_EXTRA, alarm); + alarmAlert.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_NO_USER_ACTION); + context.startActivity(alarmAlert); + + // Disable the snooze alert if this alarm is the snooze. + Alarms.disableSnoozeAlert(context, alarm.id); + // Disable this alarm if it does not repeat. + if (!alarm.daysOfWeek.isRepeatSet()) { + Alarms.enableAlarm(context, alarm.id, false); + } else { + // Enable the next alert if there is one. The above call to + // enableAlarm will call setNextAlert so avoid calling it twice. + Alarms.setNextAlert(context); + } + + // Play the alarm alert and vibrate the device. + Intent playAlarm = new Intent(Alarms.ALARM_ALERT_ACTION); + playAlarm.putExtra(Alarms.ALARM_INTENT_EXTRA, alarm); + context.startService(playAlarm); + + // Trigger a notification that, when clicked, will show the alarm alert + // dialog. No need to check for fullscreen since this will always be + // launched from a user action. + Intent notify = new Intent(context, AlarmAlert.class); + notify.putExtra(Alarms.ALARM_INTENT_EXTRA, alarm); + PendingIntent pendingNotify = PendingIntent.getActivity(context, + alarm.id, notify, 0); + + // Use the alarm's label or the default label as the ticker text and + // main text of the notification. + String label = alarm.getLabelOrDefault(context); + Notification n = new Notification(R.drawable.stat_notify_alarm, + label, alarm.time); + n.setLatestEventInfo(context, label, + context.getString(R.string.alarm_notify_text), + pendingNotify); + n.flags |= Notification.FLAG_SHOW_LIGHTS; + n.ledARGB = 0xFF00FF00; + n.ledOnMS = 500; + n.ledOffMS = 500; + + // Set the deleteIntent for when the user clicks "Clear All + // Notifications" + Intent clearAll = new Intent(context, AlarmReceiver.class); + clearAll.setAction(Alarms.CLEAR_NOTIFICATION); + n.deleteIntent = PendingIntent.getBroadcast(context, 0, clearAll, 0); + + // Send the notification using the alarm id to easily identify the + // correct notification. + NotificationManager nm = getNotificationManager(context); + nm.notify(alarm.id, n); + } + + private NotificationManager getNotificationManager(Context context) { + return (NotificationManager) + context.getSystemService(Context.NOTIFICATION_SERVICE); + } + + private void updateNotification(Context context, Alarm alarm, int timeout) { + NotificationManager nm = getNotificationManager(context); + + // If the alarm is null, just cancel the notification. + if (alarm == null) { + if (Log.LOGV) { + Log.v("Cannot update notification for killer callback"); + } + return; + } + + // Launch SetAlarm when clicked. + Intent viewAlarm = new Intent(context, SetAlarm.class); + viewAlarm.putExtra(Alarms.ALARM_ID, alarm.id); + PendingIntent intent = + PendingIntent.getActivity(context, alarm.id, viewAlarm, 0); + + // Update the notification to indicate that the alert has been + // silenced. + String label = alarm.getLabelOrDefault(context); + Notification n = new Notification(R.drawable.stat_notify_alarm, + label, alarm.time); + n.setLatestEventInfo(context, label, + context.getString(R.string.alarm_alert_alert_silenced, timeout), + intent); + n.flags |= Notification.FLAG_AUTO_CANCEL; + nm.notify(alarm.id, n); + } } diff --git a/src/com/android/alarmclock/Alarms.java b/src/com/android/alarmclock/Alarms.java index 8e71836..63a67d7 100644 --- a/src/com/android/alarmclock/Alarms.java +++ b/src/com/android/alarmclock/Alarms.java @@ -17,8 +17,6 @@ package com.android.alarmclock; import android.app.AlarmManager; -import android.app.Notification; -import android.app.NotificationManager; import android.app.PendingIntent; import android.content.ContentResolver; import android.content.ContentValues; @@ -28,7 +26,7 @@ import android.content.SharedPreferences; import android.database.Cursor; import android.net.Uri; -import android.provider.BaseColumns; +import android.os.Parcel; import android.provider.Settings; import android.text.format.DateFormat; @@ -40,354 +38,112 @@ */ public class Alarms { - final static String ALARM_ALERT_ACTION = "com.android.alarmclock.ALARM_ALERT"; - final static String ID = "alarm_id"; - final static String TIME = "alarm_time"; - final static String LABEL = "alarm_label"; + // This action triggers the AlarmReceiver as well as the AlarmKlaxon. It + // is a public action used in the manifest for receiving Alarm broadcasts + // from the alarm manager. + public static final String ALARM_ALERT_ACTION = "com.android.alarmclock.ALARM_ALERT"; - final static String PREF_SNOOZE_ID = "snooze_id"; - final static String PREF_SNOOZE_TIME = "snooze_time"; - final static String PREF_SNOOZE_LABEL = "snooze_label"; - - private final static String DM12 = "E h:mm aa"; - private final static String DM24 = "E kk:mm"; - - private final static String M12 = "h:mm aa"; - // Shared with DigitalClock - final static String M24 = "kk:mm"; - - /** - * Mapping from days in this application (where Monday is 0) to - * days in DateFormatSymbols (where Monday is 2). - */ - private static int[] DAY_MAP = new int[] { - Calendar.MONDAY, - Calendar.TUESDAY, - Calendar.WEDNESDAY, - Calendar.THURSDAY, - Calendar.FRIDAY, - Calendar.SATURDAY, - Calendar.SUNDAY, - }; - - static class DaysOfWeek { - - int mDays; - - /** - * Days of week coded as single int, convenient for DB - * storage: - * - * 0x00: no day - * 0x01: Monday - * 0x02: Tuesday - * 0x04: Wednesday - * 0x08: Thursday - * 0x10: Friday - * 0x20: Saturday - * 0x40: Sunday - */ - DaysOfWeek() { - this(0); - } - - DaysOfWeek(int days) { - mDays = days; - } + // This is a private action used when the user clears all notifications. + public static final String CLEAR_NOTIFICATION = "clear_notification"; - public String toString(Context context, boolean showNever) { - StringBuilder ret = new StringBuilder(); + // This is a private action used by the AlarmKlaxon to update the UI to + // show the alarm has been killed. + public static final String ALARM_KILLED = "alarm_killed"; - /* no days */ - if (mDays == 0) return showNever ? context.getText( - R.string.never).toString() : ""; + // Extra in the ALARM_KILLED intent to indicate to the user how long the + // alarm played before being killed. + public static final String ALARM_KILLED_TIMEOUT = "alarm_killed_timeout"; - /* every day */ - if (mDays == 0x7f) { - return context.getText(R.string.every_day).toString(); - } - - /* count selected days */ - int dayCount = 0, days = mDays; - while (days > 0) { - if ((days & 1) == 1) dayCount++; - days >>= 1; - } - - /* short or long form? */ - DateFormatSymbols dfs = new DateFormatSymbols(); - String[] dayList = (dayCount > 1) ? - dfs.getShortWeekdays() : - dfs.getWeekdays(); - - /* selected days */ - for (int i = 0; i < 7; i++) { - if ((mDays & (1 << i)) != 0) { - ret.append(dayList[DAY_MAP[i]]); - dayCount -= 1; - if (dayCount > 0) ret.append( - context.getText(R.string.day_concat)); - } - } - return ret.toString(); - } - - /** - * @param day Mon=0 ... Sun=6 - * @return true if given day is set - */ - public boolean isSet(int day) { - return ((mDays & (1 << day)) > 0); - } + // This string is used to indicate a silent alarm in the db. + public static final String ALARM_ALERT_SILENT = "silent"; - public void set(int day, boolean set) { - if (set) { - mDays |= (1 << day); - } else { - mDays &= ~(1 << day); - } - } + // This intent is sent from the notification when the user cancels the + // snooze alert. + public static final String CANCEL_SNOOZE = "cancel_snooze"; - public void set(DaysOfWeek dow) { - mDays = dow.mDays; - } + // This string is used when passing an Alarm object through an intent. + public static final String ALARM_INTENT_EXTRA = "intent.extra.alarm"; - public int getCoded() { - return mDays; - } + // This extra is the raw Alarm object data. It is used in the + // AlarmManagerService to avoid a ClassNotFoundException when filling in + // the Intent extras. + public static final String ALARM_RAW_DATA = "intent.extra.alarm_raw"; - public boolean equals(DaysOfWeek dow) { - return mDays == dow.mDays; - } + // This string is used to identify the alarm id passed to SetAlarm from the + // list of alarms. + public static final String ALARM_ID = "alarm_id"; - // Returns days of week encoded in an array of booleans. - public boolean[] getBooleanArray() { - boolean[] ret = new boolean[7]; - for (int i = 0; i < 7; i++) { - ret[i] = isSet(i); - } - return ret; - } - - public void setCoded(int days) { - mDays = days; - } - - /** - * @return true if alarm is set to repeat - */ - public boolean isRepeatSet() { - return mDays != 0; - } - - /** - * @return true if alarm is set to repeat every day - */ - public boolean isEveryDaySet() { - return mDays == 0x7f; - } - - - /** - * returns number of days from today until next alarm - * @param c must be set to today - */ - public int getNextAlarm(Calendar c) { - if (mDays == 0) return -1; - int today = (c.get(Calendar.DAY_OF_WEEK) + 5) % 7; - - int day, dayCount; - for (dayCount = 0; dayCount < 7; dayCount++) { - day = (today + dayCount) % 7; - if ((mDays & (1 << day)) > 0) { - break; - } - } - return dayCount; - } - } + final static String PREF_SNOOZE_ID = "snooze_id"; + final static String PREF_SNOOZE_TIME = "snooze_time"; - public static class AlarmColumns implements BaseColumns { - - /** - * The content:// style URL for this table - */ - public static final Uri CONTENT_URI = - Uri.parse("content://com.android.alarmclock/alarm"); - - public static final String _ID = "_id"; - - /** - * The default sort order for this table - */ - public static final String DEFAULT_SORT_ORDER = "_id ASC"; - - /** - * Hour in 24-hour localtime 0 - 23. - *Type: INTEGER
- */ - public static final String HOUR = "hour"; - - /** - * Minutes in localtime 0 - 59 - *Type: INTEGER
- */ - public static final String MINUTES = "minutes"; - - /** - * Days of week coded as integer - *Type: INTEGER
- */ - public static final String DAYS_OF_WEEK = "daysofweek"; - - /** - * Alarm time in UTC milliseconds from the epoch. - *Type: INTEGER
- */ - public static final String ALARM_TIME = "alarmtime"; - - /** - * True if alarm is active - *Type: BOOLEAN
- */ - public static final String ENABLED = "enabled"; - - /** - * True if alarm should vibrate - *Type: BOOLEAN
- */ - public static final String VIBRATE = "vibrate"; - - /** - * Message to show when alarm triggers - * Note: not currently used - *Type: STRING
- */ - public static final String MESSAGE = "message"; - - /** - * Audio alert to play when alarm triggers - *Type: STRING
- */ - public static final String ALERT = "alert"; - - static final String[] ALARM_QUERY_COLUMNS = { - _ID, HOUR, MINUTES, DAYS_OF_WEEK, ALARM_TIME, - ENABLED, VIBRATE, MESSAGE, ALERT}; - - /** - * These save calls to cursor.getColumnIndexOrThrow() - * THEY MUST BE KEPT IN SYNC WITH ABOVE QUERY COLUMNS - */ - public static final int ALARM_ID_INDEX = 0; - public static final int ALARM_HOUR_INDEX = 1; - public static final int ALARM_MINUTES_INDEX = 2; - public static final int ALARM_DAYS_OF_WEEK_INDEX = 3; - public static final int ALARM_TIME_INDEX = 4; - public static final int ALARM_ENABLED_INDEX = 5; - public static final int ALARM_VIBRATE_INDEX = 6; - public static final int ALARM_MESSAGE_INDEX = 7; - public static final int ALARM_ALERT_INDEX = 8; - } + private final static String DM12 = "E h:mm aa"; + private final static String DM24 = "E k:mm"; - /** - * getAlarm and getAlarms call this interface to report alarms in - * the database - */ - static interface AlarmSettings { - void reportAlarm( - int idx, boolean enabled, int hour, int minutes, - DaysOfWeek daysOfWeek, boolean vibrate, String message, - String alert); - } + private final static String M12 = "h:mm aa"; + // Shared with DigitalClock + final static String M24 = "kk:mm"; /** * Creates a new Alarm. */ - public synchronized static Uri addAlarm(ContentResolver contentResolver) { + public static Uri addAlarm(ContentResolver contentResolver) { ContentValues values = new ContentValues(); - values.put(Alarms.AlarmColumns.HOUR, 8); - return contentResolver.insert(AlarmColumns.CONTENT_URI, values); + values.put(Alarm.Columns.HOUR, 8); + return contentResolver.insert(Alarm.Columns.CONTENT_URI, values); } /** * Removes an existing Alarm. If this alarm is snoozing, disables * snooze. Sets next alert. */ - public synchronized static void deleteAlarm( + public static void deleteAlarm( Context context, int alarmId) { ContentResolver contentResolver = context.getContentResolver(); /* If alarm is snoozing, lose it */ - int snoozeId = getSnoozeAlarmId(context); - if (snoozeId == alarmId) disableSnoozeAlert(context); + disableSnoozeAlert(context, alarmId); - Uri uri = ContentUris.withAppendedId(AlarmColumns.CONTENT_URI, alarmId); - deleteAlarm(contentResolver, uri); + Uri uri = ContentUris.withAppendedId(Alarm.Columns.CONTENT_URI, alarmId); + contentResolver.delete(uri, "", null); setNextAlert(context); } - private synchronized static void deleteAlarm( - ContentResolver contentResolver, Uri uri) { - contentResolver.delete(uri, "", null); - } - /** * Queries all alarms * @return cursor over all alarms */ - public synchronized static Cursor getAlarmsCursor( - ContentResolver contentResolver) { + public static Cursor getAlarmsCursor(ContentResolver contentResolver) { return contentResolver.query( - AlarmColumns.CONTENT_URI, AlarmColumns.ALARM_QUERY_COLUMNS, - null, null, AlarmColumns.DEFAULT_SORT_ORDER); + Alarm.Columns.CONTENT_URI, Alarm.Columns.ALARM_QUERY_COLUMNS, + null, null, Alarm.Columns.DEFAULT_SORT_ORDER); } - /** - * Calls the AlarmSettings.reportAlarm interface on all alarms found in db. - */ - public synchronized static void getAlarms( - ContentResolver contentResolver, AlarmSettings alarmSettings) { - Cursor cursor = getAlarmsCursor(contentResolver); - getAlarms(alarmSettings, cursor); - cursor.close(); - } - - private synchronized static void getAlarms( - AlarmSettings alarmSettings, Cursor cur) { - if (cur.moveToFirst()) { - do { - // Get the field values - int id = cur.getInt(AlarmColumns.ALARM_ID_INDEX); - int hour = cur.getInt(AlarmColumns.ALARM_HOUR_INDEX); - int minutes = cur.getInt(AlarmColumns.ALARM_MINUTES_INDEX); - int daysOfWeek = cur.getInt(AlarmColumns.ALARM_DAYS_OF_WEEK_INDEX); - boolean enabled = cur.getInt(AlarmColumns.ALARM_ENABLED_INDEX) == 1 ? true : false; - boolean vibrate = cur.getInt(AlarmColumns.ALARM_VIBRATE_INDEX) == 1 ? true : false; - String message = cur.getString(AlarmColumns.ALARM_MESSAGE_INDEX); - String alert = cur.getString(AlarmColumns.ALARM_ALERT_INDEX); - alarmSettings.reportAlarm( - id, enabled, hour, minutes, new DaysOfWeek(daysOfWeek), - vibrate, message, alert); - } while (cur.moveToNext()); - } + // Private method to get a more limited set of alarms from the database. + private static Cursor getFilteredAlarmsCursor( + ContentResolver contentResolver) { + return contentResolver.query(Alarm.Columns.CONTENT_URI, + Alarm.Columns.ALARM_QUERY_COLUMNS, Alarm.Columns.WHERE_ENABLED, + null, null); } /** - * Calls the AlarmSettings.reportAlarm interface on alarm with given - * alarmId + * Return an Alarm object representing the alarm id in the database. + * Returns null if no alarm exists. */ - public synchronized static void getAlarm( - ContentResolver contentResolver, AlarmSettings alarmSetting, - int alarmId) { + public static Alarm getAlarm(ContentResolver contentResolver, int alarmId) { Cursor cursor = contentResolver.query( - ContentUris.withAppendedId(AlarmColumns.CONTENT_URI, alarmId), - AlarmColumns.ALARM_QUERY_COLUMNS, - null, null, AlarmColumns.DEFAULT_SORT_ORDER); - - getAlarms(alarmSetting, cursor); - cursor.close(); + ContentUris.withAppendedId(Alarm.Columns.CONTENT_URI, alarmId), + Alarm.Columns.ALARM_QUERY_COLUMNS, + null, null, null); + Alarm alarm = null; + if (cursor != null) { + if (cursor.moveToFirst()) { + alarm = new Alarm(cursor); + } + cursor.close(); + } + return alarm; } @@ -405,32 +161,35 @@ public synchronized static void getAlarm( * @param message corresponds to the MESSAGE column * @param alert corresponds to the ALERT column */ - public synchronized static void setAlarm( + public static void setAlarm( Context context, int id, boolean enabled, int hour, int minutes, - DaysOfWeek daysOfWeek, boolean vibrate, String message, + Alarm.DaysOfWeek daysOfWeek, boolean vibrate, String message, String alert) { ContentValues values = new ContentValues(8); ContentResolver resolver = context.getContentResolver(); - long time = calculateAlarm(hour, minutes, daysOfWeek).getTimeInMillis(); + // Set the alarm_time value if this alarm does not repeat. This will be + // used later to disable expired alarms. + long time = 0; + if (!daysOfWeek.isRepeatSet()) { + time = calculateAlarm(hour, minutes, daysOfWeek).getTimeInMillis(); + } if (Log.LOGV) Log.v( "** setAlarm * idx " + id + " hour " + hour + " minutes " + minutes + " enabled " + enabled + " time " + time); - values.put(AlarmColumns.ENABLED, enabled ? 1 : 0); - values.put(AlarmColumns.HOUR, hour); - values.put(AlarmColumns.MINUTES, minutes); - values.put(AlarmColumns.ALARM_TIME, time); - values.put(AlarmColumns.DAYS_OF_WEEK, daysOfWeek.getCoded()); - values.put(AlarmColumns.VIBRATE, vibrate); - values.put(AlarmColumns.MESSAGE, message); - values.put(AlarmColumns.ALERT, alert); - resolver.update(ContentUris.withAppendedId(AlarmColumns.CONTENT_URI, id), + values.put(Alarm.Columns.ENABLED, enabled ? 1 : 0); + values.put(Alarm.Columns.HOUR, hour); + values.put(Alarm.Columns.MINUTES, minutes); + values.put(Alarm.Columns.ALARM_TIME, time); + values.put(Alarm.Columns.DAYS_OF_WEEK, daysOfWeek.getCoded()); + values.put(Alarm.Columns.VIBRATE, vibrate); + values.put(Alarm.Columns.MESSAGE, message); + values.put(Alarm.Columns.ALERT, alert); + resolver.update(ContentUris.withAppendedId(Alarm.Columns.CONTENT_URI, id), values, null, null); - int aid = disableSnoozeAlert(context); - if (aid != -1 && aid != id) enableAlarmInternal(context, aid, false); setNextAlert(context); } @@ -441,103 +200,68 @@ public synchronized static void setAlarm( * @param enabled corresponds to the ENABLED column */ - public synchronized static void enableAlarm( + public static void enableAlarm( final Context context, final int id, boolean enabled) { - int aid = disableSnoozeAlert(context); - if (aid != -1 && aid != id) enableAlarmInternal(context, aid, false); enableAlarmInternal(context, id, enabled); setNextAlert(context); } - private synchronized static void enableAlarmInternal( - final Context context, final int id, boolean enabled) { - ContentResolver resolver = context.getContentResolver(); + private static void enableAlarmInternal(final Context context, + final int id, boolean enabled) { + enableAlarmInternal(context, getAlarm(context.getContentResolver(), id), + enabled); + } - class EnableAlarm implements AlarmSettings { - public int mHour; - public int mMinutes; - public DaysOfWeek mDaysOfWeek; - - public void reportAlarm( - int idx, boolean enabled, int hour, int minutes, - DaysOfWeek daysOfWeek, boolean vibrate, String message, - String alert) { - mHour = hour; - mMinutes = minutes; - mDaysOfWeek = daysOfWeek; - } - } + private static void enableAlarmInternal(final Context context, + final Alarm alarm, boolean enabled) { + ContentResolver resolver = context.getContentResolver(); ContentValues values = new ContentValues(2); - values.put(AlarmColumns.ENABLED, enabled ? 1 : 0); + values.put(Alarm.Columns.ENABLED, enabled ? 1 : 0); - /* If we are enabling the alarm, load hour/minutes/daysOfWeek - from db, so we can calculate alarm time */ + // If we are enabling the alarm, calculate alarm time since the time + // value in Alarm may be old. if (enabled) { - EnableAlarm enableAlarm = new EnableAlarm(); - getAlarm(resolver, enableAlarm, id); - if (enableAlarm.mDaysOfWeek == null) { - /* Under monkey, sometimes reportAlarm is never - called */ - Log.e("** enableAlarmInternal failed " + id + " h " + - enableAlarm.mHour + " m " + enableAlarm.mMinutes); - return; + long time = 0; + if (!alarm.daysOfWeek.isRepeatSet()) { + time = calculateAlarm(alarm.hour, alarm.minutes, + alarm.daysOfWeek).getTimeInMillis(); } - - long time = calculateAlarm(enableAlarm.mHour, enableAlarm.mMinutes, - enableAlarm.mDaysOfWeek).getTimeInMillis(); - values.put(AlarmColumns.ALARM_TIME, time); + values.put(Alarm.Columns.ALARM_TIME, time); } - resolver.update(ContentUris.withAppendedId(AlarmColumns.CONTENT_URI, id), - values, null, null); + resolver.update(ContentUris.withAppendedId( + Alarm.Columns.CONTENT_URI, alarm.id), values, null, null); } - - /** - * Calculates next scheduled alert - */ - static class AlarmCalculator implements AlarmSettings { - private long mMinAlert = Long.MAX_VALUE; - private int mMinIdx = -1; - private String mLabel; - - /** - * returns next scheduled alert, MAX_VALUE if none - */ - public long getAlert() { - return mMinAlert; - } - public int getIndex() { - return mMinIdx; - } - public String getLabel() { - return mLabel; - } - - public void reportAlarm( - int idx, boolean enabled, int hour, int minutes, - DaysOfWeek daysOfWeek, boolean vibrate, String message, - String alert) { - if (enabled) { - long atTime = calculateAlarm(hour, minutes, - daysOfWeek).getTimeInMillis(); - /* Log.i("** SET ALERT* idx " + idx + " hour " + hour + " minutes " + - minutes + " enabled " + enabled + " calc " + atTime); */ - if (atTime < mMinAlert) { - mMinIdx = idx; - mMinAlert = atTime; - mLabel = message; - } + public static Alarm calculateNextAlert(final Context context) { + Alarm alarm = null; + long minTime = Long.MAX_VALUE; + long now = System.currentTimeMillis(); + Cursor cursor = getFilteredAlarmsCursor(context.getContentResolver()); + if (cursor != null) { + if (cursor.moveToFirst()) { + do { + Alarm a = new Alarm(cursor); + // A time of 0 indicates this is a repeating alarm, so + // calculate the time to get the next alert. + if (a.time == 0) { + a.time = calculateAlarm(a.hour, a.minutes, a.daysOfWeek) + .getTimeInMillis(); + } else if (a.time < now) { + // Expired alarm, disable it and move along. + enableAlarmInternal(context, a, false); + continue; + } + if (a.time < minTime) { + minTime = a.time; + alarm = a; + } + } while (cursor.moveToNext()); } + cursor.close(); } - } - - static AlarmCalculator calculateNextAlert(final Context context) { - ContentResolver resolver = context.getContentResolver(); - AlarmCalculator alarmCalc = new AlarmCalculator(); - getAlarms(resolver, alarmCalc); - return alarmCalc; + return alarm; } /** @@ -545,54 +269,39 @@ static AlarmCalculator calculateNextAlert(final Context context) { * boot. */ public static void disableExpiredAlarms(final Context context) { - Cursor cur = getAlarmsCursor(context.getContentResolver()); + Cursor cur = getFilteredAlarmsCursor(context.getContentResolver()); long now = System.currentTimeMillis(); if (cur.moveToFirst()) { do { - // Get the field values - int id = cur.getInt(AlarmColumns.ALARM_ID_INDEX); - boolean enabled = cur.getInt( - AlarmColumns.ALARM_ENABLED_INDEX) == 1 ? true : false; - DaysOfWeek daysOfWeek = new DaysOfWeek( - cur.getInt(AlarmColumns.ALARM_DAYS_OF_WEEK_INDEX)); - long time = cur.getLong(AlarmColumns.ALARM_TIME_INDEX); - - if (enabled && !daysOfWeek.isRepeatSet() && time < now) { - if (Log.LOGV) Log.v( - "** DISABLE " + id + " now " + now +" set " + time); - enableAlarmInternal(context, id, false); + Alarm alarm = new Alarm(cur); + // A time of 0 means this alarm repeats. If the time is + // non-zero, check if the time is before now. + if (alarm.time != 0 && alarm.time < now) { + if (Log.LOGV) { + Log.v("** DISABLE " + alarm.id + " now " + now +" set " + + alarm.time); + } + enableAlarmInternal(context, alarm, false); } } while (cur.moveToNext()); } cur.close(); } - private static NotificationManager getNotificationManager( - final Context context) { - return (NotificationManager) context.getSystemService( - context.NOTIFICATION_SERVICE); - } - /** * Called at system startup, on time/timezone change, and whenever * the user changes alarm settings. Activates snooze if set, * otherwise loads all alarms, activates next alert. */ public static void setNextAlert(final Context context) { - int snoozeId = getSnoozeAlarmId(context); - if (snoozeId == -1) { - AlarmCalculator ac = calculateNextAlert(context); - int id = ac.getIndex(); - long atTime = ac.getAlert(); - - if (atTime < Long.MAX_VALUE) { - enableAlert(context, id, ac.getLabel(), atTime); + if (!enableSnoozeAlert(context)) { + Alarm alarm = calculateNextAlert(context); + if (alarm != null) { + enableAlert(context, alarm, alarm.time); } else { - disableAlert(context, id); + disableAlert(context); } - } else { - enableSnoozeAlert(context); } } @@ -600,35 +309,38 @@ public static void setNextAlert(final Context context) { * Sets alert in AlarmManger and StatusBar. This is what will * actually launch the alert when the alarm triggers. * - * Note: In general, apps should call setNextAlert() instead of - * this method. setAlert() is only used outside this class when - * the alert is not to be driven by the state of the db. "Snooze" - * uses this API, as we do not want to alter the alarm in the db - * with each snooze. - * - * @param id Alarm ID. + * @param alarm Alarm. * @param atTimeInMillis milliseconds since epoch */ - static void enableAlert(Context context, int id, String label, - long atTimeInMillis) { + private static void enableAlert(Context context, final Alarm alarm, + final long atTimeInMillis) { AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + if (Log.LOGV) { + Log.v("** setAlert id " + alarm.id + " atTime " + atTimeInMillis); + } + Intent intent = new Intent(ALARM_ALERT_ACTION); - if (Log.LOGV) Log.v("** setAlert id " + id + " atTime " + atTimeInMillis); - intent.putExtra(ID, id); - intent.putExtra(LABEL, label); - intent.putExtra(TIME, atTimeInMillis); + + // XXX: This is a slight hack to avoid an exception in the remote + // AlarmManagerService process. The AlarmManager adds extra data to + // this Intent which causes it to inflate. Since the remote process + // does not know about the Alarm class, it throws a + // ClassNotFoundException. + // + // To avoid this, we marshall the data ourselves and then parcel a plain + // byte[] array. The AlarmReceiver class knows to build the Alarm + // object from the byte[] array. + Parcel out = Parcel.obtain(); + alarm.writeToParcel(out, 0); + out.setDataPosition(0); + intent.putExtra(ALARM_RAW_DATA, out.marshall()); + PendingIntent sender = PendingIntent.getBroadcast( context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); - if (true) { - am.set(AlarmManager.RTC_WAKEUP, atTimeInMillis, sender); - } else { - // a five-second alarm, for testing - am.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 5000, - sender); - } + am.set(AlarmManager.RTC_WAKEUP, atTimeInMillis, sender); setStatusBarIcon(context, true); @@ -643,48 +355,57 @@ static void enableAlert(Context context, int id, String label, * * @param id Alarm ID. */ - static void disableAlert(Context context, int id) { + static void disableAlert(Context context) { AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - Intent intent = new Intent(ALARM_ALERT_ACTION); - intent.putExtra(ID, id); PendingIntent sender = PendingIntent.getBroadcast( - context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); + context, 0, new Intent(ALARM_ALERT_ACTION), + PendingIntent.FLAG_CANCEL_CURRENT); am.cancel(sender); setStatusBarIcon(context, false); saveNextAlarm(context, ""); } - static void saveSnoozeAlert(final Context context, int id, - long atTimeInMillis, String label) { + static void saveSnoozeAlert(final Context context, final int id, + final long time) { SharedPreferences prefs = context.getSharedPreferences( AlarmClock.PREFERENCES, 0); SharedPreferences.Editor ed = prefs.edit(); - ed.putInt(PREF_SNOOZE_ID, id); - ed.putLong(PREF_SNOOZE_TIME, atTimeInMillis); - ed.putString(PREF_SNOOZE_LABEL, label); - ed.commit(); - } - - /** - * @return ID of alarm disabled, if disabled, -1 otherwise - */ - static int disableSnoozeAlert(final Context context) { - int id = getSnoozeAlarmId(context); - if (id == -1) return -1; - saveSnoozeAlert(context, -1, 0, null); - return id; + if (id == -1) { + clearSnoozePreference(ed); + } else { + ed.putInt(PREF_SNOOZE_ID, id); + ed.putLong(PREF_SNOOZE_TIME, time); + ed.commit(); + } + // Set the next alert after updating the snooze. + setNextAlert(context); } /** - * @return alarm ID of snoozing alarm, -1 if snooze unset + * Disable the snooze alert if the given id matches the snooze id. */ - private static int getSnoozeAlarmId(final Context context) { + static void disableSnoozeAlert(final Context context, final int id) { SharedPreferences prefs = context.getSharedPreferences( AlarmClock.PREFERENCES, 0); - return prefs.getInt(PREF_SNOOZE_ID, -1); + int snoozeId = prefs.getInt(PREF_SNOOZE_ID, -1); + if (snoozeId == -1) { + // No snooze set, do nothing. + return; + } else if (snoozeId == id) { + // This is the same id so clear the shared prefs. + clearSnoozePreference(prefs.edit()); + } } + // Helper to remove the snooze preference. Do not use clear because that + // will erase the clock preferences. + private static void clearSnoozePreference(final SharedPreferences.Editor ed) { + ed.remove(PREF_SNOOZE_ID); + ed.remove(PREF_SNOOZE_TIME); + ed.commit(); + }; + /** * If there is a snooze set, enable it in AlarmManager * @return true if snooze is set @@ -694,20 +415,22 @@ private static boolean enableSnoozeAlert(final Context context) { AlarmClock.PREFERENCES, 0); int id = prefs.getInt(PREF_SNOOZE_ID, -1); - if (id == -1) return false; - long atTimeInMillis = prefs.getLong(PREF_SNOOZE_TIME, -1); - if (id == -1) return false; - // Try to get the label from the snooze preference. If null, use the - // default label. - String label = prefs.getString(PREF_SNOOZE_LABEL, null); - if (label == null) { - label = context.getString(R.string.default_label); + if (id == -1) { + return false; } - enableAlert(context, id, label, atTimeInMillis); + long time = prefs.getLong(PREF_SNOOZE_TIME, -1); + + // Get the alarm from the db. + final Alarm alarm = getAlarm(context.getContentResolver(), id); + // The time in the database is either 0 (repeating) or a specific time + // for a non-repeating alarm. Update this value so the AlarmReceiver + // has the right time to compare. + alarm.time = time; + + enableAlert(context, alarm, time); return true; } - /** * Tells the StatusBar whether the alarm is enabled or disabled */ @@ -724,7 +447,7 @@ private static void setStatusBarIcon(Context context, boolean enabled) { * @param minute 0-59 * @param daysOfWeek 0-59 */ - static Calendar calculateAlarm(int hour, int minute, DaysOfWeek daysOfWeek) { + static Calendar calculateAlarm(int hour, int minute, Alarm.DaysOfWeek daysOfWeek) { // start with now Calendar c = Calendar.getInstance(); @@ -752,7 +475,7 @@ static Calendar calculateAlarm(int hour, int minute, DaysOfWeek daysOfWeek) { } static String formatTime(final Context context, int hour, int minute, - DaysOfWeek daysOfWeek) { + Alarm.DaysOfWeek daysOfWeek) { Calendar c = calculateAlarm(hour, minute, daysOfWeek); return formatTime(context, c); } diff --git a/src/com/android/alarmclock/ClockPicker.java b/src/com/android/alarmclock/ClockPicker.java index 039f5b8..31150a2 100644 --- a/src/com/android/alarmclock/ClockPicker.java +++ b/src/com/android/alarmclock/ClockPicker.java @@ -83,7 +83,7 @@ public void onItemClick(AdapterView parent, View v, int position, long id) { private synchronized void selectClock(int position) { SharedPreferences.Editor ed = mPrefs.edit(); - ed.putInt("face", position); + ed.putInt(AlarmClock.PREF_CLOCK_FACE, position); ed.commit(); setResult(RESULT_OK); diff --git a/src/com/android/alarmclock/RepeatPreference.java b/src/com/android/alarmclock/RepeatPreference.java index ba33faa..6af023b 100644 --- a/src/com/android/alarmclock/RepeatPreference.java +++ b/src/com/android/alarmclock/RepeatPreference.java @@ -27,16 +27,11 @@ public class RepeatPreference extends ListPreference { - private Alarms.DaysOfWeek mDaysOfWeek = new Alarms.DaysOfWeek(); - private OnRepeatChangedObserver mOnRepeatChangedObserver; - - public interface OnRepeatChangedObserver { - /** RepeatPrefrence calls this to get initial state */ - public Alarms.DaysOfWeek getDaysOfWeek(); - - /** Called when this preference has changed */ - public void onRepeatChanged(Alarms.DaysOfWeek daysOfWeek); - } + // Initial value that can be set with the values saved in the database. + private Alarm.DaysOfWeek mDaysOfWeek = new Alarm.DaysOfWeek(0); + // New value that will be set if a positive result comes back from the + // dialog. + private Alarm.DaysOfWeek mNewDaysOfWeek = new Alarm.DaysOfWeek(0); public RepeatPreference(Context context, AttributeSet attrs) { super(context, attrs); @@ -55,17 +50,11 @@ public RepeatPreference(Context context, AttributeSet attrs) { setEntryValues(values); } - void setOnRepeatChangedObserver(OnRepeatChangedObserver onRepeatChangedObserver) { - mOnRepeatChangedObserver = onRepeatChangedObserver; - } - @Override protected void onDialogClosed(boolean positiveResult) { if (positiveResult) { - mOnRepeatChangedObserver.onRepeatChanged(mDaysOfWeek); - } else { - /* no change -- reset to initial state */ - mDaysOfWeek.set(mOnRepeatChangedObserver.getDaysOfWeek()); + mDaysOfWeek.set(mNewDaysOfWeek); + setSummary(mDaysOfWeek.toString(getContext(), true)); } } @@ -74,19 +63,23 @@ protected void onPrepareDialogBuilder(Builder builder) { CharSequence[] entries = getEntries(); CharSequence[] entryValues = getEntryValues(); - if (entries == null || entryValues == null) { - throw new IllegalStateException( - "RepeatPreference requires an entries array and an entryValues array."); - } - - mDaysOfWeek.set(mOnRepeatChangedObserver.getDaysOfWeek()); - builder.setMultiChoiceItems( entries, mDaysOfWeek.getBooleanArray(), new DialogInterface.OnMultiChoiceClickListener() { - public void onClick(DialogInterface dialog, int which, boolean isChecked) { - mDaysOfWeek.set(which, isChecked); + public void onClick(DialogInterface dialog, int which, + boolean isChecked) { + mNewDaysOfWeek.set(which, isChecked); } }); } + + public void setDaysOfWeek(Alarm.DaysOfWeek dow) { + mDaysOfWeek.set(dow); + mNewDaysOfWeek.set(dow); + setSummary(dow.toString(getContext(), true)); + } + + public Alarm.DaysOfWeek getDaysOfWeek() { + return mDaysOfWeek; + } } diff --git a/src/com/android/alarmclock/SetAlarm.java b/src/com/android/alarmclock/SetAlarm.java index c141812..03533af 100644 --- a/src/com/android/alarmclock/SetAlarm.java +++ b/src/com/android/alarmclock/SetAlarm.java @@ -16,25 +16,27 @@ package com.android.alarmclock; -import android.app.Activity; -import android.app.Dialog; import android.app.TimePickerDialog; import android.content.Context; import android.content.Intent; -import android.database.ContentObserver; -import android.media.Ringtone; import android.media.RingtoneManager; import android.net.Uri; import android.os.Bundle; -import android.os.Handler; import android.preference.CheckBoxPreference; import android.preference.EditTextPreference; import android.preference.Preference; import android.preference.PreferenceActivity; import android.preference.PreferenceScreen; import android.text.format.DateFormat; +import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup.LayoutParams; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.ListView; import android.widget.TimePicker; import android.widget.Toast; @@ -42,245 +44,156 @@ * Manages each alarm */ public class SetAlarm extends PreferenceActivity - implements Alarms.AlarmSettings, TimePickerDialog.OnTimeSetListener { + implements TimePickerDialog.OnTimeSetListener { private EditTextPreference mLabel; - private CheckBoxPreference mAlarmOnPref; private Preference mTimePref; private AlarmPreference mAlarmPref; private CheckBoxPreference mVibratePref; private RepeatPreference mRepeatPref; - private ContentObserver mAlarmsChangeObserver; private MenuItem mDeleteAlarmItem; private MenuItem mTestAlarmItem; - private int mId; - private int mHour; - private int mMinutes; - private Alarms.DaysOfWeek mDaysOfWeek = new Alarms.DaysOfWeek(); - - private boolean mReportAlarmCalled; - - private static final int DIALOG_TIMEPICKER = 0; - - private class RingtoneChangedListener implements AlarmPreference.IRingtoneChangedListener { - public void onRingtoneChanged(Uri ringtoneUri) { - saveAlarm(false); - } - } - - private class OnRepeatChangedObserver implements RepeatPreference.OnRepeatChangedObserver { - public void onRepeatChanged(Alarms.DaysOfWeek daysOfWeek) { - if (!mDaysOfWeek.equals(daysOfWeek)) { - mDaysOfWeek.set(daysOfWeek); - saveAlarm(true); - } - } - public Alarms.DaysOfWeek getDaysOfWeek() { - return mDaysOfWeek; - } - } - - private class AlarmsChangeObserver extends ContentObserver { - public AlarmsChangeObserver() { - super(new Handler()); - } - @Override - public void onChange(boolean selfChange) { - Alarms.getAlarm(getContentResolver(), SetAlarm.this, mId); - } - } + private int mId; + private boolean mEnabled; + private int mHour; + private int mMinutes; /** - * Set an alarm. Requires an Alarms.ID to be passed in as an - * extra + * Set an alarm. Requires an Alarms.ALARM_ID to be passed in as an + * extra. FIXME: Pass an Alarm object like every other Activity. */ @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); addPreferencesFromResource(R.xml.alarm_prefs); + + // Get each preference so we can retrieve the value later. mLabel = (EditTextPreference) findPreference("label"); mLabel.setOnPreferenceChangeListener( new Preference.OnPreferenceChangeListener() { public boolean onPreferenceChange(Preference p, Object newValue) { + // Set the summary based on the new label. p.setSummary((String) newValue); - saveAlarm(false, (String) newValue); return true; } }); - mAlarmOnPref = (CheckBoxPreference)findPreference("on"); mTimePref = findPreference("time"); mAlarmPref = (AlarmPreference) findPreference("alarm"); mVibratePref = (CheckBoxPreference) findPreference("vibrate"); mRepeatPref = (RepeatPreference) findPreference("setRepeat"); Intent i = getIntent(); - mId = i.getIntExtra(Alarms.ID, -1); - if (Log.LOGV) Log.v("In SetAlarm, alarm id = " + mId); - - mReportAlarmCalled = false; - /* load alarm details from database */ - Alarms.getAlarm(getContentResolver(), this, mId); - /* This should never happen, but does occasionally with the monkey. - * I believe it's a race condition where a deleted alarm is opened - * before the alarm list is refreshed. */ - if (!mReportAlarmCalled) { - Log.e("reportAlarm never called!"); - finish(); + mId = i.getIntExtra(Alarms.ALARM_ID, -1); + if (Log.LOGV) { + Log.v("In SetAlarm, alarm id = " + mId); } - mAlarmsChangeObserver = new AlarmsChangeObserver(); - getContentResolver().registerContentObserver( - Alarms.AlarmColumns.CONTENT_URI, true, mAlarmsChangeObserver); - - mAlarmPref.setRingtoneChangedListener(new RingtoneChangedListener()); - mRepeatPref.setOnRepeatChangedObserver(new OnRepeatChangedObserver()); - } + /* load alarm details from database */ + Alarm alarm = Alarms.getAlarm(getContentResolver(), mId); + mEnabled = alarm.enabled; + mLabel.setText(alarm.label); + mLabel.setSummary(alarm.label); + mHour = alarm.hour; + mMinutes = alarm.minutes; + mRepeatPref.setDaysOfWeek(alarm.daysOfWeek); + mVibratePref.setChecked(alarm.vibrate); + // Give the alert uri to the preference. + mAlarmPref.setAlert(alarm.alert); + updateTime(); - @Override - protected void onDestroy() { - super.onDestroy(); - getContentResolver().unregisterContentObserver(mAlarmsChangeObserver); + // We have to do this to get the save/cancel buttons to highlight on + // their own. + getListView().setItemsCanFocus(true); + + // Grab the content view so we can modify it. + FrameLayout content = (FrameLayout) getWindow().getDecorView() + .findViewById(com.android.internal.R.id.content); + + // Get the main ListView and remove it from the content view. + ListView lv = getListView(); + content.removeView(lv); + + // Create the new LinearLayout that will become the content view and + // make it vertical. + LinearLayout ll = new LinearLayout(this); + ll.setOrientation(LinearLayout.VERTICAL); + + // Have the ListView expand to fill the screen minus the save/cancel + // buttons. + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( + LayoutParams.FILL_PARENT, + LayoutParams.WRAP_CONTENT); + lp.weight = 1; + ll.addView(lv, lp); + + // Inflate the buttons onto the LinearLayout. + View v = LayoutInflater.from(this).inflate( + R.layout.save_cancel_alarm, ll); + + // Attach actions to each button. + Button b = (Button) v.findViewById(R.id.alarm_save); + b.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + saveAlarm(); + finish(); + } + }); + b = (Button) v.findViewById(R.id.alarm_cancel); + b.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + finish(); + } + }); + + // Replace the old content view with our new one. + setContentView(ll); } @Override - protected Dialog onCreateDialog(int id) { - Dialog d; - - switch (id) { - case DIALOG_TIMEPICKER: - d = new TimePickerDialog( - SetAlarm.this, - this, - 0, - 0, - DateFormat.is24HourFormat(SetAlarm.this)); - d.setTitle(getResources().getString(R.string.time)); - break; - default: - d = null; + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, + Preference preference) { + if (preference == mTimePref) { + new TimePickerDialog(this, this, mHour, mMinutes, + DateFormat.is24HourFormat(this)).show(); } - return d; - } - - @Override - protected void onPrepareDialog(int id, Dialog dialog) { - super.onPrepareDialog(id, dialog); - - switch (id) { - case DIALOG_TIMEPICKER: - TimePickerDialog timePicker = (TimePickerDialog)dialog; - timePicker.updateTime(mHour, mMinutes); - break; - } + return super.onPreferenceTreeClick(preferenceScreen, preference); } @Override - public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { - - if (preference == mTimePref) { - showDialog(DIALOG_TIMEPICKER); - } else if (preference == mAlarmOnPref) { - saveAlarm(true); - } else if (preference == mVibratePref) { - saveAlarm(false); - } - - return super.onPreferenceTreeClick(preferenceScreen, preference); + public void onBackPressed() { + saveAlarm(); + finish(); } public void onTimeSet(TimePicker view, int hourOfDay, int minute) { mHour = hourOfDay; mMinutes = minute; - mAlarmOnPref.setChecked(true); - saveAlarm(true); - } - - /** - * Alarms.AlarmSettings implementation. Database feeds current - * settings in through this call - */ - public void reportAlarm( - int idx, boolean enabled, int hour, int minutes, - Alarms.DaysOfWeek daysOfWeek, boolean vibrate, String label, - String alert) { - - if (label == null || label.length() == 0) { - label = getString(R.string.default_label); - } - mLabel.setText(label); - mLabel.setSummary(label); - mHour = hour; - mMinutes = minutes; - mAlarmOnPref.setChecked(enabled); - mDaysOfWeek.set(daysOfWeek); - mVibratePref.setChecked(vibrate); - - if (alert == null || alert.length() == 0) { - if (Log.LOGV) Log.v("****** reportAlarm null or 0-length alert"); - mAlarmPref.mAlert = getDefaultAlarm(); - if (mAlarmPref.mAlert == null) { - Log.e("****** Default Alarm null"); - } - } else { - mAlarmPref.mAlert = Uri.parse(alert); - if (mAlarmPref.mAlert == null) { - Log.e("****** Parsed null alarm. URI: " + alert); - } - } - if (Log.LOGV) Log.v("****** reportAlarm uri " + alert + " alert " + - mAlarmPref.mAlert); updateTime(); - updateRepeat(); - updateAlarm(mAlarmPref.mAlert); - - mReportAlarmCalled = true; - } - - /** - * picks the first alarm available - */ - private Uri getDefaultAlarm() { - RingtoneManager ringtoneManager = new RingtoneManager(this); - ringtoneManager.setType(RingtoneManager.TYPE_ALARM); - return ringtoneManager.getRingtoneUri(0); + // If the time has been changed, enable the alarm. + mEnabled = true; } private void updateTime() { - if (Log.LOGV) Log.v("updateTime " + mId); - mTimePref.setSummary(Alarms.formatTime(this, mHour, mMinutes, mDaysOfWeek)); - } - - private void updateAlarm(Uri ringtoneUri) { - if (Log.LOGV) Log.v("updateAlarm " + mId); - Ringtone ringtone = RingtoneManager.getRingtone(SetAlarm.this, ringtoneUri); - if (ringtone != null) { - mAlarmPref.setSummary(ringtone.getTitle(SetAlarm.this)); + if (Log.LOGV) { + Log.v("updateTime " + mId); } + mTimePref.setSummary(Alarms.formatTime(this, mHour, mMinutes, + mRepeatPref.getDaysOfWeek())); } - private void updateRepeat() { - if (Log.LOGV) Log.v("updateRepeat " + mId); - mRepeatPref.setSummary(mDaysOfWeek.toString(this, true)); - } - - private void saveAlarm(boolean popToast) { - saveAlarm(popToast, mLabel.getText()); - } + private void saveAlarm() { + final String alert = mAlarmPref.getAlertString(); + Alarms.setAlarm(this, mId, mEnabled, mHour, mMinutes, + mRepeatPref.getDaysOfWeek(), mVibratePref.isChecked(), + mLabel.getText(), alert); - /** - * This version of saveAlarm uses the passed in label since mLabel may - * contain the old value (i.e. during the preference value change). - */ - private void saveAlarm(boolean popToast, String label) { - if (mReportAlarmCalled && mAlarmPref.mAlert != null) { - String alertString = mAlarmPref.mAlert.toString(); - saveAlarm(this, mId, mAlarmOnPref.isChecked(), mHour, mMinutes, - mDaysOfWeek, mVibratePref.isChecked(), label, alertString, - popToast); + if (mEnabled) { + popAlarmSetToast(this, mHour, mMinutes, + mRepeatPref.getDaysOfWeek()); } } @@ -290,7 +203,7 @@ private void saveAlarm(boolean popToast, String label) { */ private static void saveAlarm( Context context, int id, boolean enabled, int hour, int minute, - Alarms.DaysOfWeek daysOfWeek, boolean vibrate, String label, + Alarm.DaysOfWeek daysOfWeek, boolean vibrate, String label, String alert, boolean popToast) { if (Log.LOGV) Log.v("** saveAlarm " + id + " " + label + " " + enabled + " " + hour + " " + minute + " vibe " + vibrate); @@ -309,7 +222,7 @@ private static void saveAlarm( * goes off. This helps prevent "am/pm" mistakes. */ static void popAlarmSetToast(Context context, int hour, int minute, - Alarms.DaysOfWeek daysOfWeek) { + Alarm.DaysOfWeek daysOfWeek) { String toastText = formatToast(context, hour, minute, daysOfWeek); Toast toast = Toast.makeText(context, toastText, Toast.LENGTH_LONG); @@ -322,7 +235,7 @@ static void popAlarmSetToast(Context context, int hour, int minute, * now" */ static String formatToast(Context context, int hour, int minute, - Alarms.DaysOfWeek daysOfWeek) { + Alarm.DaysOfWeek daysOfWeek) { long alarm = Alarms.calculateAlarm(hour, minute, daysOfWeek).getTimeInMillis(); long delta = alarm - System.currentTimeMillis();; @@ -365,7 +278,6 @@ public boolean onCreateOptionsMenu(Menu menu) { mTestAlarmItem = menu.add(0, 0, 0, "test alarm"); } - return true; } @@ -400,10 +312,10 @@ void setTestAlarm() { int nowMinute = c.get(java.util.Calendar.MINUTE); int minutes = (nowMinute + 1) % 60; - int hour = nowHour + (nowMinute == 0? 1 : 0); + int hour = nowHour + (nowMinute == 0 ? 1 : 0); - saveAlarm(this, mId, true, hour, minutes, mDaysOfWeek, true, - mLabel.getText(), mAlarmPref.mAlert.toString(), true); + saveAlarm(this, mId, true, hour, minutes, mRepeatPref.getDaysOfWeek(), + true, mLabel.getText(), mAlarmPref.getAlertString(), true); } } diff --git a/src/com/android/alarmclock/SettingsActivity.java b/src/com/android/alarmclock/SettingsActivity.java index 44c4045..f0b97fd 100644 --- a/src/com/android/alarmclock/SettingsActivity.java +++ b/src/com/android/alarmclock/SettingsActivity.java @@ -19,6 +19,7 @@ import android.media.AudioManager; import android.os.Bundle; import android.preference.CheckBoxPreference; +import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceActivity; import android.preference.PreferenceScreen; @@ -27,23 +28,23 @@ /** * Settings for the Alarm Clock. */ -public class SettingsActivity extends PreferenceActivity { +public class SettingsActivity extends PreferenceActivity + implements Preference.OnPreferenceChangeListener { private static final int ALARM_STREAM_TYPE_BIT = 1 << AudioManager.STREAM_ALARM; - + private static final String KEY_ALARM_IN_SILENT_MODE = "alarm_in_silent_mode"; - private CheckBoxPreference mAlarmInSilentModePref; - + static final String KEY_ALARM_SNOOZE = + "snooze_duration"; + static final String KEY_VOLUME_BEHAVIOR = + "volume_button_setting"; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.settings); - - mAlarmInSilentModePref = - (CheckBoxPreference) findPreference(KEY_ALARM_IN_SILENT_MODE); } @Override @@ -55,34 +56,48 @@ protected void onResume() { @Override public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) { - - if (preference == mAlarmInSilentModePref) { - + if (KEY_ALARM_IN_SILENT_MODE.equals(preference.getKey())) { + CheckBoxPreference pref = (CheckBoxPreference) preference; int ringerModeStreamTypes = Settings.System.getInt( getContentResolver(), Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); - - if (mAlarmInSilentModePref.isChecked()) { + + if (pref.isChecked()) { ringerModeStreamTypes &= ~ALARM_STREAM_TYPE_BIT; } else { ringerModeStreamTypes |= ALARM_STREAM_TYPE_BIT; } - + Settings.System.putInt(getContentResolver(), Settings.System.MODE_RINGER_STREAMS_AFFECTED, ringerModeStreamTypes); - + return true; } - + return super.onPreferenceTreeClick(preferenceScreen, preference); } + public boolean onPreferenceChange(Preference pref, Object newValue) { + final ListPreference listPref = (ListPreference) pref; + final int idx = listPref.findIndexOfValue((String) newValue); + listPref.setSummary(listPref.getEntries()[idx]); + return true; + } + private void refresh() { - int silentModeStreams = Settings.System.getInt(getContentResolver(), - Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); - mAlarmInSilentModePref.setChecked( + final CheckBoxPreference alarmInSilentModePref = + (CheckBoxPreference) findPreference(KEY_ALARM_IN_SILENT_MODE); + final int silentModeStreams = + Settings.System.getInt(getContentResolver(), + Settings.System.MODE_RINGER_STREAMS_AFFECTED, 0); + alarmInSilentModePref.setChecked( (silentModeStreams & ALARM_STREAM_TYPE_BIT) == 0); + + final ListPreference snooze = + (ListPreference) findPreference(KEY_ALARM_SNOOZE); + snooze.setSummary(snooze.getEntry()); + snooze.setOnPreferenceChangeListener(this); } - + }