Java tutorial
/* * This file is part of TrackWorkTime (TWT). * * TWT 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. * * TWT 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 TWT. If not, see <http://www.gnu.org/licenses/>. */ package org.zephyrsoft.trackworktime.timer; import hirondelle.date4j.DateTime; import hirondelle.date4j.DateTime.DayOverflow; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; import java.util.Collection; import java.util.List; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import org.apache.commons.lang3.StringUtils; import org.zephyrsoft.trackworktime.Basics; import org.zephyrsoft.trackworktime.R; import org.zephyrsoft.trackworktime.database.DAO; import org.zephyrsoft.trackworktime.location.TrackingMethod; import org.zephyrsoft.trackworktime.model.Event; import org.zephyrsoft.trackworktime.model.PeriodEnum; import org.zephyrsoft.trackworktime.model.Task; import org.zephyrsoft.trackworktime.model.TimeSum; import org.zephyrsoft.trackworktime.model.TypeEnum; import org.zephyrsoft.trackworktime.model.Week; import org.zephyrsoft.trackworktime.model.WeekDayEnum; import org.zephyrsoft.trackworktime.model.WeekPlaceholder; import org.zephyrsoft.trackworktime.options.Key; import org.zephyrsoft.trackworktime.util.DateTimeUtil; import org.zephyrsoft.trackworktime.util.Logger; /** * Manages the time tracking. * * @author Mathis Dirksen-Thedens */ public class TimerManager { private final DAO dao; private final SharedPreferences preferences; private final Context context; /** * Constructor */ public TimerManager(DAO dao, SharedPreferences preferences, Context context) { this.dao = dao; this.preferences = preferences; this.context = context; } public void insertDefaultWorkTimes(DateTime from, DateTime to, Integer taskId, String text) { DateTime running = from.getStartOfDay(); DateTime target = to.getStartOfDay(); while (running.lteq(target)) { // clock-in at start of day (00:00) WeekDayEnum weekDay = WeekDayEnum.getByValue(running.getWeekDay()); int workDuration = getNormalWorkDurationFor(weekDay); if (workDuration > 0) { createEvent(running, taskId, TypeEnum.CLOCK_IN, text); DateTime clockOutTime = running.plus(0, 0, 0, 0, workDuration, 0, DayOverflow.Spillover); if (isAutoPauseApplicable(clockOutTime)) { int pauseDuration = getAutoPauseDuration(clockOutTime); clockOutTime = clockOutTime.plus(0, 0, 0, 0, pauseDuration, 0, DayOverflow.Spillover); } createEvent(clockOutTime, null, TypeEnum.CLOCK_OUT, null); } running = running.plusDays(1); } } /** * Checks the current state. * * @return {@code true} if currently clocked in, {@code false} otherwise */ public boolean isTracking() { Event latestEvent = dao.getLastEventBeforeIncluding(DateTimeUtil.getCurrentDateTime()); return latestEvent == null ? false : latestEvent.getType().equals(TypeEnum.CLOCK_IN.getValue()); } /** * Returns {@code true} if the options are set in a way that an event is in the defined time before/after an * existing event (not counting CLOCK_OUT_NOW). */ public boolean isInIgnorePeriodForLocationBasedTracking() { DateTime now = DateTimeUtil.getCurrentDateTime(); // get first event AFTER now, subtract the minutes to ignore before events and check if the result is BEFORE now Event firstAfterNow = dao.getFirstEventAfter(now); String ignoreBeforeString = preferences .getString(Key.LOCATION_BASED_TRACKING_IGNORE_BEFORE_EVENTS.getName(), "0"); int ignoreBefore = 0; try { ignoreBefore = Integer.parseInt(ignoreBeforeString); } catch (NumberFormatException nfe) { Logger.warn("illegal value - ignore before events: {0}", ignoreBeforeString); } if (firstAfterNow != null) { DateTime firstAfterNowTime = DateTimeUtil.stringToDateTime(firstAfterNow.getTime()); if (firstAfterNowTime.minus(0, 0, 0, 0, ignoreBefore, 0, DayOverflow.Spillover).lt(now)) { return true; } } // get the last event BEFORE now, add the minutes to ignore after events and check if the result is AFTER now Event lastBeforeNow = dao.getLastEventBefore(now); String ignoreAfterString = preferences.getString(Key.LOCATION_BASED_TRACKING_IGNORE_AFTER_EVENTS.getName(), "0"); int ignoreAfter = 0; try { ignoreAfter = Integer.parseInt(ignoreAfterString); } catch (NumberFormatException nfe) { Logger.warn("illegal value - ignore after events: {0}", ignoreAfterString); } if (lastBeforeNow != null) { DateTime lastBeforeNowTime = DateTimeUtil.stringToDateTime(lastBeforeNow.getTime()); if (lastBeforeNowTime.plus(0, 0, 0, 0, ignoreAfter, 0, DayOverflow.Spillover).gt(now)) { return true; } } return false; } /** * Returns the currently active task or {@code null} if tracking is disabled at the moment. */ public Task getCurrentTask() { Event latestEvent = dao.getLastEventBefore(DateTimeUtil.getCurrentDateTime()); if (latestEvent != null && latestEvent.getType().equals(TypeEnum.CLOCK_IN.getValue())) { return dao.getTask(latestEvent.getTask()); } else { return null; } } /** * Either starts tracking (from non-tracked time) or changes the task and/or text (from already tracked time). * If the task is {@code null}, the default task is taken. If there is no default task, no task will be taken. * * @param minutesToPredate * how many minutes in the future should the event be * @param selectedTask * the task for which the time shall be tracked * @param text * free text to describe in detail what was done */ public void startTracking(int minutesToPredate, Task selectedTask, String text) { Task taskToLink = selectedTask == null ? null : selectedTask; if (taskToLink == null) { taskToLink = dao.getDefaultTask(); } createEvent(minutesToPredate, (taskToLink == null ? null : taskToLink.getId()), TypeEnum.CLOCK_IN, text); Basics.getInstance().safeCheckPersistentNotification(); } /** * Stops tracking time. * * @param minutesToPredate * how many minutes in the future should the event be */ public void stopTracking(int minutesToPredate) { createEvent(minutesToPredate, null, TypeEnum.CLOCK_OUT, null); Basics.getInstance().safeCheckPersistentNotification(); } /** * Is the event a clock-in event? {@code null} as argument will return {@code false}. */ public static boolean isClockInEvent(Event event) { return event != null && event.getType().equals(TypeEnum.CLOCK_IN.getValue()); } /** * Is the event a clock-out event? {@code null} as argument will return {@code false}. */ public static boolean isClockOutEvent(Event event) { return event != null && (event.getType().equals(TypeEnum.CLOCK_OUT.getValue()) || event.getType().equals(TypeEnum.CLOCK_OUT_NOW.getValue())); } /** * Calculate a time sum for a given period. */ public TimeSum calculateTimeSum(DateTime date, PeriodEnum periodEnum) { // TODO restructure to clarify! Logger.debug("calculating time sum for {0} containing {1}", periodEnum.name(), DateTimeUtil.dateTimeToString(date)); TimeSum ret = new TimeSum(); DateTime beginOfPeriod = null; DateTime endOfPeriod = null; List<Event> events = null; if (periodEnum == PeriodEnum.DAY) { beginOfPeriod = date.getStartOfDay(); endOfPeriod = beginOfPeriod.plusDays(1); events = dao.getEventsOnDay(date); } else if (periodEnum == PeriodEnum.WEEK) { beginOfPeriod = DateTimeUtil.getWeekStart(date); endOfPeriod = beginOfPeriod.plusDays(7); Week week = dao.getWeek(DateTimeUtil.dateTimeToString(beginOfPeriod)); events = dao.getEventsInWeek(week); } else { throw new IllegalArgumentException("unknown period type"); } Event lastEventBefore = dao.getLastEventBefore(beginOfPeriod); DateTime lastEventBeforeTime = (lastEventBefore == null ? null : DateTimeUtil.stringToDateTime(lastEventBefore.getTime())); Event firstEventAfterNow = dao.getFirstEventAfter(DateTimeUtil.getCurrentDateTime()); DateTime firstEventAfterNowTime = (firstEventAfterNow == null ? null : DateTimeUtil.stringToDateTime(firstEventAfterNow.getTime())); DateTime clockedInSince = null; if (isClockInEvent(lastEventBefore) // but only if no CLOCK_OUT_NOW would be in between: && !(lastEventBeforeTime != null && DateTimeUtil.isInPast(lastEventBeforeTime) && ((events.isEmpty() && (firstEventAfterNow == null || DateTimeUtil.isInFuture(firstEventAfterNowTime))) || (!events.isEmpty() && DateTimeUtil .isInFuture(DateTimeUtil.stringToDateTime(events.get(0).getTime())) && isClockInEvent(events.get(0)))))) { clockedInSince = beginOfPeriod; } Event lastEvent = (events.isEmpty() ? null : events.get(events.size() - 1)); DateTime lastEventTime = (lastEvent == null ? null : DateTimeUtil.stringToDateTime(lastEvent.getTime())); // insert CLOCK_OUT_NOW event if applicable if (isClockInEvent(lastEvent) && DateTimeUtil.isInPast(lastEventTime) && DateTimeUtil.isInFuture(endOfPeriod)) { Event clockOutNowEvent = createClockOutNowEvent(); events.add(clockOutNowEvent); lastEvent = clockOutNowEvent; } for (Event event : events) { DateTime eventTime = DateTimeUtil.stringToDateTime(event.getTime()); // clock-in event while not clocked in? => remember time if (clockedInSince == null && isClockInEvent(event)) { clockedInSince = eventTime; } // clock-out event while clocked in? => add time since last clock-in to result if (clockedInSince != null && isClockOutEvent(event)) { ret.substract(clockedInSince.getHour(), clockedInSince.getMinute()); ret.add(eventTime.getHour(), eventTime.getMinute()); clockedInSince = null; } } if (lastEvent != null && lastEvent.getType().equals(TypeEnum.CLOCK_OUT_NOW.getValue())) { // try to substract the auto-pause for today because it might be not counted in the database yet DateTime eventTime = DateTimeUtil.stringToDateTime(lastEvent.getTime()); if (isAutoPauseEnabled() && isAutoPauseApplicable(eventTime)) { DateTime autoPauseBegin = getAutoPauseBegin(eventTime); DateTime autoPauseEnd = getAutoPauseEnd(eventTime); ret.substract(autoPauseEnd.getHour(), autoPauseEnd.getMinute()); ret.add(autoPauseBegin.getHour(), autoPauseBegin.getMinute()); } } if (clockedInSince != null) { // still clocked in at end of period: count hours and minutes ret.substract(clockedInSince.getHour(), clockedInSince.getMinute()); ret.add(24, 0); if (periodEnum == PeriodEnum.WEEK) { // calculating for week: also count days DateTime counting = clockedInSince.plusDays(1); while (counting.lt(endOfPeriod)) { ret.add(24, 0); counting = counting.plusDays(1); } } } return ret; } /** * Get the possible finishing time for today. Takes into account the target work time for the week and also if this * is the last day in the working week. * * @param includeFlexiTime * use flexi overtime to reduce the working time - ONLY ON LAST WORKING DAY OF THE WEEK! * @return {@code null} either if today is not a work day (as defined in the options) or if the regular working time * for today is already over */ public DateTime getFinishingTime(boolean includeFlexiTime) { DateTime dateTime = DateTimeUtil.getCurrentDateTime(); WeekDayEnum weekDay = WeekDayEnum.getByValue(dateTime.getWeekDay()); if (isWorkDay(weekDay)) { TimeSum alreadyWorked = null; TimeSum target = null; boolean onEveryWorkingDayOfTheWeek = preferences .getBoolean(Key.FLEXI_TIME_TO_ZERO_ON_EVERY_DAY.getName(), false); if (!isFollowedByWorkDay(weekDay) || onEveryWorkingDayOfTheWeek) { alreadyWorked = calculateTimeSum(dateTime, PeriodEnum.WEEK); if (includeFlexiTime) { // add flexi balance from week start TimeSum flexiBalance = getFlexiBalanceAtWeekStart(DateTimeUtil.getWeekStart(dateTime)); alreadyWorked.addOrSubstract(flexiBalance); } final String targetValueString = preferences.getString(Key.FLEXI_TIME_TARGET.getName(), "0:00"); final TimeSum targetTimePerWeek = parseHoursMinutesString(targetValueString); final TimeSum targetTimePerDay = new TimeSum(); targetTimePerDay.add(0, targetTimePerWeek.getAsMinutes() / countWorkDays()); DateTime weekStart = DateTimeUtil.getWeekStart(dateTime); target = new TimeSum(); target.addOrSubstract(targetTimePerDay); // add today as well while (weekStart.getWeekDay() != dateTime.getWeekDay()) { target.addOrSubstract(targetTimePerDay); weekStart = weekStart.plusDays(1); } } else { // not the last work day of the week, only calculate the rest of the daily working time alreadyWorked = calculateTimeSum(dateTime, PeriodEnum.DAY); int targetMinutes = getNormalWorkDurationFor(weekDay); target = new TimeSum(); target.add(0, targetMinutes); } Logger.debug("alreadyWorked={0}", alreadyWorked.toString()); Logger.debug("target={0}", target.toString()); Logger.debug("isAutoPauseEnabled={0}", isAutoPauseEnabled()); Logger.debug("isAutoPauseTheoreticallyApplicable={0}", isAutoPauseTheoreticallyApplicable(dateTime)); Logger.debug("isAutoPauseApplicable={0}", isAutoPauseApplicable(dateTime)); if (isAutoPauseEnabled() && isAutoPauseTheoreticallyApplicable(dateTime) && !isAutoPauseApplicable(dateTime)) { // auto-pause is necessary, but was NOT already taken into account by calculateTimeSum(): Logger.debug( "auto-pause is necessary, but was NOT already taken into account by calculateTimeSum()"); DateTime autoPauseBegin = getAutoPauseBegin(dateTime); DateTime autoPauseEnd = getAutoPauseEnd(dateTime); alreadyWorked.substract(autoPauseEnd.getHour(), autoPauseEnd.getMinute()); alreadyWorked.add(autoPauseBegin.getHour(), autoPauseBegin.getMinute()); } int minutesRemaining = target.getAsMinutes() - alreadyWorked.getAsMinutes(); Logger.debug("minutesRemaining={0}", minutesRemaining); if (minutesRemaining >= 0) { return dateTime.plus(0, 0, 0, 0, minutesRemaining, 0, DayOverflow.Spillover); } else { return null; } } else { return null; } } /** * Get the flexi-time balance which is effective at the given week start. */ public TimeSum getFlexiBalanceAtWeekStart(DateTime weekStart) { TimeSum ret = new TimeSum(); String startValueString = preferences.getString(Key.FLEXI_TIME_START_VALUE.getName(), "0:00"); String targetWorkTimeString = preferences.getString(Key.FLEXI_TIME_TARGET.getName(), "0:00"); TimeSum startValue = parseHoursMinutesString(startValueString); TimeSum targetWorkTime = parseHoursMinutesString(targetWorkTimeString); ret.addOrSubstract(startValue); DateTime upTo = weekStart.minusDays(1); List<Week> weeksToCount = dao.getWeeksUpTo(DateTimeUtil.dateTimeToString(upTo)); for (Week week : weeksToCount) { Integer weekWorkedMinutes = week.getSum(); if (weekWorkedMinutes != null) { // add the actual work time ret.add(0, weekWorkedMinutes); // substract the target work time ret.substract(0, targetWorkTime.getAsMinutes()); } else { Logger.warn("week {0} (starting at {1}) has a null sum", week.getId(), week.getStart()); } } return ret; } /** * Parse a value of hours and minutes (positive or negative). */ public static TimeSum parseHoursMinutesString(String hoursMinutes) { TimeSum ret = new TimeSum(); if (hoursMinutes != null) { String[] startValueArray = hoursMinutes.split(":"); int hours = Integer.parseInt(startValueArray[0]); int minutes = startValueArray.length > 1 ? Integer.parseInt(startValueArray[1]) : 0; if (hoursMinutes.trim().startsWith("-")) { ret.substract(-1 * hours, minutes); } else { ret.add(hours, minutes); } } return ret; } /** * Get the normal work time (in minutes) for a specific week day. */ public int getNormalWorkDurationFor(WeekDayEnum weekDay) { if (isWorkDay(weekDay)) { String targetValueString = preferences.getString(Key.FLEXI_TIME_TARGET.getName(), "0:00"); targetValueString = DateTimeUtil.refineHourMinute(targetValueString); TimeSum targetValue = parseHoursMinutesString(targetValueString); BigDecimal minutes = new BigDecimal(targetValue.getAsMinutes()).divide(new BigDecimal(countWorkDays()), RoundingMode.HALF_UP); return minutes.intValue(); } else { return 0; } } private int countWorkDays() { int ret = 0; for (WeekDayEnum day : WeekDayEnum.values()) { if (isWorkDay(day)) { ret++; } } return ret; } /** * Is there a day in the week after the given day which is also marked as work day? That means, is the given day NOT * the last work day in the week? */ private boolean isFollowedByWorkDay(WeekDayEnum day) { WeekDayEnum nextDay = day.getNextWeekDay(); while (nextDay != null) { if (isWorkDay(nextDay)) { return true; } nextDay = nextDay.getNextWeekDay(); } return false; } /** * Is today a work day? */ public boolean isTodayWorkDay() { DateTime dateTime = DateTimeUtil.getCurrentDateTime(); WeekDayEnum weekDay = WeekDayEnum.getByValue(dateTime.getWeekDay()); return isWorkDay(weekDay); } /** * Is this a work day? */ public boolean isWorkDay(WeekDayEnum weekDay) { Key key = null; switch (weekDay) { case MONDAY: key = Key.FLEXI_TIME_DAY_MONDAY; break; case TUESDAY: key = Key.FLEXI_TIME_DAY_TUESDAY; break; case WEDNESDAY: key = Key.FLEXI_TIME_DAY_WEDNESDAY; break; case THURSDAY: key = Key.FLEXI_TIME_DAY_THURSDAY; break; case FRIDAY: key = Key.FLEXI_TIME_DAY_FRIDAY; break; case SATURDAY: key = Key.FLEXI_TIME_DAY_SATURDAY; break; case SUNDAY: key = Key.FLEXI_TIME_DAY_SUNDAY; break; default: throw new IllegalArgumentException("unknown weekday"); } return preferences.getBoolean(key.getName(), false); } /** * Create a new event at current time + the given minute amount. * * @param minutesToPredate * how many minutes to add to "now" * @param taskId * the task id (may be {@code null}) * @param type * the type * @param text * the text (may be {@code null}) */ public void createEvent(int minutesToPredate, Integer taskId, TypeEnum type, String text) { if (minutesToPredate < 0) { throw new IllegalArgumentException("no negative minute amount allowed"); } DateTime targetTime = DateTimeUtil.getCurrentDateTime(); targetTime = targetTime.plus(0, 0, 0, 0, minutesToPredate, 0, DayOverflow.Spillover); createEvent(targetTime, taskId, type, text); } /** * Create a new event at the given time. * * @param dateTime * the time for which the new event should be created * @param taskId * the task id (may be {@code null}) * @param type * the type * @param text * the text (may be {@code null}) */ public void createEvent(DateTime dateTime, Integer taskId, TypeEnum type, String text) { createEvent(dateTime, taskId, type, text, false); } /** * Create a new event at the given time. * * @param dateTime * the time for which the new event should be created * @param taskId * the task id (may be {@code null}) * @param type * the type * @param text * the text (may be {@code null}) * @param insertedByRestore * true if the event is inserted by a restore. In that case, auto pause and refresh of notifications are * suppressed. */ public void createEvent(DateTime dateTime, Integer taskId, TypeEnum type, String text, boolean insertedByRestore) { if (dateTime == null) { throw new IllegalArgumentException("date/time has to be given"); } if (type == null) { throw new IllegalArgumentException("type has to be given"); } String weekStart = DateTimeUtil.getWeekStartAsString(dateTime); String time = DateTimeUtil.dateTimeToString(dateTime); Week currentWeek = dao.getWeek(weekStart); if (currentWeek == null) { currentWeek = createPersistentWeek(weekStart); } if (!insertedByRestore && type == TypeEnum.CLOCK_OUT) { tryToInsertAutoPause(dateTime); } Event event = new Event(null, currentWeek.getId(), taskId, type.getValue(), time, text); Logger.debug("TRACKING: {0} @ {1} taskId={2} text={3}", type.name(), time, taskId, text); event = dao.insertEvent(event); updateWeekSum(currentWeek); if (!insertedByRestore) { Basics.getInstance().safeCheckPersistentNotification(); } } /** * Update the week's total worked sum. */ public void updateWeekSum(Week week) { TimeSum sum = calculateTimeSum(DateTimeUtil.stringToDateTime(week.getStart()), PeriodEnum.WEEK); int minutes = sum.getAsMinutes(); Logger.info("updating the time sum to {0} minutes for the week beginning at {1}", minutes, week.getStart()); Week weekToUse = week; if (week instanceof WeekPlaceholder) { weekToUse = createPersistentWeek(week.getStart()); } weekToUse.setSum(minutes); dao.updateWeek(weekToUse); // TODO update the sum of the last week(s) if type is CLOCK_OUT? // TODO update the sum of the next week(s) if type is CLOCK_IN? } private Week createPersistentWeek(String weekStart) { Week week = new Week(null, weekStart, 0); week = dao.insertWeek(week); return week; } /** * Create a new NON-PERSISTENT (!) event of the type CLOCK_OUT_NOW. */ public Event createClockOutNowEvent() { DateTime now = DateTimeUtil.getCurrentDateTime(); String weekStart = DateTimeUtil.getWeekStartAsString(now); Week currentWeek = dao.getWeek(weekStart); return new Event(null, (currentWeek == null ? null : currentWeek.getId()), null, TypeEnum.CLOCK_OUT_NOW.getValue(), DateTimeUtil.dateTimeToString(now), null); } private void tryToInsertAutoPause(DateTime dateTime) { if (isAutoPauseEnabled() && isAutoPauseApplicable(dateTime)) { // insert auto-pause events DateTime begin = getAutoPauseBegin(dateTime); DateTime end = getAutoPauseEnd(dateTime); Logger.debug("inserting auto-pause, begin={0}, end={1}", begin, end); Event lastBeforePause = dao.getLastEventBefore(begin); createEvent(begin, null, TypeEnum.CLOCK_OUT, null); createEvent(end, (lastBeforePause == null ? null : lastBeforePause.getTask()), TypeEnum.CLOCK_IN, (lastBeforePause == null ? null : lastBeforePause.getText())); } else { Logger.debug("NOT inserting auto-pause"); } } /** * Determines if the auto-pause mechanism is enabled. */ public boolean isAutoPauseEnabled() { return preferences.getBoolean(Key.AUTO_PAUSE_ENABLED.getName(), false); } /** * Determines if the auto-pause can be applied to the given day. */ public boolean isAutoPauseApplicable(DateTime dateTime) { DateTime end = getAutoPauseEnd(dateTime); // auto-pause is theoretically applicable return isAutoPauseTheoreticallyApplicable(dateTime) // given time is after auto-pause end, so auto-pause should really be applied && dateTime.gt(end); } private boolean isAutoPauseTheoreticallyApplicable(DateTime dateTime) { DateTime begin = getAutoPauseBegin(dateTime); DateTime end = getAutoPauseEnd(dateTime); if (begin.lt(end)) { Event lastEventBeforeBegin = dao.getLastEventBefore(begin); Event lastEventBeforeEnd = dao.getLastEventBefore(end); // is clocked in before begin return lastEventBeforeBegin != null && lastEventBeforeBegin.getType().equals(TypeEnum.CLOCK_IN.getValue()) // no event is in auto-pause interval && lastEventBeforeBegin.getId().equals(lastEventBeforeEnd.getId()); } else { // begin is equal to end or (even worse) begin is after end => no auto-pause return false; } } /** * Calculates the begin of the auto-pause for the given day. */ public DateTime getAutoPauseBegin(DateTime dateTime) { return DateTimeUtil.parseTimeFor(dateTime, getAutoPauseData(Key.AUTO_PAUSE_BEGIN.getName(), "23.59")); } /** * Calculates the length of the auto-pause for the given day (in minutes). */ public int getAutoPauseDuration(DateTime dateTime) { DateTime pauseBegin = getAutoPauseBegin(dateTime); DateTime pauseEnd = getAutoPauseEnd(dateTime); long pauseSeconds = pauseBegin.numSecondsFrom(pauseEnd); return (int) (pauseSeconds / 60); } /** * Calculates the end of the auto-pause for the given day. */ public DateTime getAutoPauseEnd(DateTime dateTime) { return DateTimeUtil.parseTimeFor(dateTime, getAutoPauseData(Key.AUTO_PAUSE_END.getName(), "00.00")); } private String getAutoPauseData(String key, String defaultTime) { String ret = preferences.getString(key, defaultTime); ret = DateTimeUtil.refineTime(ret); return ret; } // ======== registration of automatic work time tracking methods ======== public void activateTrackingMethod(TrackingMethod method) { Collection<TrackingMethod> activeMethods = readCurrentlyActiveTrackingMethods(); if (!activeMethods.contains(method)) { activeMethods.add(method); writeCurrentlyActiveTrackingMethods(activeMethods); } } public void deactivateTrackingMethod(TrackingMethod method) { Collection<TrackingMethod> activeMethods = readCurrentlyActiveTrackingMethods(); if (activeMethods.contains(method)) { activeMethods.remove(method); writeCurrentlyActiveTrackingMethods(activeMethods); } } private Collection<TrackingMethod> readCurrentlyActiveTrackingMethods() { String activeMethodsString = preferences.getString(context.getString(R.string.keyActiveMethods), ""); String[] activeMethodsStrings = StringUtils.split(activeMethodsString, ','); Collection<TrackingMethod> result = new ArrayList<TrackingMethod>(activeMethodsStrings.length); for (String activeMethodString : activeMethodsStrings) { result.add(TrackingMethod.valueOf(activeMethodString)); } return result; } private void writeCurrentlyActiveTrackingMethods(Collection<TrackingMethod> methods) { StringBuilder result = new StringBuilder(); for (TrackingMethod method : methods) { if (result.length() > 0) { result.append(","); } result.append(method.name()); } final Editor editor = preferences.edit(); editor.putString(context.getString(R.string.keyActiveMethods), result.toString()); editor.commit(); } private boolean isTrackingMethodCurrentlyActive(TrackingMethod method) { Collection<TrackingMethod> activeMethods = readCurrentlyActiveTrackingMethods(); return activeMethods.contains(method); } public boolean clockInWithTrackingMethod(TrackingMethod method) { boolean currentlyClockedInWithMethod = getTrackingMethodClockInState(method); if (getTrackingMethodsGenerateEventsSeparately()) { Logger.debug("clocking in with method {0} forcibly", method.name()); return setTrackingMethodClockInStateForcibly(method, true); } else if (currentlyClockedInWithMethod) { Logger.debug("already clocked in with method {0}", method.name()); return false; } else { Logger.debug("clocking in with method {0}", method.name()); return setTrackingMethodClockInState(method, true); } } public boolean clockOutWithTrackingMethod(TrackingMethod method) { boolean currentlyClockedInWithMethod = getTrackingMethodClockInState(method); if (getTrackingMethodsGenerateEventsSeparately()) { Logger.debug("clocking out with method {0} forcibly", method.name()); return setTrackingMethodClockInStateForcibly(method, false); } else if (!currentlyClockedInWithMethod) { Logger.debug("not clocked in with method {0}", method.name()); return false; } else { Logger.debug("clocking out with method {0}", method.name()); return setTrackingMethodClockInState(method, false); } } private boolean getTrackingMethodClockInState(TrackingMethod method) { return preferences.getBoolean(context.getString(method.getPreferenceKeyId()), false); } private boolean getTrackingMethodsGenerateEventsSeparately() { return preferences.getBoolean(context.getString(R.string.keyEachTrackingMethodGeneratesEventsSeparately), false); } private boolean setTrackingMethodClockInState(TrackingMethod method, boolean state) { final Editor editor = preferences.edit(); editor.putBoolean(context.getString(method.getPreferenceKeyId()), state); editor.commit(); return createEventIfNecessary(method, state); } private boolean setTrackingMethodClockInStateForcibly(TrackingMethod method, boolean state) { final Editor editor = preferences.edit(); editor.putBoolean(context.getString(method.getPreferenceKeyId()), state); editor.commit(); return createEventForcibly(method, state); } private boolean createEventIfNecessary(TrackingMethod method, boolean state) { if (state) { // method is clocked in now - should we generate a clock-in event? if (!isClockedInWithAnyOtherTrackingMethod(method) && !isTracking()) { // we are not clocked in already by hand or by another method, so generate an event (first method // clocking in) startTracking(0, null, null); Logger.debug("method {0}: started tracking", method); return true; } else { Logger.debug( "method {0}: NOT started tracking (was not first method or already clocked in manually)", method); return false; } } else { // method is clocked out now - should we generate a clock-out event? if (!isClockedInWithAnyOtherTrackingMethod(method) && isTracking()) { // we are not clocked in by hand or by another method, so generate an event (last method clocking out) stopTracking(0); Logger.debug("method {0}: stopped tracking", method); return true; } else { Logger.debug( "method {0}: NOT stopped tracking (was not last method or already clocked out manually)", method); return false; } } } private boolean createEventForcibly(TrackingMethod method, boolean state) { if (state) { // method is clocked in now - should we generate a clock-in event? if (!isTracking()) { startTracking(0, null, null); Logger.debug("method {0}: started tracking forcibly", method); return true; } else { Logger.debug("method {0}: NOT started tracking forcibly (already clocked in)", method); return false; } } else { // method is clocked out now - should we generate a clock-out event? if (isTracking()) { stopTracking(0); Logger.debug("method {0}: stopped tracking forcibly", method); return true; } else { Logger.debug("method {0}: NOT stopped tracking forcibly (already clocked out)", method); return false; } } } private boolean isClockedInWithAnyOtherTrackingMethod(TrackingMethod methodToIgnore) { Collection<TrackingMethod> activeMethods = readCurrentlyActiveTrackingMethods(); for (TrackingMethod method : activeMethods) { if (method.equals(methodToIgnore)) { continue; } if (isClockedInWithTrackingMethod(method)) { return true; } } return false; } private boolean isClockedInWithTrackingMethod(TrackingMethod method) { return getTrackingMethodClockInState(method); } }