Java tutorial
/******************************************************************************* * Copyright (c) 2013 See AUTHORS file. * * This file is part of SleepFighter. * * SleepFighter is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * SleepFighter 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. * See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with SleepFighter. If not, see <http://www.gnu.org/licenses/>. ******************************************************************************/ package se.toxbee.sleepfighter.model; import java.util.Arrays; import java.util.Map; import org.joda.time.DateTime; import org.joda.time.DateTimeConstants; import org.joda.time.MutableDateTime; import org.joda.time.ReadableDateTime; import se.toxbee.sleepfighter.model.audio.AudioConfig; import se.toxbee.sleepfighter.model.audio.AudioSource; import se.toxbee.sleepfighter.model.audio.AudioSourceType; import se.toxbee.sleepfighter.model.challenge.ChallengeConfigSet; import se.toxbee.sleepfighter.utils.message.Message; import se.toxbee.sleepfighter.utils.message.MessageBus; import se.toxbee.sleepfighter.utils.message.MessageBusHolder; import se.toxbee.sleepfighter.utils.model.IdProvider; import se.toxbee.sleepfighter.utils.string.StringUtils; import android.provider.Settings; import com.google.common.base.Preconditions; import com.google.common.collect.Maps; import com.j256.ormlite.field.DatabaseField; import com.j256.ormlite.table.DatabaseTable; /** * Alarm models the alarm settings and business logic for an alarm. * * Actual model fields are described in {@link Field} * * @version 1.0 * @since Sep 16, 2013 */ @DatabaseTable(tableName = "alarm") public class Alarm implements IdProvider, MessageBusHolder { /** * Enumeration of fields in an Alarm. * * @author Centril<twingoow@gmail.com> / Mazdak Farrokhzad. * @version 1.0 * @since Sep 19, 2013 */ public static enum Field { TIME, ACTIVATED, ENABLED_DAYS, NAME, ID, REPEATING, AUDIO_SOURCE, AUDIO_CONFIG, SPEECH, FLASH } /* -------------------------------- * Defined Events. * -------------------------------- */ /** * All events fired by {@link Alarm} extends this interface. * * @author Centril<twingoow@gmail.com> / Mazdak Farrokhzad. * @version 1.0 * @since Sep 19, 2013 */ public static interface AlarmEvent extends Message { /** * Returns the alarm that triggered the event. * * @return the alarm. */ public Alarm getAlarm(); /** * Returns the Field that was modified in the alarm. * * @return the modified field. */ public Field getModifiedField(); /** * Returns the old value. * * @return the old value. */ public Object getOldValue(); } /** * Base implementation of AlarmEvent * * @author Centril<twingoow@gmail.com> / Mazdak Farrokhzad. * @version 1.0 * @since Sep 19, 2013 */ public static class BaseAlarmEvent implements AlarmEvent { private Field field; private Alarm alarm; private Object oldValue; private BaseAlarmEvent(Alarm alarm, Field field, Object oldValue) { this.alarm = alarm; this.field = field; this.oldValue = oldValue; } public Alarm getAlarm() { return this.alarm; } public Field getModifiedField() { return this.field; } @Override public Object getOldValue() { return this.oldValue; } } /** * ScheduleChangeEvent occurs when a scheduling related constraint is modified, these are:<br/> * <ul> * <li>{@link Field#TIME}</li> * <li>{@link Field#ACTIVATED}</li> * <li>{@link Field#ENABLED_DAYS}</li> * </ul> * * @author Centril<twingoow@gmail.com> / Mazdak Farrokhzad. * @version 1.0 * @since Sep 19, 2013 */ public static class ScheduleChangeEvent extends BaseAlarmEvent { private ScheduleChangeEvent(Alarm alarm, Field field, Object oldValue) { super(alarm, field, oldValue); } } /** * MetaChangeEvent occurs when a name related constraint is modified, these are:<br/> * <ul> * <li>{@link Field#NAME}</li> * <li>{@link Field#ID}</li> * </ul> * * @author Centril<twingoow@gmail.com> / Mazdak Farrokhzad. * @version 1.0 * @since Sep 19, 2013 */ public static class MetaChangeEvent extends BaseAlarmEvent { private MetaChangeEvent(Alarm alarm, Field field, Object oldValue) { super(alarm, field, oldValue); } } /** * AudioChangeEvent occurs when a audio related constraint is modified, these are:<br/> * <ul> * <li>{@link Field#AUDIO_SOURCE}</li> * <li>{@link Field#AUDIO_CONFIG}</li> * </ul> * * * @author Centril<twingoow@gmail.com> / Mazdak Farrokhzad. * @version 1.0 * @since Sep 27, 2013 */ public static class AudioChangeEvent extends BaseAlarmEvent { private AudioChangeEvent(Alarm alarm, Field field, Object oldValue) { super(alarm, field, oldValue); } } /* -------------------------------- * Fields. * -------------------------------- */ @DatabaseField(generatedId = true) private int id = NOT_COMMITTED_ID; /** IDs for non-committed Alarms. */ public static final int NOT_COMMITTED_ID = -1; @DatabaseField private boolean isActivated; @DatabaseField private String name; /** The value for unnamed strings is {@value #UNNAMED} */ public static final String UNNAMED = null; @DatabaseField private int hour; @DatabaseField private int minute; @DatabaseField private int second; /** The weekdays that this alarm can ring. */ @DatabaseField(width = 7) private boolean[] enabledDays = { true, true, true, true, true, true, true }; private static final int MAX_WEEK_LENGTH = DateTimeConstants.DAYS_PER_WEEK; private static final int MAX_WEEK_INDEX = MAX_WEEK_LENGTH - 1; @DatabaseField private int unnamedPlacement; @DatabaseField private boolean isRepeating = false; @DatabaseField private boolean isFlash = false; // whether this alarm is the preset alarm(the default alarm) @DatabaseField private boolean isPresetAlarm = false; @DatabaseField(foreign = true, canBeNull = true) private AudioSource audioSource = new AudioSource(AudioSourceType.RINGTONE, Settings.System.DEFAULT_ALARM_ALERT_URI.toString()); @DatabaseField(foreign = true, canBeNull = false) private AudioConfig audioConfig = new AudioConfig(100, true); @DatabaseField(foreign = true, canBeNull = false) private SnoozeConfig snoozeConfig = new SnoozeConfig(true, 9); @DatabaseField // the time and weather will be read out when the alarm goes off. private boolean isSpeech = false; /** The value {@link #getNextMillis()} returns when Alarm can't happen. */ public static final Long NEXT_NON_REAL = null; @DatabaseField(foreign = true, canBeNull = false) private ChallengeConfigSet challenges = new ChallengeConfigSet(true); private MessageBus<Message> bus; /* -------------------------------- * Constructors. * -------------------------------- */ /** * Constructs an alarm to current time. */ public Alarm() { this(new DateTime()); } /** * Copy constructor * * @param rhs the alarm to copy from. */ public Alarm(Alarm rhs) { // Reset id. this.setId(NOT_COMMITTED_ID); // Copy data. this.hour = rhs.hour; this.minute = rhs.minute; this.second = rhs.second; this.isActivated = rhs.isActivated; this.enabledDays = rhs.enabledDays; this.name = rhs.name; this.isRepeating = rhs.isRepeating; this.unnamedPlacement = 0; // Copy dependencies. this.bus = rhs.bus; this.audioSource = new AudioSource(rhs.audioSource); this.audioConfig = new AudioConfig(rhs.audioConfig); this.snoozeConfig = new SnoozeConfig(rhs.snoozeConfig); this.challenges = new ChallengeConfigSet(rhs.challenges); this.isSpeech = rhs.isSpeech; this.isFlash = rhs.isFlash; } /** * Constructs an alarm given an hour and minute. * * @param hour the hour the alarm should occur. * @param minute the minute the alarm should occur. */ public Alarm(int hour, int minute) { this(hour, minute, 0); } /** * Constructs an alarm given an hour, minute and, second. * * @param hour the hour the alarm should occur. * @param minute the minute the alarm should occur. * @param second the second the alarm should occur. */ public Alarm(int hour, int minute, int second) { this.setTime(hour, minute, second); } /** * Constructs an alarm with values derived from a unix epoch timestamp. * * @param time time in unix epoch timestamp. */ public Alarm(long time) { this.setTime(time); } /** * Sets the hour, minute and second of this alarm derived from a {@link ReadableDateTime} object. * * @param time a {@link ReadableDateTime} object. */ public Alarm(ReadableDateTime time) { this.setTime(time); } /* -------------------------------- * Public methods. * -------------------------------- */ /** * Sets the message bus, if not set, no events will be received. * * @param bus the buss that receives events. */ public void setMessageBus(MessageBus<Message> bus) { this.bus = bus; // Pass it on! this.challenges.setMessageBus(bus); this.audioConfig.setMessageBus(bus); this.snoozeConfig.setMessageBus(bus); } /** * Returns the message bus, or null if not set. * * @return the message bus. */ public MessageBus<Message> getMessageBus() { return this.bus; } /** * Returns the ID of the alarm. * * @return the ID of the alarm. */ public int getId() { return this.id; } /** * Sets the ID of the alarm.<br/> * Should only be used for testing. * * @param id the ID of the alarm. */ public void setId(int id) { if (this.id == id) { return; } int old = this.id; this.id = id; this.publish(new MetaChangeEvent(this, Field.ID, old)); } /** * Returns the name of the Alarm. * * @return the name of the Alarm. */ public String getName() { return name; } /** * Sets the name of the Alarm. * * @param name the name of the Alarm to set. */ public void setName(String name) { if (this.name != null && this.name.equals(name)) { return; } if (name == null) { if (this.name == null) { return; } throw new IllegalArgumentException("A named Alarm can not be unnamed."); } String old = this.name; this.name = name; this.unnamedPlacement = 0; this.publish(new MetaChangeEvent(this, Field.NAME, old)); } /** * Sets the hour and minute of this alarm.<br/> * This is the equivalent of {@link #setTime(int, int, int)} with <code>(hour, minute, 0)</code>. * * @param hour the hour the alarm should occur. * @param minute the minute the alarm should occur. */ public synchronized void setTime(int hour, int minute) { this.setTime(hour, minute, 0); } /** * Sets the hour, minute and second of this alarm. * * @param hour the hour the alarm should occur. * @param minute the minute the alarm should occur. * @param second the second the alarm should occur. */ public synchronized void setTime(int hour, int minute, int second) { if (this.hour == hour && this.minute == minute && this.second == second) { return; } if (hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59) { throw new IllegalArgumentException(); } int[] old = new int[] { this.hour, this.minute, this.second }; this.hour = hour; this.minute = minute; this.second = second; this.publish(new ScheduleChangeEvent(this, Field.TIME, old)); } /** * Sets the hour, minute and second of this alarm derived from a unix epoch timestamp. * * @param time time in unix epoch timestamp. */ public synchronized void setTime(long time) { this.setTime(new DateTime(time)); } /** * Sets the hour, minute and second of this alarm derived from a {@link ReadableDateTime} object. * * @param time a {@link ReadableDateTime} object. */ public synchronized void setTime(ReadableDateTime time) { this.setTime(time.getHourOfDay(), time.getMinuteOfHour(), time.getSecondOfMinute()); } /** * Returns the weekdays days this alarm is enabled for.<br/> * For performance, a direct reference is returned. * * @return the weekdays alarm is enabled for. */ public synchronized boolean[] getEnabledDays() { return this.enabledDays.clone(); } /** * Sets the weekdays this alarm is enabled for.<br/> * For performance, the passed value is not cloned. * * @param enabledDays the weekdays alarm should be enabled for. */ public synchronized void setEnabledDays(boolean[] enabledDays) { Preconditions.checkNotNull(enabledDays); if (enabledDays.length != MAX_WEEK_LENGTH) { throw new IllegalArgumentException( "A week has 7 days, but an array with: " + enabledDays.length + " was passed"); } boolean[] old = this.enabledDays; this.enabledDays = enabledDays.clone(); this.publish(new ScheduleChangeEvent(this, Field.ENABLED_DAYS, old)); } /** * Returns when this alarm will ring.<br/> * If {@link #canHappen()} returns false, -1 will be returned. * * @param now the current time in unix epoch timestamp. * @return the time in unix epoch timestamp when alarm will next ring. */ public synchronized Long getNextMillis(long now) { if (!this.canHappen()) { return NEXT_NON_REAL; } MutableDateTime next = new MutableDateTime(now); next.setHourOfDay(this.hour); next.setMinuteOfHour(this.minute); next.setSecondOfMinute(this.second); // Check if alarm was earlier today. If so, move to next day if (next.isBefore(now)) { next.addDays(1); } // Offset for weekdays int offset = 0; // first weekday to check (0-6), getDayOfWeek returns (1-7) int weekday = next.getDayOfWeek() - 1; // Find the weekday the alarm should run, should at most run seven times for (int i = 0; i < 7; i++) { // Wrap to first weekday if (weekday > MAX_WEEK_INDEX) { weekday = 0; } if (this.enabledDays[weekday]) { // We've found the closest day the alarm is enabled for offset = i; break; } weekday++; offset++; } if (offset > 0) { next.addDays(offset); } return next.getMillis(); } /** * Returns true if the alarm can ring in the future,<br/> * that is: if {@link #isActivated()} and some weekday is enabled. * * @return true if the alarm can ring in the future. */ public synchronized boolean canHappen() { if (!this.isActivated()) { return false; } for (boolean day : this.enabledDays) { if (day == true) { return true; } } return false; } /** * Returns the hour the alarm occurs. * * @return the hour the alarm occurs. */ public int getHour() { return this.hour; } /** * Returns the minute the alarm occurs. * * @return the minute the alarm occurs. */ public int getMinute() { return this.minute; } /** * Returns the second the alarm occurs. * * @return the second the alarm occurs. */ public int getSecond() { return this.second; } /** * Sets whether or not the alarm should be active. * * @param isActivated whether or not the alarm should be active. */ public void setActivated(boolean isActivated) { if (this.isActivated == isActivated) { return; } boolean old = this.isActivated; this.isActivated = isActivated; this.publish(new ScheduleChangeEvent(this, Field.ACTIVATED, old)); } /** * Returns true if the alarm is active. * * @return true if the alarm is active. */ public boolean isActivated() { return this.isActivated; } /** * Returns true if the alarm is unnamed = ({@link #getName()} == {@link #UNNAMED}. * * @return true if the alarm is unnamed. */ public boolean isUnnamed() { return this.name == UNNAMED; } /** * Returns the unnamed placement.<br/> * The unnamed placement is a number that is set for Alarms that honor {@link #isUnnamed()}.<br/> * It is a leaping number that is set upon addition to {@link AlarmList#add(int, Alarm)}. * * Let us assume that we have 3 alarms which all start out as unnamed. * Their placements will be 1,2,3,4. * * When the second alarm is removed, or renamed to "Holidays", * the 3rd alarm will not change its placement to 2. * * If then a 4th alarm is added, it will usurp the place that the 2nd alarm had, * and its unnamed placement will be 2. */ public int getUnnamedPlacement() { return this.unnamedPlacement; } /** * Sets the unnamed placement of alarm, see {@link #getUnnamedPlacement()}.<br/> * This should be set directly after constructor, otherwise provided for testing. * * @param placement the unnamed placement of alarm. * @throws IllegalArgumentException if {@link #isUnnamed()} returns false. */ public void setUnnamedPlacement(int placement) { if (!this.isUnnamed()) { throw new IllegalArgumentException("Can't set numeric placement when alarm is already named."); } this.unnamedPlacement = placement; } @Override public String toString() { final Map<String, String> prop = Maps.newHashMap(); prop.put("id", Integer.toString(this.getId())); prop.put("name", this.getName()); prop.put("time", StringUtils.joinTime(this.hour, this.minute, this.second)); prop.put("weekdays", Arrays.toString(this.enabledDays)); prop.put("activated", Boolean.toString(this.isActivated())); prop.put("repeating", Boolean.toString(this.isRepeating())); prop.put("audio_source", this.getAudioSource() == null ? null : this.getAudioSource().toString()); prop.put("audio_config", this.getAudioConfig().toString()); return "Alarm[" + StringUtils.PROPERTY_MAP_JOINER.join(prop) + "]"; } /** * Returns the Alarm:s time in the format: hh:mm. * * @return the formatted time. */ public String getTimeString() { return StringUtils.joinTime(this.getHour(), this.getMinute()); } /** * <p><code>{@link Alarm#hashCode()} == {@link Alarm#getId()}</code></p> * {@inheritDoc} */ @Override public int hashCode() { return this.id; } /** * <p>Two alarms are considered equal iff <code>{@link Alarm#hashCode()} == {@link Alarm#getId()}</code></p> * {@inheritDoc} */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } Alarm rhs = (Alarm) obj; return this.id == rhs.id; } /** * Sets if the alarm is repeating or not. * * @param isRepeating true if it is repeating. */ public void setRepeat(boolean isRepeating) { if (this.isRepeating == isRepeating) { return; } boolean old = this.isRepeating; this.isRepeating = isRepeating; this.publish(new ScheduleChangeEvent(this, Field.REPEATING, old)); } /** * Returns whether or not this alarm is repeating or not. * * @return true if it is repeating. */ public boolean isRepeating() { return this.isRepeating; } /** * Sets if the alarm is flashing or not. * * @param isFlash true if it is flashing. */ public void setFlash(boolean isFlash) { if (this.isFlash == isFlash) { return; } boolean old = this.isFlash; this.isFlash = isFlash; this.publish(new BaseAlarmEvent(this, Field.FLASH, old)); } /** * Returns whether or not this alarm is flashing or not. * * @return true if it is flashing. */ public boolean isFlashEnabled() { return this.isFlash; } // if true, then the time and weather will be read out when the alarm goes off. public boolean isSpeech() { return this.isSpeech; } public void setSpeech(boolean isSpeech) { if (this.isSpeech == isSpeech) { return; } boolean old = this.isSpeech; this.isSpeech = isSpeech; this.publish(new BaseAlarmEvent(this, Field.SPEECH, old)); } /** * Sets the audio source for this alarm. * * @param source the audio source to set. */ public void setAudioSource(AudioSource source) { if (this.audioSource == source) { return; } AudioSource old = this.audioSource; this.audioSource = source; this.publish(new AudioChangeEvent(this, Field.AUDIO_SOURCE, old)); } /** * Returns the audio source of this Alarm. * * @return the audio source. */ public AudioSource getAudioSource() { return this.audioSource; } /** * Returns the audio configuration for this alarm. * * @return the audio configuration. */ public AudioConfig getAudioConfig() { return this.audioConfig; } /** * Returns the snooze configuration for the alarm. * * @return the snooze configuration */ public SnoozeConfig getSnoozeConfig() { return this.snoozeConfig; } /** * Returns the ChallengeConfigSet for this alarm.<br/> * Modifications may be done directly to the returned set<br/> * as it is a well isolated module/unit. * * @return the ChallengeConfigSet object. */ public ChallengeConfigSet getChallengeSet() { return this.challenges; } /* -------------------------------- * PERSISTENCE ONLY METHODS. * -------------------------------- */ /** * <p><strong>NOTE:</strong> this method is only intended for persistence purposes.<br/> * This method is motivated and needed due to OrmLite not supporting results from joins.<br/> * This is also a better method than reflection which is particularly expensive on android.</p> * * <p>Sets the {@link AudioConfig}, bypassing any and all checks, and does not send any event to bus.</p> * * @param config the {@link AudioConfig} to set. */ public void setFetched(AudioConfig config) { this.audioConfig = config; } /** * <p><strong>NOTE:</strong> this method is only intended for persistence purposes.<br/> * This method is motivated and needed due to OrmLite not supporting results from joins.<br/> * This is also a better method than reflection which is particularly expensive on android.</p> * * <p>Sets the {@link AudioSource}, bypassing any and all checks, and does not send any event to bus.</p> * * @param source the {@link AudioSource} to set. */ public void setFetched(AudioSource source) { this.audioSource = source; } /** * <p><strong>NOTE:</strong> this method is only intended for persistence purposes.<br/> * This method is motivated and needed due to OrmLite not supporting results from joins.<br/> * This is also a better method than reflection which is particularly expensive on android.</p> * * <p>Sets the {@link SnoozeConfig}, bypassing any and all checks, and does not send any event to bus.</p> * * @param source the {@link SnoozeConfig} to set. */ public void setFetched(SnoozeConfig config) { this.snoozeConfig = config; } /** * <p><strong>NOTE:</strong> this method is only intended for persistence purposes and factorization.<br/> * This method is motivated and needed due to OrmLite not supporting results from joins.<br/> * This is also a better method than reflection which is particularly expensive on android.</p> * * <p>Sets the {@link ChallengeConfigSet}, bypassing any and all checks, and does not send any event to bus.</p> * * @param challenges the {@link ChallengeConfigSet} to set. */ public void setChallenges(ChallengeConfigSet challenges) { this.challenges = challenges; this.challenges.setMessageBus(this.getMessageBus()); } /** * Sets whether or not this is a preset alarm. * * @param isPresetAlarm true if it is a preset alarm, otherwise false. */ public void setIsPresetAlarm(boolean isPresetAlarm) { this.isPresetAlarm = isPresetAlarm; } /** * Returns whether or not this is a preset alarm. * * @return true if it is a preset alarm, otherwise false. */ public boolean isPresetAlarm() { return this.isPresetAlarm; } /* -------------------------------- * Private Methods. * -------------------------------- */ /** * Publishes an event to event bus. * * @param event the event to publish. */ private void publish(AlarmEvent event) { if (this.bus == null) { return; } this.bus.publish(event); } }