This is an interesting question, I will share my thought on this question and give a solution as well.
Terminology:
App Lock Type: A generic name for pin/pincode/password/passcode, etc. (in the following section, I will use the pin name to demonstrate)
PinActivity: A screen where users input their pin to verify themself
Story:
For apps that require users to input pin, they usually want to make sure sensitive information is not leaked/stolen by other people. So we will categorize app activities into 2 groups.
Normal activities: Doesn't contain any sensitive information, usually before users logged in to the app, such as SplashActivity, LoginActivity, RegistrationActivity, PinActivity, etc.
Secured activities: Contain sensitive information, usually after users logged in, such as MainActivity, HomeActivity, UserInfoActivity, etc.
Conditions:
For secured activities, we must make sure users always input their pin before viewing the content by showing the PinActivity. This activity will be shown in the following scenarios:
[1] When users open a secured activity form a normal activity, such as from SplashActivity to MainActivity
[2] When users open a secured activity by tapping on Notifications, such as they tap on a notification to open MainActivity
[3] When users tap on the app from the Recents screen
[4] When the app starts a secured activity from another place like Services, Broadcast Receiver, etc.
Implementation:
For case [1] [2] and [4], before start a secured activity we will add an extra to the original intent. I will create a file named IntentUtils.kt
IntentUtils.kt
const val EXTRA_IS_PIN_REQUIRED = "EXTRA_IS_PIN_REQUIRED"
fun Intent.secured(): Intent {
return this.apply {
putExtra(EXTRA_IS_PIN_REQUIRED, true)
}
}
Use this class from normal activities, notifications, services, etc.
startActivity(Intent(this, MainActivity::class.java).secured())
For case [3], I will use 2 APIs:
First I create a base activity, all normal activitis must extend from this class
BaseActivity.kt
open class BaseActivity : AppCompatActivity() {
// This method indicates that a pin is required if
// users want to see the content inside.
open fun isPinRequired() = false
}
Second I create a secured activity, all secured activities must extend from this class
SecuredActivity.kt
open class SecuredActivity : BaseActivity() {
override fun isPinRequired() = true
// This is useful when launch a secured activity with
// singleTop, singleTask, singleInstance launch mode
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
setIntent(intent)
}
}
Third I create a class that extends from the Application, all logic are inside this class
MyApplication.kt
class MyApplication : Application() {
private var wasEnterBackground = false
override fun onCreate() {
super.onCreate()
registerActivityLifecycleCallbacks(ActivityLifecycleCallbacksImpl())
ProcessLifecycleOwner.get().lifecycle.addObserver(LifecycleObserverImpl())
}
private fun showPinActivity() {
startActivity(Intent(this, PinActivity::class.java))
}
inner class LifecycleObserverImpl : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onEnterBackground() {
wasEnterBackground = true
}
}
inner class ActivityLifecycleCallbacksImpl : ActivityLifecycleCallbacks {
override fun onActivityResumed(activity: Activity) {
val baseActivity = activity as BaseActivity
if (!wasEnterBackground) {
// Handle case [1] [2] and [4]
val removed = removeIsPinRequiredKeyFromActivity(activity)
if (removed) {
showPinActivity()
}
} else {
// Handle case [3]
wasEnterBackground = false
if (baseActivity.isPinRequired()) {
removeIsPinRequiredKeyFromActivity(activity)
showPinActivity()
}
}
}
private fun removeIsPinRequiredKeyFromActivity(activity: Activity): Boolean {
val key = EXTRA_IS_PIN_REQUIRED
if (activity.intent.hasExtra(key)) {
activity.intent.removeExtra(key)
return true
}
return false
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
override fun onActivityStarted(activity: Activity) {}
override fun onActivityPaused(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivityDestroyed(activity: Activity) {}
}
}
Conclusion:
This solution works for those cases that I mentioned before, but I haven't tested the following scenarios:
- When start a secured activity has launch mode singleTop|singleTask|singleInstance
- When application killed by the system on low memory
- Other scenarios that someone might encounter (if yes please let me know in the comments section).