Timers in Android

One thing that’s always been curious to me is the lack of a simple timer class in Android. Instead you need to work around the lack in one of two ways. The easiest way is via a Handler. Its not hard to do, but its a little bit of busy work. The problem with this is that it’s only appropriate for very short timers or for things tied heavily to the UI. Its a non-exact timer and may be delayed indefinitely if the activity is paused or the phone goes idle. It won’t last beyond the Context that sets it at all. The other way is via an alarm. Alarms are persistent across Contexts and may be fairly exact. But they’re based on BroadcastReceivers, which can be tricky to work with- since the app may be started to process the alarm and call to the BroadcastReceiver, you have to code it carefully to get any data to persist until the broadcast occurs. In addition, Google keeps changing the AlarmManager API with respect to how exact timers are and correctly using it is a bit tricky.

So timers definitely need a bit of abstraction. The existing interface is just painful. The goal here is to encapsulate both of the two types of timers (since each has a use), and to allow a user to easily switch between them. We want the two timers to implement an interface like this:

package com.gabesechan.android.reusable.timer;

public interface Timer {
    interface TimerCallback {
        boolean tick();
    }


    void start();
    void stop();
}

One of the major questions for any timer API is autorepeat. Some APIs have a timer default to one shot, some have them default to repeating and you have to manually cancel them. I’ve taken a third route here- the tick function returns a boolean. If true, the timer repeats. If false, it doesn’t. This allows a timer function to end itself whenever it wishes. The other major question is recognizing one timer from another (in the case of multiple timers) and passing data into the timer. In classical C style programming, you generally pass in a void* to the timer function. In Java we can avoid this- since we pass in a callback object for the timer, that object can hold any additional data it needs, and can be distinguished by the simple expedient of using separate objects.

So let’s start off with the simple case- the handler based timer. This one is pretty trivial- start is just posting to a Handler. Repeat is just posting again to that same handler. Stop is removing callbacks from that handler, with some synchronization code to make sure that a call to stop while a callback is processing is fully threadsafe.

package com.gabesechan.android.reusable.timer;

import android.os.Handler;


public class SimpleTimer implements Timer{
    private long mTimeOut;
    private TimerCallback mCallback;
    private Handler mHandler;
    private Runnable mPostRunnable;
    private boolean mRunning;

    public SimpleTimer(long ms, TimerCallback callback) {
        mTimeOut = ms;
        mCallback = callback;
        mHandler = new Handler();
        mPostRunnable = new Runnable() {
            @Override
            public void run() {
                boolean again = mCallback.tick();
                if(again) {
                    synchronized (mHandler) {
                        if (mRunning) {
                            mHandler.postDelayed(mPostRunnable, mTimeOut);
                        }
                    }
                }
            }
        };
    }

    public void start() {
        synchronized (mHandler) {
            mRunning = true;
            mHandler.postDelayed(mPostRunnable, mTimeOut);
        }
    }

    public void stop() {
        synchronized (mHandler) {
            mRunning = false;
            mHandler.removeCallbacks(mPostRunnable);
        }
    }
}

The alarm based timer is more difficult. The first problem- it needs to be based off a BroadcastReceiver. This receiver may be created just for this call and have no access to any existing variables that aren’t in its Intent. So we can’t just pass it a generic callback object. We work around that by introducing a new PersistentTimerCallback that implements Parcelable as well as TimerCallback. We can then attach the PersistentTimerCallback to the Intent, allowing it to be called when the broadcast occurs. We didn’t want to add this requirement to the simpler timer, since its a lot of work for each callback that’s not needed.

The next problem to deal with is the mess Android has made of AlarmManager. Before v19, the am.set() function set an exact alarm. Starting with v19, based on the apk’s target SDK version, set is inexact (it may be off by several minutes) and setExact is exact (setInexact does the same as set). Now as of v23, neither exact or inexact will occur when the phone is idle, they only occur if the phone is in use. To override that, you need to call setAndAllowWhileIdle or setExactAndAllowWhileIdle. So we not only need to figure out what our target SDK is, we also need to make sure not to call a function that doesn’t exist on our current device. Oh and as a bonus, the repeating functions only work for inexact and non-idle timers. You have to roll your own repeat functionality otherwise.

To simplify this, I’ve made an assumption- I don’t really see a usecase for an exact timer without idle, or an inexact with idle. So I can forget about setAndAllowIdle, or using setExact on devices that support setExactAndAllowIdle. I’m also going to ignore the repeat functionality that’s built in- I figure that if I need to write it on my own anyway for some cases, I may as well for all cases.

