Issue
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>
Answered By - Chuba Samuel - DCOR
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.