Android Wear / SharedPreferences

Match Timer – Part 2

In the previous article we looked at some of the decisions behind re-writing the Footy Timer app (which was previously developed for the I’m Watch) in order to implement it on Android Wear. In this article we’ll begin looking at the code.

matchtimerWe’ll start with the class at the heart of the app which is responsible for holding the current timer state. This is essentially a class containing four long values: The first contains the time the timer started; the second contains the time the timer was stopped (or 0 if the timer is currently running); the third contains the timer the game was paused (or 0 if the time is not currently paused); and the fourth contains the total time that timer has been paused. From these four values we can maintain the timer state, and also keep enough information to calculate all of the information that we need to display. The basic function of this class is to maintain these values. It provides a set of methods which will retrieve the timer state:

public final class MatchTimer {
    .
    .
    .
    public static final int MINUTE_MILLIS = 60000;

    private long start;
    private long currentStoppage;
    private long totalStoppages;
    private long end;
    .
    .
    .
    public long getElapsed() {
        if (isRunning()) {
            return System.currentTimeMillis() - start;
        }
        if (end > 0) {
            return end - start;
        }
        return 0;
    }

    public boolean isRunning() {
        return start > 0 && end == 0;
    }

    public boolean isPaused() {
        return currentStoppage > 0;
    }

    public int getElapsedMinutes() {
        return (int) ((System.currentTimeMillis() - start) / MINUTE_MILLIS);
    }

    public long getTotalStoppages() {
        long now = System.currentTimeMillis();
        if (isPaused()) {
            return totalStoppages + (now - currentStoppage);
        }
        return totalStoppages;
    }

    public long getPlayed() {
        return getElapsed() - getTotalStoppages();
    }

    public long getStartTime() {
        return start;
    }
    .
    .
    .
}

This is all pretty basic Java, there’s nothing Android specific so I won’t bother explaining it. The next set of methods augment these and permit the timer state to be changed:

public final class MatchTimer {
    .
    .
    .
    public void start() {
        if (end > 0) {
            start = System.currentTimeMillis() - (end - start);
            end = 0;
        } else {
            start = System.currentTimeMillis();
        }
        save();
    }

    public void stop() {
        if (isPaused()) {
            resume();
        }
        end = System.currentTimeMillis();
        save();
    }

    public void pause() {
        currentStoppage = System.currentTimeMillis();
        save();
    }

    public void resume() {
        totalStoppages += System.currentTimeMillis() - currentStoppage;
        currentStoppage = 0L;
        save();
    }

    public void reset() {
        resetWithoutSave();
        save();
    }

    private void resetWithoutSave() {
        start = 0L;
        currentStoppage = 0L;
        totalStoppages = 0L;
        end = 0L;
    }
}

Once again, this is basic Java, but there’s a save() method which we’ve yet to see, and this gives a clue to the final part of this class, and this is where we have something that deserves a little explanation.

In the previous article we discussed how we only want to wake very occasionally, and we don’t want to maintain any long running services or processes, so we need some method of maintaining the timer state. We’ll use SharedPreferences to do this:

public final class MatchTimer implements SharedPreferences.OnSharedPreferenceChangeListener {
    private static final String KEY_START = "com.stylingandroid.matchtimer.KEY_START";
    private static final String KEY_CURRENT_STOPPAGE = "com.stylingandroid.matchtimer.KEY_CURRENT_STOPPAGE";
    private static final String KEY_TOTAL_STOPPAGES = "com.stylingandroid.matchtimer.KEY_TOTAL_STOPPAGES";
    private static final String KEY_END = "com.stylingandroid.matchtimer.KEY_END";
    private static final String PREFERENCES = "MatchTimer";

    private final SharedPreferences preferences;

    public static MatchTimer newInstance(Context context) {
        SharedPreferences preferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE);
        long start = preferences.getLong(KEY_START, 0);
        long currentStoppage = preferences.getLong(KEY_CURRENT_STOPPAGE, 0);
        long totalStoppages = preferences.getLong(KEY_TOTAL_STOPPAGES, 0);
        long end = preferences.getLong(KEY_END, 0);
        return new MatchTimer(preferences, start, currentStoppage, totalStoppages, end);
    }

    private MatchTimer(SharedPreferences preferences, long start, long currentStoppage, long totalStoppages, long end) {
        this.preferences = preferences;
        this.start = start;
        this.currentStoppage = currentStoppage;
        this.totalStoppages = totalStoppages;
        this.end = end;
    }

    public void save() {
        preferences.edit()
                .putLong(KEY_START, start)
                .putLong(KEY_CURRENT_STOPPAGE, currentStoppage)
                .putLong(KEY_TOTAL_STOPPAGES, totalStoppages)
                .putLong(KEY_END, end)
                .apply();
    }

    public void registerForUpdates() {
        preferences.registerOnSharedPreferenceChangeListener(this);
    }

    public void unregisterForUpdates() {
        preferences.unregisterOnSharedPreferenceChangeListener(this);
    }

    @Override
    public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
        long value = sharedPreferences.getLong(key, 0L);
        if (key.equals(KEY_START)) {
            start = value;
        } else if (key.equals(KEY_END)) {
            end = value;
        } else if (key.equals(KEY_CURRENT_STOPPAGE)) {
            currentStoppage = value;
        } else if (key.equals(KEY_TOTAL_STOPPAGES)) {
            totalStoppages = value;
        }
    }
    .
    .
    .
}

What we have here is a newInstance() method which will obtain a MatchTimer instance from SharedPreferences (i.e. it will obtain an instance from the persisted state). We also have the save() method, which we saw earlier, which will persist the current timer state to SharedPreferences.

The final aspect of this is that there may be occasions where one of the components is holding a reference to a MatchTimer object which needs to be updated because another component has changed the state (we’ll see this later in the series). So we provide a couple of methods which will register and unregister the MatchTimer instance to receive automatic updates when the underlying SharedPreferences values are changed.

So we have the basic timer now defined. In the next article we’ll look at how we can maintain the timer state and wake up when we need to.

Match Timer is available on Google Play.


Get it on Google Play

© 2014, Mark Allison. All rights reserved.

Copyright © 2014 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

2 Comments

  1. I have a question for you. You are using multiple instances of MatchTimer and all the instances are updated using SharedPreferencesChangeListener whenever an update occurs. Instead of it, wouldn’t it be better if you use singleton instance of MatchTimer everywhere? Is there a specific intention why you did this this way?

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.