The final difficulty is stopping. Stopping an alarm requires you to be able to recreate an exact intent. This requires us to know the request code. My first attempts had me generating request codes on my own, but I found it useful to be able to stop an alarm by request code even if you don’t have a reference to the original Timer instance (for example if the callback starts an Activity that later wants to kill the timer). So now you need to give it a request code when you create a timer, and you can request to stop a timer by request id. There’s also the problem of synchronization. There’s a chance of the timer’s repeat code having a race condition with the stop function. In the SimpleTimer, I avoided this with a synchronization block. I can’t do that here, because the BroadcastRceiver may exist independently. However, we know the BroadcastReceiver runs on the UI thread. So if I make the stop actually happen on the UI thread, we can assure that they’re serialized. The one catch this brings up is this- if stop is called from a non-UI thread and a BroadcastReceiver call was already enqueued on the main thread, there may be 1 more tick of the timer before we cancel it. There’s really no way to catch this. If the call to stop is made on the UI thread, everything is fine.

Here’s the code:

package com.gabesechan.android.reusable.timer;

import android.annotation.TargetApi;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Parcelable;
import android.os.SystemClock;

public class PersistentTimer implements Timer{
    private long mTimeOut;
    private Intent mAlarmIntent;
    private Context mContext;
    private boolean mExact;
    private int mRequestNumber;

    private static int sTargetSdkVersion = -1;
    private static Handler sUiHandler = new Handler();

    private static final String EXTRA_OBJECT = "object";
    private static final String EXTRA_TIMEOUT = "timeout";
    private static final String EXTRA_EXACT = "exact";
    private static final String EXTRA_REQUEST_NUM = "requestNum";

    public static class AlarmReceiver extends BroadcastReceiver {

        public AlarmReceiver() {

        }

        @Override
        public void onReceive(Context ctx, Intent intent) {
            PersistentTimerCallback callback = (PersistentTimerCallback)intent.getParcelableExtra(EXTRA_OBJECT);
            boolean again = callback.tick();
            if(again) {
                long timeout = intent.getLongExtra(EXTRA_TIMEOUT, 0);
                boolean exact = intent.getBooleanExtra(EXTRA_EXACT, false);
                int requestNumber = intent.getIntExtra(EXTRA_REQUEST_NUM, 0);
                setSingleTimer(ctx, intent, timeout, exact, requestNumber);
            }
        }
    }

    public interface PersistentTimerCallback extends Timer.TimerCallback, Parcelable {

    }

    public PersistentTimer(Context ctx, long ms, PersistentTimerCallback callback, int requestCode, boolean exact) {
        mTimeOut = ms;
        mContext = ctx;
        mExact = exact;
        mRequestNumber = requestCode;
        mAlarmIntent = new Intent(ctx, AlarmReceiver.class);
        mAlarmIntent.putExtra(EXTRA_OBJECT, callback);
        mAlarmIntent.putExtra(EXTRA_TIMEOUT, mTimeOut);
        mAlarmIntent.putExtra(EXTRA_REQUEST_NUM, mRequestNumber);
        PackageManager pm = ctx.getPackageManager();
        if(sTargetSdkVersion == -1) {
            try {
                ApplicationInfo applicationInfo = pm.getApplicationInfo(ctx.getPackageName(), 0);
                if (applicationInfo != null) {
                    sTargetSdkVersion = applicationInfo.targetSdkVersion;
                }
            } catch (PackageManager.NameNotFoundException ex) {
                sTargetSdkVersion = 1; //default to simplest
            }
        }
    }

    @TargetApi(23)
    private static void setSingleTimer(Context ctx, Intent intent, long timeout, boolean exact, int requestNumber) {
        PendingIntent pi = PendingIntent.getBroadcast(ctx, requestNumber, intent, 0);
        AlarmManager am =( AlarmManager)ctx.getSystemService(Context.ALARM_SERVICE);
        if(exact && sTargetSdkVersion  >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            am.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + timeout, pi);
        }
        else if (exact && sTargetSdkVersion >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            am.setExact(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + timeout, pi);
        }
        else {
            am.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + timeout, pi);
        }
    }

    @Override
    public void start() {
        sUiHandler.post(new Runnable() {
            @Override
            public void run() {
                setSingleTimer(mContext, mAlarmIntent, mTimeOut, mExact, mRequestNumber);
            }
        });
    }

    @Override
    public void stop() {
        stop(mContext, mRequestNumber);
    }

    private static void doStop(Context context, final int requestCode) {
        AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
        PendingIntent pi = PendingIntent.getBroadcast(context, requestCode, new Intent(context, AlarmReceiver.class), 0);
        am.cancel(pi);
    }

    static public void stop(final Context context, final int requestCode) {
        if(Looper.myLooper() != Looper.getMainLooper()) {
            sUiHandler.post(new Runnable() {
                @Override
                public void run() {
                    doStop(context, requestCode);
                }
            });
        }
        else {
            doStop(context, requestCode);
        }
    }
}

You also need to add the following to the Application tag of your manifest:


        

This Post Has Been Viewed 1,354 Times

Leave a Reply