/* * Copyright (C) 2011 Virginia Tech Department of Computer Science * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package sofia.util; import java.lang.ref.WeakReference; import sofia.app.Screen; import sofia.app.internal.LifecycleInjection; import sofia.app.internal.ScreenMixin; import sofia.internal.events.EventDispatcher; import android.content.Context; import android.os.Handler; import android.os.Looper; //------------------------------------------------------------------------- /** * <p> * This class provides a very simple API for calling methods either once or * repeatedly after a delay. Instead of creating objects of this class using * the {@code new} keyword, you can use the following static "factory" methods * to queue up delayed method calls in the background: * </p> * <dl> * <dt>{@code callOnce(Object receiver, String methodName, long delay)}</dt> * <dd>Requests that an object's method be called once after a number of * milliseconds has passed.</dd> * <dt>{@code callRepeatedly(Object receiver, String methodName, * long delay)}</dt> * <dd>Requests that an object's method be called repeatedly after a number of * milliseconds has passed.</dd> * <dt>{@code callRepeatedly(Object receiver, String methodName, * long initialDelay, long repeatDelay)}</dt> * <dd>Requests that an object's method be called repeatedly, with separate * controls over the initial delay (before the first call) and the repeat * delay (between subsequent calls).</dd> * </dl> * <p> * Each of these methods returns a {@code Timer} object, which can be stored * and used later if necessary. For example, you may want to keep the timer * in a field so that you can stop the repetition in response to a user action * (like a button click) or some other external event. * </p> * * <h3>Methods the timer can call</h3> * <p> * The method whose name is passed to these factory methods must be public, * take no arguments, and return either {@code void} or {@code boolean}. Be * careful: if a method with the correct name is found but the arguments do not * match, then no error will be produced at runtime, and the method will simply * not be called. * </p><p> * The version of the method that returns {@code boolean} is useful for timers * created by {@code callRepeatedly}. In this case, returning {@code true} will * cause the timer to stop (and the method will not be called again), or * returning {@code true} will cause the timer to continue processing and fire * again after the delay. If the method returns {@code void} instead, then the * timer will run indefinitely, until it is stopped by calling {@link #stop()}. * </p> * * <h3>Automatic lifecycle management</h3> * <p> * If the <strong>receiver</strong> for the timed method call is a * {@link Screen}, then the timer will be automatically managed as part of the * screen's lifecycle; the timer will be paused and resumed automatically * during the screen's {@code onPause} and {@code onResume} methods, * respectively. * </p><p> * If the method receiver is another object, then you will manually pause and * resume the timer. Backing out of an activity or leaving the application does * <strong>not</strong> automatically stop the timer, in most cases. * </p> * * <h3>Threading concerns</h3> * <p> * Timed methods are always called on the main GUI thread, regardless of which * thread they are started on. This is to avoid problems when users might want * to start timed method calls on the physics thread (for example, in reaction * to a collision between shapes). * </p> * * @author Tony Allevato */ public class Timer { //~ Fields ................................................................ private WeakReference<Object> receiverRef; private long initialDelay; private long repeatDelay; private Handler handler; private long startTime; private long lastPostTime; private EventDispatcher timerFired; //~ Constructors .......................................................... // ---------------------------------------------------------- private Timer(Object receiver, EventDispatcher event, long initialDelay, long repeatDelay) { this.receiverRef = new WeakReference<Object>(receiver); this.timerFired = event; this.initialDelay = initialDelay; this.repeatDelay = repeatDelay; handler = new Handler(Looper.getMainLooper()); startTime = 0; lastPostTime = 0; if (receiver instanceof Context) { ScreenMixin.tryToAddLifecycleInjection((Context) receiver, lifecycleInjection); } } //~ Factory methods ....................................................... // ---------------------------------------------------------- /** * Calls a method once after the specified delay. * * @param receiver the object on which the method will be called * @param methodName the name of the method to call * @param delay the delay, in milliseconds * @return the {@link Timer} object, which you can use to execute finer * grained control over the timer if needed */ public static Timer callOnce( Object receiver, String methodName, long delay) { Timer timer = new Timer(receiver, new EventDispatcher(methodName), delay, 0); timer.start(); return timer; } // ---------------------------------------------------------- /** * Calls a method repeatedly, waiting the specified amount of time between * each call (and before the first call). * * @param receiver the object on which the method will be called * @param methodName the name of the method to call * @param delay the delay, in milliseconds, before the first call and * between subsequent calls * @return the {@link Timer} object, which you can use to execute finer * grained control over the timer if needed */ public static Timer callRepeatedly( Object receiver, String methodName, long delay) { return callRepeatedly(receiver, methodName, delay, delay); } // ---------------------------------------------------------- /** * Calls a method repeatedly, providing separate control over the initial * delay (time before the first call) and the repetition delay (time * between subsequent calls). * * @param receiver the object on which the method will be called * @param methodName the name of the method to call * @param initialDelay the delay, in milliseconds, before the first call * @param repeatDelay the delay, in milliseconds, between subsequent calls * to the method * @return the {@link Timer} object, which you can use to execute finer * grained control over the timer if needed */ public static Timer callRepeatedly( Object receiver, String methodName, long initialDelay, long repeatDelay) { Timer timer = new Timer( receiver, new EventDispatcher(methodName), initialDelay, repeatDelay); timer.start(); return timer; } //~ Public methods ........................................................ // ---------------------------------------------------------- /** * <p> * Starts the timer if it has been previously stopped. * </p><p> * Calling one of the factory methods * ({@link #callDelayed(Object, String, long)}, * {@link #callRepeatedly(Object, String, long)}, or * {@link #callRepeatedly(Object, String, long, long)} will create a timer * that is started immediately, so you do not need to call the * {@code start} method on it. This method exists so that you can restart * a timer that you have previously called {@link #stop()} on. * </p> */ public void start() { if (startTime == 0) { stop(); startTime = System.currentTimeMillis(); post(initialDelay); } } // ---------------------------------------------------------- /** * Stops the timer, preventing it from firing in the future. To restart the * timer, call its {@link #start()} method. */ public void stop() { handler.removeCallbacks(timerTask); startTime = 0; } // ---------------------------------------------------------- /** * Gets a value indicating whether or not the timer is currently running. * * @return true if the timer is running, or false if it is not */ public boolean isRunning() { return startTime != 0; } //~ Private methods ....................................................... // ---------------------------------------------------------- private void post(long delay) { lastPostTime = System.currentTimeMillis(); handler.postDelayed(timerTask, delay); } //~ Inner classes ......................................................... // ---------------------------------------------------------- /** * The runnable that is posted to the handler's event queue. */ private final Runnable timerTask = new Runnable() { // ---------------------------------------------------------- @Override public void run() { // Subtract the actual time taken by the method execution from the // following delay so that method calls remain fairly synchronized // with respect to each other. long startTime = System.currentTimeMillis(); Object receiver = receiverRef.get(); if (receiver != null) { boolean stop = timerFired.dispatch(receiver); // The ideal way to stop a repeating timer is to return true // from the handler method, but some users might call stop() // inside the handler instead (especially if it's a void // method). The check startTime != 0 makes this work correctly // as well. if (!stop && startTime != 0 && repeatDelay > 0) { long realDelay = repeatDelay - (System.currentTimeMillis() - startTime); post(realDelay); } } } }; // ---------------------------------------------------------- /** * Hooks into the screen's lifecycle to pause and resume the timer if the * method receiver is a {@code Screen}. */ private LifecycleInjection lifecycleInjection = new LifecycleInjection() { //~ Fields ............................................................ private long timeRemaining; //~ Public methods .................................................... // ---------------------------------------------------------- @Override public void pause() { if (isRunning()) { handler.removeCallbacks(timerTask); timeRemaining = System.currentTimeMillis() - lastPostTime; } } // ---------------------------------------------------------- @Override public void resume() { if (timeRemaining != 0) { post(timeRemaining); } } }; }