diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index f82226f..c43adde 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -21,21 +21,24 @@ android {
applicationId = "com.bintianqi.owndroid"
minSdk = 21
targetSdk = 34
- versionCode = 27
- versionName = "5.2"
+ versionCode = 28
+ versionName = "5.3"
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..efeb69c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -45,7 +45,6 @@
-
onAuthSucceed()
+ BiometricPrompt.ERROR_NEGATIVE_BUTTON -> fallback = true
+ else -> canStartAuth.value = true
+ }
+ Log.e("OwnDroid", errString.toString())
+ }
+ }
+ lifecycleScope.launch(Dispatchers.Main) {
+ while(true){
+ if(fallback){
+ val fallbackPromptInfo = Builder()
+ .setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
+ .setTitle(context.getText(R.string.authenticate))
+ .setSubtitle(context.getText(R.string.auth_with_password))
+ .setConfirmationRequired(true)
+ .build()
+ val executor = ContextCompat.getMainExecutor(requireContext())
+ val biometricPrompt = BiometricPrompt(requireActivity(), executor, callback)
+ biometricPrompt.authenticate(fallbackPromptInfo)
+ 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, promptInfo, callback, canStartAuth)
+ }
+ }
+ }
+ }
+}
+
+@Composable
+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 = stringResource(R.string.authenticate),
+ style = MaterialTheme.typography.headlineLarge,
+ color = MaterialTheme.colorScheme.onBackground
+ )
+ LaunchedEffect(Unit){
+ delay(300)
+ startAuth(activity, promptInfo, callback)
+ canStartAuth.value = false
+ }
+ Button(
+ onClick = {
+ startAuth(activity, promptInfo, callback)
+ canStartAuth.value = false
+ },
+ enabled = canStartAuth.value
+ ){
+ Text(text = stringResource(R.string.start))
+ }
+ }
+}
+
+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.build())
+}
diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt
index b2aa449..1fcb3e9 100644
--- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt
@@ -6,9 +6,11 @@ import android.content.ComponentName
import android.content.Context
import android.os.Build.VERSION
import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -27,12 +29,15 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.view.WindowCompat
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
@@ -45,21 +50,65 @@ import java.util.Locale
var backToHome = false
@ExperimentalMaterial3Api
-class MainActivity : ComponentActivity() {
+class MainActivity : FragmentActivity() {
+ private var auth = false
@SuppressLint("UnrememberedMutableState")
override fun onCreate(savedInstanceState: Bundle?) {
+ registerActivityResult(this)
enableEdgeToEdge()
WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)
- registerActivityResult(this)
- val locale = applicationContext.resources.configuration.locale
- zhCN = locale==Locale.SIMPLIFIED_CHINESE||locale==Locale.CHINESE||locale==Locale.CHINA
+ setContentView(R.layout.base)
val sharedPref = applicationContext.getSharedPreferences("data", Context.MODE_PRIVATE)
- setContent {
- val materialYou = mutableStateOf(sharedPref.getBoolean("material_you",true))
- val blackTheme = mutableStateOf(sharedPref.getBoolean("black_theme", false))
- OwnDroidTheme(materialYou.value, blackTheme.value){
- Home(materialYou, blackTheme)
+ 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")
+ }
+ 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() {
+ @OptIn(ExperimentalMaterial3Api::class)
+ @SuppressLint("UnrememberedMutableState")
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ val sharedPref = context?.getSharedPreferences("data", Context.MODE_PRIVATE)!!
+ 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){
+ Home(materialYou, blackTheme)
+ }
}
}
}
@@ -96,18 +145,18 @@ fun Home(materialYou:MutableState, blackTheme:MutableState){
popEnterTransition = Animations.navHostPopEnterTransition,
popExitTransition = Animations.navHostPopExitTransition
){
- composable(route = "HomePage", content = { HomePage(navCtrl, pkgName)})
- composable(route = "SystemManage", content = { SystemManage(navCtrl) })
- composable(route = "ManagedProfile", content = {ManagedProfile(navCtrl)})
- composable(route = "Permissions", content = { DpmPermissions(navCtrl)})
- composable(route = "ApplicationManage", content = { ApplicationManage(navCtrl, pkgName, dialogStatus)})
- composable(route = "UserRestriction", content = { UserRestriction(navCtrl)})
- composable(route = "UserManage", content = { UserManage(navCtrl)})
- composable(route = "Password", content = { Password(navCtrl)})
- composable(route = "AppSetting", content = { AppSetting(navCtrl, materialYou, blackTheme)})
- composable(route = "Network", content = {Network(navCtrl)})
- composable(route = "PackageSelector"){PackageSelector(navCtrl, pkgName)}
- composable(route = "PermissionPicker"){PermissionPicker(navCtrl)}
+ composable(route = "HomePage"){ HomePage(navCtrl, pkgName) }
+ composable(route = "SystemManage"){ SystemManage(navCtrl) }
+ composable(route = "ManagedProfile"){ ManagedProfile(navCtrl) }
+ composable(route = "Permissions"){ DpmPermissions(navCtrl) }
+ composable(route = "ApplicationManage"){ ApplicationManage(navCtrl, pkgName, dialogStatus)}
+ composable(route = "UserRestriction"){ UserRestriction(navCtrl) }
+ composable(route = "UserManage"){ UserManage(navCtrl) }
+ composable(route = "Password"){ Password(navCtrl) }
+ composable(route = "AppSetting"){ AppSetting(navCtrl, materialYou, blackTheme) }
+ composable(route = "Network"){ Network(navCtrl) }
+ composable(route = "PackageSelector"){ PackageSelector(navCtrl, pkgName) }
+ composable(route = "PermissionPicker"){ PermissionPicker(navCtrl) }
}
LaunchedEffect(Unit){
val profileInited = sharedPref.getBoolean("ManagedProfileActivated",false)
diff --git a/app/src/main/java/com/bintianqi/owndroid/Setting.kt b/app/src/main/java/com/bintianqi/owndroid/Setting.kt
index c6efe35..dadc215 100644
--- a/app/src/main/java/com/bintianqi/owndroid/Setting.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/Setting.kt
@@ -5,18 +5,13 @@ 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
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
@@ -26,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){
@@ -56,9 +48,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 +59,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.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")}
}
}
@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 +92,39 @@ 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())) {
+ 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() }
+ )
+ 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 {
+ Text(text = stringResource(R.string.auth_on_start))
+ }
+ }
+ }
+}
+
@Composable
private fun About(){
val context = LocalContext.current
diff --git a/app/src/main/res/anim/enter.xml b/app/src/main/res/anim/enter.xml
new file mode 100644
index 0000000..56ebee1
--- /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..facb15f
--- /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/layout/base.xml b/app/src/main/res/layout/base.xml
new file mode 100644
index 0000000..b6d7e67
--- /dev/null
+++ b/app/src/main/res/layout/base.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index bb31a11..3212770 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,9 +479,20 @@
使用安卓的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 df0a295..014617b 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,9 +494,20 @@
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
+ Lock when switch to background
+
Read external storage
Write 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 @@
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 58301ad..016e4a5 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -7,6 +7,7 @@ navigation-compose = "2.7.7"
material3 = "1.2.1"
accompanist-drawablepainter = "0.35.0-alpha"
shizuku = "13.1.5"
+biometric = "1.2.0-alpha05"
[libraries]
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" }
@@ -16,6 +17,7 @@ androidx-ui-graphics = { module = "androidx.compose.ui:ui-graphics" }
androidx-ui = { module = "androidx.compose.ui:ui" }
accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanist-drawablepainter" }
+androidx-biometric = { group = "androidx.biometric", name = "biometric", version.ref = "biometric" }
shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku" }
shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku" }