I am building a security sensitive app, and I need the app to return to login screen if the app is idle for N minutes.
First, I need to implement a global activity watcher for all activities.
- Timer start when a activity is started (and timer stop when activity is stopped)
- Later we will implemenent a mechanism to check for user interaction (touch event).
- When
activeCount > 0
, it means the application is still in foreground/active, thus launch theLoginActivity
when timeout. Make sure to clear activity task/stack when launchingLoginActivity
to prevent ability to go back. - When
activeCount <= 0
, it means the application is send to background, thus we just finish/exit. Launching an activity when application in background will cause the activity to force appear and interrupt the user's current app. - Skip this process if current activity is
LoginActivity
.
class ActivityWatcher : Application.ActivityLifecycleCallbacks { companion object { private const val TAG = "ActivityWatcher" } private var activeCount: Int = 0 private var lockTimer: Timer = Timer() private var sleep: Long = 5000 fun initSleep() { } fun startLockTimer(activity: Activity?) { // val sharedPref = PreferenceManager.getDefaultSharedPreferences(activity?.applicationContext) // var sleep = sharedPref.getString(activity?.getString(R.string.pref_key_auto_lock_duration), "0").toLong() * 1000 Log.d(TAG, "startLockTimer=$sleep") if (sleep > 0) { lockTimer.cancel() lockTimer = Timer() lockTimer.schedule(sleep) { Log.d(TAG, "Timer triggered") activity?.apply { // just to be safe try { if (activeCount > 0) { finish() val intent = Intent(this, LoginActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK startActivity(intent) } else { // moveTaskToBack(true) finishAffinity() } } catch (e: Exception) { e.printStackTrace() Crashlytics.logException(e) } } } } } private fun stopLockTimer() { Log.d(TAG, "stopLockTimer") lockTimer.cancel() lockTimer = Timer() } override fun onActivityCreated(activity: Activity?, bundle: Bundle?) { } override fun onActivityStarted(activity: Activity?) { activeCount++ Log.d(TAG, "onActivityStarted=$activeCount") Log.d(TAG, activity?.javaClass?.name) if (activity !is LoginActivity) { startLockTimer(activity) } else { // just in case stopLockTimer() } } override fun onActivityPaused(activity: Activity) { } override fun onActivityResumed(activity: Activity?) { } override fun onActivityStopped(activity: Activity?) { activeCount-- Log.d(TAG, "onActivityStopped=$activeCount") if (activeCount > 0 && activity !is LoginActivity) { // when switch activity, will trigger start then stop (end up not starting any) // stopLockTimer() } } override fun onActivityDestroyed(activity: Activity?) { } override fun onActivitySaveInstanceState(activity: Activity?, p1: Bundle?) { }}
Extend Application and call registerActivityLifecycleCallbacks.
class LuaApp : MultiDexApplication() { override fun onCreate() { super.onCreate() val watcher = ActivityWatcher() // store in global singleton App.activityWatcher = watcher registerActivityLifecycleCallbacks(watcher) }}
NOTE: Refer to Android Register/Extend Application Class.
Create App
singleton class to hold ActivityWatcher
reference.
object App { lateinit var activityWatcher: ActivityWatcher}
Create a base activity class to listen to onUserInteraction.
- Whenever user interaction happens (not idle), the timer is restarted.
open class LuaActivity: AppCompatActivity() { companion object { private const val TAG : String = "LuaActivity" } override fun onUserInteraction() { Log.d(TAG, "onUserInteraction") super.onUserInteraction() if (activity !is LoginActivity) { // no App.activityWatcher.startLockTimer(this) } } }
Make sure every activity extend LuaActivity
instead of the usual AppCompatActivity
.
class MainActivity : LuaActivity() {}
NOTE: Refer to Kotlin Singleton.