I am using androidx.biometric:biometric:1.0.1 everything works fine but when I have a device without a biometric sensor (or when the user didn't set his fingerprint or etc) and I try to use DeviceCredentials after doing authentication my function input data is not valid.
class MainActivity : AppCompatActivity() {
private val TAG = MainActivity::class.java.name
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<View>(R.id.first).setOnClickListener {
authenticate(MyData(1, "first"))
}
findViewById<View>(R.id.second).setOnClickListener {
authenticate(MyData(2, "second"))
}
}
private fun authenticate(data: MyData) {
Log.e(TAG, "starting auth with $data")
val biometricPrompt = BiometricPrompt(
this,
ContextCompat.getMainExecutor(this),
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
Log.e(TAG, "auth done : $data")
}
})
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setDeviceCredentialAllowed(true)
.setTitle("title")
.build()
biometricPrompt.authenticate(promptInfo)
}
}
data class MyData(
val id: Int,
val text: String
)
First I click on my first button, authenticate, then I click my second button and authenticate, then android logcat is like this:
E/com.test.biometrictest.MainActivity: starting auth with MyData(id=1, text=first)
E/com.test.biometrictest.MainActivity: auth done : MyData(id=1, text=first)
E/com.test.biometrictest.MainActivity: starting auth with MyData(id=2, text=second)
E/com.test.biometrictest.MainActivity: auth done : MyData(id=1, text=first)
as you see in last line MyData id and text is invalid! autneticate function input(data) is not the same when onAuthenticationSucceeded is called!
(if you try to test it be sure to use DeviceCredentials not biometrics, I mean pattern or password, unset your fingerprint)
Why data is not valid in callBack?
it works ok on android 10 or with fingerprint
I don`t want to use onSaveInstanceState.
When you create a new instance of BiometricPrompt class, it adds a LifecycleObserver to the activity and as I figured out it never removes it. So when you have multiple instances of BiometricPrompt in an activity, there are multiple LifecycleObserver at the same time that cause this issue.
For devices prior to Android Q, there is a transparent activity named DeviceCredentialHandlerActivity and a bridge class named DeviceCredentialHandlerBridge which support device credential authentication. BiometricPrompt manages the bridge in different states and finally calls the callback methods in the onResume state (when back to the activity after leaving credential window) if needed. When there are multiple LifecycleObserver, The first one will handle the result and reset the bridge, so there is nothing to do by other observers. This the reason that the first callback implementation calls twice in your code.
Solution:
You should remove LifecycleObserver from activity when you create a new instance of BiometricPrompt class. Since there is no direct access to the observer, you need use reflection here. I modified your code based on this solution as below:
class MainActivity : AppCompatActivity() {
private val TAG = MainActivity::class.java.name
private var lastLifecycleObserver: LifecycleObserver? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<View>(R.id.first).setOnClickListener {
authenticate(MyData(1, "first"))
}
findViewById<View>(R.id.second).setOnClickListener {
authenticate(MyData(2, "second"))
}
}
private fun authenticate(data: MyData) {
Log.e(TAG, "starting auth with $data")
lastLifecycleObserver?.let {
lifecycle.removeObserver(it)
lastLifecycleObserver = null
}
val biometricPrompt = BiometricPrompt(
this,
ContextCompat.getMainExecutor(this),
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
Log.e(TAG, "auth done : $data")
}
})
var field = BiometricPrompt::class.java.getDeclaredField("mLifecycleObserver")
field.isAccessible = true
lastLifecycleObserver = field.get(biometricPrompt) as LifecycleObserver
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setDeviceCredentialAllowed(true)
.setTitle("title")
.build()
biometricPrompt.authenticate(promptInfo)
}
}
data class MyData(
val id: Int,
val text: String
)
So it seems strange but I managed to get it working by introducing a parameter to MainActivity
here is the working code:
class MainActivity : AppCompatActivity() {
var dataParam : MyData? = null
companion object {
private val TAG = MainActivity::class.java.name
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<View>(R.id.firstBtn).setOnClickListener {
authenticate(MyData(1, "first"))
}
findViewById<View>(R.id.secondBtn).setOnClickListener {
authenticate(MyData(2, "second"))
}
}
private fun authenticate(data: MyData) {
Log.e(TAG, "starting auth with $data")
dataParam = data
val biometricPrompt = BiometricPrompt(
this,
ContextCompat.getMainExecutor(this),
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
Log.e(TAG, "auth done : $dataParam")
}
})
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setDeviceCredentialAllowed(true)
.setTitle("title")
.build()
biometricPrompt.authenticate(promptInfo)
}
}
data class MyData(
val id: Int,
val text: String
)
The output is now:
E/com.worldsnas.bioissuesample.MainActivity: starting auth with MyData(id=1, text=first)
E/com.worldsnas.bioissuesample.MainActivity: auth done : MyData(id=1, text=first)
E/com.worldsnas.bioissuesample.MainActivity: starting auth with MyData(id=2, text=second)
E/com.worldsnas.bioissuesample.MainActivity: auth done : MyData(id=2, text=second)
Since you are asking about setDeviceCredentialAllowed(true), it's safe to assume you aren't following the recommended implementation that uses CryptoObject. (Also check out this blog post.)
The setDeviceCredentialAllowed(true) functionality will only work on API 21+, but you have multiple options for handling it in your app depending on your minSdkVersion.
API 23+
if your app is targeting API 23+, then you can do
if (keyguardManager.isDeviceSecure()){
biometricPrompt.authenticate(promptInfo)
}
API 16 to pre-API 23
If your app must make the check pre API 23, you can use
if (keyguardManager.isKeyguardSecure) {
biometricPrompt.authenticate(promptInfo)
}
KeyguardManager.isKeyguardSecure() is equivalent to isDeviceSecure() unless the device is SIM-locked.
API 14 to API 16
If you are targeting lower than API 16 or SIM-lock is an issue, then you should simply rely on the error codes in the callback onAuthenticationError().
P.S.
You should replace private val TAG = MainActivity::class.java.name with private val TAG = "MainActivity".
Related
I am working on a multiplayer game server. To do that I implemented an okhttp websocket. I use callbackFlow to handle the callback functions like onMessage or onFailure. This is my repo code:
class WebSocketRepository {
fun socketEventsFlow(): Flow<GameServerResponse> = callbackFlow {
val socketListener = object : WebSocketListener() {
override fun onMessage(webSocket: WebSocket, text: String) {
val message = Gson().fromJson(text, GameServerResponse::class.java)
trySendBlocking(message)
}
}
attachWebSocketListener(socketListener)
awaitClose {
socket.close(1000, "application in Background")
}
}
private fun attachWebSocketListener(listener: WebSocketListener) {
val client = OkHttpClient()
val request = Request
.Builder()
.url("ws://10.0.2.2:8080")
.build()
socket = client.newWebSocket(request, listener)
}
}
I then forward pass this Flow through the viewModel to the Activity to for example launch a new fragment. This is the viewModel's code:
class MainActivityViewModel #Inject constructor(
private val repository: WebSocketRepository
) : ViewModel() {
val events = repository.socketEventsFlow()
}
I finally collect the flow in the activity like this:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: MainActivityViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.events.collect { event ->
//Do stuff here
}
}
}
}
}
There are two problems though.
If the user closes the app (in the background, not fully terminate it), the socket is closed due to the awaitClose statement. I could easily fix that by setting Lifecycle.state.STARTED to Lifecycle.state.CREATED. I worry though that if I do that I would run into problems. For example navigating while the app is in the background. I don't want to create a new socket every time the user closes the app though.
The second problem is the "more important" one. How do I correctly handle reconncetions with this approach? If for example the internet connections drops the server will detect that the client is not there anymore and terminate the connection. I want the app to try to automatically reconnect though. But I have no clue how to implement that. I believe that I would somehow have to re-collect the viewModel's event Flow. But I have no idea on how to actually do that. Is there a better way to handle reconnections?
I have tried to setup fingerprint, pattern/password/pin, faceUnlock for my app. But Biometric doesn't works it always showing fingerprint with use pattern dialog. Is Still Biometrics not support FaceUnlock? If Not supported Biometric dependency means which library I should use in my app which contains fingerprint, pattern/password/pin and faceUnlock.
class LoginActivity : AppCompatActivity(){
private lateinit var biometricPrompt: BiometricPrompt
private lateinit var executor: Executor
private lateinit var callBack: BiometricPrompt.AuthenticationCallback
lateinit var prompt: BiometricPrompt.PromptInfo
private var keyguardManager: KeyguardManager? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivityLoginBinding>(this, R.layout.activity_login)
executor = ContextCompat.getMainExecutor(this)
biometricPrompt = BiometricPrompt(this, executor, object: BiometricPrompt.AuthenticationCallback(){
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
tvAuthStatus.text = "Authentication Failed"
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
startActivity(Intent(this#LoginActivity, MainActivity::class.java))
finish()
tvAuthStatus.text = "Authentication Success"
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
tvAuthStatus.text = "Error" + errString
}
})
prompt = BiometricPrompt.PromptInfo.Builder()
.setTitle("RoomDB FingerPrint Login")
.setNegativeButtonText("Login using fingerprint or face")
.setNegativeButtonText("Cancel")
.build()
btnAuth.setOnClickListener {
biometricPrompt.authenticate(prompt)
}
}
}
Maybe, in your case, the reason is your test device is released with the face authentication feature before the same API for face authentication appears in the official Android SDK. It makes third-party applications in your test device cannot use face authentication. Check this library, it said about this thing in README.md "I have a device that can be unlocked using Fingerprint/Face/Iris and(or) I can use this biometric type in pre-installed apps. But it doesn't work on 3rd party apps. Can you help?"
https://github.com/sergeykomlach/AdvancedBiometricPromptCompat#i-have-a-device-that-can-be-unlocked-using-fingerprintfaceiris-andor-i-can-use-this-biometric-type-in-pre-installed-apps-but-it-doesnt-work-on-3rd-party-apps-can--you-help
Hope it can help.
Following is my code to detect biometric functionality
class MainActivity : AppCompatActivity() {
private var executor: Executor? = null
private var biometricPrompt: BiometricPrompt? = null
private var promptInfo: BiometricPrompt.PromptInfo? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val bioMetricManager = BiometricManager.from(this)
when (bioMetricManager.canAuthenticate()) {
BiometricManager.BIOMETRIC_SUCCESS -> {
executor = ContextCompat.getMainExecutor(this)
promptInfo = BiometricPrompt.PromptInfo.Builder().setTitle("Biometric login for leaao")
.setSubtitle("Log in using biometric credentials").setDeviceCredentialAllowed(true).build()
biometricPrompt = BiometricPrompt(this,
executor!!, object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
Log.i("here4","Authentication error: $errString")
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
Log.i("here5","Success")
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
Log.i("here6","Authentication failed: ")
}
})
biometricPrompt?.authenticate(promptInfo!!)
}
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
Log.i("here","No biometric features available on this device.")
}
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
Log.i("here2","Biometric features are currently unavailable.")
}
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
Log.i("here3","The user hasn't associated any biometric credentials with their account.")
}
}
}
}
I have added <uses-permission android:name="android.permission.USE_BIOMETRIC" /> to Manifest file and added implementation 'androidx.biometric:biometric:1.0.1' to gradle
Still my code fails to detect biometric prompt. I have enabled face recognition on my mobile but it does not work when i open my app. It goes to BIOMETRIC_ERROR_HW_UNAVAILABLE case in my code.
By default, the BiometricManager seeks BIOMETRIC_STRONG sensors on the device, unless you specify them in .setAllowedAutnenticators(). However, you should know that as of now, there are no devices with STRONG face/iris sensors. That means that only the fingerprint sensor is "seen" as STRONG and all of the face and iris sensors (with some exceptions, where they are not seen as a sensor at all) are "seen" as BIOMETRIC_WEAK. With "seen" I mean they were set as such by the manufacturer. So having said that, something like this should be able to fix your issue:
BiometricPrompt.PromptInfo.Builder()
.setTitle(...)
.setDescription(...)
.setAllowedAuthenticators(
BiometricManager.Authenticators.BIOMETRIC_STRONG or
BiometricManager.Authenticators.BIOMETRIC_WEAK
)
and when checking for available authentication:
when (BiometricManager.from(requireContext()).canAuthenticate(
BiometricManager.Authenticators.BIOMETRIC_STRONG
or BiometricManager.Authenticators.BIOMETRIC_WEAK
)) { ... }
In Google's official codelab about advanced-coroutines-codelab sample, they've used ConflatedBroadcastChannel to watch a variable/object change.
I've used the same technique in one of my side projects, and when resuming the listening activity, sometimes ConflatedBroadcastChannel fires it's recent value, causing the execution of flatMapLatest body without any change.
I think this is happening while the system collects the garbage since I can reproduce this issue by calling System.gc() from another activity.
Here's the code
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
val tvCount = findViewById<TextView>(R.id.tv_count)
viewModel.count.observe(this, Observer {
tvCount.text = it
Toast.makeText(this, "Incremented", Toast.LENGTH_LONG).show();
})
findViewById<Button>(R.id.b_inc).setOnClickListener {
viewModel.increment()
}
findViewById<Button>(R.id.b_detail).setOnClickListener {
startActivity(Intent(this, DetailActivity::class.java))
}
}
}
MainViewModel.kt
class MainViewModel : ViewModel() {
companion object {
val TAG = MainViewModel::class.java.simpleName
}
class IncrementRequest
private var tempCount = 0
private val requestChannel = ConflatedBroadcastChannel<IncrementRequest>()
val count = requestChannel
.asFlow()
.flatMapLatest {
tempCount++
Log.d(TAG, "Incrementing number to $tempCount")
flowOf("Number is $tempCount")
}
.asLiveData()
fun increment() {
requestChannel.offer(IncrementRequest())
}
}
DetailActivity.kt
class DetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_detail)
val button = findViewById<Button>(R.id.b_gc)
val timer = object : CountDownTimer(5000, 1000) {
override fun onFinish() {
button.isEnabled = true
button.text = "CALL SYSTEM.GC() AND CLOSE ACTIVITY"
}
override fun onTick(millisUntilFinished: Long) {
button.text = "${TimeUnit.MILLISECONDS.toSeconds(millisUntilFinished)} second(s)"
}
}
button.setOnClickListener {
System.gc()
finish()
}
timer.start()
}
}
Here's the full source code :
CoroutinesFlowTest.zip
Why is this happening?
What am I missing?
Quoting from the official response, (The simple and straightforward solution)
The problem here is that you are trying to use
ConflatedBroadcastChannel for events, while it is designed to
represent current state as shown in the codelab. Every time the
downstream LiveData is reactivated it receives the most recent state
and performs the incrementing action. Don't use
ConflatedBroadcastChannel for events.
To fix it, you can replace ConflatedBroadcastChannel with
BroadcastChannel<IncrementRequest>(1) (non-conflated channel, which is
Ok for events to use) and it'll work as you expect it too.
In addition to the answer of Kiskae:
This might not be your case, but you can try to use BroadcastChannel(1).asFlow().conflate on a receiver side, but in my case it led to a bug where the code on a receiver side didn't get triggered sometimes (I think because conflate works in a separate coroutine or something).
Or you can use a custom version of stateless ConflatedBroadcastChannel (found here).
class StatelessBroadcastChannel<T> constructor(
private val broadcast: BroadcastChannel<T> = ConflatedBroadcastChannel()
) : BroadcastChannel<T> by broadcast {
override fun openSubscription(): ReceiveChannel<T> = broadcast
.openSubscription()
.apply { poll() }
}
On Coroutine 1.4.2 and Kotlin 1.4.31
Without using live data
private var tempCount = 0
private val requestChannel = BroadcastChannel<IncrementRequest>(Channel.CONFLATED)
val count = requestChannel
.asFlow()
.flatMapLatest {
tempCount++
Log.d(TAG, "Incrementing number to $tempCount")
flowOf("Number is $tempCount")
}
Use Flow and Coroutine
lifecycleScope.launchWhenStarted {
viewModel.count.collect {
tvCount.text = it
Toast.makeText(this#MainActivity, "Incremented", Toast.LENGTH_SHORT).show()
}
}
Without using BroadcastChannel
private var tempCount = 0
private val requestChannel = MutableStateFlow("")
val count: StateFlow<String> = requestChannel
fun increment() {
tempCount += 1
requestChannel.value = "Number is $tempCount"
}
The reason is very simple, ViewModels can persist outside of the lifecycle of Activities. By moving to another activity and garbagecollecting you're disposing of the original MainActivity but keeping the original MainViewModel.
Then when you return from DetailActivity it recreates MainActivity but reuses the viewmodel, which still has the broadcastchannel with a last known value, triggering the callback when count.observe is called.
If you add logging to observe the onCreate and onDestroy methods of the activity you should see the lifecycle getting advanced, while the viewmodel should only be created once.
I'm developing Kotlin application using Firebase Phone Authentication. I'm confused on implementing this verifyphonenumber.
private fun startPhoneNumberVerification(phoneNumber: String, mCallbacks: PhoneAuthProvider.OnVerificationStateChangedCallbacks?) {
Log.d("phoneNumber==", "" + phoneNumber);
PhoneAuthProvider.getInstance().verifyPhoneNumber(
phoneNumber, // Phone number to verify
60, // Timeout duration
TimeUnit.SECONDS, // Unit of timeout
this#LoginActivity, // Activity (for callback binding)
mCallbacks)
}
Implemented above code and getting error("None of the following functions can be called with the aruguments supplied") and also "creating extension function PhoneAuthProvider?.verifyPhoneNumber". Can someone Please guide me?
I can't think of anything else so I'm assuming the Firebase callbacks parameter is annotated as nonnull.
TL;DR: remove the ? from your callbacks parameter or no-op when it's null so Kotlin can do some magic type inference.
Since you are sure the callback will not be null as you will initialise this in onCreate, declare it as
lateinit var mCallbacks: PhoneAuthProvider.OnVerificationStateChangedCallbacks
This works for me
class MainActivity : AppCompatActivity() {
var fbAuth = FirebaseAuth.getInstance()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
var btnLogin = findViewById<Button>(R.id.btnLogin)
btnLogin.setOnClickListener {view ->
signIn(view,"user#company.com", "pass")
}
}
fun signIn(view: View,email: String, password: String){
showMessage(view,"Authenticating...")
fbAuth.signInWithEmailAndPassword(email, password).addOnCompleteListener(this, OnCompleteListener<AuthResult> { task ->
if(task.isSuccessful){
var intent = Intent(this, LoggedInActivity::class.java)
intent.putExtra("id", fbAuth.currentUser?.email)
startActivity(intent)
}else{
showMessage(view,"Error: ${task.exception?.message}")
}
})
}
fun showMessage(view:View, message: String){
Snackbar.make(view, message, Snackbar.LENGTH_INDEFINITE).setAction("Action", null).show()
}
}