package net.sabi.pester; import java.util.Arrays; import java.util.Comparator; import danger.app.Application; import danger.app.DataStore; import danger.app.Event; import danger.app.SettingsDB; import danger.app.SettingsDBException; import danger.internal.Date; import danger.util.ByteArray; import danger.util.StdActiveList; import danger.util.DEBUG; public class Alarms extends StdActiveList { // max # records in a datastore public static final int MAX_ALARM_COUNT = 50; private static Alarms sAlarmList = null; private static Listener sListener; private static SettingsDB sSettingsDB; private static DataStore sDeletedAlarms; private DataStore mDataStore; private Alarms() { mDataStore = DataStore.createDataStore("alarms", true /* auto sync */); // register us for Event.EVENT_DATASTORE_RESTORED, which only // seems to be documented at: // mDataStore.setAutoSyncNotifyee(sListener); refreshFromDataStore(false); } private int[] deletedAlarmsCreationIDs() { byte[][] deletedAlarmsData = sDeletedAlarms.getRecords(); int[] ids = new int[deletedAlarmsData.length]; for (int i = 0 ; i < deletedAlarmsData.length ; ++i) ids[i] = ByteArray.readInt(deletedAlarmsData[i], 0); Arrays.sort(ids); return ids; } private void dumpAlarms() { // XXX enable via IPC, make validity checking int i; DEBUG.p("== ALARMS =="); for (i = 0 ; i < size() ; ++i) { DEBUG.p(((Alarm)getItem(i)).description()); } } private void dumpDatastore() { int i; DEBUG.p("== DATASTORE CONTENTS =="); byte[][] alarmsData = mDataStore.getRecords(); for (i = 0 ; i < alarmsData.length ; ++i) { Alarm alarm = new Alarm(); alarm.fromByteArray(alarmsData[i]); alarm.setUID(mDataStore.getRecordUID(i)); DEBUG.p(alarm.description()); } } void refreshFromDataStore(boolean datastoreRestored) { sAlarmList = null; int[] deleted = null; if (datastoreRestored) { deleted = deletedAlarmsCreationIDs(); DEBUG.p("+++ BEFORE RESOLUTION +++"); dumpAlarms(); dumpDatastore(); } removeAllItems(); // XXX no, we can't do this because some alarms may be (a) in the process of being edited, (b) sitting at expiry, or (c) are periodic or snoozed and need their absolute time preserved. Instead, we should just add all the new records - but what about conflicts? if (datastoreRestored) mDataStore.doneResolvingConflict(); // renumbers on-device records if (datastoreRestored) { DEBUG.p("+++ AFTER RESOLUTION +++"); dumpDatastore(); } byte[][] alarmsData = mDataStore.getRecords(); int i; for (i = 0 ; i < alarmsData.length ; ++i) { Alarm alarm = new Alarm(); alarm.fromByteArray(alarmsData[i]); alarm.setUID(mDataStore.getRecordUID(i)); if (Arrays.binarySearch(deleted, alarm.getCreationID()) >= 0) { mDataStore.removeRecord(i); continue; } insertItemSorted(alarm, alarm); alarm.resume(); } try { MessageFinder.setMessageListFromByteArray(sSettingsDB.getBytes(KEY_RECENT_MESSAGES)); } catch (SettingsDBException e) { MessageFinder.setDefaultMessageList(); } DEBUG.p("+++ AFTER RESTORATION +++"); dumpAlarms(); dumpDatastore(); sAlarmList = this; } public static Alarms getList() { if (sAlarmList == null) { sSettingsDB = new SettingsDB("settings", true /* auto sync */); sDeletedAlarms = DataStore.createDataStore("deleted alarms"); sListener = new Listener(); new Alarms(); Application.registerForEvent(sListener, Event.EVENT_TIME_CHANGED); } return sAlarmList; } public static boolean canCreateAlarm() { return (sAlarmList.size() < MAX_ALARM_COUNT); } public static void addAlarm(Alarm alarm) { sAlarmList.insertItemSorted(alarm, alarm); } public static void removeAlarm(Alarm alarm) { sAlarmList.removeItem(alarm); } public static void recentMessagesChanged() { sSettingsDB.setBytes(KEY_RECENT_MESSAGES, MessageFinder.messageListAsByteArray()); } protected void onItemAdded(Object item, int index) { if (sAlarmList == null) // restoring from service return; Alarm alarm = (Alarm)item; index = mDataStore.addRecord(alarm.toByteArray()); alarm.setUID(mDataStore.getRecordUID(index)); DEBUG.p("ADD" + alarm.description()); } protected void onItemRemoved(Object item, int index) { if (sAlarmList == null) // restoring from service (after hard reset) return; Alarm alarm = (Alarm)item; int uid = alarm.getUID(); if (uid == 0) return; mDataStore.removeRecordByUID(uid); if (uid < 0) mDataStore.removeRecordByUID(-uid); while (sDeletedAlarms.getRecordCount() >= MAX_ALARM_COUNT) sDeletedAlarms.removeRecord(0); byte[] idBytes = new byte[4]; ByteArray.writeInt(idBytes, 0, alarm.getCreationID()); sDeletedAlarms.addRecord(idBytes); DEBUG.p("DEL" + alarm.description()); } public void onItemUpdated(Object item, int index) { if (sAlarmList == null) // restoring from service return; Alarm alarm = (Alarm)item; mDataStore.setRecordDataByUID(alarm.getUID(), alarm.toByteArray(), true); DEBUG.p("MOD" + alarm.description()); } private static String KEY_DEFAULT_ALARM = "default alarm"; private static String KEY_RECENT_MESSAGES = "recent messages"; public static Alarm getDefaultAlarm() { Alarm defaultAlarm = new Alarm(); try { defaultAlarm.fromByteArray(sSettingsDB.getBytes(KEY_DEFAULT_ALARM)); } catch (SettingsDBException e) { defaultAlarm = new Alarm(); defaultAlarm.setDate(new Date()); defaultAlarm.setPeriod(600, false); } return defaultAlarm; } public static void setDefaultAlarm(Alarm alarm) { sSettingsDB.setBytes(KEY_DEFAULT_ALARM, alarm.toByteArray()); } static class Listener extends danger.app.Listener implements danger.util.ActiveList.ForEach { public void receive(Object item) { ((Alarm)item).timeChanged(); } public boolean receiveEvent(Event e) { if (e.type == Event.EVENT_TIME_CHANGED) { Alarms.getList().forEach(this); ((Pester)Application.getCurrentApp()).resetMidnightCheck(); return true; } else if (e.type == Event.EVENT_DATASTORE_RESTORED) { String dbName = (String)e.argument; DEBUG.p("Pester: DATASTORE_RESTORED " + dbName); if (dbName.endsWith("alarms")) { // because we only get/set the default alarm on demand // there's no need to handle conflicts with the settings... // XXX but what happens if we set a (default) alarm, then // the SettingsDB restores? Alarms.getList().refreshFromDataStore(true); } } return super.receiveEvent(e); } } }