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" }