package com.androsz.electricsleepbeta.db; import com.androsz.electricsleepbeta.R; import com.androsz.electricsleepbeta.util.PointD; import static com.androsz.electricsleepbeta.db.ElectricSleepProvider.TimestampColumns; import com.google.android.apps.analytics.GoogleAnalyticsTracker; import android.content.ContentValues; import android.content.Context; import android.content.res.Resources; import android.database.Cursor; import android.net.Uri; import android.os.Build.VERSION; import android.os.Build; import android.provider.BaseColumns; import android.text.format.DateUtils; import android.text.format.Time; import android.util.Log; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.StreamCorruptedException; import java.util.ArrayList; import java.util.Calendar; import java.util.List; import java.util.TimeZone; public class SleepSession implements BaseColumns, SleepSessionKeys, TimestampColumns { private static final String TAG = SleepSession.class.getSimpleName(); /** Value that marks whether or not the database row id is valid. */ private static final long ROW_INVALID = 0; /** * Path used to access sleep session both via the provider as well as the * table name. */ public static final String PATH = "sleep_sessions"; public static final Uri CONTENT_URI = ElectricSleepProvider.BASE_CONTENT_URI .buildUpon().appendPath(PATH).build(); /** * The MIME type of {@link #CONTENT_URI}. */ public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.androsz.electricsleepbeta." + PATH; /** * The MIME type of a {@link #CONTENT_URI} sub-directory of a single row. */ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.androsz.electricsleepbeta." + PATH; public static final String DEFAULT_SORT_ORDER = START_TIMESTAMP + " DESC"; public static final String SORT_ORDER_LIMIT_60 = DEFAULT_SORT_ORDER + " LIMIT 60"; static String[] PROJECTION = new String[] { _ID, START_TIMESTAMP, START_JULIAN_DAY, END_TIMESTAMP, TIMEZONE, DATA, DURATION, NOTE, RATING, SPIKES, CALIBRATION_LEVEL, MIN, FELL_ASLEEP_TIMESTAMP, CREATED_ON, UPDATED_ON }; float mCalibrationLevel; List<PointD> mData; long mEndTimestamp; long mId; String mNote; TimeZone mTimezone; double mMin; int mRating; int mSpikes; long mDuration; long mFellAsleepTimestamp; long mStartTimestamp; int mStartJulianDay; long mCreatedOn; long mUpdatedOn; public SleepSession(final Cursor cursor) { mId = cursor.getLong(cursor.getColumnIndex(_ID)); mStartTimestamp = cursor .getLong(cursor.getColumnIndex(START_TIMESTAMP)); mStartJulianDay = cursor .getInt(cursor.getColumnIndex(START_JULIAN_DAY)); mEndTimestamp = cursor.getLong(cursor.getColumnIndex(END_TIMESTAMP)); mTimezone = TimeZone.getTimeZone(cursor.getString(cursor .getColumnIndex(TIMEZONE))); mCalibrationLevel = cursor.getFloat(cursor .getColumnIndex(CALIBRATION_LEVEL)); mMin = cursor.getDouble(cursor.getColumnIndex(MIN)); mRating = cursor.getInt(cursor.getColumnIndex(RATING)); mSpikes = cursor.getInt(cursor.getColumnIndex(SPIKES)); mDuration = cursor.getLong(cursor.getColumnIndex(DURATION)); mFellAsleepTimestamp = cursor.getLong(cursor .getColumnIndex(FELL_ASLEEP_TIMESTAMP)); mNote = cursor.getString(cursor.getColumnIndex(NOTE)); mCreatedOn = cursor.getLong(cursor.getColumnIndex(CREATED_ON)); mUpdatedOn = cursor.getLong(cursor.getColumnIndex(UPDATED_ON)); try { mData = (List<PointD>) byteArrayToObject(cursor.getBlob(cursor .getColumnIndexOrThrow(DATA))); } catch (final Exception e) { GoogleAnalyticsTracker.getInstance().trackEvent( Integer.toString(VERSION.SDK_INT), Build.MODEL, "sleepSessionInstatiation : " + e.getMessage(), 0); } } public SleepSession(final long startTimestamp, final long endTimestamp, final List<PointD> data, final double min, final float calibrationLevel, final int rating, final long duration, final int spikes, final long fellAsleep, final String note) { mStartTimestamp = startTimestamp; mStartJulianDay = getZeoJulianDay(startTimestamp); mEndTimestamp = endTimestamp; mData = data; mMin = min; mCalibrationLevel = calibrationLevel; mRating = rating; mDuration = duration; mSpikes = spikes; mFellAsleepTimestamp = fellAsleep; mNote = note; mTimezone = TimeZone.getDefault(); } public float getCalibrationLevel() { return mCalibrationLevel; } public List<PointD> getData() { return mData; } /** * Return annotation of date / time text such as: Thu, Jan 14 * * WARNING: the following does not follow the 6pm to 6am convention. */ public String getDayText(final Context context) { // TODO The following must follow the 6pm to 6am rule. return DateUtils.formatDateTime(context, mStartTimestamp, DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_ALL); } public long getDuration() { return mDuration; } @Deprecated public CharSequence getDurationText(final Resources res) { return getTimespanText(mDuration, res); } public String getEfficiency() { return Integer.toString(getSleepScore()); } public int getEndJulianDay() { final Time local = new Time(); local.set(getEndTimestamp()); final long millis = local.normalize(true /* ignore DST */); return Time.getJulianDay(millis, local.gmtoff); } public long getEndTimestamp() { return mEndTimestamp; } public long getEndTimeOfDay() { final Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(getEndTimestamp()); return cal.get(Calendar.MINUTE) + (cal.get(Calendar.HOUR_OF_DAY) * 60); } public long getLocalizedStartTimestamp() { return mStartTimestamp + mTimezone.getOffset(mStartTimestamp); } public long getLocalizedEndTimestamp() { return mEndTimestamp + mTimezone.getOffset(mEndTimestamp); } @Deprecated public String getNote() { return mNote; } public String getNotes() { return mNote; } public int getRating() { return mRating; } public int getSleepScore() { int score = 0; final float rating = (getRating() - 1) / 4f; final float deepPct = 1 - ((float) getSpikes() / getData().size()); final float fifteenMinutes = 1000 * 60 * 15; final float eightHours = 1000 * 60 * 60 * 8; final float diffFrom8Hours = 1 - Math.max(0, Math.abs((getDuration() - eightHours)) / eightHours); long timeToFallAsleep = getTimeToFallAsleep(); if (timeToFallAsleep == DID_NOT_FALL_ASLEEP) { // we didn't fall asleep. Give a zero-score? score = 0; } else { final float diffTimeToFallAsleep = fifteenMinutes / Math.max(timeToFallAsleep, fifteenMinutes); score = Math .round((rating + deepPct + diffFrom8Hours + diffTimeToFallAsleep) / 4 * 100); } return score; } public int getSpikes() { return mSpikes; } /** * Computes the julian day for the given timestamp while taking into * consideration Zeo's rules for gauging a night of sleep between the 12am * to 6am time window. */ public static int getZeoJulianDay(final long timestamp) { final Time startTime = new Time(); startTime.set(timestamp); int julianDay = Time.getJulianDay(startTime.normalize(true), startTime.gmtoff); // Begin process of determining if this record was from 12am to 6am. final Time midnight = new Time(); midnight.set(startTime); midnight.hour = 0; midnight.minute = 0; midnight.second = 0; midnight.normalize(true); final Time morning = new Time(); morning.set(midnight); morning.hour = 6; morning.normalize(true); if (Time.compare(startTime, midnight) >= 0 && Time.compare(startTime, morning) <= 0) { --julianDay; } return julianDay; } /** * Return the julian day for this sleep session while taking into account * Zeo's 6pm to 6am rule for determining what date this sleep applies to. * * The Zeo 6pm to 6am rule states that any sleep that occurs from 12am to * 6am is actually sleep representing the day prior. */ public int getStartJulianDay() { return getZeoJulianDay(mStartTimestamp); } public long getStartTimestamp() { return mStartTimestamp; } public long getStartTimeOfDay() { final Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(getStartTimestamp()); return cal.get(Calendar.MINUTE) + (cal.get(Calendar.HOUR_OF_DAY) * 60); } public static final long DID_NOT_FALL_ASLEEP = 0; public long getTimeToFallAsleep() { if (mFellAsleepTimestamp > mStartTimestamp) { return mFellAsleepTimestamp - getStartTimestamp(); } else { return DID_NOT_FALL_ASLEEP; } } public CharSequence getTimeToFallAsleepText(final Resources res) { long timeToFallAsleep = getTimeToFallAsleep(); if (timeToFallAsleep == DID_NOT_FALL_ASLEEP) { return "----"; } return getTimespanText(timeToFallAsleep, res); } public CharSequence getTotalRecordAbbrevTime(final Resources res) { return getTimespanAbbrevText(mDuration, res); } public CharSequence getTotalRecordTime(final Resources res) { return getTimespanText(mDuration, res); } public String getTimesDisrupted() { return Integer.toString(mSpikes); } public TimeZone getTimeZone() { return mTimezone; } /** * Return a title for the night of sleep similar to the form: M/D/Y HH:MM - * HH:MM but of course using the user's Android settings. * * WARNING: timezone is not properly handled here. */ public String getTitle(final Context context) { return DateUtils.formatDateTime(context, mStartTimestamp, DateUtils.FORMAT_ABBREV_ALL | DateUtils.FORMAT_NUMERIC_DATE) + " " + DateUtils.formatDateTime(context, mStartTimestamp, DateUtils.FORMAT_SHOW_TIME) + " - " + DateUtils.formatDateTime(context, mEndTimestamp, DateUtils.FORMAT_SHOW_TIME); } public ContentValues toContentValues() { final ContentValues values = new ContentValues(11); if (mId != ROW_INVALID) { values.put(_ID, mId); } values.put(START_TIMESTAMP, mStartTimestamp); values.put(START_JULIAN_DAY, mStartJulianDay); values.put(END_TIMESTAMP, mEndTimestamp); values.put(TIMEZONE, mTimezone.getID()); values.put(DURATION, mDuration); values.put(NOTE, mNote); values.put(RATING, mRating); values.put(SPIKES, mSpikes); values.put(CALIBRATION_LEVEL, mCalibrationLevel); values.put(MIN, mMin); values.put(FELL_ASLEEP_TIMESTAMP, mFellAsleepTimestamp); try { values.put(DATA, SleepSession.objectToByteArray(mData)); } catch (IOException e) { Log.w(TAG, "Failure to marshall sleep data to byte array."); GoogleAnalyticsTracker.getInstance().trackEvent( Integer.toString(VERSION.SDK_INT), Build.MODEL, "createSessionIOException : " + e.getMessage(), 0); } return values; } public static Object byteArrayToObject(final byte[] bytes) throws StreamCorruptedException, IOException, ClassNotFoundException { final ByteArrayInputStream bais = new ByteArrayInputStream(bytes); final ObjectInputStream ois = new ObjectInputStream(bais); return ois.readObject(); } public static Calendar getTimeDiffCalendar(final long time) { // set calendar to GMT +0 final Calendar timeDiffCalendar = Calendar.getInstance(TimeZone .getTimeZone(TimeZone.getAvailableIDs(0)[0])); timeDiffCalendar.setTimeInMillis(time); return timeDiffCalendar; } public static CharSequence getTimespanAbbrevText(long timespanMs, final Resources res) { final Calendar timespan = getTimeDiffCalendar(timespanMs); final int hours = Math.min(24, timespan.get(Calendar.HOUR_OF_DAY)); final int minutes = timespan.get(Calendar.MINUTE); final int seconds = timespan.get(Calendar.SECOND); StringBuilder builder = new StringBuilder(); if (hours > 0) { builder.append(Integer.toString(hours) + "h"); } if (minutes > 0) { if (hours > 0) { builder.append(" "); } builder.append(Integer.toString(minutes) + "m"); } if (seconds > 0 && hours == 0) { if (hours > 0 || minutes > 0) { builder.append(" "); } builder.append(Integer.toString(seconds) + "s"); } return builder.toString(); } public static CharSequence getTimespanText(long timespanMs, final Resources res) { final Calendar timespan = getTimeDiffCalendar(timespanMs); final int hours = Math.min(24, timespan.get(Calendar.HOUR_OF_DAY)); final int minutes = timespan.get(Calendar.MINUTE); return res.getQuantityString(R.plurals.hour, hours, hours) + " " + res.getQuantityString(R.plurals.minute, minutes, minutes); } public static byte[] objectToByteArray(final Object obj) throws IOException { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(obj); return baos.toByteArray(); } /** * Given a cursor generate a List of long values that contain: start * timestamp end timestamp start julian day end julian day database row id * * WARNING We assume that the cursor given to us will have projection in the * form: new String[] {_ID, START_TIMESTAMP, END_TIMESTAMP, TIMEZONE} */ public static List<Long[]> getStartEndTimestamps(Cursor cursor) { List<Long[]> result = new ArrayList<Long[]>(); if (cursor.moveToFirst()) { do { // TODO this is convoluted final long id = cursor.getLong(0); final long startTimestamp = cursor.getLong(1); final long endTimestamp = cursor.getLong(2); final SleepSession session = new SleepSession(cursor); result.add(new Long[] { startTimestamp, endTimestamp, (long) session.getStartJulianDay(), (long) session.getEndJulianDay(), id }); } while (cursor.moveToNext()); } return result; } } interface SleepSessionKeys { String CALIBRATION_LEVEL = "calibration_level"; String DATA = "data"; String DURATION = "duration"; String END_TIMESTAMP = "end_timestamp"; String FELL_ASLEEP_TIMESTAMP = "fell_asleep_timestamp"; String MIN = "min"; String NOTE = "note"; String RATING = "rating"; String SLEEP_DATA = "data"; String SPIKES = "spikes"; String START_JULIAN_DAY = "start_julian_day"; String START_TIMESTAMP = "start_timestamp"; String TIMEZONE = "timezone"; }