Android Open Source - Pomotivity Pomodoro Api






From Project

Back to project page Pomotivity.

License

The source code is released under:

MIT License

If you think the Android project Pomotivity listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

package com.mindfulst.dneves.pomotivity;
/*  w  ww. j a v a 2 s .  co  m*/
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;

import org.joda.time.DateTime;
import org.joda.time.Days;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;

import java.util.EventListener;
import java.util.EventObject;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Class responsible for communicating with the underlying pomodoro logic.
 * <p/>
 * This serves as a layer between the UI on the various devices and the logic.
 * Notifications, sounds, UI and update of third party services (e.g. sharing) are outside the scope of the API.
 * State, core logic and methods are the scope of this API.
 * <p/>
 * P.s. Also a nice way for me to hack the whole thing here and move to the right place later ;)
 */
public class PomodoroApi {
  public class AlreadyRunningException extends Exception {}

  public final class PomodoroEvent extends EventObject {
    public final int           currentTime;
    public final PomodoroState currentState;

    /**
     * Constructor.
     *
     * @param source       PomodoroApi that triggered the event.
     * @param currentTime  Milliseconds since the current state started (break or pomodoro).
     * @param currentState Current state of execution.
     */
    protected PomodoroEvent(Object source, int currentTime, PomodoroState currentState) {
      super(source);
      this.currentTime = currentTime;
      this.currentState = currentState;
    }
  }

  /**
   * Interface to be implemented by all listeners of pomodoro actions.
   * <p/>
   * On a typical pomodoro the order of the events are:
   * pomodoroStarted - start() was called (could be user or auto-start)
   * pomodoroTicked - this is called every second, including breaks, so the app can update the UI
   * pomodoroEnded - this is called when the pomodoro part (before break) ends
   * breakStarted - this is called immediately after the break part starts
   * pomodoroFinished - this is called when both the pomodoro and break end or the stop() was called
   * the best way to know if it was due to an early stop is to check if (event.currentTime > 0)
   * <p/>
   * At any point in time, while pomodoros are running:
   * paused - called when pause() is called
   * resumed - called when resume() is called
   */
  public interface PomodoroEventListener extends EventListener {
    /**
     * Action triggered when a new Pomodoro starts.
     *
     * @param event Information about the Pomodoro Event.
     */
    public void pomodoroStarted(final PomodoroEvent event);

    /**
     * Action triggered every time the clock ticks.
     * WARNING: Not guaranteed to be called from the UI thread!
     *
     * @param event Information about the Pomodoro Event.
     */
    public void pomodoroTicked(final PomodoroEvent event);

    /**
     * Action triggered when the Pomodoro part ends (before the break).
     * WARNING: Not guaranteed to be called from the UI thread!
     *
     * @param event Information about the Pomodoro Event.
     */
    public void pomodoroEnded(final PomodoroEvent event);

    /**
     * Action triggered when the Break part starts.
     * Same for short and long break. You can get the current state from the event object.
     * WARNING: Not guaranteed to be called from the UI thread!
     *
     * @param event Information about the Pomodoro Event.
     */
    public void breakStarted(final PomodoroEvent event);

    /**
     * Action triggered when the current Pomodoro finishes (could be early termination via stop()).
     * WARNING: Not guaranteed to be called from the UI thread!
     *
     * @param event Information about the Pomodoro Event.
     */
    public void pomodoroFinished(final PomodoroEvent event);

    /**
     * Action triggered when pause() is called.
     *
     * @param event Information about the Pomodoro Event.
     */
    public void paused(final PomodoroEvent event);

    /**
     * Action triggered when resume() is called.
     *
     * @param event Information about the Pomodoro Event.
     */
    public void resumed(final PomodoroEvent event);
  }

  /**
   * Enum with actions that can be notified to listeners.
   */
  private enum ListenerAction {
    START, TICK, END_POMODORO, START_BREAK, FINISH, PAUSED, RESUMED
  }

  /**
   * Enum with possible internal Pomodoro states.
   */
  public enum PomodoroState {
    NONE, POMODORO, SHORT_BREAK, LONG_BREAK
  }

  /**
   * Class responsible for keeping simple stats state.
   */
  public static final class Stats {
    public final int finishedToday;
    public final int allTime;
    public final int totalDays;

