/*********************************************************************************
* TotalCross Software Development Kit *
* Copyright (C) 2001 Rob Nielsen *
* Copyright (C) 2001-2012 SuperWaba Ltda. *
* All Rights Reserved *
* *
* This library and virtual machine is distributed in the hope that it will *
* be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. *
* *
* This file is covered by the GNU LESSER GENERAL PUBLIC LICENSE VERSION 3.0 *
* A copy of this license is located in file license.txt at the root of this *
* SDK or can be downloaded here: *
* http://www.gnu.org/licenses/lgpl-3.0.txt *
* *
*********************************************************************************/
package totalcross.pim.palm.builtin;
import totalcross.io.DataStream;
import totalcross.io.ObjectPDBFile;
import totalcross.io.PDBFile;
import totalcross.io.Storable;
import totalcross.sys.Time;
/**
* Provides a link to the standard Palm Datebook database.
*
* @author <A HREF="mailto:rnielsen@cygnus.uwa.edu.au">Robert Nielsen</A>,
* @version 1.0.0 16 October 1999
*/
public class Datebook implements Storable
{
/** the datebook PDBFile */
private static ObjectPDBFile datebookCat;
public static void initDatebook() throws totalcross.io.IOException
{
if (datebookCat == null)
datebookCat = new ObjectPDBFile("DatebookDB.date.DATA");
}
/**
* Gets the number of Datebook's in the database
*
* @return the number of datebooks
*/
public static int datebookCount()
{
return datebookCat.getRecordCount();
}
/**
* Gets a Datebook from the database
*
* @param i
* the index to get
* @return the retrieved datebook
*/
public static Datebook getDate(int i)
{
Datebook datebook = new Datebook();
if (datebookCat.loadObjectAt(datebook, i))
return datebook;
return null;
}
/**
* Gets a Datebook from the database and places it into the given Datebook.
* Any previous data in the datebook is erased.
*
* @param i
* the index to get
* @param datebook
* the datebook object to place the datebook into.
*/
public static boolean getDate(int i, Datebook datebook)
{
return datebookCat.loadObjectAt(datebook, i);
}
/**
* Adds a new Datebook to the database at the proper position in the dates
* sort order.
*
* @param datebook
* the datebook to add
* @return true if successful, false otherwise
*/
public static boolean addDate(Datebook datebook)
{
return datebookCat.insertObjectAt(datebook, findDatePosition(datebook));
}
/**
* Gets the first date which is not a repeatable date
*
* @return the index of the last repeating date
*/
public static int getRepeatRange()
{
Datebook d = new Datebook();
int n = datebookCat.getRecordCount();
for (int i = 0; i < n; i++)
{
if (datebookCat.loadObjectAt(d, i) && d.repeatType == 0)
return Math.max(i - 1, 0); // guich@401_37
}
return n - 1;
}
/**
* Finds the suitable position for a new datebook entry
*
* @param d
* the Datebook object
* @return the index where to insert as int
*/
public static int findDatePosition(Datebook d)
{
if (datebookCat.getRecordCount() == 0) // guich@401_35
return 0;
// if date is repeating sort it at end of repeating dates
// cause they are not sorted, or maybe it doesn't matter if not
if (d.repeatType > 0)
// so return the last repeating date
return getRepeatRange();
// if it's non repeating date
if (d.repeatType == 0)
{
int e = datebookCat.getRecordCount() - 1; // search until end of record
// int two datebook object for comparision
Datebook c = new Datebook();
Datebook l = new Datebook();
// do some quick checks
if (d.repeatType == 0 && datebookCat.loadObjectAt(c, e))
{
// if date greater than last date then return last index+1
if (compareTime(d.startDate, c.startDate) > 0)
return e + 1;
// if date less than last date and greater that last-1 return last index
if (datebookCat.loadObjectAt(l, e - 1) && compareTime(d.startDate, l.startDate) > 0)
return e;
}
// if the before check does not hit
// get the range of the repeating dates
int r = getRepeatRange();
// set start and end value of search
int s = r + 1; // search from last repeating date
int now = 0;
// start the search loop
while (s <= e)
{
// compute the middle of the range
int m = (s + e) >> 1;
int m2 = m - 1;
//Vm.debug("m="+m);
// load date at middle and date at middle -1
if (!datebookCat.loadObjectAt(c, m)) // guich@402_3: if a date was deleted but archived til next hotsync, this returns false
{
if (now < 0)
e--;
else
s++;
continue;
}
// search for the first valid record before the current one
while (m2 >= 0 && !datebookCat.loadObjectAt(l, m2))
m2--;
if (m2 < 0)
{
if (now < 0)
e--;
else
s++;
continue;
}
// compare if date between two dates, then return position
now = compareTime(d.startDate, c.startDate);
if (now < 0 && compareTime(d.startDate, l.startDate) > 0)
return m;
else
// compare if date less than middle
if (now < 0)
{
// if the prev date is repeating, then we are at the start
if (l.repeatType > 0)
return m;
e = m;
}
// do the next loop with new m
else
s = m;
}
}
// if all fails return -1
return -1;
}
/**
* Compares two Time objects
*
* @param a
* the first Time object
* @param b
* the second Time object
* @return 0 if a == b, -1 if a < b, 1 if a > b
*/
private static int compareTime(Time a, Time b)
{
// prepare the ints for compare
int ad = (a.year * 10000) + (a.month * 100) + a.day;
int bd = (b.year * 10000) + (b.month * 100) + b.day;
// if a < b
if (ad < bd)
return -1;
// if a > b
if (ad > bd)
return 1;
// if same dates, compares the times
ad = a.hour < 0 ? 0 : ((a.hour * 10000) + (a.minute * 100) + a.second);
bd = b.hour < 0 ? 0 : ((b.hour * 10000) + (b.minute * 100) + b.second);
return (ad < bd) ? -1 : (ad > bd) ? 1 : 0;
}
/**
* Deletes a Datebook entry in the database
*
* @param i
* the index to delete
* @return true if successful, false otherwise
*/
public static boolean delDate(int i)
{
return datebookCat.setObjectAttribute(i, PDBFile.REC_ATTR_DELETE);
}
/**
* Changes the Datebook at the given index
*
* @param i
* the index to change
* @param datebook
* a Datebook with the values you want the Datebook at i to have
* @return true if successful, false otherwise
*/
public static boolean changeDate(int i, Datebook datebook)
{
if (datebook == null)
return false;
return datebookCat.deleteObjectAt(i) ? datebookCat.insertObjectAt((Storable) datebook, i) : false;
}
/*
* Only for testing purposes added and not removed anymore ;-) [ag] Gets the
* record attributes from a Datebook entry @param i the index to get the
* attributes from @returnthe record attributes
*
* public static byte getAttribute(int i) { return
* datebookCat.getObjectAttribute(i); }
*/
/////////////////////////////////////
// individual datebook stuff below //
/////////////////////////////////////
// flag positions for each of the elements of an appointment
// private static final int WHEN = 1 << 7;
private static final int ALARM = 1 << 6;
private static final int REPEAT = 1 << 5;
private static final int NOTE = 1 << 4;
private static final int EXCEPTIONS = 1 << 3;
private static final int DESCRIPTION = 1 << 2;
/**
* Constant for alarmUnits. Note that, to make the alarm really work, you
* need to do a reset on the device (use totalcross.sys.Vm.reboot) or do a
* hotsync.
*/
public static final int ALARM_MINUTES = 0;
/**
* Constant for alarmUnits. Note that, to make the alarm really work, you
* need to do a reset on the device (use totalcross.sys.Vm.reboot) or do a
* hotsync.
*/
public static final int ALARM_HOURS = 1;
/**
* Constant for alarmUnits. Note that, to make the alarm really work, you
* need to do a reset on the device (use totalcross.sys.Vm.reboot) or do a
* hotsync.
*/
public static final int ALARM_DAYS = 2;
/**
* Constant for repeatType. Note that, to make the alarm really work, you
* need to do a reset on the device (use totalcross.sys.Vm.reboot) or do a
* hotsync.
*/
public static final int REPEAT_NONE = 0;
/**
* Constant for repeatType. Note that, to make the alarm really work, you
* need to do a reset on the device (use totalcross.sys.Vm.reboot) or do a
* hotsync.
*/
public static final int REPEAT_DAILY = 1;
/**
* Constant for repeatType. Note that, to make the alarm really work, you
* need to do a reset on the device (use totalcross.sys.Vm.reboot) or do a
* hotsync.
*/
public static final int REPEAT_WEEKLY = 2;
/**
* Constant for repeatType. Note that, to make the alarm really work, you
* need to do a reset on the device (use totalcross.sys.Vm.reboot) or do a
* hotsync.
*/
public static final int REPEAT_MONTHLY_BY_DAY = 3;
/**
* Constant for repeatType. Note that, to make the alarm really work, you
* need to do a reset on the device (use totalcross.sys.Vm.reboot) or do a
* hotsync.
*/
public static final int REPEAT_MONTHLY_BY_DATE = 4;
/**
* Constant for repeatType. Note that, to make the alarm really work, you
* need to do a reset on the device (use totalcross.sys.Vm.reboot) or do a
* hotsync.
*/
public static final int REPEAT_YEARLY = 5;
/** The time and date this datebook item is to start */
public Time startDate = new Time();
/**
* The time this datebook item is to end - date info is ignored. If this
* value is null, this appointment has no time, regardless of any time
* information in startDate.
*/
public Time endTime;
/**
* The units of time that the alarm should go off before the start date.
* Should be one of ALARM_XXX constants. Note that, to make the alarm really
* work, you need to do a reset on the device (use totalcross.sys.Vm.reboot)
* or do a hotsync.
*/
public int alarmUnits = ALARM_MINUTES;
/**
* The number of <code>alarmUnit</code>s before <code>startDate</code>
* that the alarm should go off, or -1 for no alarm.
*/
public int alarmAdvance = -1;
/** The type of repeat (one of the <code>REPEAT_XXX</code> constants) */
public int repeatType = REPEAT_NONE;
/** The end date for repeating (or null for forever) */
public Time repeatEndDate;
/** The interval (in repeatTypes) between repetitions */
public int repeatFrequency = 1;
/**
* Indicates the day of week to repeat. Only applicable when
* <code>repeatType</code> is <code>REPEAT_MONTHLY_BY_DAY</code>. Should
* be in the range 0 (Sunday) to 6 (Saturday). This field is used in
* conjunction with <code>repeatMonthlyCount</code> to represent things
* like the 3rd Sunday every month by setting <code>repeatMonthlyDay</code>
* to 0 and <code>repeatMonthlyCount</code> to 3.
*/
public int repeatMonthlyDay;
/**
* Indicates the number of <code>repeatMonthlyDay</code>s into each month
* this appointment should repeat. Only applicable when repeatType is
* REPEAT_MONTHLY_BY_DAY.
*/
public int repeatMonthlyCount = 1;
/**
* Indicates which days of the week to repeat on. Only applicable when
* repeatType is REPEAT_WEEKLY. The array holds 7 elements with Sunday at
* position 0 through to Saturday at position 6. If the value is true, this
* appointment should appear on that day.
*/
public boolean[] repeatWeekdays = new boolean[7];
/**
* Indicated whether the week display should start with Sunday (false) or
* Monday (true). Only applicable with repeatType is REPEAT_WEEKLY and I'm
* not entirely sure why it's here. It's probably best not to mess with it.
*/
public boolean repeatWeekStartsOnMonday;
/** A note giving extra information */
public String note;
/**
* An array of dates in chronological order that the appointment shouldn't
* repeat on. Time information is ignored.
*/
public Time[] exceptions;
/** A description for this appointment */
public String description;
/**
* Constructs a new empty datebook
*/
public Datebook()
{
}
/**
* Send the state information of this object to the given object PDBFile
* using the given DataStream. If any Storable objects need to be saved as
* part of the state, their saveState() method can be called too.
* @throws totalcross.io.IOException
*/
public void saveState(DataStream ds) throws totalcross.io.IOException
{
if (endTime == null) // no time
ds.writeInt(-1); // two empty times in a row
else
{
writeTime(ds, startDate);
writeTime(ds, endTime);
}
writeDate(ds, startDate);
int flags = (alarmAdvance != -1 ? ALARM : 0) | (repeatType != REPEAT_NONE ? REPEAT : 0)
| (note != null ? NOTE : 0) | (exceptions != null ? EXCEPTIONS : 0) | DESCRIPTION;
ds.writeByte(flags);
ds.writeByte(0); // align. - 0x20
if ((flags & ALARM) > 0)
{
ds.writeByte(alarmAdvance);
ds.writeByte(alarmUnits);
}
if ((flags & REPEAT) > 0)
{
ds.writeByte(repeatType);
ds.writeByte(0); // align. - 0x3C
writeDate(ds, repeatEndDate);
ds.writeByte(repeatFrequency);
int repeatOn = 0;
if (repeatType == REPEAT_WEEKLY)
{
if (repeatWeekdays == null || repeatWeekdays.length != 7)
repeatOn = 1;
else
{
for (int i = 0, mult = 1; i < 7; i++, mult <<= 1)
repeatOn |= repeatWeekdays[i] ? mult : 0;
}
}
else if (repeatType == REPEAT_MONTHLY_BY_DAY)
repeatOn = (repeatMonthlyCount - 1) * 7 + repeatMonthlyDay;
ds.writeByte(repeatOn);
ds.writeByte(repeatType == REPEAT_WEEKLY && repeatWeekStartsOnMonday ? 1 : 0);
ds.writeByte(0); // align. 03
}
if ((flags & EXCEPTIONS) > 0)
{
int numExceptions = exceptions.length;
ds.writeShort(numExceptions);
for (int i = 0; i < numExceptions; i++)
writeDate(ds, exceptions[i]);
}
if (description == null)
ds.writeCString("none");
else
ds.writeCString(description);
if ((flags & NOTE) > 0)
ds.writeCString(note);
}
/**
* Load state information from the given DataStream into this object If any
* Storable objects need to be loaded as part of the state, their loadState()
* method can be called too.
*
* @throws totalcross.io.IOException
*/
public void loadState(DataStream ds) throws totalcross.io.IOException
{
int pStartTime = ds.readUnsignedShort();
int pEndTime = ds.readUnsignedShort();
int packedDate = ds.readUnsignedShort();
if (pStartTime == noDateOrTime && packedDate == noDateOrTime)
startDate = null;
else
{
if (startDate == null)
startDate = new Time();
readTime(pStartTime, startDate);
readDate(packedDate, startDate);
}
if (pEndTime == noDateOrTime || pStartTime == noDateOrTime)
endTime = null;
else
{
if (endTime == null)
endTime = new Time();
readTime(pEndTime, endTime);
readDate(packedDate, endTime);
}
int flags = ds.readUnsignedByte();
ds.skipBytes(1);
if ((flags & ALARM) > 0)
{
alarmAdvance = ds.readByte();
alarmUnits = ds.readByte();
}
else
alarmAdvance = -1;
if ((flags & REPEAT) > 0)
{
repeatType = ds.readByte();
ds.skipBytes(1);
packedDate = ds.readUnsignedShort();
if (packedDate == noDateOrTime)
repeatEndDate = null;
else
{
if (repeatEndDate == null)
repeatEndDate = new Time();
readDate(packedDate, repeatEndDate);
}
repeatFrequency = ds.readUnsignedByte();
int repeatOn = ds.readUnsignedByte();
repeatWeekStartsOnMonday = ds.readUnsignedByte() > 0;
if (repeatType == REPEAT_WEEKLY)
{
if (repeatWeekdays == null || repeatWeekdays.length != 7)
repeatWeekdays = new boolean[7];
for (int i = 0, mult = 1; i < 7; i++, mult <<= 1)
repeatWeekdays[i] = (repeatOn & mult) > 0;
}
else
repeatWeekdays = null;
if (repeatType == REPEAT_MONTHLY_BY_DAY)
{
repeatMonthlyDay = repeatOn % 7;
repeatMonthlyCount = repeatOn / 7 + 1;
}
ds.skipBytes(1);
}
else
repeatType = REPEAT_NONE;
if ((flags & EXCEPTIONS) > 0)
{
int numExceptions = ds.readUnsignedShort();
exceptions = new Time[numExceptions];
for (int i = 0; i < numExceptions; i++)
{
exceptions[i] = new Time();
readDate(ds.readUnsignedShort(), exceptions[i]);
}
}
else
exceptions = null;
description = ((flags & DESCRIPTION) > 0) ? ds.readCString() : null;
// notes can be big so free up this one before loading in the
// new one
note = null;
if ((flags & NOTE) > 0)
note = ds.readCString();
}
/**
* Gets a unique ID for this class. It is up to the user to ensure that the
* ID of each class of Storable contained in a single ObjectPDBFile is unique
* and the ID of each instance in a class is the same.
*/
public byte getID()
{
return 0; // don't read or write type byte
}
/**
* Returns an object of the same class as this object.
*
* @return a class. Any data is irrelevent.
*/
public Storable getInstance()
{
return new Datebook();
}
////////////////////////////////////////////////////
// Utility methods for reading and writing dates //
////////////////////////////////////////////////////
/** Palm notation for no date or time */
protected static int noDateOrTime = (1 << 16) - 1;
/**
* Writes the time information to the stream as a Palm TimeType
*
* @param ds
* the stream to write to
* @param time
* the time to write (only hour and minutes used)
* @throws totalcross.io.IOException
*/
protected void writeTime(DataStream ds, Time time) throws totalcross.io.IOException
{
if (time == null)
ds.writeShort(-1);
else
ds.writeShort(((time.hour & 255) << 8) | (time.minute & 255));
}
/**
* Writes the date information to the stream as a Palm DateType
*
* @param ds
* the stream to write to
* @param date
* the date to write (only day, month, year used)
* @throws totalcross.io.IOException
*/
protected void writeDate(DataStream ds, Time date) throws totalcross.io.IOException
{
if (date == null)
ds.writeShort(-1);
else
ds.writeShort((((date.year - 1904) & 127) << 9) | ((date.month & 15) << 5) | (date.day & 31));
}
/**
* Reads time information from a short read from a stream in TimeType format
*
* @param packedTime
* the packed representation
* @param time
* the object to load the information into. (h,m,s,millis set)
*/
protected void readTime(int packedTime, Time time)
{
if (packedTime == noDateOrTime)
time.minute = time.hour = 0;
else
{
time.minute = packedTime & 255;
packedTime >>>= 8;
time.hour = packedTime & 255;
}
time.second = time.millis = 0;
}
/**
* Reads date information from a short read from a stream in DateType format
*
* @param packedDate
* the packed representation
* @param date
* the object to load the information into (day,month,year set)
*/
protected void readDate(int packedDate, Time date)
{
if (packedDate == noDateOrTime)
date.day = date.month = date.year = 0;
else
{
date.day = (packedDate & 31);
packedDate >>>= 5;
date.month = (packedDate & 15);
packedDate >>>= 4;
date.year = (packedDate & 127) + 1904;
}
}
}