Android: How to set passcode/PIN for my app only?

3.3k Views Asked by At

I am working on an app where I need to set security feature by setting passcode/password/pin for the user so that whenever they open the app from the background, the app asks for password/passcode/pin. I read a few articles/solutions like this but they didn't help me. The same functionality we found in normal banking apps in which whenever you open an app, they will ask for passcode/fingerprint.

I have already set up the logic to save the passcode/pin in shared preference but I am not sure about when to ask it. I know that we can't replace splash screen with passcode/pin activity, because sometimes from app's homeScreen/MainActivity, user press home button and when they again open the app from recent apps, the app should ask for passcode/pin to resume the app use.

Any help would be appreciated.

1

There are 1 best solutions below

3
Son Truong On

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).