    /**
     * Default constructor.
     */
    protected Stats() {
      finishedToday = 0;
      allTime = 0;
      totalDays = 0;
    }

    /**
     * Attribute constructor.
     *
     * @param finishedToday Today's counter.
     * @param allTime       All time pomodoro counter.
     * @param totalDays     Total days with Pomodoros.
     */
    protected Stats(int finishedToday, int allTime, int totalDays) {
      this.finishedToday = finishedToday;
      this.allTime = allTime;
      this.totalDays = totalDays;
    }

    /**
     * Constructor that loads the initial values from the preferences.
     *
     * @param context     Context where to get the attribute keys from.
     * @param preferences Preferences to load the attributes from.
     */
    protected Stats(Context context, SharedPreferences preferences) {
      this.finishedToday = preferences.getInt(context.getString(R.string.finished_today_key), 0);
      this.allTime = preferences.getInt(context.getString(R.string.all_time_key), 0);
      this.totalDays = preferences.getInt(context.getString(R.string.total_days_key), 0);
    }

    /**
     * Resets the today counter but keeps the other counters intact.
     *
     * @return new Stats instance with reset today counter.
     */
    protected Stats resetToday() {
      return new Stats(0, allTime, totalDays);
    }

    /**
     * Increments both finished today and all time.
     *
     * @return new Stats instance with the incremented counters.
     */
    protected Stats incrementCounter() {
      return new Stats(finishedToday + 1, allTime + 1, totalDays);
    }

    /**
     * Increments the day counter.
     * This will reset the finishedToday value.
     *
     * @return new Stats instance with the information about the next day.
     */
    protected Stats nextDay() {
      return new Stats(0, allTime, totalDays + 1);
    }

    @Override
    public String toString() {
      return String
          .format("PomodoroApi.Stats(finishedToday:%d, allTime:%d, totalDays:%d)", finishedToday, allTime, totalDays);
    }

    /**
     * Saves all the Stats attributes but doesn't call apply() or commit().
     *
     * @param context    Context where to get the attribute keys from.
     * @param prefEditor Editor used to save the state.
     */
    protected void save(Context context, SharedPreferences.Editor prefEditor) {
      prefEditor.putInt(context.getString(R.string.finished_today_key), finishedToday)
                .putInt(context.getString(R.string.all_time_key), allTime)
                .putInt(context.getString(R.string.total_days_key), totalDays);
    }
  }

  private static final String DEBUG_TAG            = "pomoapi";
  /**/
  public static final int    POMODORO_DURATION    = 25 * 60;
  public static final int    SHORT_BREAK_DURATION = 5 * 60;
  public static final int    LONG_BREAK_DURATION  = 14 * 60;
  /*/
  // Test times (for debugging)
  public static final int    POMODORO_DURATION    = 6;
  public static final int    SHORT_BREAK_DURATION = 3;
  public static final int    LONG_BREAK_DURATION  = 4;
  /**/

  private static PomodoroApi mInstance = new PomodoroApi();

  private PomodoroEventListener mListener = null;

  private final ScheduledExecutorService mExecutionService =
      Executors.newSingleThreadScheduledExecutor();

  private AtomicReference<ScheduledFuture> mCurrentPomodoro =
      new AtomicReference<ScheduledFuture>();
  private boolean                          mIsPaused        = false;

  private       Stats                          mStats            = new Stats();
  private       DateTime                       mLastPomodoroDate = new DateTime(0).withTime(4, 0, 0, 0);
  private final AtomicReference<PomodoroState> mCurrentState     =
      new AtomicReference<PomodoroState>(PomodoroState.NONE);
  private final AtomicInteger                  mCurrentTime      = new AtomicInteger(POMODORO_DURATION);

  public static PomodoroApi getInstance() {
    return mInstance;
  }

  private PomodoroApi() {}

  public Stats getStats() {
    return mStats;
  }

  public void save(Context context, SharedPreferences.Editor prefEditor) {
    DateTimeFormatter formatter = ISODateTimeFormat.dateTime();
    prefEditor.putString(context.getString(R.string.last_pomodoro_key), formatter.print(mLastPomodoroDate));
    mStats.save(context, prefEditor);
  }

