androidandroid-lifecyclefingerprint

How to invalidate fingerprint authentication when user minimises app but not when moving across activities?


I have an app in which fingerprint lock is used to protect the app from being used without first authenticating with the fingerprint. The issue is that the verification only happens at first launch of the app; so after first launch, no more verification until when the app is being launched again, But I want the current verification to be valid when moving across different activities in the app, but it should be invalidated if the user minimises the app or when the phone screen goes off, so that the user would be asked to verify fingerprint again after minimizing and resuming back into the app.

I have tried using a base activity and overriding the onPause and onResume, but the methods get called even when moving across activities, which is not what I want.


Solution

  • Solved this few weeks ago while working on a completely different project from the one I was working on when I asked the question. I am glad to share my workaround.

    The app is locked after three seconds of minimizing the app or the screen going off; as long as the fingerprint switch is enabled in the app's preferences setting.

    First, I created a BaseActivity class from which all other project activities inherit from except the Fingerprint activity class which inherits directly from AppCompatActivity()

    package com.domainName.appName.ui.activities
    
    import android.content.Intent
    import android.os.Bundle
    import android.os.Handler
    import android.util.Log
    import androidx.appcompat.app.AppCompatActivity
    
    
    open class BaseActivity : AppCompatActivity() {
    private var securitySet=false
    private var backPressed=false
    private var goingToFingerprint=false
    companion object{
    private var handler:Handler?=null
    }
       override fun 
      onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        prefSaver.save(prefFileName, "launched_new_activity",sValue = true,saveImmediately=true)
        if(handler==null){
            handler= Handler(mainLooper)
        }      
    
      handler?.removeCallbacksAndMessages(null)
        
        }
    
    override fun onDestroy() {
        super.onDestroy()        
    handler?.removeCallbacksAndMessages(null)
       }
    
    override fun onStart() {
        handler?.removeCallbacksAndMessages(null)
        goingToFingerprint=false
         securitySet=prefChecker.check(prefFileName,"fingerprint_security_switch",false)
        super.onStart()
    }
    override fun onPause() {
        super.onPause()
        prefSaver.save(prefFileName,"launched_new_activity",sValue = false,saveImmediately=true)
        if(securitySet && !backPressed){
           if(!goingToFingerprint){postLock()}
        }
    }
    
    override fun onResume() {
        super.onResume()
        backPressed=false
        if(prefChecker.check(prefFileName,"app_locked",false)&&securitySet){
            prefSaver.save(prefFileName,"launched_new_activity",sValue = true,saveImmediately=true)
                goingToFingerprint=true
            startActivity(Intent(this,FingerprintActivity::class.java))
        }
    }
    
    override fun onBackPressed() {
        backPressed=true
        super.onBackPressed()
    }
    private fun postLock(){
    handler?.removeCallbacksAndMessages(null)
    handler?.postDelayed({
          if(!prefChecker.check(prefFileName,"launched_new_activity",false)) {
              prefSaver.save(prefFileName,"app_locked",sValue = true,saveImmediately=true)
          }
    },3000)
    }
    
    }
    

    I created an InitialActivity which has noHistory mode set in the Android manifest and acts as the app launcher activity. The InitialActivity inherits from BaseActivity as well and simply calls startActivity() in its onCreate method for the MainActivity activity.

    The InitialActivity overrides onPause this way :

    override fun onPause() {
        if(prefChecker.check(prefFileName,"fingerprint_security_switch",false)) {
            prefSaver.save(prefFileName,"app_locked",sValue = true,true)
    /*setting this so app is locked on First Launch if the fingerprint switch is on.
    The fingerprint switch is turned on and off from the SettingsActivity */
        }
        super.onPause()
        }
    

    Then FingerprintActivity is implemented this way:

        package com.domainName.appName.ui.activities
    import android.Manifest
    import android.content.Intent
    import android.content.pm.PackageManager
    import android.os.Bundle
    import android.view.Gravity
    import android.widget.Toast
    import androidx.appcompat.app.AppCompatActivity
    import androidx.biometric.BiometricManager
    import androidx.biometric.BiometricPrompt
    import androidx.biometric.BiometricPrompt.PromptInfo
    import androidx.core.app.ActivityCompat
    import androidx.core.content.ContextCompat
    
    
    class FingerprintActivity : AppCompatActivity() {
    
       private lateinit var executor: Executor
       var biometricPrompt: BiometricPrompt? = null
    var promptInfo: PromptInfo? = null
    var notifIntentLabel: String? = null
    var finishImmediately=false
    override fun 
    onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState);setContentView(R.layout.fingerprint_main)
    
        executor = ContextCompat.getMainExecutor(this)
        
        finishImmediately = intent.getBooleanExtra("finishImmediately",false)
        if(finishImmediately)finish()
    
        biometricPrompt = BiometricPrompt(this,
            executor, object : BiometricPrompt.AuthenticationCallback() {
                override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                    super.onAuthenticationError(errorCode, errString)
                        biometricPrompt?.cancelAuthentication()
                    val intent=Intent(this@FingerprintActivity,FingerprintActivity::class.java)
                    intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK
                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                    intent.putExtra("finishImmediately",true)
                    startActivity(intent)
                }
    
                override fun onAuthenticationSucceeded(
                    result: BiometricPrompt.AuthenticationResult
                ) {
                    super.onAuthenticationSucceeded(result)
                    prefSaver.save(prefFileName,"app_locked",sValue = false,true)
                    finish()
                }
            })
        promptInfo = PromptInfo.Builder()
            .setTitle("AppName Biometric Login")
            .setSubtitle("\t\tLog in using your fingerprint")
            .setNegativeButtonText("Cancel")
            .build()
        val biometricManager = BiometricManager.from(this)
        when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)) {
            BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> showToast("No fingerprint hardware detected on this device.")
            BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> showToast("Fingerprint hardware features are currently unavailable on this device.")
            BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> showToast("There is no any fingerprint credential associated with your account.\nPlease go to your phone settings to enrol fingerprints.")
            BiometricManager.BIOMETRIC_SUCCESS -> authenticateUser()
            BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> showToast("Fingerprint feature on device requires updating and is therefore currently unavailable.")
            BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> showToast("Fingerprint features not supported on device")
            BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> showToast("Fingerprint features status unknown")
        }
    }
    
    private fun showToast(txt_to_show: String) {
        val t = Toast.makeText(this, txt_to_show, Toast.LENGTH_SHORT)
        t.setGravity(Gravity.CENTER or Gravity.CENTER_HORIZONTAL, 0, 0)
        t.show()
    }
    
    private fun authenticateUser() {
        if(!finishImmediately){
            if (ActivityCompat.checkSelfPermission(
                    this,
                    Manifest.permission.USE_FINGERPRINT
                ) == PackageManager.PERMISSION_GRANTED
            ) {
                promptInfo?.let{ biometricPrompt?.authenticate(it)}
            } else if (ActivityCompat.checkSelfPermission(
                    this,
                    Manifest.permission.USE_BIOMETRIC
                ) == PackageManager.PERMISSION_GRANTED
            ) {
                promptInfo?.let{ biometricPrompt?.authenticate(it)}
            } else {
                showToast("Please grant the AppName app the necessary permissions to use fingerprint hardware.")
            }
        }
    }
    }
    

    prefSaver and prefChecker are basically the class. The names were used for descriptive purpose. The class was implemented this way :

    package com.domainName.appName.utils
    
    import android.annotation.SuppressLint
    import android.content.Context;
    import android.content.SharedPreferences;
    
    class PrefCheckerOrSaver(private val context:Context?){
    fun <T>check(sFile: String,keyS: String,defaultS: T):T{
        if(context==null)return defaultS
        val sPref:SharedPreferences= context.getSharedPreferences(sFile,Context.MODE_PRIVATE)
        return when(defaultS){
            is String->sPref.getString(keyS,defaultS) as T
            is Int-> sPref.getInt(keyS,defaultS) as T
            is Long-> sPref.getLong(keyS,defaultS) as T
            is Boolean-> sPref.getBoolean(keyS,defaultS) as T
            else-> defaultS
        }
    }
    @SuppressLint("ApplySharedPref")
    fun save(sFile:String, sKey:String, sValue:Any, saveImmediately:Boolean=false){
        if(context==null)return
        val sharedPreferences:SharedPreferences = context.getSharedPreferences(sFile, Context.MODE_PRIVATE);
        val editor:SharedPreferences.Editor = sharedPreferences.edit();
        when(sValue) {
            is String-> editor.putString(sKey, sValue)
            is Int->editor.putInt(sKey, sValue)
            is Long-> editor.putLong(sKey, sValue)
            is Boolean->editor.putBoolean(sKey, sValue)
        }
        if(saveImmediately){editor.commit()}
        else{ editor.apply()}
    }
    }
    

    Permission in app manifest

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
    package="com.domainName.appName">
    
    <uses-permission android:name="android.permission.USE_FINGERPRINT" />
    <uses-permission android:name="android.permission.USE_BIOMETRIC" />
    
    <application
        android:name=".MainApplication"
        android:allowBackup="false"
        android:icon="${appIcon}"
        android:label="@string/app_name"
        android:roundIcon="${appIconRound}"
        android:resizeableActivity="true"
        android:supportsRtl="false"
        android:theme="@style/ThemeAppMain"
        tools:targetApi="n"
        >
    
        <activity
            android:name=".ui.activities.InitialActivity"
            android:label="@string/app_name"
            android:exported="true"
            android:noHistory="true"
            >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    <activity android:name=".ui.activities.BaseActivity" />
    <activity android:name=".ui.activities.MainActivity" android:launchMode="singleTask"/>
    
    <activity android:name=".ui.activities.SettingsActivity" />
    
    <activity android:name=".ui.activities.FingerprintActivity"
    android:launchMode="singleTask"
    android:noHistory="true"
    />
    </application>
    
    </manifest>