I recently needed to add an inactivity timer to a pair of new apps. If the user was logged in but inactive past a configurable period, the app should display an error when resumed and return to the login screen. It must be non-intrusive and not interrupt any foreground apps. It could not require a background service.
For Android, this fit well into the standard activity architecture. In particular, well-behaved activities should normally remain quiet when paused and should finish themselves rather than be killed by some other activity. So I created the following simple utility class to enable this. The primary hooks are:
- initialize – Called when the first (main) activity starts.
- startMonitoring – Called when the user logs in.
- monitor – Called from each activity’s onResume. I added it to my common fragment superclass.
It also has an optional requestExit to request an immediate exit based on some other criteria.
The deus ex machina is the simple activity.finish(). This ripples through all active activities so that they politely exit whenever the sShouldExitNow flag is set.
public class UserActivityMonitor { private static long sUserTimeoutMillisecs = 5 * 60 * 1000; // Default to 5 minutes private static boolean sIsMonitoring = false; private static boolean sShouldExitNow = false; private static long sLastActionMillisecs = 0; /** * Initialize monitoring. Called at app startup. */ public static void initialize(long timeoutMillisecs) { sUserTimeoutMillisecs = timeoutMillisecs; sIsMonitoring = false; sShouldExitNow = false; sLastActionMillisecs = 0; } /** * Start monitoring user activity for idle timeout. */ public static void startMonitoring() { sIsMonitoring = true; sLastActionMillisecs = System.currentTimeMillis(); } /** * The given activity is at the foreground. * Record the user's activity and, if necessary, handle an exit or timeout request. * If timed out, start the postTimeoutActivity (if provided) on a new task. */ public static void monitor(Activity currentActivity, Intent postTimeoutActivity) { if (sShouldExitNow) { // Calls here will ripple through and finish all activities currentActivity.finish(); } else { if (sIsMonitoring) { checkForIdleTimeout(currentActivity, postTimeoutActivity); } } sLastActionMillisecs = System.currentTimeMillis(); } /** * Request to exit the app by triggering all activities to finish. */ public static void requestExit() { sShouldExitNow = true; } /** * Check for an inactivity timeout. * If we have timed out, display a message, finish the activity, * and set the "should exit now" flag so that other activities quit. * If timed out, start the postTimeoutIntent (if provided) on a new task. */ private static void checkForIdleTimeout(final Activity activity, final Intent postTimeoutIntent) { if (User.getCurrentUser().isLoggedIn() && sLastActionMillisecs > 0) { long idleMillisecs = System.currentTimeMillis() - sLastActionMillisecs; if (idleMillisecs > sUserTimeoutMillisecs) { // The first time we detect a timeout, display a popup message UiUtils.showError(activity.getString(R.string.msg_timeout), activity.getString(R.string.msg_timeout_title), activity, new IErrorDisplayListener() { @Override public void onResponse(int buttonClicked) { sShouldExitNow = true; activity.finish(); if (postTimeoutIntent != null) { postTimeoutIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); activity.startActivity(postTimeoutIntent); } } }); } } } } |