  /**
   * Loads the state from the given preferences.
   *
   * @param context     Context where to get the attribute keys from.
   * @param preferences Preferences to load the state from.
   */
  public void load(Context context, SharedPreferences preferences) {
    mStats = new Stats(context, preferences);
    DateTimeFormatter formatter = ISODateTimeFormat.dateTime();
    final String defaultDate = formatter.print(new DateTime(0).withTime(4, 0, 0, 0));
    String lastPomodoroStr = preferences.getString(context.getString(R.string.last_pomodoro_key), defaultDate);
    try {
      mLastPomodoroDate = formatter.parseDateTime(lastPomodoroStr);
    }
    catch (Exception e) {
      e.printStackTrace();
    }

    // We don't change the date because we only only to count another day when a pomodoro finishes
    // This is just to avoid displaying today's pomodoros if a few days have passed since last running the app
    DateTime now = DateTime.now().withTime(4, 0, 0, 0);
    if (Days.daysBetween(now, mLastPomodoroDate).getDays() != 0) {
      mStats = mStats.resetToday();
    }
  }

  /**
   * Starts the pomodoro timer.
   *
   * @throws com.mindfulst.dneves.pomotivity.PomodoroApi.AlreadyRunningException if you call this
   *                                                                             method while a
   *                                                                             pomodoro is
   *                                                                             running.
   */
  public synchronized void start() throws AlreadyRunningException {
    // We don't care if it stops after this point, only that you called it while it was logically
    // running. Also, it is synchronized so, if it is null, there's no way it'll get another value
    // after this point ;)
    if (mCurrentPomodoro.get() != null) {
      throw new AlreadyRunningException();
    }

    final Runnable pomodoroTick = new Runnable() {
      private final long mStartTime = System.nanoTime();

      private void endPomodoro() {
        incrementStats();
        notifyListener(ListenerAction.END_POMODORO, 0, mCurrentState.get());

        // Force other threads to update
        try {
          Thread.sleep(1);
        }
        catch (InterruptedException e) {
          Log.d(DEBUG_TAG, "Ooops, thread was interruped");
        }

        // Start the break
        if (mStats.finishedToday % 4 == 0) {
          mCurrentState.set(PomodoroState.LONG_BREAK);
          mCurrentTime.set(LONG_BREAK_DURATION);
        }
        else {
          mCurrentState.set(PomodoroState.SHORT_BREAK);
          mCurrentTime.set(SHORT_BREAK_DURATION);
        }
        notifyListener(ListenerAction.START_BREAK, mCurrentTime.get(), mCurrentState.get());
      }

      private void endBreak() {
        stop();
        if (mAutoStart) {
          try {
            // Force the UI to catch up and and give time to breath. We only need to do it here because of the
            // autostart, which will trigger another notification update immediately.
            Thread.sleep(1000);
            start();
          }
          catch (AlreadyRunningException e) {
            Log.w(DEBUG_TAG, "It failed to auto-start because it was already running, but I just stopped...");
          }
          catch (InterruptedException e) {
            Log.w(DEBUG_TAG, "Ooops, thread was interruped");
          }
        }
      }

      @Override
      public void run() {
        if (mIsPaused) {
          return;
        }

        if (mCurrentTime.decrementAndGet() <= 0) {
          if (mCurrentState.get() == PomodoroState.POMODORO) {
            Log.d(DEBUG_TAG, "Pomodoro ended after " + ((System.nanoTime() - mStartTime) * 1e-9));
            endPomodoro();
          }
          else { // LONG or SHORT break
            Log.d(DEBUG_TAG, "Pomodoro and break ended after " + ((System.nanoTime() - mStartTime) * 1e-9));
            endBreak();
          }
        }
        else {
          Log.d(DEBUG_TAG, "Timer: " + mCurrentTime.get());
          notifyListener(ListenerAction.TICK, mCurrentTime.get(), mCurrentState.get());
        }
      }
    };

    Log.i(DEBUG_TAG, "Pomodoro started");
    mIsPaused = false;
    mCurrentState.set(PomodoroState.POMODORO);
    mCurrentTime.set(POMODORO_DURATION);
    mCurrentPomodoro.set(mExecutionService.scheduleAtFixedRate(pomodoroTick, 1, 1, TimeUnit.SECONDS));
    notifyListener(ListenerAction.START, POMODORO_DURATION, mCurrentState.get());
  }

