package net.sabi.pester; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import danger.app.Application; import danger.app.IPCMessage; import danger.audio.RingToneObject; import danger.internal.Date; import danger.system.Hardware; import danger.text.Collator; import danger.util.LocaleUtils; import danger.util.StdActiveList; import danger.util.StdActiveObject; import danger.util.DEBUG; import danger.util.format.DateFormat; import danger.util.format.StringFormat; import java.util.Comparator; public class Alarm extends StdActiveObject implements Comparator { private static final int VERSION_1 = 1; // persisted private String mMessage; private int mType; private int mPeriod; private Date mDate; private RingToneObject mAlert; private int mCreationID; // transient private int mState; private int mAbsoluteFireTime; // valid if periodic or snoozed, // and modified since reboot private danger.app.Alarm mAlarm; private int mUID; public Alarm() { mState = STATE_INVALID; mAlarm = new danger.app.Alarm(0, Application.getCurrentApp(), this); mCreationID = Hardware.getSystemTime(); mUID = 0; mAbsoluteFireTime = -1; } public String getMessage() { return mMessage; } public int getPeriod() { return mPeriod; } public boolean getUsesPeriod() { return mType != TYPE_DATE; } public boolean getRepeating() { return mType == TYPE_PERIODIC_REPEATING; } public Date getDate() { return mDate; } public RingToneObject getAlert() { return mAlert; } public int getUID() { return mUID; } public int getCreationID() { return mCreationID; } public void setMessage(String message) { mMessage = message; } public void setPeriod(int period, boolean repeating) { mType = repeating ? TYPE_PERIODIC_REPEATING : TYPE_PERIODIC; mPeriod = period; } public void setDate(Date date) { mType = TYPE_DATE; mDate = date; } public void setAlert(RingToneObject alert) { mAlert = alert.isValid() ? alert : null; } public void setUID(int uid) { mUID = uid; } public void snoozeForMinutes(int minutes) { mDate = new Date(); mDate.addMinutes(minutes); mAbsoluteFireTime = Hardware.getAbsoluteTime() + (minutes * 60); resume(); } public byte[] toByteArray() { try { ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); DataOutputStream dataStream = new DataOutputStream(byteStream); dataStream.writeByte(VERSION_1); dataStream.writeUTF(mMessage); dataStream.writeInt(mPeriod); dataStream.writeInt(mDate.getUnixTimeGMT()); dataStream.writeInt(mType); dataStream.writeInt(mAlert == null ? 0 : mAlert.getID()); dataStream.writeInt(mCreationID); dataStream.flush(); return byteStream.toByteArray(); } catch (Exception e) { // XXX do something DEBUG.p("failed to write alarm: " + e); } return null; } public void fromByteArray(byte[] data) { try { ByteArrayInputStream byteStream = new ByteArrayInputStream(data); DataInputStream dataStream = new DataInputStream(byteStream); byte version = dataStream.readByte(); if (version != VERSION_1) { // XXX barf } mMessage = dataStream.readUTF(); mPeriod = dataStream.readInt(); mDate = new Date(dataStream.readInt()); mType = dataStream.readInt(); int alertID = dataStream.readInt(); mAlert = (alertID == 0 ? null : new RingToneObject(alertID)); mCreationID = dataStream.readInt(); mAbsoluteFireTime = -1; // just in case } catch (Exception e) { // XXX do something, prevernt alarm from being activated DEBUG.p("failed to read alarm: " + e); } } void beginEditing() { mState = STATE_EDITING; mAlarm.deactivate(); } public static int secondsFromNow(Date date) { return date.getDangerTimeGMT() - new Date().getDangerTimeGMT(); } protected int remainingInterval() { return secondsFromNow(mDate); } void resume() { mState = STATE_SCHEDULED; int interval = remainingInterval(); if (interval < 0) interval = 0; mAlarm.resetWake(interval); DEBUG.p("resetWake: " + description() + " - " + interval + "s left"); update(); } void timeChanged() { if (mState != STATE_SCHEDULED) return; if (mAbsoluteFireTime != -1) { int realSecondsLeft = mAbsoluteFireTime - Hardware.getAbsoluteTime(); mDate = new Date(); mDate.addSeconds(realSecondsLeft < 0 ? 0 : realSecondsLeft); // no need to call resume(): alarm uses absolute time update(); } else { resume(); } } void schedule() { if (getUsesPeriod()) { mDate = new Date(); mDate.addSeconds(mPeriod); mAbsoluteFireTime = Hardware.getAbsoluteTime() + mPeriod; } resume(); } void dismiss() { if (getRepeating()) schedule(); else ((StdActiveList)getDelegate()).removeItem(this); } void cancel() { mState = STATE_INVALID; mAlarm.deactivate(); } // XXX not sure if this is featureful enough to be worthwhile public IPCMessage getCalendarIPCMessage() { if (mState != STATE_SCHEDULED) return null; IPCMessage message = new IPCMessage(); message.addItem("action", "new"); message.addItem("title", getMessage()); message.addItem("start", mDate.getUnixTimeGMT()); return message; } public String description() { StringBuffer sb = new StringBuffer(); if (mUID != 0) sb.append('(').append(mUID).append(") "); switch (mState) { case STATE_INVALID: sb.append("inv "); break; case STATE_EDITING: sb.append("edi "); break; case STATE_SCHEDULED: sb.append("sch "); break; } switch (mType) { case TYPE_PERIODIC_REPEATING: sb.append("r-"); case TYPE_PERIODIC: sb.append("per(").append(mPeriod).append("s)"); if (mState != STATE_SCHEDULED) break; case TYPE_DATE: sb.append("(").append(getDateTimeString()).append(")"); break; } sb.append(": "); sb.append('"').append(mMessage).append('"'); return sb.toString(); } public static String dateTimeString(Date date, boolean relative) { if (date == null) return null; String layout = LocaleUtils.getDateTimePattern(); String dateFormat = LocaleUtils.getMediumDateFormat(); String dateString; String timeFormat; if (relative) { timeFormat = LocaleUtils.getShortTimeFormat(); int daysFromToday = date.getDaysBetween(new Date()); if (daysFromToday == 0) dateString = "Today"; // XXX localize else if (daysFromToday == 1) dateString = "Tomorrow"; // XXX localize else if (daysFromToday > 1 && daysFromToday < 7) dateString = date.getDayString(); else dateString = DateFormat.withFormat(dateFormat, date); } else { dateString = DateFormat.withFormat(dateFormat, date); timeFormat = LocaleUtils.getMediumTimeFormat(); } String timeString = DateFormat.withFormat(timeFormat, date); return StringFormat.withFormat(layout, timeString, dateString); } protected static void addUnit(StringBuffer sb, int n, String unit) { if (n == 0) return; if (sb.length() > 0) sb.append(", "); if (n == 1) sb.append("one ").append(unit); else sb.append(n).append(' ').append(unit).append("s"); } public static String intervalString(int i) { // XXX localize if (i < 0) return "expired"; StringBuffer sb = new StringBuffer(); if (i < 60) { addUnit(sb, i, "second"); } else { i /= 60; // minutes if (i < 24 * 60) { addUnit(sb, i / 60, "hour"); addUnit(sb, i % 60, "minute"); } else { i /= 60; // hours if (i < 365) { addUnit(sb, i / 24, "day"); addUnit(sb, i % 24, "hour"); } else { i /= 24; // days addUnit(sb, i / 365, "year"); // XXX actually 365.242199 addUnit(sb, i % 365, "day"); } } } return sb.toString(); } public int getSecondsUntilNextIntervalStringUpdate() { int i = remainingInterval(); if (i <= 1) return 0; if (i <= 60) return 1; if (i <= 24 * 60 * 60) { i %= 60; return (i == 0 ? 60 : i + 1); } i %= 3600; return (i == 0 ? 3600 : i + 1); } public String getDateTimeString(boolean relative) { return dateTimeString(mDate, relative); } public String getDateTimeString() { return getDateTimeString(false); } public String getIntervalString() { return intervalString(remainingInterval()); } public String toString() { return getDateTimeString(true) + " - " + mMessage; } public int compare(Object arg0, Object arg1) { Alarm alarm0 = (Alarm)arg0, alarm1 = (Alarm)arg1; int result = alarm0.getDate().compareTo(alarm1.getDate()); if (result != 0) return result; result = Collator.getInstance().compare(alarm0.getMessage(), alarm1.getMessage()); if (result != 0) return result; return alarm0.hashCode() - alarm1.hashCode(); } public static final int TYPE_PERIODIC = 0; public static final int TYPE_PERIODIC_REPEATING = 1; public static final int TYPE_DATE = 2; public static final int STATE_INVALID = 0; public static final int STATE_EDITING = 1; public static final int STATE_SCHEDULED = 2; }