7

My android application requires a password to be entered in the first activity. I want to be able to automatically send the application back to the password entry screen after the application has been idle for a fixed amount of time.

The application has multiple activities, but I would like the timeout to be global for all activities. So, it wouldn't be sufficient to create a timer thread in the onPause() method of an Activity.

I'm not sure what the best definition for the application being idle is, but no activities being active would be sufficient.

midhunhk
  • 5,560
  • 7
  • 52
  • 83
Brian Pellin
  • 2,859
  • 3
  • 23
  • 14

4 Answers4

9

I know another answer is accepted already, but I came across this working on a similar problem and think I'm going to try an alternate much simpler approach that I figured I may as well document if anyone else wants to try to go down the same path.enter code here

The general idea is just to track the system clock time in a SharedPreference whenever any Activity pauses - sounds simple enough, but alas, there's a security hole if that's all you use, since that clock resets on reboot. To work around that:

  • Have an Application subclass or shared static singleton class with a global unlocked-since-boot state (initially false). This value should live as long as your Application's process.
  • Save the system time (realtime since boot) in every relevant Activity's onPause into a SharedPreference if the current app state is unlocked.
  • If the appwide unlocked-since-boot state is false (clean app start - either the app or the phone restarted), show the lock screen. Otherwise, check the SharedPreference's value at the lockable activity's onResume; if it's nonexistent or greater than the SharedPreference value + the timeout interval, also show the lock screen.
  • When the app is unlocked, set the appwide unlocked-since-boot state to true.

Besides the timeout, this approach will also automatically lock your app if your app is killed and restarts or if your phone restarts, but I don't think that's an especially bad problem for most apps. It's a little over-safe and may lock unecessarily on users who task switch a lot, but I think it's a worthwhile tradeoff for reduced code and complexity by a total removal of any background process / wakelock concerns (no services, alarms, or receivers necessary).

To work around process-killing locking the app regardless of time, instead of sharing an appwide singleton for unlocked-since-boot, you could use a SharedPreference and register a listener for the system boot broadcast intent to set that Preference to false. That re-adds some of the complexity of the initial solution with the benefit being a little more convenience in the case that the app's process is killed while backgrounded within the timeout interval, although for most apps it's probably overkill.

Yoni Samlan
  • 37,905
  • 5
  • 60
  • 62
  • 1
    why do you need the time to be saved in sharedPrefs? save in the singelton/application the last realtime as you stated which is initialized to null at the start of the app, then if it exists it means the app was not killed and you can use it, if the os has been restarted then there will be null.... – ndori Apr 30 '18 at 12:28
3

I dealt with this by using the AlarmManager to schedule and cancel timeout action.

Then in the onPause() event of all of my activites, I schedule the alarm. In the onResume() event of all of my activities, I check to see if the alarm goes off. If the alarm went off, I shutdown my app. If the alarm hasn't gone off yet I cancel it.

I created Timeout.java to manage my alarms. When the alarm goes off a intent is fired:

public class Timeout {
    private static final int REQUEST_ID = 0;
    private static final long DEFAULT_TIMEOUT = 5 * 60 * 1000;  // 5 minutes

    private static PendingIntent buildIntent(Context ctx) {
        Intent intent = new Intent(Intents.TIMEOUT);
        PendingIntent sender = PendingIntent.getBroadcast(ctx, REQUEST_ID, intent, PendingIntent.FLAG_CANCEL_CURRENT);

        return sender;
    }

    public static void start(Context ctx) {
        ctx.startService(new Intent(ctx, TimeoutService.class));

        long triggerTime = System.currentTimeMillis() + DEFAULT_TIMEOUT;

        AlarmManager am = (AlarmManager) ctx.getSystemService(Context.ALARM_SERVICE);

        am.set(AlarmManager.RTC, triggerTime, buildIntent(ctx));
    }