  private void notifyListener(ListenerAction action, int currentTime, PomodoroState state) {
    PomodoroEventListener listener = mListener;
    if (listener == null) {
      return;
    }

    try {
      // I used actions because creating and passing callables for something so static isn't convenient ;)
      PomodoroEvent event = new PomodoroEvent(this, currentTime, state);
      switch (action) {
        case START:
          mListener.pomodoroStarted(event);
          break;
        case TICK:
          mListener.pomodoroTicked(event);
          break;
        case END_POMODORO:
          mListener.pomodoroEnded(event);
          break;
        case START_BREAK:
          mListener.breakStarted(event);
          break;
        case FINISH:
          mListener.pomodoroFinished(event);
          break;
        case PAUSED:
          mListener.paused(event);
          break;
        case RESUMED:
          mListener.resumed(event);
          break;
      }
    }
    catch (Exception e) {
      Log.d(DEBUG_TAG, "Exception thrown while calling the listener: " + e.toString());
    }
  }

  private void incrementStats() {
    // We consider the start of the day at 4am as this should be the least convenient time to use pomodoros
    // see https://www.ted.com/talks/rives_on_4_a_m
    DateTime now = DateTime.now().withTime(4, 0, 0, 0);
    if (Days.daysBetween(now, mLastPomodoroDate).getDays() != 0) {
      mStats = mStats.nextDay();
      mLastPomodoroDate = now;
    }
    // Do this after the next day because it will reset the today counter
    mStats = mStats.incrementCounter();
    Log.d(DEBUG_TAG, "Current stats: " + mStats);
  }

  /**
   * Stops the current timer or does nothing if no timer is running.
   */
  protected void stop() {
    ScheduledFuture pomodoro = mCurrentPomodoro.getAndSet(null);
    if (pomodoro != null) {
      pomodoro.cancel(false);
      Log.i(DEBUG_TAG, "Timer stopped");
      notifyListener(ListenerAction.FINISH, mCurrentTime.get(), mCurrentState.get());
      mCurrentState.set(PomodoroState.NONE);
    }
  }

  /**
   * Pauses the current timer or does nothing if no timer is running.
   */
  protected void pause() {
    if (mIsPaused == true) {
      return;
    }

    mIsPaused = true;
    Log.i(DEBUG_TAG, "Timer paused");
    ScheduledFuture pomodoro = mCurrentPomodoro.get();
    if (pomodoro != null) {
      notifyListener(ListenerAction.PAUSED, mCurrentTime.get(), mCurrentState.get());
    }
  }

  /**
   * Resumes the current timer or does nothing if no timer is running.
   */
  protected void resume() {
    if (mIsPaused == false) {
      return;
    }

    Log.i(DEBUG_TAG, "Timer resumed");
    // This means that we may have to wait almost a second before the next run, but it's a simple
    // mechanism ;)
    mIsPaused = false;
    ScheduledFuture pomodoro = mCurrentPomodoro.get();
    if (pomodoro != null) {
      notifyListener(ListenerAction.RESUMED, mCurrentTime.get(), mCurrentState.get());
    }
  }

  /**
   * Sets the auto start flag.
   * If set to true, when a pomodoro finishes, another one will start immediately.
   *
   * @param autoStart new value.
   */
  protected void setAutoStart(boolean autoStart) {
    mAutoStart = autoStart;
  }

  public void setPomodoroListener(PomodoroEventListener listener) {
    mListener = listener;
  }
}




Java Source Code List

com.mindfulst.dneves.pomotivity.ApplicationTest.java
com.mindfulst.dneves.pomotivity.MainActivity.java
com.mindfulst.dneves.pomotivity.MainActivity.java
com.mindfulst.dneves.pomotivity.PomodoroApi.java
com.mindfulst.dneves.pomotivity.util.SystemUiHiderBase.java
com.mindfulst.dneves.pomotivity.util.SystemUiHiderHoneycomb.java
com.mindfulst.dneves.pomotivity.util.SystemUiHider.java