From 11bd8a28a666adda8ed2d1664ead32950e8326bb Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Tue, 14 May 2024 21:51:34 +0800 Subject: [PATCH 1/6] add basic auth (PIN) --- app/build.gradle.kts | 8 +- app/src/main/AndroidManifest.xml | 5 +- .../main/java/com/bintianqi/owndroid/Auth.kt | 92 +++++++++++++++++++ .../com/bintianqi/owndroid/MainActivity.kt | 32 ++++--- gradle/libs.versions.toml | 2 + 5 files changed, 123 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/com/bintianqi/owndroid/Auth.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f82226f..5561787 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -24,18 +24,21 @@ android { versionCode = 27 versionName = "5.2" multiDexEnabled = false - signingConfig = signingConfigs.getByName("testkey") } buildTypes { release { - project.gradle.startParameter.excludedTaskNames.add("lint") + //project.gradle.startParameter.excludedTaskNames.add("lint") isMinifyEnabled = true isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) + signingConfig = signingConfigs.getByName("testkey") + } + debug { + signingConfig = signingConfigs.getByName("testkey") } } compileOptions { @@ -75,4 +78,5 @@ dependencies { implementation(libs.androidx.navigation.compose) implementation(libs.shizuku.provider) implementation(libs.shizuku.api) + implementation(libs.androidx.biometric) } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ea56320..9d982af 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -45,7 +45,10 @@ - + + , blackTheme:MutableState Date: Wed, 15 May 2024 12:42:59 +0800 Subject: [PATCH 2/6] use AuthFragment instead of AuthActivity --- app/src/main/AndroidManifest.xml | 4 -- .../main/java/com/bintianqi/owndroid/Auth.kt | 59 ++++++++++--------- .../com/bintianqi/owndroid/MainActivity.kt | 43 +++++++++----- app/src/main/res/layout/base.xml | 8 +++ 4 files changed, 68 insertions(+), 46 deletions(-) create mode 100644 app/src/main/res/layout/base.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9d982af..efeb69c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -45,10 +45,6 @@ - - + + + + From 405b897754ce4961c708181f732af0de31db140a Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Wed, 15 May 2024 13:48:37 +0800 Subject: [PATCH 3/6] support using biometrics to auth --- .../main/java/com/bintianqi/owndroid/Auth.kt | 52 +++++++++++++++---- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/bintianqi/owndroid/Auth.kt b/app/src/main/java/com/bintianqi/owndroid/Auth.kt index 686f77e..6075013 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Auth.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Auth.kt @@ -10,6 +10,7 @@ import android.widget.Toast import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt.AuthenticationCallback +import androidx.biometric.BiometricPrompt.PromptInfo import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -22,12 +23,15 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity import com.bintianqi.owndroid.ui.theme.OwnDroidTheme +import kotlinx.coroutines.* class AuthFragment: Fragment() { + @OptIn(DelicateCoroutinesApi::class) @SuppressLint("UnrememberedMutableState") override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { val sharedPref = context?.getSharedPreferences("data", Context.MODE_PRIVATE)!! @@ -40,6 +44,11 @@ class AuthFragment: Fragment() { fragmentTransaction.commit() } } + val promptInfo = PromptInfo.Builder() + .setTitle("Auth") + .setSubtitle("Auth OwnDroid with password or biometric") + .setConfirmationRequired(true) + var fallback = false val callback = object: AuthenticationCallback() { override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { super.onAuthenticationSucceeded(result) @@ -48,16 +57,32 @@ class AuthFragment: Fragment() { override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { super.onAuthenticationError(errorCode, errString) if(errorCode == BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL) onAuthSucceed() + if(errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON) fallback = true if(errorCode == BiometricPrompt.ERROR_CANCELED) return Toast.makeText(context, errString, Toast.LENGTH_SHORT).show() } } + GlobalScope.launch(Dispatchers.Main) { + while(true){ + if(fallback){ + val fallbackPromptInfo = PromptInfo.Builder() + .setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL) + .setTitle("Auth") + .setSubtitle("Auth OwnDroid with password") + .setConfirmationRequired(true) + .build() + authWithBiometricPrompt(requireActivity(), fallbackPromptInfo, callback) + break + } + delay(50) + } + } return ComposeView(requireContext()).apply { setContent { val materialYou = mutableStateOf(sharedPref.getBoolean("material_you",true)) val blackTheme = mutableStateOf(sharedPref.getBoolean("black_theme", false)) OwnDroidTheme(materialYou.value, blackTheme.value) { - Auth(this@AuthFragment.requireActivity(), callback) + Auth(this@AuthFragment.requireActivity(), callback, promptInfo) } } } @@ -65,7 +90,8 @@ class AuthFragment: Fragment() { } @Composable -fun Auth(activity: FragmentActivity, callback: AuthenticationCallback) { +fun Auth(activity: FragmentActivity, callback: AuthenticationCallback, promptInfo: PromptInfo.Builder) { + val context = LocalContext.current Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, @@ -74,7 +100,19 @@ fun Auth(activity: FragmentActivity, callback: AuthenticationCallback) { Text(text = "Authenticate", style = MaterialTheme.typography.headlineLarge, color = MaterialTheme.colorScheme.onBackground) Button( onClick = { - authWithBiometricPrompt(activity, callback) + val bioManager = BiometricManager.from(context) + when(BiometricManager.BIOMETRIC_SUCCESS){ + bioManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) -> + promptInfo + .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG) + .setNegativeButtonText("Use password") + bioManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) -> + promptInfo + .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_WEAK) + .setNegativeButtonText("Use password") + else -> promptInfo.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL) + } + authWithBiometricPrompt(activity, promptInfo.build(), callback) } ){ Text(text = "Start") @@ -82,14 +120,8 @@ fun Auth(activity: FragmentActivity, callback: AuthenticationCallback) { } } -private fun authWithBiometricPrompt(activity: FragmentActivity, callback: AuthenticationCallback) { +private fun authWithBiometricPrompt(activity: FragmentActivity, promptInfo: PromptInfo, callback: AuthenticationCallback) { val executor = ContextCompat.getMainExecutor(activity.applicationContext) val biometricPrompt = BiometricPrompt(activity, executor, callback) - val promptInfo = BiometricPrompt.PromptInfo.Builder() - .setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL) - .setTitle("Auth") - .setConfirmationRequired(true) - .setSubtitle("Enter password") - .build() biometricPrompt.authenticate(promptInfo) } From fcfaedd257ecbc080fc4dccf1755586e31b2964c Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Thu, 16 May 2024 13:40:27 +0800 Subject: [PATCH 4/6] add a switch in Settings to enable or disable auth --- .../main/java/com/bintianqi/owndroid/Auth.kt | 25 ++++++----- .../com/bintianqi/owndroid/MainActivity.kt | 7 +++- .../java/com/bintianqi/owndroid/Setting.kt | 41 +++++++++++++++---- app/src/main/res/values-zh-rCN/strings.xml | 3 ++ app/src/main/res/values/strings.xml | 3 ++ 5 files changed, 60 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/com/bintianqi/owndroid/Auth.kt b/app/src/main/java/com/bintianqi/owndroid/Auth.kt index 6075013..ccad6a8 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Auth.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Auth.kt @@ -101,16 +101,21 @@ fun Auth(activity: FragmentActivity, callback: AuthenticationCallback, promptInf Button( onClick = { val bioManager = BiometricManager.from(context) - when(BiometricManager.BIOMETRIC_SUCCESS){ - bioManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) -> - promptInfo - .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG) - .setNegativeButtonText("Use password") - bioManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) -> - promptInfo - .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_WEAK) - .setNegativeButtonText("Use password") - else -> promptInfo.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL) + val sharedPref = context.getSharedPreferences("data", Context.MODE_PRIVATE) + if(sharedPref.getBoolean("bio_auth", false)){ + when(BiometricManager.BIOMETRIC_SUCCESS){ + bioManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) -> + promptInfo + .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG) + .setNegativeButtonText("Use password") + bioManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) -> + promptInfo + .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_WEAK) + .setNegativeButtonText("Use password") + else -> promptInfo.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL) + } + }else{ + promptInfo.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL) } authWithBiometricPrompt(activity, promptInfo.build(), callback) } diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index c2e0a3c..ba44883 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -60,9 +60,14 @@ class MainActivity : FragmentActivity() { WindowCompat.setDecorFitsSystemWindows(window, false) super.onCreate(savedInstanceState) setContentView(R.layout.base) + val sharedPref = applicationContext.getSharedPreferences("data", Context.MODE_PRIVATE) val fragmentManager = supportFragmentManager val transaction = fragmentManager.beginTransaction() - transaction.add(R.id.base, AuthFragment(), "auth") + if(sharedPref.getBoolean("auth", false)){ + transaction.add(R.id.base, AuthFragment(), "auth") + }else{ + transaction.add(R.id.base, homeFragment, "home") + } transaction.commit() val locale = applicationContext.resources?.configuration?.locale zhCN = locale==Locale.SIMPLIFIED_CHINESE||locale==Locale.CHINESE||locale==Locale.CHINA diff --git a/app/src/main/java/com/bintianqi/owndroid/Setting.kt b/app/src/main/java/com/bintianqi/owndroid/Setting.kt index eb59a9a..2337fc9 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Setting.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Setting.kt @@ -14,9 +14,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.getValue +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -56,9 +54,10 @@ fun AppSetting(navCtrl:NavHostController, materialYou: MutableState, bl popExitTransition = Animations.navHostPopExitTransition, modifier = Modifier.padding(top = it.calculateTopPadding()) ){ - composable(route = "Home"){Home(localNavCtrl)} - composable(route = "Settings"){Settings(materialYou, blackTheme)} - composable(route = "About"){About()} + composable(route = "Home"){ Home(localNavCtrl) } + composable(route = "Theme"){ ThemeSettings(materialYou, blackTheme) } + composable(route = "Auth"){ AuthSettings() } + composable(route = "About"){ About() } } } } @@ -66,13 +65,14 @@ fun AppSetting(navCtrl:NavHostController, materialYou: MutableState, bl @Composable private fun Home(navCtrl: NavHostController){ Column(modifier = Modifier.fillMaxSize()){ - SubPageItem(R.string.setting,"",R.drawable.settings_fill0){navCtrl.navigate("Settings")} + SubPageItem(R.string.setting,"",R.drawable.settings_fill0){navCtrl.navigate("Theme")} + SubPageItem(R.string.security,"",R.drawable.settings_fill0){navCtrl.navigate("Auth")} SubPageItem(R.string.about,"",R.drawable.info_fill0){navCtrl.navigate("About")} } } @Composable -private fun Settings(materialYou:MutableState, blackTheme:MutableState){ +private fun ThemeSettings(materialYou:MutableState, blackTheme:MutableState){ val sharedPref = LocalContext.current.getSharedPreferences("data", Context.MODE_PRIVATE) Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())) { if(VERSION.SDK_INT>=31){ @@ -98,6 +98,31 @@ private fun Settings(materialYou:MutableState, blackTheme:MutableState< } } +@Composable +private fun AuthSettings(){ + val sharedPref = LocalContext.current.getSharedPreferences("data", Context.MODE_PRIVATE) + var auth by remember{ mutableStateOf(sharedPref.getBoolean("auth",false)) } + Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())) { + if(VERSION.SDK_INT>=30){ + SwitchItem( + R.string.lock_owndroid, "", null, + { auth }, + { + sharedPref.edit().putBoolean("auth",it).apply() + auth = sharedPref.getBoolean("auth",false) + } + ) + } + if(auth){ + SwitchItem( + R.string.enable_bio_auth, "", null, + { sharedPref.getBoolean("bio_auth",false) }, + { sharedPref.edit().putBoolean("bio_auth",it).apply() } + ) + } + } +} + @Composable private fun About(){ val myContext = LocalContext.current diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index bb31a11..0c73510 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -480,6 +480,9 @@ 源代码 纯黑夜间主题 需要打开夜间模式 + 安全 + 锁定OwnDroid + 使用生物识别 读取外部存储 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index df0a295..44c4853 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -495,6 +495,9 @@ Source code Black theme Require dark mode on + Security + Lock OwnDroid + Auth with biometrics Read external storage From a2082641b1932d73872cde565fc5cebfbb9aa088 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Thu, 16 May 2024 21:17:53 +0800 Subject: [PATCH 5/6] optimize UI and simplify code of Authenticaion --- app/build.gradle.kts | 4 +- .../main/java/com/bintianqi/owndroid/Auth.kt | 126 ++++++++++-------- .../java/com/bintianqi/owndroid/Setting.kt | 37 +++-- app/src/main/res/anim/enter.xml | 18 +++ app/src/main/res/anim/exit.xml | 8 ++ .../main/res/drawable/format_paint_fill0.xml | 9 ++ app/src/main/res/values-zh-rCN/strings.xml | 8 ++ app/src/main/res/values/strings.xml | 8 ++ app/src/main/res/values/themes.xml | 1 - 9 files changed, 144 insertions(+), 75 deletions(-) create mode 100644 app/src/main/res/anim/enter.xml create mode 100644 app/src/main/res/anim/exit.xml create mode 100644 app/src/main/res/drawable/format_paint_fill0.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5561787..c43adde 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -21,8 +21,8 @@ android { applicationId = "com.bintianqi.owndroid" minSdk = 21 targetSdk = 34 - versionCode = 27 - versionName = "5.2" + versionCode = 28 + versionName = "5.3" multiDexEnabled = false } diff --git a/app/src/main/java/com/bintianqi/owndroid/Auth.kt b/app/src/main/java/com/bintianqi/owndroid/Auth.kt index ccad6a8..7bc5594 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Auth.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Auth.kt @@ -3,14 +3,15 @@ package com.bintianqi.owndroid import android.annotation.SuppressLint import android.content.Context import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Toast +import android.widget.FrameLayout import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt.AuthenticationCallback -import androidx.biometric.BiometricPrompt.PromptInfo +import androidx.biometric.BiometricPrompt.PromptInfo.Builder import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -18,35 +19,38 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.lifecycleScope import com.bintianqi.owndroid.ui.theme.OwnDroidTheme import kotlinx.coroutines.* class AuthFragment: Fragment() { - @OptIn(DelicateCoroutinesApi::class) @SuppressLint("UnrememberedMutableState") override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - val sharedPref = context?.getSharedPreferences("data", Context.MODE_PRIVATE)!! + val context = requireContext() + val sharedPref = context.getSharedPreferences("data", Context.MODE_PRIVATE)!! + val canStartAuth = mutableStateOf(true) val onAuthSucceed = { val fragmentManager = this.parentFragmentManager - val fragment = fragmentManager.findFragmentByTag("auth") - if(fragment != null) { - val fragmentTransaction = fragmentManager.beginTransaction() - fragmentTransaction.replace(R.id.base, homeFragment) - fragmentTransaction.commit() + val transaction = fragmentManager.beginTransaction() + transaction.setCustomAnimations(R.anim.enter, R.anim.exit) + transaction.add(R.id.base, homeFragment) + requireActivity().findViewById(R.id.base).bringChildToFront(homeFragment.view) + transaction.commit() + lifecycleScope.launch { + delay(500) + fragmentManager.beginTransaction().remove(this@AuthFragment).commit() } } - val promptInfo = PromptInfo.Builder() - .setTitle("Auth") - .setSubtitle("Auth OwnDroid with password or biometric") + val promptInfo = Builder() + .setTitle(context.getText(R.string.authenticate)) + .setSubtitle(context.getText(R.string.auth_with_bio)) .setConfirmationRequired(true) var fallback = false val callback = object: AuthenticationCallback() { @@ -56,22 +60,26 @@ class AuthFragment: Fragment() { } override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { super.onAuthenticationError(errorCode, errString) - if(errorCode == BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL) onAuthSucceed() - if(errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON) fallback = true - if(errorCode == BiometricPrompt.ERROR_CANCELED) return - Toast.makeText(context, errString, Toast.LENGTH_SHORT).show() + when(errorCode){ + BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> onAuthSucceed() + BiometricPrompt.ERROR_NEGATIVE_BUTTON -> fallback = true + else -> canStartAuth.value = true + } + Log.e("OwnDroid", errString.toString()) } } - GlobalScope.launch(Dispatchers.Main) { + lifecycleScope.launch(Dispatchers.Main) { while(true){ if(fallback){ - val fallbackPromptInfo = PromptInfo.Builder() + val fallbackPromptInfo = Builder() .setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL) - .setTitle("Auth") - .setSubtitle("Auth OwnDroid with password") + .setTitle(context.getText(R.string.authenticate)) + .setSubtitle(context.getText(R.string.auth_with_password)) .setConfirmationRequired(true) .build() - authWithBiometricPrompt(requireActivity(), fallbackPromptInfo, callback) + val executor = ContextCompat.getMainExecutor(requireContext()) + val biometricPrompt = BiometricPrompt(requireActivity(), executor, callback) + biometricPrompt.authenticate(fallbackPromptInfo) break } delay(50) @@ -82,7 +90,7 @@ class AuthFragment: Fragment() { val materialYou = mutableStateOf(sharedPref.getBoolean("material_you",true)) val blackTheme = mutableStateOf(sharedPref.getBoolean("black_theme", false)) OwnDroidTheme(materialYou.value, blackTheme.value) { - Auth(this@AuthFragment.requireActivity(), callback, promptInfo) + Auth(this@AuthFragment, promptInfo, callback, canStartAuth) } } } @@ -90,43 +98,57 @@ class AuthFragment: Fragment() { } @Composable -fun Auth(activity: FragmentActivity, callback: AuthenticationCallback, promptInfo: PromptInfo.Builder) { - val context = LocalContext.current +fun Auth(activity: Fragment, promptInfo: Builder, callback: AuthenticationCallback, canStartAuth: MutableState) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background) ){ - Text(text = "Authenticate", style = MaterialTheme.typography.headlineLarge, color = MaterialTheme.colorScheme.onBackground) + Text( + text = stringResource(R.string.authenticate), + style = MaterialTheme.typography.headlineLarge, + color = MaterialTheme.colorScheme.onBackground + ) + LaunchedEffect(Unit){ + startAuth(activity, promptInfo, callback) + canStartAuth.value = false + } Button( onClick = { - val bioManager = BiometricManager.from(context) - val sharedPref = context.getSharedPreferences("data", Context.MODE_PRIVATE) - if(sharedPref.getBoolean("bio_auth", false)){ - when(BiometricManager.BIOMETRIC_SUCCESS){ - bioManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) -> - promptInfo - .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG) - .setNegativeButtonText("Use password") - bioManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) -> - promptInfo - .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_WEAK) - .setNegativeButtonText("Use password") - else -> promptInfo.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL) - } - }else{ - promptInfo.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL) - } - authWithBiometricPrompt(activity, promptInfo.build(), callback) - } + startAuth(activity, promptInfo, callback) + canStartAuth.value = false + }, + enabled = canStartAuth.value ){ - Text(text = "Start") + Text(text = stringResource(R.string.start)) } } } -private fun authWithBiometricPrompt(activity: FragmentActivity, promptInfo: PromptInfo, callback: AuthenticationCallback) { - val executor = ContextCompat.getMainExecutor(activity.applicationContext) +private fun startAuth(activity: Fragment, promptInfo: Builder, callback: AuthenticationCallback){ + val context = activity.requireContext() + val bioManager = BiometricManager.from(context) + val sharedPref = context.getSharedPreferences("data", Context.MODE_PRIVATE) + if(sharedPref.getBoolean("bio_auth", false)){ + when(BiometricManager.BIOMETRIC_SUCCESS){ + bioManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) -> + promptInfo + .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG) + .setNegativeButtonText(context.getText(R.string.use_password)) + bioManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) -> + promptInfo + .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_WEAK) + .setNegativeButtonText(context.getText(R.string.use_password)) + else -> promptInfo + .setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL) + .setSubtitle(context.getText(R.string.auth_with_password)) + } + }else{ + promptInfo + .setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL) + .setSubtitle(context.getText(R.string.auth_with_password)) + } + val executor = ContextCompat.getMainExecutor(context) val biometricPrompt = BiometricPrompt(activity, executor, callback) - biometricPrompt.authenticate(promptInfo) + biometricPrompt.authenticate(promptInfo.build()) } diff --git a/app/src/main/java/com/bintianqi/owndroid/Setting.kt b/app/src/main/java/com/bintianqi/owndroid/Setting.kt index 2337fc9..ee79319 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Setting.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Setting.kt @@ -5,10 +5,7 @@ import android.content.Intent import android.net.Uri import android.os.Build.VERSION import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme.typography @@ -24,10 +21,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController -import com.bintianqi.owndroid.ui.Animations -import com.bintianqi.owndroid.ui.SubPageItem -import com.bintianqi.owndroid.ui.SwitchItem -import com.bintianqi.owndroid.ui.TopBar +import com.bintianqi.owndroid.ui.* @Composable fun AppSetting(navCtrl:NavHostController, materialYou: MutableState, blackTheme: MutableState){ @@ -65,8 +59,8 @@ fun AppSetting(navCtrl:NavHostController, materialYou: MutableState, bl @Composable private fun Home(navCtrl: NavHostController){ Column(modifier = Modifier.fillMaxSize()){ - SubPageItem(R.string.setting,"",R.drawable.settings_fill0){navCtrl.navigate("Theme")} - SubPageItem(R.string.security,"",R.drawable.settings_fill0){navCtrl.navigate("Auth")} + SubPageItem(R.string.theme,"",R.drawable.format_paint_fill0){navCtrl.navigate("Theme")} + SubPageItem(R.string.security,"",R.drawable.lock_fill0){navCtrl.navigate("Auth")} SubPageItem(R.string.about,"",R.drawable.info_fill0){navCtrl.navigate("About")} } } @@ -103,16 +97,14 @@ private fun AuthSettings(){ val sharedPref = LocalContext.current.getSharedPreferences("data", Context.MODE_PRIVATE) var auth by remember{ mutableStateOf(sharedPref.getBoolean("auth",false)) } Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())) { - if(VERSION.SDK_INT>=30){ - SwitchItem( - R.string.lock_owndroid, "", null, - { auth }, - { - sharedPref.edit().putBoolean("auth",it).apply() - auth = sharedPref.getBoolean("auth",false) - } - ) - } + SwitchItem( + R.string.lock_owndroid, "", null, + { auth }, + { + sharedPref.edit().putBoolean("auth",it).apply() + auth = sharedPref.getBoolean("auth",false) + } + ) if(auth){ SwitchItem( R.string.enable_bio_auth, "", null, @@ -120,6 +112,11 @@ private fun AuthSettings(){ { sharedPref.edit().putBoolean("bio_auth",it).apply() } ) } + Box(modifier = Modifier.padding(horizontal = 8.dp)){ + Information { + Text(text = stringResource(R.string.auth_on_start)) + } + } } } diff --git a/app/src/main/res/anim/enter.xml b/app/src/main/res/anim/enter.xml new file mode 100644 index 0000000..fd3f154 --- /dev/null +++ b/app/src/main/res/anim/enter.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/anim/exit.xml b/app/src/main/res/anim/exit.xml new file mode 100644 index 0000000..80a3c74 --- /dev/null +++ b/app/src/main/res/anim/exit.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/drawable/format_paint_fill0.xml b/app/src/main/res/drawable/format_paint_fill0.xml new file mode 100644 index 0000000..3ef2f9e --- /dev/null +++ b/app/src/main/res/drawable/format_paint_fill0.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 0c73510..54066d4 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -46,6 +46,7 @@ 文件不存在 IO异常 当前状态: + 开始 点击以激活 @@ -478,11 +479,18 @@ 使用安卓的Device admin、Device owner、Profile owner,全方位掌控你的设备 使用教程 源代码 + 主题 纯黑夜间主题 需要打开夜间模式 + 安全 锁定OwnDroid 使用生物识别 + 验证 + 在OwnDroid启动时使用锁屏密码或生物识别进行验证 + 使用密码 + 使用密码进行验证 + 使用生物识别进行验证 读取外部存储 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 44c4853..28d2cd1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -49,6 +49,7 @@ File not exist IO Exception Current status:  + Start Click to activate @@ -493,11 +494,18 @@ Use Device admin, Profile owner and Device owner privilege to take full control of your device. User guide Source code + Theme Black theme Require dark mode on + Security Lock OwnDroid Auth with biometrics + Authenticate + Authenticating with keyguard password or biometrics when OwnDroid launch + Use password + Authenticate OwnDroid with password + Authenticate OwnDroid with biometrics Read external storage diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index c9379a0..55bb275 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -3,6 +3,5 @@ From 734f7763d178cdfa517fc0bfa69678a887a51d41 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Fri, 17 May 2024 12:48:18 +0800 Subject: [PATCH 6/6] add a option to lock OwnDroid when it is switched to background --- .../main/java/com/bintianqi/owndroid/Auth.kt | 19 ++++++------ .../com/bintianqi/owndroid/MainActivity.kt | 31 ++++++++++++++++--- .../java/com/bintianqi/owndroid/Setting.kt | 5 +++ app/src/main/res/anim/enter.xml | 8 ++--- app/src/main/res/anim/exit.xml | 4 +-- app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 7 files changed, 49 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/bintianqi/owndroid/Auth.kt b/app/src/main/java/com/bintianqi/owndroid/Auth.kt index 7bc5594..2305b26 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Auth.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Auth.kt @@ -7,7 +7,6 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.FrameLayout import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt.AuthenticationCallback @@ -19,7 +18,10 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView @@ -28,7 +30,9 @@ import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import com.bintianqi.owndroid.ui.theme.OwnDroidTheme -import kotlinx.coroutines.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch class AuthFragment: Fragment() { @SuppressLint("UnrememberedMutableState") @@ -40,13 +44,7 @@ class AuthFragment: Fragment() { val fragmentManager = this.parentFragmentManager val transaction = fragmentManager.beginTransaction() transaction.setCustomAnimations(R.anim.enter, R.anim.exit) - transaction.add(R.id.base, homeFragment) - requireActivity().findViewById(R.id.base).bringChildToFront(homeFragment.view) - transaction.commit() - lifecycleScope.launch { - delay(500) - fragmentManager.beginTransaction().remove(this@AuthFragment).commit() - } + transaction.remove(this@AuthFragment).commit() } val promptInfo = Builder() .setTitle(context.getText(R.string.authenticate)) @@ -110,6 +108,7 @@ fun Auth(activity: Fragment, promptInfo: Builder, callback: AuthenticationCallba color = MaterialTheme.colorScheme.onBackground ) LaunchedEffect(Unit){ + delay(300) startAuth(activity, promptInfo, callback) canStartAuth.value = false } diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index ba44883..85c3fbd 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -48,11 +48,10 @@ import com.bintianqi.owndroid.ui.theme.OwnDroidTheme import kotlinx.coroutines.delay import java.util.Locale -val homeFragment = HomeFragment() - var backToHome = false @ExperimentalMaterial3Api class MainActivity : FragmentActivity() { + private var auth = false @SuppressLint("UnrememberedMutableState") override fun onCreate(savedInstanceState: Bundle?) { registerActivityResult(this) @@ -63,15 +62,39 @@ class MainActivity : FragmentActivity() { val sharedPref = applicationContext.getSharedPreferences("data", Context.MODE_PRIVATE) val fragmentManager = supportFragmentManager val transaction = fragmentManager.beginTransaction() + transaction.add(R.id.base, HomeFragment(), "home") if(sharedPref.getBoolean("auth", false)){ transaction.add(R.id.base, AuthFragment(), "auth") - }else{ - transaction.add(R.id.base, homeFragment, "home") } transaction.commit() val locale = applicationContext.resources?.configuration?.locale zhCN = locale==Locale.SIMPLIFIED_CHINESE||locale==Locale.CHINESE||locale==Locale.CHINA } + + override fun onResume() { + super.onResume() + if(auth){ + val sharedPref = applicationContext.getSharedPreferences("data", Context.MODE_PRIVATE) + if( + sharedPref.getBoolean("auth", false) && + sharedPref.getBoolean("lock_in_background", false) + ){ + val fragmentManager = supportFragmentManager + val fragment = fragmentManager.findFragmentByTag("auth") + if(fragment == null){ + val transaction = fragmentManager.beginTransaction() + transaction.setCustomAnimations(R.anim.enter, R.anim.exit) + transaction.add(R.id.base, AuthFragment(), "auth").commit() + } + } + auth = false + } + } + + override fun onRestart() { + super.onRestart() + auth = true + } } class HomeFragment: Fragment() { diff --git a/app/src/main/java/com/bintianqi/owndroid/Setting.kt b/app/src/main/java/com/bintianqi/owndroid/Setting.kt index ee79319..225fbc3 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Setting.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Setting.kt @@ -111,6 +111,11 @@ private fun AuthSettings(){ { sharedPref.getBoolean("bio_auth",false) }, { sharedPref.edit().putBoolean("bio_auth",it).apply() } ) + SwitchItem( + R.string.lock_in_background, "", null, + { sharedPref.getBoolean("lock_in_background",false) }, + { sharedPref.edit().putBoolean("lock_in_background",it).apply() } + ) } Box(modifier = Modifier.padding(horizontal = 8.dp)){ Information { diff --git a/app/src/main/res/anim/enter.xml b/app/src/main/res/anim/enter.xml index fd3f154..56ebee1 100644 --- a/app/src/main/res/anim/enter.xml +++ b/app/src/main/res/anim/enter.xml @@ -2,16 +2,16 @@ + android:duration="200" /> diff --git a/app/src/main/res/anim/exit.xml b/app/src/main/res/anim/exit.xml index 80a3c74..facb15f 100644 --- a/app/src/main/res/anim/exit.xml +++ b/app/src/main/res/anim/exit.xml @@ -1,8 +1,8 @@ + android:toAlpha="0.0" /> diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 54066d4..3212770 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -491,6 +491,7 @@ 使用密码 使用密码进行验证 使用生物识别进行验证 + 处于后台时锁定 读取外部存储 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 28d2cd1..014617b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -506,6 +506,7 @@ Use password Authenticate OwnDroid with password Authenticate OwnDroid with biometrics + Lock when switch to background Read external storage