    public static void cancel(Context ctx) {
        AlarmManager am = (AlarmManager) ctx.getSystemService(Context.ALARM_SERVICE);

        am.cancel(buildIntent(ctx));

        ctx.startService(new Intent(ctx, TimeoutService.class));

    }

}

Then, I created a service to capture the intent generated by the alarm. It sets some global state in my instance of the application class to indicate that the app should lock:

public class TimeoutService extends Service {
    private BroadcastReceiver mIntentReceiver;

    @Override
    public void onCreate() {
        super.onCreate();

        mIntentReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();

                if ( action.equals(Intents.TIMEOUT) ) {
                    timeout(context);
                }
            }
        };

        IntentFilter filter = new IntentFilter();
        filter.addAction(Intents.TIMEOUT);
        registerReceiver(mIntentReceiver, filter);

    }

    private void timeout(Context context) {
        App.setShutdown();

        NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        nm.cancelAll();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        unregisterReceiver(mIntentReceiver);
    }

    public class TimeoutBinder extends Binder {
        public TimeoutService getService() {
            return TimeoutService.this;
        }
    }

    private final IBinder mBinder = new TimeoutBinder();

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

}

Finally, I created a subclass of Activity that all of my app's activities subclass from to manage locking and unlocking:

public class LockingActivity extends Activity {

    @Override
    protected void onPause() {
        super.onPause();

        Timeout.start(this);
    }

    @Override
    protected void onResume() {
        super.onResume();

        Timeout.cancel(this);
        checkShutdown();
    }

    private void checkShutdown() {
        if ( App.isShutdown() ) {
            finish();
        }

    }

}

Using onPause and onResume to start and stop the timeout gives me the following semantics. As long as one of my application's activities is active, the timeout clock is not running. Since I used an Alarm type of AlarmManager.RTC, whenever the phone goes to sleep the timeout clock runs. If the timeout happens while the phone is asleep, then my service will pick up the timeout as soon as the phone wakes up. Additionally, the clock runs when any other activity is open.

For a more detailed version of these, you can see how I actually implemented them in my application https://github.com/bpellin/keepassdroid

Brian Pellin
  • 2,859
  • 3
  • 23
  • 14
  • your code never uses the TimeoutService class. Could you show how and where you're using this? – Peanut May 23 '11 at 16:09
  • I used to start the service whenever the application starts, and stop it whenever the application ended. This generated a lot of concerned emails, because people saw the service running and assumed it was using undue resources. So, I added code in Timeout's start and stop methods to start and stop the service. I edited my answer to reflect this. – Brian Pellin Jun 20 '11 at 02:16
2

Check out how OpenIntents Safe implements this functionality.

yanchenko
  • 56,576
  • 33
  • 147
  • 165
0

This has been a really helpful post for me. To back the concept given by @Yoni Samlan . I have implemented it this way

public void pause() {
        // Record timeout time in case timeout service is killed    
        long time = System.currentTimeMillis();     
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
        SharedPreferences.Editor edit = preferences.edit();
        edit.putLong("Timeout_key", time);// start recording the current time as soon as app is asleep
        edit.apply();
    }

    public void resume() {       
        // Check whether the timeout has expired
        long cur_time = System.currentTimeMillis();
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
        long timeout_start = preferences.getLong("Timeout_key", -1);
        // The timeout never started
        if (timeout_start == -1) {
            return;
        }   
        long timeout;
        try {
            //timeout = Long.parseLong(sTimeout);
            timeout=idle_delay;
        } catch (NumberFormatException e) {
            timeout = 60000;
        }
        // We are set to never timeout
        if (timeout == -1) {
            return;
        }
        if (idle){
        long diff = cur_time - timeout_start;
        if (diff >= timeout) {  
            //Toast.makeText(act, "We have timed out", Toast.LENGTH_LONG).show(); 
            showLockDialog();
        }
        }
    } 

Call pause method from onPause and resume method from onResume.

Chetandalal
  • 674
  • 1
  • 7
  • 18