mod callback;
use jni::{
objects::{GlobalRef, JObject, JValueGen},
JNIEnv,
};
use crate::{BiometricStrength, Error, Result, Text};
pub(crate) type RawContext = ();
#[derive(Debug)]
pub(crate) struct Context;
impl Context {
pub(crate) fn new(_: RawContext) -> Self {
Self
}
pub(crate) fn authenticate<F>(
&self,
text: Text,
policy: &Policy,
callback: F,
) -> Result<()>
where
F: Fn(Result<()>) + Send + 'static,
{
self.authenticate_inner(text, policy, callback)
}
fn authenticate_inner<F>(
&self,
text: Text,
policy: &Policy,
callback: F,
) -> Result<()>
where
F: Fn(Result<()>) + Send + 'static,
{
if text.android.title.is_empty() {
return Err(Error::InvalidText);
}
robius_android_env::with_activity(|env, context| {
let callback_class = callback::get_callback_class(env)?;
let callback_boxed_dyn = Box::new(callback) as Box<dyn Fn(Result<()>) + Send>;
let callback_boxed_boxed_ptr = Box::into_raw(Box::new(callback_boxed_dyn));
let result = env.with_local_frame(16, |env| {
show_prompt(
env,
context,
callback_class,
callback_boxed_boxed_ptr as i64,
policy,
&text,
)
});
if result.is_err() {
if env.exception_check().unwrap_or(false) {
let _ = env.exception_clear();
}
drop(unsafe { Box::from_raw(callback_boxed_boxed_ptr) });
}
result
})
.map_err(Error::from)?
}
}
fn show_prompt(
env: &mut JNIEnv<'_>,
context: &JObject<'_>,
callback_class: &GlobalRef,
callback_box_ptr: i64,
policy: &Policy,
text: &Text,
) -> Result<()> {
let callback_instance = construct_callback(env, callback_class, callback_box_ptr)?;
let cancellation_signal = construct_cancellation_signal(env)?;
let executor = get_executor(env, context)?;
let biometric_prompt =
construct_biometric_prompt(env, context, policy, text, &executor, &callback_instance)?;
env.call_method(
biometric_prompt,
"authenticate",
"(\
Landroid/os/CancellationSignal;\
Ljava/util/concurrent/Executor;\
Landroid/hardware/biometrics/BiometricPrompt$AuthenticationCallback;\
)V",
&[
JValueGen::Object(&cancellation_signal),
JValueGen::Object(&executor),
JValueGen::Object(&callback_instance),
],
)?;
Ok(())
}
#[derive(Debug)]
pub(crate) struct Policy {
strength: Option<BiometricStrength>,
password: bool,
}
impl Policy {
#[inline]
pub(crate) fn set_action_id(&mut self, _: String) -> Result<()> {
Ok(())
}
}
#[derive(Debug)]
pub(crate) struct PolicyBuilder {
biometrics: Option<BiometricStrength>,
password: bool,
}
impl PolicyBuilder {
pub(crate) const fn new() -> Self {
Self {
biometrics: Some(BiometricStrength::Strong),
password: true,
}
}
pub(crate) const fn biometrics(self, biometrics: Option<BiometricStrength>) -> Self {
Self { biometrics, ..self }
}
pub(crate) const fn password(self, password: bool) -> Self {
Self { password, ..self }
}
pub(crate) const fn companion(self, _: bool) -> Self {
self
}
pub(crate) const fn wrist_detection(self, _: bool) -> Self {
self
}
pub(crate) fn action_ids(self, _: Vec<String>) -> Self {
self
}
pub(crate) const fn build(self) -> Option<Policy> {
if self.biometrics.is_none() && !self.password {
return None;
}
Some(Policy {
strength: self.biometrics,
password: self.password,
})
}
}
fn construct_callback<'a>(
env: &mut JNIEnv<'a>,
class: &GlobalRef,
callback_box_ptr: i64,
) -> Result<JObject<'a>> {
env.new_object(class, "(J)V", &[JValueGen::Long(callback_box_ptr)])
.map_err(|e| e.into())
}
fn construct_cancellation_signal<'a>(env: &mut JNIEnv<'a>) -> Result<JObject<'a>> {
env.new_object("android/os/CancellationSignal", "()V", &[])
.map_err(|e| e.into())
}
fn get_executor<'a, 'o, O>(env: &mut JNIEnv<'a>, context: O) -> Result<JObject<'a>>
where
O: AsRef<JObject<'o>>,
{
env.call_method(
context,
"getMainExecutor",
"()Ljava/util/concurrent/Executor;",
&[],
)?
.l()
.map_err(|e| e.into())
}
fn construct_biometric_prompt<'a>(
env: &mut JNIEnv<'a>,
context: &JObject<'_>,
policy: &Policy,
text: &Text,
executor: &JObject<'_>,
negative_button_listener: &JObject<'_>,
) -> Result<JObject<'a>> {
const STRONG: i32 = 0xf;
const WEAK: i32 = 0xff;
const CREDENTIAL: i32 = 0x8000;
const ANDROID_R_STRING_CANCEL: i32 = 17039360;
let sdk_int = env
.get_static_field("android/os/Build$VERSION", "SDK_INT", "I")?
.i()?;
let biometrics_mask = match policy.strength {
Some(BiometricStrength::Strong) => STRONG,
Some(BiometricStrength::Weak) => WEAK,
None => 0,
};
let credential_allowed = policy.password && sdk_int >= 29;
if biometrics_mask == 0 && !credential_allowed {
return Err(Error::Unavailable);
}
let builder = env.new_object(
"android/hardware/biometrics/BiometricPrompt$Builder",
"(Landroid/content/Context;)V",
&[JValueGen::Object(context)],
)?;
env.call_method(
&builder,
"setTitle",
"(Ljava/lang/CharSequence;)Landroid/hardware/biometrics/BiometricPrompt$Builder;",
&[JValueGen::Object(
&env.new_string(text.android.title)?.into(),
)],
)?;
if let Some(subtitle) = text.android.subtitle {
env.call_method(
&builder,
"setSubtitle",
"(Ljava/lang/CharSequence;)Landroid/hardware/biometrics/BiometricPrompt$Builder;",
&[JValueGen::Object(&env.new_string(subtitle)?.into())],
)?;
}
if let Some(description) = text.android.description {
env.call_method(
&builder,
"setDescription",
"(Ljava/lang/CharSequence;)Landroid/hardware/biometrics/BiometricPrompt$Builder;",
&[JValueGen::Object(&env.new_string(description)?.into())],
)?;
}
if sdk_int >= 30 {
env.call_method(
&builder,
"setAllowedAuthenticators",
"(I)Landroid/hardware/biometrics/BiometricPrompt$Builder;",
&[JValueGen::Int(
biometrics_mask | if credential_allowed { CREDENTIAL } else { 0 },
)],
)?;
} else if credential_allowed {
env.call_method(
&builder,
"setDeviceCredentialAllowed",
"(Z)Landroid/hardware/biometrics/BiometricPrompt$Builder;",
&[JValueGen::Bool(1)],
)?;
}
if !credential_allowed {
let cancel_text = env
.call_method(
context,
"getString",
"(I)Ljava/lang/String;",
&[JValueGen::Int(ANDROID_R_STRING_CANCEL)],
)?
.l()?;
env.call_method(
&builder,
"setNegativeButton",
"(\
Ljava/lang/CharSequence;\
Ljava/util/concurrent/Executor;\
Landroid/content/DialogInterface$OnClickListener;\
)Landroid/hardware/biometrics/BiometricPrompt$Builder;",
&[
JValueGen::Object(&cancel_text),
JValueGen::Object(executor),
JValueGen::Object(negative_button_listener),
],
)?;
}
env.call_method(
builder,
"build",
"()Landroid/hardware/biometrics/BiometricPrompt;",
&[],
)?
.l()
.map_err(|e| e.into())
}