From 380675cf8faad1e341072cc6b385f049b57b1060 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sat, 7 Dec 2024 12:31:13 +0800 Subject: [PATCH 01/12] Use main NavHost to navigate across all pages Upgrade dependencies Upload dependencies to GitHub in workflow --- .github/workflows/build.yml | 3 + .../com/bintianqi/owndroid/MainActivity.kt | 270 +++++++++++++----- .../java/com/bintianqi/owndroid/Settings.kt | 97 ++----- .../bintianqi/owndroid/dpm/Applications.kt | 32 +-- .../java/com/bintianqi/owndroid/dpm/DPM.kt | 16 +- .../bintianqi/owndroid/dpm/ManagedProfile.kt | 104 ++----- .../com/bintianqi/owndroid/dpm/Network.kt | 217 +++++--------- .../com/bintianqi/owndroid/dpm/Password.kt | 149 +++------- .../com/bintianqi/owndroid/dpm/Permissions.kt | 135 +++------ .../dpm/{ShizukuActivate.kt => Shizuku.kt} | 46 +-- .../java/com/bintianqi/owndroid/dpm/System.kt | 238 ++++----------- .../bintianqi/owndroid/dpm/UserRestriction.kt | 124 ++------ .../java/com/bintianqi/owndroid/dpm/Users.kt | 143 ++-------- .../com/bintianqi/owndroid/ui/Components.kt | 81 ++++-- app/src/main/res/values-ru/strings.xml | 20 +- app/src/main/res/values-tr/strings.xml | 20 +- app/src/main/res/values-zh-rCN/strings.xml | 20 +- app/src/main/res/values/strings.xml | 22 +- gradle/libs.versions.toml | 10 +- 19 files changed, 647 insertions(+), 1100 deletions(-) rename app/src/main/java/com/bintianqi/owndroid/dpm/{ShizukuActivate.kt => Shizuku.kt} (88%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b628522..c9b8903 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -58,6 +58,9 @@ jobs: name: OwnDroid-CI-${{ env.SHORT_SHA }}-release-signed path: app/build/outputs/apk/release/app-release.apk + - name: Generate and submit dependency graph + uses: gradle/actions/dependency-submission@v4 + upload-telegram: name: Upload Builds if: ${{ success() }} diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index 16ce7d6..c3bee73 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll @@ -27,6 +26,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.typography +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -53,15 +53,70 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import com.bintianqi.owndroid.dpm.AffiliationID +import com.bintianqi.owndroid.dpm.AlwaysOnVPNPackage import com.bintianqi.owndroid.dpm.ApplicationManage -import com.bintianqi.owndroid.dpm.DpmPermissions -import com.bintianqi.owndroid.dpm.ManagedProfile +import com.bintianqi.owndroid.dpm.CACert +import com.bintianqi.owndroid.dpm.ChangeTime +import com.bintianqi.owndroid.dpm.ChangeTimeZone +import com.bintianqi.owndroid.dpm.ChangeUserIcon +import com.bintianqi.owndroid.dpm.ChangeUsername +import com.bintianqi.owndroid.dpm.CreateUser +import com.bintianqi.owndroid.dpm.CreateWorkProfile +import com.bintianqi.owndroid.dpm.CurrentUserInfo +import com.bintianqi.owndroid.dpm.DeleteWorkProfile +import com.bintianqi.owndroid.dpm.DeviceAdmin +import com.bintianqi.owndroid.dpm.DeviceInfo +import com.bintianqi.owndroid.dpm.DeviceOwner +import com.bintianqi.owndroid.dpm.DisableAccountManagement +import com.bintianqi.owndroid.dpm.DisableKeyguardFeatures +import com.bintianqi.owndroid.dpm.FRPPolicy +import com.bintianqi.owndroid.dpm.InstallSystemUpdate +import com.bintianqi.owndroid.dpm.IntentFilter +import com.bintianqi.owndroid.dpm.Keyguard +import com.bintianqi.owndroid.dpm.LockScreenInfo +import com.bintianqi.owndroid.dpm.LockTaskMode +import com.bintianqi.owndroid.dpm.MTEPolicy +import com.bintianqi.owndroid.dpm.WorkProfile +import com.bintianqi.owndroid.dpm.NearbyStreamingPolicy import com.bintianqi.owndroid.dpm.Network +import com.bintianqi.owndroid.dpm.NetworkLogging +import com.bintianqi.owndroid.dpm.NetworkOptions +import com.bintianqi.owndroid.dpm.OrgOwnedProfile +import com.bintianqi.owndroid.dpm.OverrideAPN import com.bintianqi.owndroid.dpm.Password +import com.bintianqi.owndroid.dpm.PasswordComplexity +import com.bintianqi.owndroid.dpm.PasswordInfo +import com.bintianqi.owndroid.dpm.PasswordQuality +import com.bintianqi.owndroid.dpm.PermissionPolicy +import com.bintianqi.owndroid.dpm.Permissions +import com.bintianqi.owndroid.dpm.PreferentialNetworkService +import com.bintianqi.owndroid.dpm.PrivateDNS +import com.bintianqi.owndroid.dpm.ProfileOwner +import com.bintianqi.owndroid.dpm.RecommendedGlobalProxy +import com.bintianqi.owndroid.dpm.ResetPassword +import com.bintianqi.owndroid.dpm.ResetPasswordToken +import com.bintianqi.owndroid.dpm.RestrictionData +import com.bintianqi.owndroid.dpm.SecurityLogging +import com.bintianqi.owndroid.dpm.Shizuku +import com.bintianqi.owndroid.dpm.SupportMessages +import com.bintianqi.owndroid.dpm.SuspendPersonalApp import com.bintianqi.owndroid.dpm.SystemManage -import com.bintianqi.owndroid.dpm.UserManage +import com.bintianqi.owndroid.dpm.SystemOptions +import com.bintianqi.owndroid.dpm.SystemUpdatePolicy +import com.bintianqi.owndroid.dpm.TransferOwnership +import com.bintianqi.owndroid.dpm.UserOperation +import com.bintianqi.owndroid.dpm.UserOptions import com.bintianqi.owndroid.dpm.UserRestriction +import com.bintianqi.owndroid.dpm.UserRestrictionItem +import com.bintianqi.owndroid.dpm.UserSessionMessage +import com.bintianqi.owndroid.dpm.Users +import com.bintianqi.owndroid.dpm.WifiAuthKeypair +import com.bintianqi.owndroid.dpm.WifiSecurityLevel +import com.bintianqi.owndroid.dpm.WifiSsidPolicy +import com.bintianqi.owndroid.dpm.WipeData import com.bintianqi.owndroid.dpm.dhizukuErrorStatus +import com.bintianqi.owndroid.dpm.dhizukuPermissionGranted import com.bintianqi.owndroid.dpm.getDPM import com.bintianqi.owndroid.dpm.getReceiver import com.bintianqi.owndroid.dpm.isDeviceAdmin @@ -70,6 +125,7 @@ import com.bintianqi.owndroid.dpm.isProfileOwner import com.bintianqi.owndroid.dpm.setDefaultAffiliationID import com.bintianqi.owndroid.dpm.toggleInstallAppActivity import com.bintianqi.owndroid.ui.Animations +import com.bintianqi.owndroid.ui.MyScaffold import com.bintianqi.owndroid.ui.theme.OwnDroidTheme import com.rosan.dhizuku.api.Dhizuku import kotlinx.coroutines.delay @@ -119,7 +175,7 @@ class MainActivity : FragmentActivity() { } if (sharedPref.getBoolean("dhizuku", false)) { if (Dhizuku.init(applicationContext)) { - if (!Dhizuku.isPermissionGranted()) { dhizukuErrorStatus.value = 2 } + if (!dhizukuPermissionGranted()) { dhizukuErrorStatus.value = 2 } } else { sharedPref.edit().putBoolean("dhizuku", false).apply() dhizukuErrorStatus.value = 1 @@ -157,20 +213,105 @@ fun Home(vm: MyViewModel) { popExitTransition = Animations.navHostPopExitTransition ) { composable(route = "HomePage") { HomePage(navCtrl) } + + composable(route = "Permissions") { Permissions(navCtrl) } + composable(route = "Shizuku") { Shizuku(navCtrl) } + composable(route = "DeviceAdmin") { DeviceAdmin(navCtrl) } + composable(route = "ProfileOwner") { ProfileOwner(navCtrl) } + composable(route = "DeviceOwner") { DeviceOwner(navCtrl) } + composable(route = "DeviceInfo") { DeviceInfo(navCtrl) } + composable(route = "LockScreenInfo") { LockScreenInfo(navCtrl) } + composable(route = "SupportMessages") { SupportMessages(navCtrl) } + composable(route = "TransferOwnership") { TransferOwnership(navCtrl) } + composable(route = "System") { SystemManage(navCtrl) } - composable(route = "ManagedProfile") { ManagedProfile(navCtrl) } - composable(route = "Permissions") { DpmPermissions(navCtrl) } + composable(route = "SystemOptions") { SystemOptions(navCtrl) } + composable(route = "Keyguard") { Keyguard(navCtrl) } + composable(route = "ChangeTime") { ChangeTime(navCtrl) } + composable(route = "ChangeTimeZone") { ChangeTimeZone(navCtrl) } + composable(route = "PermissionPolicy") { PermissionPolicy(navCtrl) } + composable(route = "MTEPolicy") { MTEPolicy(navCtrl) } + composable(route = "NearbyStreamingPolicy") { NearbyStreamingPolicy(navCtrl) } + composable(route = "LockTaskMode") { LockTaskMode(navCtrl) } + composable(route = "CACert") { CACert(navCtrl) } + composable(route = "SecurityLogs") { SecurityLogging(navCtrl) } + composable(route = "DisableAccountManagement") { DisableAccountManagement(navCtrl) } + composable(route = "SystemUpdatePolicy") { SystemUpdatePolicy(navCtrl) } + composable(route = "InstallSystemUpdate") { InstallSystemUpdate(navCtrl) } + composable(route = "FRPPolicy") { FRPPolicy(navCtrl) } + composable(route = "WipeData") { WipeData(navCtrl) } + + composable(route = "Network") { Network(navCtrl) } + composable(route = "NetworkOptions") { NetworkOptions(navCtrl) } + composable(route = "MinWifiSecurityLevel") { WifiSecurityLevel(navCtrl) } + composable(route = "WifiSsidPolicy") { WifiSsidPolicy(navCtrl) } + composable(route = "PrivateDNS") { PrivateDNS(navCtrl) } + composable(route = "AlwaysOnVpn") { AlwaysOnVPNPackage(navCtrl) } + composable(route = "RecommendedGlobalProxy") { RecommendedGlobalProxy(navCtrl) } + composable(route = "NetworkLog") { NetworkLogging(navCtrl) } + composable(route = "WifiAuthKeypair") { WifiAuthKeypair(navCtrl) } + composable(route = "PreferentialNetworkService") { PreferentialNetworkService(navCtrl) } + composable(route = "OverrideAPN") { OverrideAPN(navCtrl) } + + composable(route = "WorkProfile") { WorkProfile(navCtrl) } + composable(route = "OrgOwnedWorkProfile") { OrgOwnedProfile(navCtrl) } + composable(route = "CreateWorkProfile") { CreateWorkProfile(navCtrl) } + composable(route = "SuspendPersonalApp") { SuspendPersonalApp(navCtrl) } + composable(route = "IntentFilter") { IntentFilter(navCtrl) } + composable(route = "DeleteWorkProfile") { DeleteWorkProfile(navCtrl) } + composable(route = "Applications") { ApplicationManage(navCtrl, dialogStatus) } + composable(route = "UserRestriction") { UserRestriction(navCtrl) } - composable(route = "Users") { UserManage(navCtrl) } + composable(route = "UR-Internet") { + MyScaffold(R.string.network_and_internet, 0.dp, navCtrl) { RestrictionData.internet.forEach { UserRestrictionItem(it) } } + } + composable(route = "UR-Connectivity") { + MyScaffold(R.string.connectivity, 0.dp, navCtrl) { RestrictionData.connectivity.forEach { UserRestrictionItem(it) } } + } + composable(route = "UR-Applications") { + MyScaffold(R.string.applications, 0.dp, navCtrl) { RestrictionData.applications.forEach { UserRestrictionItem(it) } } + } + composable(route = "UR-Users") { + MyScaffold(R.string.users, 0.dp, navCtrl) { RestrictionData.users.forEach { UserRestrictionItem(it) } } + } + composable(route = "UR-Media") { + MyScaffold(R.string.media, 0.dp, navCtrl) { RestrictionData.media.forEach { UserRestrictionItem(it) } } + } + composable(route = "UR-Other") { + MyScaffold(R.string.other, 0.dp, navCtrl) { RestrictionData.other.forEach { UserRestrictionItem(it) } } + } + + composable(route = "Users") { Users(navCtrl) } + composable(route = "UserInfo") { CurrentUserInfo(navCtrl) } + composable(route = "UserOptions") { UserOptions(navCtrl) } + composable(route = "UserOperation") { UserOperation(navCtrl) } + composable(route = "CreateUser") { CreateUser(navCtrl) } + composable(route = "ChangeUsername") { ChangeUsername(navCtrl) } + composable(route = "ChangeUserIcon") { ChangeUserIcon(navCtrl) } + composable(route = "UserSessionMessage") { UserSessionMessage(navCtrl) } + composable(route = "AffiliationID") { AffiliationID(navCtrl) } + composable(route = "Password") { Password(navCtrl) } - composable(route = "Settings") { AppSetting(navCtrl, vm) } - composable(route = "Network") { Network(navCtrl) } + composable(route = "PasswordInfo") { PasswordInfo(navCtrl) } + composable(route = "ResetPasswordToken") { ResetPasswordToken(navCtrl) } + composable(route = "ResetPassword") { ResetPassword(navCtrl) } + composable(route = "RequirePasswordComplexity") { PasswordComplexity(navCtrl) } + composable(route = "DisableKeyguardFeatures") { DisableKeyguardFeatures(navCtrl) } + composable(route = "RequirePasswordQuality") { PasswordQuality(navCtrl) } + + composable(route = "Settings") { Settings(navCtrl) } + composable(route = "Options") { SettingsOptions(navCtrl) } + composable(route = "Appearance") { Appearance(navCtrl, vm) } + composable(route = "AuthSettings") { AuthSettings(navCtrl) } + composable(route = "Automation") { Automation(navCtrl) } + composable(route = "About") { About(navCtrl) } + composable(route = "PackageSelector") { PackageSelector(navCtrl) } } LaunchedEffect(Unit) { - val profileInited = sharedPref.getBoolean("ManagedProfileActivated", false) - val profileNotActivated = !profileInited && context.isProfileOwner && (VERSION.SDK_INT < 24 || (VERSION.SDK_INT >= 24 && dpm.isManagedProfile(receiver))) + val profileInitialized = sharedPref.getBoolean("ManagedProfileActivated", false) + val profileNotActivated = !profileInitialized && context.isProfileOwner && (VERSION.SDK_INT < 24 || dpm.isManagedProfile(receiver)) if(profileNotActivated) { dpm.setProfileEnabled(receiver) sharedPref.edit().putBoolean("ManagedProfileActivated", true).apply() @@ -203,58 +344,60 @@ private fun HomePage(navCtrl:NavHostController) { else if(deviceAdmin) R.string.device_admin else R.string.click_to_activate ) } - Column(modifier = Modifier.background(colorScheme.background).statusBarsPadding().verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 25.dp)) - Text( - text = stringResource(R.string.app_name), style = typography.headlineLarge, - modifier = Modifier.padding(start = 10.dp), color = colorScheme.onBackground - ) - Spacer(Modifier.padding(vertical = 8.dp)) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp, horizontal = 8.dp) - .clip(RoundedCornerShape(15)) - .background(color = colorScheme.primary) - .clickable(onClick = { navCtrl.navigate("Permissions") }) - .padding(vertical = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(modifier = Modifier.padding(start = 22.dp)) - Icon( - painter = painterResource(if(activated) R.drawable.check_circle_fill1 else R.drawable.block_fill0), - contentDescription = null, - tint = colorScheme.onPrimary + Scaffold { + Column(modifier = Modifier.padding(it).verticalScroll(rememberScrollState())) { + Spacer(Modifier.padding(vertical = 25.dp)) + Text( + text = stringResource(R.string.app_name), style = typography.headlineLarge, + modifier = Modifier.padding(start = 10.dp) ) - Spacer(modifier = Modifier.padding(start = 10.dp)) - Column { - Text( - text = stringResource(if(activated) R.string.activated else R.string.deactivated), - style = typography.headlineSmall, - color = colorScheme.onPrimary, - modifier = Modifier.padding(bottom = 2.dp) + Spacer(Modifier.padding(vertical = 8.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp, horizontal = 8.dp) + .clip(RoundedCornerShape(15)) + .background(color = colorScheme.primary) + .clickable(onClick = { navCtrl.navigate("Permissions") }) + .padding(vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = Modifier.padding(start = 22.dp)) + Icon( + painter = painterResource(if(activated) R.drawable.check_circle_fill1 else R.drawable.block_fill0), + contentDescription = null, + tint = colorScheme.onPrimary ) - if(activateType != "") { Text(text = activateType, color = colorScheme.onPrimary) } + Spacer(modifier = Modifier.padding(start = 10.dp)) + Column { + Text( + text = stringResource(if(activated) R.string.activated else R.string.deactivated), + style = typography.headlineSmall, + color = colorScheme.onPrimary, + modifier = Modifier.padding(bottom = 2.dp) + ) + if(activateType != "") { Text(text = activateType, color = colorScheme.onPrimary) } + } } + HomePageItem(R.string.system, R.drawable.android_fill0, "System", navCtrl) + if(deviceOwner || profileOwner) { HomePageItem(R.string.network, R.drawable.wifi_fill0, "Network", navCtrl) } + if( + (VERSION.SDK_INT < 24 && !deviceOwner) || ( + VERSION.SDK_INT >= 24 && (dpm.isProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE) || + (profileOwner && dpm.isManagedProfile(receiver))) + ) + ) { + HomePageItem(R.string.work_profile, R.drawable.work_fill0, "ManagedProfile", navCtrl) + } + if(deviceOwner || profileOwner) HomePageItem(R.string.applications, R.drawable.apps_fill0, "Applications", navCtrl) + if(VERSION.SDK_INT >= 24 && (profileOwner || deviceOwner)) { + HomePageItem(R.string.user_restriction, R.drawable.person_off, "UserRestriction", navCtrl) + } + HomePageItem(R.string.users,R.drawable.manage_accounts_fill0,"Users", navCtrl) + HomePageItem(R.string.password_and_keyguard, R.drawable.password_fill0, "Password", navCtrl) + HomePageItem(R.string.settings, R.drawable.settings_fill0, "Settings", navCtrl) + Spacer(Modifier.padding(vertical = 20.dp)) } - HomePageItem(R.string.system, R.drawable.android_fill0, "System", navCtrl) - if(deviceOwner || profileOwner) { HomePageItem(R.string.network, R.drawable.wifi_fill0, "Network", navCtrl) } - if( - (VERSION.SDK_INT < 24 && !deviceOwner) || ( - VERSION.SDK_INT >= 24 && (dpm.isProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE) || - (profileOwner && dpm.isManagedProfile(receiver))) - ) - ) { - HomePageItem(R.string.work_profile, R.drawable.work_fill0, "ManagedProfile", navCtrl) - } - if(deviceOwner || profileOwner) HomePageItem(R.string.applications, R.drawable.apps_fill0, "Applications", navCtrl) - if(VERSION.SDK_INT >= 24 && (profileOwner || deviceOwner)) { - HomePageItem(R.string.user_restrict, R.drawable.person_off, "UserRestriction", navCtrl) - } - HomePageItem(R.string.users,R.drawable.manage_accounts_fill0,"Users", navCtrl) - HomePageItem(R.string.password_and_keyguard, R.drawable.password_fill0, "Password", navCtrl) - HomePageItem(R.string.settings, R.drawable.settings_fill0, "Settings", navCtrl) - Spacer(Modifier.padding(vertical = 20.dp)) } } @@ -263,22 +406,19 @@ fun HomePageItem(name: Int, imgVector: Int, navTo: String, navCtrl: NavHostContr Row( modifier = Modifier .fillMaxWidth() - .clip(RoundedCornerShape(25)) .clickable(onClick = { navCtrl.navigate(navTo) }) - .padding(vertical = 13.dp), + .padding(vertical = 12.dp), verticalAlignment = Alignment.CenterVertically ) { Spacer(Modifier.padding(start = 30.dp)) Icon( painter = painterResource(imgVector), - contentDescription = null, - tint = colorScheme.onBackground + contentDescription = null ) Spacer(Modifier.padding(start = 15.dp)) Text( text = stringResource(name), style = typography.headlineSmall, - color = colorScheme.onBackground, modifier = Modifier.padding(bottom = if(zhCN) { 2 } else { 0 }.dp) ) } diff --git a/app/src/main/java/com/bintianqi/owndroid/Settings.kt b/app/src/main/java/com/bintianqi/owndroid/Settings.kt index db21e8a..44f3b18 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Settings.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Settings.kt @@ -8,21 +8,15 @@ import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -37,68 +31,36 @@ import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController -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.FunctionItem +import com.bintianqi.owndroid.ui.MyScaffold import com.bintianqi.owndroid.ui.SwitchItem -import com.bintianqi.owndroid.ui.TopBar import java.security.SecureRandom @Composable -fun AppSetting(navCtrl:NavHostController, vm: MyViewModel) { - val localNavCtrl = rememberNavController() - val backStackEntry by localNavCtrl.currentBackStackEntryAsState() - Scaffold( - topBar = { - TopBar(backStackEntry, navCtrl, localNavCtrl) - } - ) { - NavHost( - navController = localNavCtrl, startDestination = "Home", - enterTransition = Animations.navHostEnterTransition, - exitTransition = Animations.navHostExitTransition, - popEnterTransition = Animations.navHostPopEnterTransition, - popExitTransition = Animations.navHostPopExitTransition, - modifier = Modifier.padding(top = it.calculateTopPadding()) - ) { - composable(route = "Home") { Home(localNavCtrl) } - composable(route = "Options") { Options() } - composable(route = "Theme") { ThemeSettings(vm) } - composable(route = "Auth") { AuthSettings() } - composable(route = "Automation") { Automation() } - composable(route = "About") { About() } - } +fun Settings(navCtrl: NavHostController) { + MyScaffold(R.string.settings, 0.dp, navCtrl) { + FunctionItem(R.string.options, "", R.drawable.tune_fill0) { navCtrl.navigate("Options") } + FunctionItem(R.string.appearance, "", R.drawable.format_paint_fill0) { navCtrl.navigate("Appearance") } + FunctionItem(R.string.security, "", R.drawable.lock_fill0) { navCtrl.navigate("AuthSettings") } + FunctionItem(R.string.automation_api, "", R.drawable.apps_fill0) { navCtrl.navigate("Automation") } + FunctionItem(R.string.about, "", R.drawable.info_fill0) { navCtrl.navigate("About") } } } @Composable -private fun Home(navCtrl: NavHostController) { - Column(modifier = Modifier.fillMaxSize()) { - SubPageItem(R.string.options, "", R.drawable.tune_fill0) { navCtrl.navigate("Options") } - SubPageItem(R.string.appearance, "", R.drawable.format_paint_fill0) { navCtrl.navigate("Theme") } - SubPageItem(R.string.security, "", R.drawable.lock_fill0) { navCtrl.navigate("Auth") } - SubPageItem(R.string.automation_api, "", R.drawable.apps_fill0) { navCtrl.navigate("Automation") } - SubPageItem(R.string.about, "", R.drawable.info_fill0) { navCtrl.navigate("About") } - } -} - -@Composable -private fun Options() { +fun SettingsOptions(navCtrl: NavHostController) { val sharedPref = LocalContext.current.getSharedPreferences("data", Context.MODE_PRIVATE) - Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(start = 20.dp, end = 16.dp)) { + MyScaffold(R.string.options, 0.dp, navCtrl) { SwitchItem( R.string.show_dangerous_features, "", R.drawable.warning_fill0, { sharedPref.getBoolean("dangerous_features", false) }, - { sharedPref.edit().putBoolean("dangerous_features", it).apply() }, padding = false + { sharedPref.edit().putBoolean("dangerous_features", it).apply() } ) } } @Composable -private fun ThemeSettings(vm: MyViewModel) { +fun Appearance(navCtrl: NavHostController, vm: MyViewModel) { val theme by vm.theme.collectAsStateWithLifecycle() var darkThemeMenu by remember { mutableStateOf(false) } val darkThemeTextID = when(theme.darkTheme) { @@ -106,7 +68,7 @@ private fun ThemeSettings(vm: MyViewModel) { false -> R.string.off null -> R.string.follow_system } - Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())) { + MyScaffold(R.string.appearance, 0.dp, navCtrl) { if(VERSION.SDK_INT >= 31) { SwitchItem( R.string.material_you_color, "", null, @@ -115,7 +77,7 @@ private fun ThemeSettings(vm: MyViewModel) { ) } Box { - SubPageItem(R.string.dark_theme, stringResource(darkThemeTextID)) { darkThemeMenu = true } + FunctionItem(R.string.dark_theme, stringResource(darkThemeTextID)) { darkThemeMenu = true } DropdownMenu( expanded = darkThemeMenu, onDismissRequest = { darkThemeMenu = false }, offset = DpOffset(x = 25.dp, y = 0.dp) @@ -154,44 +116,42 @@ private fun ThemeSettings(vm: MyViewModel) { } @Composable -private fun AuthSettings() { +fun AuthSettings(navCtrl: NavHostController) { val sharedPref = LocalContext.current.getSharedPreferences("data", Context.MODE_PRIVATE) var auth by remember{ mutableStateOf(sharedPref.getBoolean("auth",false)) } - Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(start = 20.dp, end = 16.dp)) { + MyScaffold(R.string.security, 0.dp, navCtrl) { SwitchItem( R.string.lock_owndroid, "", null, auth, { sharedPref.edit().putBoolean("auth", it).apply() auth = sharedPref.getBoolean("auth", false) - }, padding = false + } ) if(auth) { SwitchItem( R.string.enable_bio_auth, "", null, { sharedPref.getBoolean("bio_auth", false) }, - { sharedPref.edit().putBoolean("bio_auth", it).apply() }, padding = false + { sharedPref.edit().putBoolean("bio_auth", it).apply() } ) SwitchItem( R.string.lock_in_background, stringResource(R.string.developing), null, { sharedPref.getBoolean("lock_in_background", false) }, - { sharedPref.edit().putBoolean("lock_in_background", it).apply() }, padding = false + { sharedPref.edit().putBoolean("lock_in_background", it).apply() } ) } SwitchItem( R.string.protect_storage, "", null, { sharedPref.getBoolean("protect_storage", false) }, - { sharedPref.edit().putBoolean("protect_storage", it).apply() }, padding = false + { sharedPref.edit().putBoolean("protect_storage", it).apply() } ) } } @Composable -private fun Automation() { +fun Automation(navCtrl: NavHostController) { val context = LocalContext.current val sharedPref = context.getSharedPreferences("data", Context.MODE_PRIVATE) - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.automation_api), style = typography.headlineLarge) + MyScaffold(R.string.automation_api, 8.dp, navCtrl) { Spacer(Modifier.padding(vertical = 5.dp)) var key by remember { mutableStateOf("") } OutlinedTextField( @@ -229,18 +189,15 @@ private fun Automation() { } @Composable -private fun About() { +fun About(navCtrl: NavHostController) { val context = LocalContext.current val pkgInfo = context.packageManager.getPackageInfo(context.packageName,0) val verCode = pkgInfo.versionCode val verName = pkgInfo.versionName - Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.about), style = typography.headlineLarge, modifier = Modifier.padding(start = 26.dp)) - Spacer(Modifier.padding(vertical = 5.dp)) - Text(text = stringResource(R.string.app_name)+" v$verName ($verCode)", modifier = Modifier.padding(start = 26.dp)) + MyScaffold(R.string.about, 0.dp, navCtrl) { + Text(text = stringResource(R.string.app_name)+" v$verName ($verCode)", modifier = Modifier.padding(start = 16.dp)) Spacer(Modifier.padding(vertical = 5.dp)) - SubPageItem(R.string.project_homepage, "GitHub", R.drawable.open_in_new) { shareLink(context, "https://github.com/BinTianqi/OwnDroid") } + FunctionItem(R.string.project_homepage, "GitHub", R.drawable.open_in_new) { shareLink(context, "https://github.com/BinTianqi/OwnDroid") } } } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt index b604aa7..3ebb4c7 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -90,7 +90,7 @@ import com.bintianqi.owndroid.ui.Information import com.bintianqi.owndroid.ui.ListItem import com.bintianqi.owndroid.ui.NavIcon import com.bintianqi.owndroid.ui.RadioButtonItem -import com.bintianqi.owndroid.ui.SubPageItem +import com.bintianqi.owndroid.ui.FunctionItem import com.bintianqi.owndroid.ui.SwitchItem import java.util.concurrent.Executors @@ -119,7 +119,7 @@ fun ApplicationManage(navCtrl:NavHostController, dialogStatus: MutableIntState) keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }), trailingIcon = { - Icon(painter = painterResource(R.drawable.checklist_fill0), contentDescription = null, + Icon(painter = painterResource(R.drawable.list_fill0), contentDescription = null, modifier = Modifier .clip(RoundedCornerShape(50)) .clickable(onClick = { @@ -216,7 +216,7 @@ private fun Home( if(VERSION.SDK_INT >= 24 && profileOwner && dpm.isManagedProfile(receiver)) { Text(text = stringResource(R.string.scope_is_work_profile), textAlign = TextAlign.Center,modifier = Modifier.fillMaxWidth()) } - SubPageItem(R.string.app_info,"", R.drawable.open_in_new) { + FunctionItem(R.string.app_info,"", R.drawable.open_in_new) { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) intent.setData(Uri.parse("package:$pkgName")) startActivity(context, intent, null) @@ -242,37 +242,37 @@ private fun Home( onClickBlank = { appControlAction = 3; appControlDialog = true } ) if((VERSION.SDK_INT >= 33 && profileOwner) || (VERSION.SDK_INT >= 30 && deviceOwner)) { - SubPageItem(R.string.ucd, "", R.drawable.do_not_touch_fill0) { navCtrl.navigate("UserControlDisabled") } + FunctionItem(R.string.ucd, "", R.drawable.do_not_touch_fill0) { navCtrl.navigate("UserControlDisabled") } } if(VERSION.SDK_INT>=23) { - SubPageItem(R.string.permission_manage, "", R.drawable.key_fill0) { navCtrl.navigate("PermissionManage") } + FunctionItem(R.string.permission_manage, "", R.drawable.key_fill0) { navCtrl.navigate("PermissionManage") } } if(VERSION.SDK_INT >= 30 && profileOwner && dpm.isManagedProfile(receiver)) { - SubPageItem(R.string.cross_profile_package, "", R.drawable.work_fill0) { navCtrl.navigate("CrossProfilePackage") } + FunctionItem(R.string.cross_profile_package, "", R.drawable.work_fill0) { navCtrl.navigate("CrossProfilePackage") } } if(profileOwner) { - SubPageItem(R.string.cross_profile_widget, "", R.drawable.widgets_fill0) { navCtrl.navigate("CrossProfileWidget") } + FunctionItem(R.string.cross_profile_widget, "", R.drawable.widgets_fill0) { navCtrl.navigate("CrossProfileWidget") } } if(VERSION.SDK_INT >= 34 && deviceOwner) { - SubPageItem(R.string.credential_manage_policy, "", R.drawable.license_fill0) { navCtrl.navigate("CredentialManagePolicy") } + FunctionItem(R.string.credential_manage_policy, "", R.drawable.license_fill0) { navCtrl.navigate("CredentialManagePolicy") } } - SubPageItem(R.string.permitted_accessibility_services, "", R.drawable.settings_accessibility_fill0) { navCtrl.navigate("Accessibility") } - SubPageItem(R.string.permitted_ime, "", R.drawable.keyboard_fill0) { navCtrl.navigate("IME") } - SubPageItem(R.string.enable_system_app, "", R.drawable.enable_fill0) { + FunctionItem(R.string.permitted_accessibility_services, "", R.drawable.settings_accessibility_fill0) { navCtrl.navigate("Accessibility") } + FunctionItem(R.string.permitted_ime, "", R.drawable.keyboard_fill0) { navCtrl.navigate("IME") } + FunctionItem(R.string.enable_system_app, "", R.drawable.enable_fill0) { if(pkgName != "") dialogStatus.intValue = 1 } if(VERSION.SDK_INT >= 28 && deviceOwner) { - SubPageItem(R.string.keep_uninstalled_packages, "", R.drawable.delete_fill0) { navCtrl.navigate("KeepUninstalled") } + FunctionItem(R.string.keep_uninstalled_packages, "", R.drawable.delete_fill0) { navCtrl.navigate("KeepUninstalled") } } if(VERSION.SDK_INT >= 28) { - SubPageItem(R.string.clear_app_storage, "", R.drawable.mop_fill0) { + FunctionItem(R.string.clear_app_storage, "", R.drawable.mop_fill0) { if(pkgName != "") dialogStatus.intValue = 2 } } - SubPageItem(R.string.install_app, "", R.drawable.install_mobile_fill0) { navCtrl.navigate("InstallApp") } - SubPageItem(R.string.uninstall_app, "", R.drawable.delete_fill0) { navCtrl.navigate("UninstallApp") } + FunctionItem(R.string.install_app, "", R.drawable.install_mobile_fill0) { navCtrl.navigate("InstallApp") } + FunctionItem(R.string.uninstall_app, "", R.drawable.delete_fill0) { navCtrl.navigate("UninstallApp") } if(VERSION.SDK_INT >= 34 && (deviceOwner || dpm.isOrgProfile(receiver))) { - SubPageItem(R.string.set_default_dialer, "", R.drawable.call_fill0) { + FunctionItem(R.string.set_default_dialer, "", R.drawable.call_fill0) { if(pkgName != "") dialogStatus.intValue = 3 } } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt index 9691977..1529bd9 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt @@ -153,7 +153,7 @@ private fun binderWrapperPackageInstaller(appContext: Context): PackageInstaller fun Context.getPI(): PackageInstaller { val sharedPref = this.getSharedPreferences("data", Context.MODE_PRIVATE) if(sharedPref.getBoolean("dhizuku", false)) { - if (!Dhizuku.isPermissionGranted()) { + if (!dhizukuPermissionGranted()) { dhizukuErrorStatus.value = 2 backToHomeStateFlow.value = true return this.packageManager.packageInstaller @@ -167,7 +167,7 @@ fun Context.getPI(): PackageInstaller { fun Context.getDPM(): DevicePolicyManager { val sharedPref = this.getSharedPreferences("data", Context.MODE_PRIVATE) if(sharedPref.getBoolean("dhizuku", false)) { - if (!Dhizuku.isPermissionGranted()) { + if (!dhizukuPermissionGranted()) { dhizukuErrorStatus.value = 2 backToHomeStateFlow.value = true return this.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager @@ -193,7 +193,7 @@ fun Context.resetDevicePolicy() { val dpm = getDPM() val receiver = getReceiver() RestrictionData.getAllRestrictions().forEach { - dpm.clearUserRestriction(receiver, it) + dpm.clearUserRestriction(receiver, it.id) } dpm.accountTypesWithManagementDisabled?.forEach { dpm.setAccountManagementDisabled(receiver, it, false) @@ -428,7 +428,7 @@ fun parseSecurityEventData(event: SecurityLog.SecurityEvent): JsonElement? { val payload = event.data as Array<*> buildJsonObject { put("mac", payload[0] as String) - (payload[2] as String).let { if(it != "") put("reason", it) } + (payload[1] as String).let { if(it != "") put("reason", it) } } } SecurityLog.TAG_CAMERA_POLICY_SET -> { @@ -619,3 +619,11 @@ fun setDefaultAffiliationID(context: Context) { } } } + +fun dhizukuPermissionGranted() = + try { + Dhizuku.isPermissionGranted() + } catch(_: Exception) { + false + } + diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/ManagedProfile.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/ManagedProfile.kt index 88f9b95..937c1ca 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/ManagedProfile.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/ManagedProfile.kt @@ -22,21 +22,17 @@ import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -55,88 +51,47 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController import com.bintianqi.owndroid.R -import com.bintianqi.owndroid.ui.Animations +import com.bintianqi.owndroid.ui.CardItem import com.bintianqi.owndroid.ui.CheckBoxItem import com.bintianqi.owndroid.ui.CopyTextButton import com.bintianqi.owndroid.ui.InfoCard -import com.bintianqi.owndroid.ui.SubPageItem +import com.bintianqi.owndroid.ui.FunctionItem +import com.bintianqi.owndroid.ui.MyScaffold import com.bintianqi.owndroid.ui.SwitchItem -import com.bintianqi.owndroid.ui.TopBar +import com.bintianqi.owndroid.yesOrNo @Composable -fun ManagedProfile(navCtrl: NavHostController) { - val localNavCtrl = rememberNavController() - val backStackEntry by localNavCtrl.currentBackStackEntryAsState() - Scaffold( - topBar = { - TopBar(backStackEntry, navCtrl, localNavCtrl) - } - ) { - NavHost( - navController = localNavCtrl, startDestination = "Home", - enterTransition = Animations.navHostEnterTransition, - exitTransition = Animations.navHostExitTransition, - popEnterTransition = Animations.navHostPopEnterTransition, - popExitTransition = Animations.navHostPopExitTransition, - modifier = Modifier.padding(top = it.calculateTopPadding()) - ) { - composable(route = "Home") { Home(localNavCtrl) } - composable(route = "OrgOwnedWorkProfile") { OrgOwnedProfile() } - composable(route = "CreateWorkProfile") { CreateWorkProfile() } - composable(route = "SuspendPersonalApp") { SuspendPersonalApp() } - composable(route = "IntentFilter") { IntentFilter() } - composable(route = "DeleteWorkProfile") { DeleteWorkProfile() } - } - } -} - -@Composable -private fun Home(navCtrl: NavHostController) { +fun WorkProfile(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val profileOwner = context.isProfileOwner - Column( - modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()) - ) { - Text( - text = stringResource(R.string.work_profile), - style = typography.headlineLarge, - modifier = Modifier.padding(top = 8.dp, bottom = 5.dp, start = 16.dp) - ) + MyScaffold(R.string.work_profile, 0.dp, navCtrl) { if(VERSION.SDK_INT >= 30 && profileOwner && dpm.isManagedProfile(receiver)) { - SubPageItem(R.string.org_owned_work_profile, "", R.drawable.corporate_fare_fill0) { navCtrl.navigate("OrgOwnedWorkProfile") } + FunctionItem(R.string.org_owned_work_profile, "", R.drawable.corporate_fare_fill0) { navCtrl.navigate("OrgOwnedWorkProfile") } } if(VERSION.SDK_INT<24 || (VERSION.SDK_INT>=24 && dpm.isProvisioningAllowed(ACTION_PROVISION_MANAGED_PROFILE))) { - SubPageItem(R.string.create_work_profile, "", R.drawable.work_fill0) { navCtrl.navigate("CreateWorkProfile") } + FunctionItem(R.string.create_work_profile, "", R.drawable.work_fill0) { navCtrl.navigate("CreateWorkProfile") } } if(dpm.isOrgProfile(receiver)) { - SubPageItem(R.string.suspend_personal_app, "", R.drawable.block_fill0) { navCtrl.navigate("SuspendPersonalApp") } + FunctionItem(R.string.suspend_personal_app, "", R.drawable.block_fill0) { navCtrl.navigate("SuspendPersonalApp") } } if(profileOwner && (VERSION.SDK_INT < 24 || (VERSION.SDK_INT >= 24 && dpm.isManagedProfile(receiver)))) { - SubPageItem(R.string.intent_filter, "", R.drawable.filter_alt_fill0) { navCtrl.navigate("IntentFilter") } + FunctionItem(R.string.intent_filter, "", R.drawable.filter_alt_fill0) { navCtrl.navigate("IntentFilter") } } if(profileOwner && (VERSION.SDK_INT < 24 || (VERSION.SDK_INT >= 24 && dpm.isManagedProfile(receiver)))) { - SubPageItem(R.string.delete_work_profile, "", R.drawable.delete_forever_fill0) { navCtrl.navigate("DeleteWorkProfile") } + FunctionItem(R.string.delete_work_profile, "", R.drawable.delete_forever_fill0) { navCtrl.navigate("DeleteWorkProfile") } } - Spacer(Modifier.padding(vertical = 30.dp)) } } @Composable -private fun CreateWorkProfile() { +fun CreateWorkProfile(navCtrl: NavHostController) { val context = LocalContext.current val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.create_work_profile), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.create_work_profile, 8.dp, navCtrl) { var skipEncrypt by remember { mutableStateOf(false) } var offlineProvisioning by remember { mutableStateOf(true) } var migrateAccount by remember { mutableStateOf(false) } @@ -206,14 +161,11 @@ private fun CreateWorkProfile() { @SuppressLint("NewApi") @Composable -private fun OrgOwnedProfile() { +fun OrgOwnedProfile(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.org_owned_work_profile), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) - Text(text = stringResource(R.string.is_org_owned_profile,dpm.isOrganizationOwnedDeviceWithManagedProfile)) + MyScaffold(R.string.org_owned_work_profile, 8.dp, navCtrl, false) { + CardItem(R.string.org_owned_work_profile, dpm.isOrganizationOwnedDeviceWithManagedProfile.yesOrNo) Spacer(Modifier.padding(vertical = 5.dp)) if(!dpm.isOrganizationOwnedDeviceWithManagedProfile) { SelectionContainer { @@ -229,14 +181,13 @@ private fun OrgOwnedProfile() { @SuppressLint("NewApi") @Composable -private fun SuspendPersonalApp() { +fun SuspendPersonalApp(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current var suspend by remember { mutableStateOf(dpm.getPersonalAppsSuspendedReasons(receiver) != PERSONAL_APPS_NOT_SUSPENDED) } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) + MyScaffold(R.string.suspend_personal_app, 8.dp, navCtrl) { SwitchItem( R.string.suspend_personal_app, "", null, suspend, @@ -277,16 +228,13 @@ private fun SuspendPersonalApp() { } @Composable -private fun IntentFilter() { +fun IntentFilter(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { + MyScaffold(R.string.intent_filter, 8.dp, navCtrl) { var action by remember { mutableStateOf("") } - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.intent_filter), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) OutlinedTextField( value = action, onValueChange = { action = it }, label = { Text("Action") }, @@ -328,7 +276,7 @@ private fun IntentFilter() { } @Composable -private fun DeleteWorkProfile() { +fun DeleteWorkProfile(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val focusMgr = LocalFocusManager.current @@ -337,14 +285,7 @@ private fun DeleteWorkProfile() { var euicc by remember { mutableStateOf(false) } var silent by remember { mutableStateOf(false) } var reason by remember { mutableStateOf("") } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text( - text = stringResource(R.string.delete_work_profile), - style = typography.headlineLarge, - modifier = Modifier.padding(6.dp),color = colorScheme.error - ) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.delete_work_profile, 8.dp, navCtrl) { CheckBoxItem(R.string.wipe_external_storage, externalStorage, { externalStorage = it }) if(VERSION.SDK_INT >= 28) { CheckBoxItem(R.string.wipe_euicc, euicc, { euicc = it }) } CheckBoxItem(R.string.wipe_silently, silent, { silent = it }) @@ -367,7 +308,6 @@ private fun DeleteWorkProfile() { ) { Text(stringResource(R.string.delete)) } - Spacer(Modifier.padding(vertical = 30.dp)) } if(warning) { LaunchedEffect(Unit) { silent = reason == "" } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt index 334acde..f7cdd3e 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt @@ -43,21 +43,17 @@ import android.telephony.data.ApnSetting.PROTOCOL_UNSTRUCTURED import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.ScrollState import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight @@ -69,14 +65,12 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -88,7 +82,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager @@ -99,92 +92,23 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController import com.bintianqi.owndroid.R import com.bintianqi.owndroid.exportFile import com.bintianqi.owndroid.exportFilePath import com.bintianqi.owndroid.formatFileSize import com.bintianqi.owndroid.selectedPackage -import com.bintianqi.owndroid.ui.Animations import com.bintianqi.owndroid.ui.CheckBoxItem +import com.bintianqi.owndroid.ui.FunctionItem import com.bintianqi.owndroid.ui.InfoCard import com.bintianqi.owndroid.ui.ListItem +import com.bintianqi.owndroid.ui.MyScaffold import com.bintianqi.owndroid.ui.RadioButtonItem -import com.bintianqi.owndroid.ui.SubPageItem import com.bintianqi.owndroid.ui.SwitchItem -import com.bintianqi.owndroid.ui.TopBar import com.bintianqi.owndroid.writeClipBoard import kotlin.math.max @Composable -fun Network(navCtrl: NavHostController) { - val localNavCtrl = rememberNavController() - val backStackEntry by localNavCtrl.currentBackStackEntryAsState() - val scrollState = rememberScrollState() - val wifiMacDialog = remember { mutableStateOf(false) } - Scaffold( - topBar = { - TopBar(backStackEntry,navCtrl,localNavCtrl) { - if(backStackEntry?.destination?.route == "Home" && scrollState.maxValue > 80) { - Text( - text = stringResource(R.string.network), - modifier = Modifier.alpha((maxOf(scrollState.value-30,0)).toFloat()/80) - ) - } - } - } - ) { - NavHost( - navController = localNavCtrl, startDestination = "Home", - enterTransition = Animations.navHostEnterTransition, - exitTransition = Animations.navHostExitTransition, - popEnterTransition = Animations.navHostPopEnterTransition, - popExitTransition = Animations.navHostPopExitTransition, - modifier = Modifier.padding(top = it.calculateTopPadding()) - ) { - composable(route = "Home") { Home(localNavCtrl, scrollState, wifiMacDialog) } - composable(route = "Switches") { Switches() } - composable(route = "MinWifiSecurityLevel") { WifiSecLevel() } - composable(route = "WifiSsidPolicy") { WifiSsidPolicy() } - composable(route = "PrivateDNS") { PrivateDNS() } - composable(route = "AlwaysOnVpn") { AlwaysOnVPNPackage(navCtrl) } - composable(route = "RecommendedGlobalProxy") { RecommendedGlobalProxy() } - composable(route = "NetworkLog") { NetworkLog() } - composable(route = "WifiAuthKeypair") { WifiAuthKeypair() } - composable(route = "PreferentialNetworkService") { PreferentialNetworkService() } - composable(route = "APN") { APN() } - } - } - if(wifiMacDialog.value && VERSION.SDK_INT >= 24) { - val context = LocalContext.current - val dpm = context.getDPM() - val receiver = context.getReceiver() - AlertDialog( - onDismissRequest = { wifiMacDialog.value = false }, - confirmButton = { TextButton(onClick = { wifiMacDialog.value = false }) { Text(stringResource(R.string.confirm)) } }, - title = { Text(stringResource(R.string.wifi_mac_address)) }, - text = { - val mac = dpm.getWifiMacAddress(receiver) - OutlinedTextField( - value = mac ?: stringResource(R.string.none), - onValueChange = {}, readOnly = true, modifier = Modifier.fillMaxWidth(), textStyle = typography.bodyLarge, - trailingIcon = { - if(mac != null) IconButton(onClick = { writeClipBoard(context, mac) }) { - Icon(painter = painterResource(R.drawable.content_copy_fill0), contentDescription = stringResource(R.string.copy)) - } - } - ) - }, - modifier = Modifier.fillMaxWidth() - ) - } -} - -@Composable -private fun Home(navCtrl:NavHostController, scrollState: ScrollState, wifiMacDialog: MutableState) { +fun Network(navCtrl:NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -192,58 +116,75 @@ private fun Home(navCtrl:NavHostController, scrollState: ScrollState, wifiMacDia val profileOwner = context.isProfileOwner val sharedPref = context.getSharedPreferences("data", Context.MODE_PRIVATE) val dhizuku = sharedPref.getBoolean("dhizuku", false) - Column(modifier = Modifier.fillMaxSize().verticalScroll(scrollState)) { - Text( - text = stringResource(R.string.network), - style = typography.headlineLarge, - modifier = Modifier.padding(top = 8.dp, bottom = 5.dp, start = 16.dp) - ) + var wifiMacDialog by remember { mutableStateOf(false) } + MyScaffold(R.string.network, 0.dp, navCtrl) { if(VERSION.SDK_INT >= 24 && (deviceOwner || dpm.isOrgProfile(receiver))) { - SubPageItem(R.string.wifi_mac_address, "", R.drawable.wifi_fill0) { wifiMacDialog.value = true } + FunctionItem(R.string.wifi_mac_address, "", R.drawable.wifi_fill0) { wifiMacDialog = true } } if(VERSION.SDK_INT >= 30) { - SubPageItem(R.string.options, "", R.drawable.tune_fill0) { navCtrl.navigate("Switches") } + FunctionItem(R.string.options, "", R.drawable.tune_fill0) { navCtrl.navigate("NetworkOptions") } } if(VERSION.SDK_INT >= 33 && (deviceOwner || dpm.isOrgProfile(receiver))) { - SubPageItem(R.string.min_wifi_security_level, "", R.drawable.wifi_password_fill0) { navCtrl.navigate("MinWifiSecurityLevel") } + FunctionItem(R.string.min_wifi_security_level, "", R.drawable.wifi_password_fill0) { navCtrl.navigate("MinWifiSecurityLevel") } } if(VERSION.SDK_INT >= 33 && (deviceOwner || dpm.isOrgProfile(receiver))) { - SubPageItem(R.string.wifi_ssid_policy, "", R.drawable.wifi_fill0) { navCtrl.navigate("WifiSsidPolicy") } + FunctionItem(R.string.wifi_ssid_policy, "", R.drawable.wifi_fill0) { navCtrl.navigate("WifiSsidPolicy") } } if(VERSION.SDK_INT >= 29 && deviceOwner) { - SubPageItem(R.string.private_dns, "", R.drawable.dns_fill0) { navCtrl.navigate("PrivateDNS") } + FunctionItem(R.string.private_dns, "", R.drawable.dns_fill0) { navCtrl.navigate("PrivateDNS") } } if(VERSION.SDK_INT >= 24 && (deviceOwner || profileOwner)) { - SubPageItem(R.string.always_on_vpn, "", R.drawable.vpn_key_fill0) { navCtrl.navigate("AlwaysOnVpn") } + FunctionItem(R.string.always_on_vpn, "", R.drawable.vpn_key_fill0) { navCtrl.navigate("AlwaysOnVpn") } } if(deviceOwner) { - SubPageItem(R.string.recommended_global_proxy, "", R.drawable.vpn_key_fill0) { navCtrl.navigate("RecommendedGlobalProxy") } + FunctionItem(R.string.recommended_global_proxy, "", R.drawable.vpn_key_fill0) { navCtrl.navigate("RecommendedGlobalProxy") } } if(VERSION.SDK_INT >= 26 && !dhizuku && (deviceOwner || (profileOwner && dpm.isManagedProfile(receiver)))) { - SubPageItem(R.string.retrieve_net_logs, "", R.drawable.description_fill0) { navCtrl.navigate("NetworkLog") } + FunctionItem(R.string.network_logging, "", R.drawable.description_fill0) { navCtrl.navigate("NetworkLog") } } if(VERSION.SDK_INT >= 31 && (deviceOwner || profileOwner)) { - SubPageItem(R.string.wifi_auth_keypair, "", R.drawable.key_fill0) { navCtrl.navigate("WifiAuthKeypair") } + FunctionItem(R.string.wifi_auth_keypair, "", R.drawable.key_fill0) { navCtrl.navigate("WifiAuthKeypair") } } if(VERSION.SDK_INT >= 33 && (deviceOwner || profileOwner)) { - SubPageItem(R.string.preferential_network_service, "", R.drawable.globe_fill0) { navCtrl.navigate("PreferentialNetworkService") } + FunctionItem(R.string.preferential_network_service, "", R.drawable.globe_fill0) { navCtrl.navigate("PreferentialNetworkService") } } if(VERSION.SDK_INT >= 28 && deviceOwner) { - SubPageItem(R.string.override_apn_settings, "", R.drawable.cell_tower_fill0) { navCtrl.navigate("APN") } + FunctionItem(R.string.override_apn_settings, "", R.drawable.cell_tower_fill0) { navCtrl.navigate("OverrideAPN") } } - Spacer(Modifier.padding(vertical = 30.dp)) + } + if(wifiMacDialog && VERSION.SDK_INT >= 24) { + val context = LocalContext.current + val dpm = context.getDPM() + val receiver = context.getReceiver() + AlertDialog( + onDismissRequest = { wifiMacDialog = false }, + confirmButton = { TextButton(onClick = { wifiMacDialog = false }) { Text(stringResource(R.string.confirm)) } }, + title = { Text(stringResource(R.string.wifi_mac_address)) }, + text = { + val mac = dpm.getWifiMacAddress(receiver) + OutlinedTextField( + value = mac ?: stringResource(R.string.none), + onValueChange = {}, readOnly = true, modifier = Modifier.fillMaxWidth(), textStyle = typography.bodyLarge, + trailingIcon = { + if(mac != null) IconButton(onClick = { writeClipBoard(context, mac) }) { + Icon(painter = painterResource(R.drawable.content_copy_fill0), contentDescription = stringResource(R.string.copy)) + } + } + ) + }, + modifier = Modifier.fillMaxWidth() + ) } } @Composable -private fun Switches() { +fun NetworkOptions(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val deviceOwner = context.isDeviceOwner var dialog by remember { mutableIntStateOf(0) } - Column(modifier = Modifier.fillMaxSize()) { - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.options, 0.dp, navCtrl) { if(VERSION.SDK_INT>=30 && (deviceOwner || dpm.isOrgProfile(receiver))) { SwitchItem(R.string.lockdown_admin_configured_network, "", R.drawable.wifi_password_fill0, { dpm.hasLockdownAdminConfiguredNetworks(receiver) }, { dpm.setConfiguredNetworksLockdownState(receiver,it) }, @@ -262,15 +203,12 @@ private fun Switches() { @SuppressLint("NewApi") @Composable -private fun WifiSecLevel() { +fun WifiSecurityLevel(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() var selectedWifiSecLevel by remember { mutableIntStateOf(0) } LaunchedEffect(Unit) { selectedWifiSecLevel = dpm.minimumRequiredWifiSecurityLevel } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.min_wifi_security_level), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.min_wifi_security_level, 8.dp, navCtrl) { RadioButtonItem( R.string.wifi_security_open, selectedWifiSecLevel == WIFI_SECURITY_OPEN, @@ -307,11 +245,11 @@ private fun WifiSecLevel() { @SuppressLint("NewApi") @Composable -private fun WifiSsidPolicy() { +fun WifiSsidPolicy(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val focusMgr = LocalFocusManager.current - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { + MyScaffold(R.string.wifi_ssid_policy, 8.dp, navCtrl) { var selectedPolicyType by remember { mutableIntStateOf(-1) } val ssidList = remember { mutableStateListOf() } val refreshPolicy = { @@ -321,9 +259,6 @@ private fun WifiSsidPolicy() { ssidList.addAll(policy?.ssids ?: mutableSetOf()) } LaunchedEffect(Unit) { refreshPolicy() } - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.wifi_ssid_policy), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) RadioButtonItem( R.string.none, selectedPolicyType == -1, @@ -387,20 +322,17 @@ private fun WifiSsidPolicy() { ) { Text(stringResource(R.string.apply)) } - Spacer(Modifier.padding(vertical = 30.dp)) } } @SuppressLint("NewApi") @Composable -private fun PrivateDNS() { +fun PrivateDNS(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.private_dns), style = typography.headlineLarge) + MyScaffold(R.string.private_dns, 8.dp, navCtrl) { val dnsStatus = mapOf( PRIVATE_DNS_MODE_UNKNOWN to stringResource(R.string.unknown), PRIVATE_DNS_MODE_OFF to stringResource(R.string.disabled), @@ -462,7 +394,6 @@ private fun PrivateDNS() { Text(stringResource(R.string.set_dns_host)) } InfoCard(R.string.info_set_private_dns_host) - Spacer(Modifier.padding(vertical = 30.dp)) } } @@ -499,9 +430,7 @@ fun AlwaysOnVPNPackage(navCtrl: NavHostController) { false } } - Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 8.dp)) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.always_on_vpn), style = typography.headlineLarge, modifier = Modifier.padding(vertical = 8.dp)) + MyScaffold(R.string.always_on_vpn, 8.dp, navCtrl) { OutlinedTextField( value = pkgName, onValueChange = { pkgName = it }, @@ -509,7 +438,7 @@ fun AlwaysOnVPNPackage(navCtrl: NavHostController) { keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }), trailingIcon = { - Icon(painter = painterResource(R.drawable.checklist_fill0), contentDescription = null, + Icon(painter = painterResource(R.drawable.list_fill0), contentDescription = null, modifier = Modifier .clip(RoundedCornerShape(50)) .clickable(onClick = { @@ -536,12 +465,11 @@ fun AlwaysOnVPNPackage(navCtrl: NavHostController) { Text(stringResource(R.string.clear_current_config)) } InfoCard(R.string.info_always_on_vpn) - Spacer(Modifier.padding(vertical = 30.dp)) } } @Composable -private fun RecommendedGlobalProxy() { +fun RecommendedGlobalProxy(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -551,10 +479,7 @@ private fun RecommendedGlobalProxy() { var specifyPort by remember { mutableStateOf(false) } var proxyPort by remember { mutableStateOf("") } var exclList by remember { mutableStateOf("") } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.recommended_global_proxy), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.recommended_global_proxy, 8.dp, navCtrl) { RadioButtonItem(R.string.proxy_type_off, proxyType == 0, { proxyType = 0 }) RadioButtonItem(R.string.proxy_type_pac, proxyType == 1, { proxyType = 1 }) RadioButtonItem(R.string.proxy_type_direct, proxyType == 2, { proxyType = 2 }) @@ -641,7 +566,7 @@ private fun RecommendedGlobalProxy() { @SuppressLint("NewApi") @Composable -private fun NetworkLog() { +fun NetworkLogging(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -650,10 +575,7 @@ private fun NetworkLog() { LaunchedEffect(Unit) { fileSize = logFile.length() } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.retrieve_net_logs), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.network_logging, 8.dp, navCtrl) { SwitchItem(R.string.enable, "", null, { dpm.isNetworkLoggingEnabled(receiver) }, { dpm.setNetworkLoggingEnabled(receiver,it) }, padding = false) Text(stringResource(R.string.log_file_size_is, formatFileSize(fileSize))) Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { @@ -688,15 +610,12 @@ private fun NetworkLog() { @SuppressLint("NewApi") @Composable -private fun WifiAuthKeypair() { +fun WifiAuthKeypair(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val focusMgr = LocalFocusManager.current - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - var keyPair by remember { mutableStateOf("") } - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.wifi_auth_keypair), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) + var keyPair by remember { mutableStateOf("") } + MyScaffold(R.string.wifi_auth_keypair, 8.dp, navCtrl) { OutlinedTextField( value = keyPair, label = { Text(stringResource(R.string.alias)) }, @@ -739,7 +658,7 @@ private fun WifiAuthKeypair() { @SuppressLint("NewApi") @Composable -fun PreferentialNetworkService() { +fun PreferentialNetworkService(navCtrl: NavHostController) { val focusMgr = LocalFocusManager.current val context = LocalContext.current val dpm = context.getDPM() @@ -778,10 +697,7 @@ fun PreferentialNetworkService() { refresh() } LaunchedEffect(Unit) { initialize() } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.preferential_network_service), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.preferential_network_service, 8.dp, navCtrl) { SwitchItem( title = R.string.enabled, desc = "", icon = null, state = masterEnabled, onCheckedChange = { masterEnabled = it }, padding = false @@ -895,25 +811,21 @@ fun PreferentialNetworkService() { ) { Text(stringResource(R.string.apply)) } - Spacer(Modifier.padding(vertical = 30.dp)) } } @SuppressLint("NewApi") @Composable -private fun APN() { +fun OverrideAPN(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - val setting = dpm.getOverrideApns(receiver) - var inputNum by remember { mutableStateOf("0") } - var nextStep by remember { mutableStateOf(false) } - val builder = Builder() - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.override_apn_settings), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) + val setting = dpm.getOverrideApns(receiver) + var inputNum by remember { mutableStateOf("0") } + var nextStep by remember { mutableStateOf(false) } + val builder = Builder() + MyScaffold(R.string.override_apn_settings, 8.dp, navCtrl) { Text(text = stringResource(id = R.string.developing)) Spacer(Modifier.padding(vertical = 5.dp)) SwitchItem(R.string.enable, "", null, { dpm.isOverrideApnEnabled(receiver) }, { dpm.setOverrideApnsEnabled(receiver,it) }, padding = false) @@ -1275,6 +1187,5 @@ private fun APN() { } } } - Spacer(Modifier.padding(vertical = 30.dp)) } } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt index 69a291d..4e486ec 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt @@ -35,25 +35,20 @@ import android.os.Build.VERSION import android.os.UserManager import android.widget.Toast import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.ScrollState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -65,7 +60,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource @@ -75,103 +69,56 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat.startActivity import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController import com.bintianqi.owndroid.R import com.bintianqi.owndroid.toggle -import com.bintianqi.owndroid.ui.Animations import com.bintianqi.owndroid.ui.CardItem import com.bintianqi.owndroid.ui.CheckBoxItem +import com.bintianqi.owndroid.ui.FunctionItem import com.bintianqi.owndroid.ui.InfoCard import com.bintianqi.owndroid.ui.Information +import com.bintianqi.owndroid.ui.MyScaffold import com.bintianqi.owndroid.ui.RadioButtonItem -import com.bintianqi.owndroid.ui.SubPageItem -import com.bintianqi.owndroid.ui.TopBar import com.bintianqi.owndroid.yesOrNo -@Composable -fun Password(navCtrl: NavHostController) { - val localNavCtrl = rememberNavController() - val backStackEntry by localNavCtrl.currentBackStackEntryAsState() - val scrollState = rememberScrollState() - Scaffold( - topBar = { - TopBar(backStackEntry,navCtrl,localNavCtrl) { - if(backStackEntry?.destination?.route == "Home" && scrollState.maxValue > 100) { - Text( - text = stringResource(R.string.password_and_keyguard), - modifier = Modifier.alpha((maxOf(scrollState.value-30,0)).toFloat()/80) - ) - } - } - } - ) { - NavHost( - navController = localNavCtrl, startDestination = "Home", - enterTransition = Animations.navHostEnterTransition, - exitTransition = Animations.navHostExitTransition, - popEnterTransition = Animations.navHostPopEnterTransition, - popExitTransition = Animations.navHostPopExitTransition, - modifier = Modifier.padding(top = it.calculateTopPadding()) - ) { - composable(route = "Home") { Home(localNavCtrl,scrollState) } - composable(route = "PasswordInfo") { PasswordInfo() } - composable(route = "ResetPasswordToken") { ResetPasswordToken() } - composable(route = "ResetPassword") { ResetPassword() } - composable(route = "RequirePasswordComplexity") { PasswordComplexity() } - composable(route = "DisableKeyguardFeatures") { DisableKeyguardFeatures() } - composable(route = "RequirePasswordQuality") { PasswordQuality() } - } - } -} - @SuppressLint("NewApi") @Composable -private fun Home(navCtrl:NavHostController, scrollState: ScrollState) { +fun Password(navCtrl: NavHostController) { val context = LocalContext.current val sharedPrefs = context.getSharedPreferences("data", Context.MODE_PRIVATE) val deviceAdmin = context.isDeviceAdmin val deviceOwner = context.isDeviceOwner val profileOwner = context.isProfileOwner var dialog by remember { mutableIntStateOf(0) } - Column(modifier = Modifier.fillMaxSize().verticalScroll(scrollState)) { - Text( - text = stringResource(R.string.password_and_keyguard), - style = typography.headlineLarge, - modifier = Modifier.padding(top = 8.dp, bottom = 5.dp, start = 16.dp) - ) - SubPageItem(R.string.password_info, "", R.drawable.info_fill0) { navCtrl.navigate("PasswordInfo") } + MyScaffold(R.string.password_and_keyguard, 0.dp, navCtrl) { + FunctionItem(R.string.password_info, "", R.drawable.info_fill0) { navCtrl.navigate("PasswordInfo") } if(sharedPrefs.getBoolean("dangerous_features", false)) { if(VERSION.SDK_INT >= 26 && (deviceOwner || profileOwner)) { - SubPageItem(R.string.reset_password_token, "", R.drawable.key_vertical_fill0) { navCtrl.navigate("ResetPasswordToken") } + FunctionItem(R.string.reset_password_token, "", R.drawable.key_vertical_fill0) { navCtrl.navigate("ResetPasswordToken") } } if(deviceAdmin || deviceOwner || profileOwner) { - SubPageItem(R.string.reset_password, "", R.drawable.lock_reset_fill0) { navCtrl.navigate("ResetPassword") } + FunctionItem(R.string.reset_password, "", R.drawable.lock_reset_fill0) { navCtrl.navigate("ResetPassword") } } } if(VERSION.SDK_INT >= 31 && (deviceOwner || profileOwner)) { - SubPageItem(R.string.required_password_complexity, "", R.drawable.password_fill0) { navCtrl.navigate("RequirePasswordComplexity") } + FunctionItem(R.string.required_password_complexity, "", R.drawable.password_fill0) { navCtrl.navigate("RequirePasswordComplexity") } } if(deviceAdmin) { - SubPageItem(R.string.disable_keyguard_features, "", R.drawable.screen_lock_portrait_fill0) { navCtrl.navigate("DisableKeyguardFeatures") } + FunctionItem(R.string.disable_keyguard_features, "", R.drawable.screen_lock_portrait_fill0) { navCtrl.navigate("DisableKeyguardFeatures") } } if(deviceOwner) { - SubPageItem(R.string.max_time_to_lock, "", R.drawable.schedule_fill0) { dialog = 1 } - SubPageItem(R.string.pwd_expiration_timeout, "", R.drawable.lock_clock_fill0) { dialog = 3 } - SubPageItem(R.string.max_pwd_fail, "", R.drawable.no_encryption_fill0) { dialog = 4 } + FunctionItem(R.string.max_time_to_lock, "", R.drawable.schedule_fill0) { dialog = 1 } + FunctionItem(R.string.pwd_expiration_timeout, "", R.drawable.lock_clock_fill0) { dialog = 3 } + FunctionItem(R.string.max_pwd_fail, "", R.drawable.no_encryption_fill0) { dialog = 4 } } if(VERSION.SDK_INT >= 26 && (deviceOwner || profileOwner)) { - SubPageItem(R.string.required_strong_auth_timeout, "", R.drawable.fingerprint_off_fill0) { dialog = 2 } + FunctionItem(R.string.required_strong_auth_timeout, "", R.drawable.fingerprint_off_fill0) { dialog = 2 } } if(deviceAdmin){ - SubPageItem(R.string.pwd_history, "", R.drawable.history_fill0) { dialog = 5 } + FunctionItem(R.string.pwd_history, "", R.drawable.history_fill0) { dialog = 5 } } if(VERSION.SDK_INT < 31 && (deviceOwner || profileOwner)) { - SubPageItem(R.string.required_password_quality, "", R.drawable.password_fill0) { navCtrl.navigate("RequirePasswordQuality") } + FunctionItem(R.string.required_password_quality, "", R.drawable.password_fill0) { navCtrl.navigate("RequirePasswordQuality") } } - Spacer(Modifier.padding(vertical = 30.dp)) } if(dialog != 0) { val dpm = context.getDPM() @@ -263,16 +210,13 @@ private fun Home(navCtrl:NavHostController, scrollState: ScrollState) { } @Composable -private fun PasswordInfo() { +fun PasswordInfo(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val deviceOwner = context.isDeviceOwner val profileOwner = context.isProfileOwner - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.password_info), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.password_info, 8.dp, navCtrl) { if(VERSION.SDK_INT >= 29) { val passwordComplexity = mapOf( PASSWORD_COMPLEXITY_NONE to R.string.password_complexity_none, @@ -293,17 +237,14 @@ private fun PasswordInfo() { @SuppressLint("NewApi") @Composable -private fun ResetPasswordToken() { +fun ResetPasswordToken(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() var token by remember { mutableStateOf("") } val tokenByteArray = token.toByteArray() val focusMgr = LocalFocusManager.current - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.reset_password_token), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.reset_password_token, 8.dp, navCtrl) { OutlinedTextField( value = token, onValueChange = { token = it }, label = { Text(stringResource(R.string.token)) }, @@ -367,7 +308,7 @@ private fun ResetPasswordToken() { } @Composable -private fun ResetPassword() { +fun ResetPassword(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -378,10 +319,7 @@ private fun ResetPassword() { val tokenByteArray = token.toByteArray() val flags = remember { mutableStateListOf() } var confirmDialog by remember { mutableStateOf(false) } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.reset_password),style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.reset_password, 8.dp, navCtrl) { if(VERSION.SDK_INT >= 26) { OutlinedTextField( value = token, onValueChange = { token = it }, @@ -444,7 +382,6 @@ private fun ResetPassword() { } } InfoCard(R.string.info_reset_password) - Spacer(Modifier.padding(vertical = 30.dp)) } if(confirmDialog) { var confirmPassword by remember { mutableStateOf("") } @@ -494,7 +431,7 @@ private fun ResetPassword() { @SuppressLint("NewApi") @Composable -private fun PasswordComplexity() { +fun PasswordComplexity(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val passwordComplexity = mapOf( @@ -502,19 +439,12 @@ private fun PasswordComplexity() { PASSWORD_COMPLEXITY_LOW to R.string.password_complexity_low, PASSWORD_COMPLEXITY_MEDIUM to R.string.password_complexity_medium, PASSWORD_COMPLEXITY_HIGH to R.string.password_complexity_high - ).toList() - var selectedItem by remember { mutableIntStateOf(passwordComplexity[0].first) } + ) + var selectedItem by remember { mutableIntStateOf(PASSWORD_COMPLEXITY_NONE) } LaunchedEffect(Unit) { selectedItem = dpm.requiredPasswordComplexity } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.required_password_complexity), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) - for(index in 0..3) { - RadioButtonItem( - passwordComplexity[index].second, - selectedItem == passwordComplexity[index].first, - { selectedItem = passwordComplexity[index].first } - ) + MyScaffold(R.string.required_password_complexity, 8.dp, navCtrl) { + passwordComplexity.forEach { + RadioButtonItem(it.value, selectedItem == it.key, { selectedItem = it.key }) } Spacer(Modifier.padding(vertical = 5.dp)) Button( @@ -533,12 +463,11 @@ private fun PasswordComplexity() { ) { Text(stringResource(R.string.require_set_new_password)) } - Spacer(Modifier.padding(vertical = 30.dp)) } } @Composable -private fun DisableKeyguardFeatures() { +fun DisableKeyguardFeatures(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -579,10 +508,7 @@ private fun DisableKeyguardFeatures() { } calculateCustomFeature() } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.disable_keyguard_features), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.disable_keyguard_features, 8.dp, navCtrl) { RadioButtonItem(R.string.enable_all, state == 0, { state = 0 }) RadioButtonItem(R.string.disable_all, state == 1, { state = 1 }) RadioButtonItem(R.string.custom, state == 2 , { state = 2 }) @@ -630,12 +556,11 @@ private fun DisableKeyguardFeatures() { ) { Text(text = stringResource(R.string.apply)) } - Spacer(Modifier.padding(vertical = 30.dp)) } } @Composable -private fun PasswordQuality() { +fun PasswordQuality(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -647,15 +572,12 @@ private fun PasswordQuality() { PASSWORD_QUALITY_ALPHANUMERIC to R.string.password_quality_alphanumeric, PASSWORD_QUALITY_BIOMETRIC_WEAK to R.string.password_quality_biometrics_weak, PASSWORD_QUALITY_NUMERIC_COMPLEX to R.string.password_quality_numeric_complex - ).toList() - var selectedItem by remember { mutableIntStateOf(passwordQuality[0].first) } + ) + var selectedItem by remember { mutableIntStateOf(PASSWORD_QUALITY_UNSPECIFIED) } LaunchedEffect(Unit) { selectedItem=dpm.getPasswordQuality(receiver) } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.required_password_quality), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) - for(index in 1..6) { - RadioButtonItem(passwordQuality[index].second, selectedItem == passwordQuality[index].first, { selectedItem = passwordQuality[index].first }) + MyScaffold(R.string.required_password_quality, 8.dp, navCtrl) { + passwordQuality.forEach { + RadioButtonItem(it.value, selectedItem == it.key, { selectedItem = it.key }) } Spacer(Modifier.padding(vertical = 5.dp)) Button( @@ -667,7 +589,6 @@ private fun PasswordQuality() { ) { Text(stringResource(R.string.apply)) } - Spacer(Modifier.padding(vertical = 30.dp)) } } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt index 22bdcae..2e4a93f 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt @@ -13,7 +13,6 @@ import android.os.RemoteException import android.os.UserManager import android.widget.Toast import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -23,7 +22,6 @@ import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.typography import androidx.compose.runtime.* import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource @@ -31,10 +29,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController import com.bintianqi.owndroid.R import com.bintianqi.owndroid.backToHomeStateFlow import com.bintianqi.owndroid.ui.* @@ -44,47 +38,9 @@ import com.rosan.dhizuku.api.Dhizuku import com.rosan.dhizuku.api.DhizukuRequestPermissionListener import kotlinx.coroutines.launch -@Composable -fun DpmPermissions(navCtrl:NavHostController) { - val localNavCtrl = rememberNavController() - val backStackEntry by localNavCtrl.currentBackStackEntryAsState() - val scrollState = rememberScrollState() - Scaffold( - topBar = { - TopBar(backStackEntry,navCtrl,localNavCtrl) { - if(backStackEntry?.destination?.route=="Home"&&scrollState.maxValue > 100) { - Text( - text = stringResource(R.string.permission), - modifier = Modifier.alpha((maxOf(scrollState.value-30,0)).toFloat()/80) - ) - } - } - } - ) { - NavHost( - navController = localNavCtrl, startDestination = "Home", - enterTransition = Animations.navHostEnterTransition, - exitTransition = Animations.navHostExitTransition, - popEnterTransition = Animations.navHostPopEnterTransition, - popExitTransition = Animations.navHostPopExitTransition, - modifier = Modifier.padding(top = it.calculateTopPadding()) - ) { - composable(route = "Home") { Home(localNavCtrl,scrollState) } - composable(route = "Shizuku") { ShizukuActivate() } - composable(route = "DeviceAdmin") { DeviceAdmin() } - composable(route = "ProfileOwner") { ProfileOwner() } - composable(route = "DeviceOwner") { DeviceOwner() } - composable(route = "DeviceInfo") { DeviceInfo() } - composable(route = "LockScreenInfo") { LockScreenInfo() } - composable(route = "SupportMsg") { SupportMsg() } - composable(route = "TransformOwnership") { TransferOwnership() } - } - } -} - @SuppressLint("NewApi") @Composable -private fun Home(localNavCtrl:NavHostController,listScrollState:ScrollState) { +fun Permissions(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -95,12 +51,7 @@ private fun Home(localNavCtrl:NavHostController,listScrollState:ScrollState) { val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager var dialog by remember { mutableIntStateOf(0) } val enrollmentSpecificId = if(VERSION.SDK_INT >= 31 && (deviceOwner || profileOwner)) dpm.enrollmentSpecificId else "" - Column(modifier = Modifier.fillMaxSize().verticalScroll(listScrollState)) { - Text( - text = stringResource(R.string.permission), - style = typography.headlineLarge, - modifier = Modifier.padding(top = 8.dp, bottom = 5.dp, start = 16.dp) - ) + MyScaffold(R.string.permissions, 0.dp, navCtrl) { if(!dpm.isDeviceOwnerApp(context.packageName)) { SwitchItem( R.string.dhizuku, "", null, @@ -109,43 +60,42 @@ private fun Home(localNavCtrl:NavHostController,listScrollState:ScrollState) { onClickBlank = { dialog = 4 } ) } - SubPageItem( + FunctionItem( R.string.device_admin, stringResource(if(deviceAdmin) R.string.activated else R.string.deactivated), - operation = { localNavCtrl.navigate("DeviceAdmin") } + operation = { navCtrl.navigate("DeviceAdmin") } ) if(profileOwner || !userManager.isSystemUser) { - SubPageItem( + FunctionItem( R.string.profile_owner, stringResource(if(profileOwner) R.string.activated else R.string.deactivated), - operation = { localNavCtrl.navigate("ProfileOwner") } + operation = { navCtrl.navigate("ProfileOwner") } ) } if(!profileOwner && userManager.isSystemUser) { - SubPageItem( + FunctionItem( R.string.device_owner, stringResource(if(deviceOwner) R.string.activated else R.string.deactivated), - operation = { localNavCtrl.navigate("DeviceOwner") } + operation = { navCtrl.navigate("DeviceOwner") } ) } - SubPageItem(R.string.shizuku,"") { localNavCtrl.navigate("Shizuku") } - SubPageItem(R.string.device_info, "", R.drawable.perm_device_information_fill0) { localNavCtrl.navigate("DeviceInfo") } + FunctionItem(R.string.shizuku,"") { navCtrl.navigate("Shizuku") } + FunctionItem(R.string.device_info, "", R.drawable.perm_device_information_fill0) { navCtrl.navigate("DeviceInfo") } if((VERSION.SDK_INT >= 26 && deviceOwner) || (VERSION.SDK_INT>=24 && profileOwner)) { - SubPageItem(R.string.org_name, "", R.drawable.corporate_fare_fill0) { dialog = 2 } + FunctionItem(R.string.org_name, "", R.drawable.corporate_fare_fill0) { dialog = 2 } } if(VERSION.SDK_INT >= 31 && (profileOwner || deviceOwner)) { - SubPageItem(R.string.org_id, "", R.drawable.corporate_fare_fill0) { dialog = 3 } + FunctionItem(R.string.org_id, "", R.drawable.corporate_fare_fill0) { dialog = 3 } } if(enrollmentSpecificId != "") { - SubPageItem(R.string.enrollment_specific_id, "", R.drawable.id_card_fill0) { dialog = 1 } + FunctionItem(R.string.enrollment_specific_id, "", R.drawable.id_card_fill0) { dialog = 1 } } if(VERSION.SDK_INT >= 24 && (deviceOwner || dpm.isOrgProfile(receiver))) { - SubPageItem(R.string.device_owner_lock_screen_info, "", R.drawable.screen_lock_portrait_fill0) { localNavCtrl.navigate("LockScreenInfo") } + FunctionItem(R.string.lock_screen_info, "", R.drawable.screen_lock_portrait_fill0) { navCtrl.navigate("LockScreenInfo") } } if(VERSION.SDK_INT >= 24 && deviceAdmin) { - SubPageItem(R.string.support_msg, "", R.drawable.chat_fill0) { localNavCtrl.navigate("SupportMsg") } + FunctionItem(R.string.support_messages, "", R.drawable.chat_fill0) { navCtrl.navigate("SupportMessages") } } if(VERSION.SDK_INT >= 28 && (deviceOwner || profileOwner)) { - SubPageItem(R.string.transfer_ownership, "", R.drawable.admin_panel_settings_fill0) { localNavCtrl.navigate("TransformOwnership") } + FunctionItem(R.string.transfer_ownership, "", R.drawable.admin_panel_settings_fill0) { navCtrl.navigate("TransferOwnership") } } - Spacer(Modifier.padding(vertical = 30.dp)) } if(dialog != 0) { var input by remember { mutableStateOf("") } @@ -157,7 +107,7 @@ private fun Home(localNavCtrl:NavHostController,listScrollState:ScrollState) { 2 -> R.string.org_name 3 -> R.string.org_id 4 -> R.string.dhizuku - else -> R.string.permission + else -> R.string.permissions } )) }, @@ -176,7 +126,7 @@ private fun Home(localNavCtrl:NavHostController,listScrollState:ScrollState) { 1 -> R.string.enrollment_specific_id 2 -> R.string.org_name 3 -> R.string.org_id - else -> R.string.permission + else -> R.string.permissions } )) }, @@ -237,7 +187,7 @@ private fun toggleDhizukuMode(status: Boolean, context: Context) { dhizukuErrorStatus.value = 1 return } - if(Dhizuku.isPermissionGranted()) { + if(dhizukuPermissionGranted()) { sharedPref.edit().putBoolean("dhizuku", true).apply() Dhizuku.init(context) backToHomeStateFlow.value = true @@ -260,17 +210,16 @@ private fun toggleDhizukuMode(status: Boolean, context: Context) { @SuppressLint("NewApi") @Composable -private fun LockScreenInfo() { +fun LockScreenInfo(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current var infoText by remember { mutableStateOf(dpm.deviceOwnerLockScreenInfo?.toString() ?: "") } - Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 8.dp)) { - Text(text = stringResource(R.string.device_owner_lock_screen_info), style = typography.headlineLarge) + MyScaffold(R.string.lock_screen_info, 8.dp, navCtrl) { OutlinedTextField( value = infoText, - label = { Text(stringResource(R.string.device_owner_lock_screen_info)) }, + label = { Text(stringResource(R.string.lock_screen_info)) }, onValueChange = { infoText = it }, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), keyboardActions = KeyboardActions { focusMgr.clearFocus() }, @@ -302,15 +251,13 @@ private fun LockScreenInfo() { } @Composable -private fun DeviceAdmin() { +fun DeviceAdmin(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() var deactivateDialog by remember { mutableStateOf(false) } val deviceAdmin = context.isDeviceAdmin - Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 8.dp)) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.device_admin), style = typography.headlineLarge) + MyScaffold(R.string.device_admin, 8.dp, navCtrl) { Text(text = stringResource(if(context.isDeviceAdmin) R.string.activated else R.string.deactivated), style = typography.titleLarge) Spacer(Modifier.padding(vertical = 5.dp)) AnimatedVisibility(deviceAdmin) { @@ -361,15 +308,13 @@ private fun DeviceAdmin() { } @Composable -private fun ProfileOwner() { +fun ProfileOwner(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() var deactivateDialog by remember { mutableStateOf(false) } val profileOwner = context.isProfileOwner - Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 8.dp)) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.profile_owner), style = typography.headlineLarge) + MyScaffold(R.string.profile_owner, 8.dp, navCtrl) { Text(stringResource(if(profileOwner) R.string.activated else R.string.deactivated), style = typography.titleLarge) Spacer(Modifier.padding(vertical = 5.dp)) if(VERSION.SDK_INT >= 24 && profileOwner) { @@ -416,14 +361,12 @@ private fun ProfileOwner() { } @Composable -private fun DeviceOwner() { +fun DeviceOwner(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() var deactivateDialog by remember { mutableStateOf(false) } val deviceOwner = context.isDeviceOwner - Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 8.dp)) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.device_owner), style = typography.headlineLarge) + MyScaffold(R.string.device_owner, 8.dp, navCtrl) { Text(text = stringResource(if(deviceOwner) R.string.activated else R.string.deactivated), style = typography.titleLarge) Spacer(Modifier.padding(vertical = 5.dp)) AnimatedVisibility(deviceOwner) { @@ -488,15 +431,12 @@ private fun DeviceOwner() { } @Composable -fun DeviceInfo() { +fun DeviceInfo(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() var dialog by remember { mutableIntStateOf(0) } - Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 8.dp)) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.device_info), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.device_info, 8.dp, navCtrl) { if(VERSION.SDK_INT>=34 && (context.isDeviceOwner || dpm.isOrgProfile(receiver))) { CardItem(R.string.financed_device, dpm.isDeviceFinanced.yesOrNo) } @@ -532,7 +472,7 @@ fun DeviceInfo() { @SuppressLint("NewApi") @Composable -private fun SupportMsg() { +fun SupportMessages(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -543,10 +483,7 @@ private fun SupportMsg() { longMsg = dpm.getLongSupportMessage(receiver)?.toString() ?: "" } LaunchedEffect(Unit) { refreshMsg() } - Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 8.dp)) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.support_msg), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.support_messages, 8.dp, navCtrl) { OutlinedTextField( value = shortMsg, label = { Text(stringResource(R.string.short_support_msg)) }, @@ -608,22 +545,18 @@ private fun SupportMsg() { } } InfoCard(R.string.info_long_support_message) - Spacer(Modifier.padding(vertical = 30.dp)) } } @SuppressLint("NewApi") @Composable -private fun TransferOwnership() { +fun TransferOwnership(navCtrl: NavHostController) { val context = LocalContext.current val focusMgr = LocalFocusManager.current var input by remember { mutableStateOf("") } val componentName = ComponentName.unflattenFromString(input) var dialog by remember { mutableStateOf(false) } - Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 8.dp)) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.transfer_ownership), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.transfer_ownership, 8.dp, navCtrl) { OutlinedTextField( value = input, onValueChange = { input = it }, label = { Text(stringResource(R.string.target_component_name)) }, modifier = Modifier.fillMaxWidth(), diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/ShizukuActivate.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Shizuku.kt similarity index 88% rename from app/src/main/java/com/bintianqi/owndroid/dpm/ShizukuActivate.kt rename to app/src/main/java/com/bintianqi/owndroid/dpm/Shizuku.kt index 8c67c72..a7c11b3 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/ShizukuActivate.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Shizuku.kt @@ -10,14 +10,12 @@ import android.os.IBinder import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -34,8 +32,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController import com.bintianqi.owndroid.IUserService import com.bintianqi.owndroid.R +import com.bintianqi.owndroid.ui.MyScaffold import kotlinx.coroutines.delay import kotlinx.coroutines.launch import rikka.shizuku.Shizuku @@ -43,7 +43,7 @@ import rikka.shizuku.Shizuku private var waitGrantPermission = false @Composable -fun ShizukuActivate() { +fun Shizuku(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -69,19 +69,14 @@ fun ShizukuActivate() { shizukuService.value = null userServiceControl(context, true) } - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 8.dp) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally - ) { - AnimatedVisibility(bindShizuku) { + MyScaffold(R.string.shizuku, 0.dp, navCtrl, false) { + AnimatedVisibility(visible = bindShizuku, modifier = Modifier.fillMaxWidth()) { Button( onClick = { userServiceControl(context, true) outputText = "" - } + }, + modifier = Modifier.wrapContentWidth(Alignment.CenterHorizontally) ) { Text(stringResource(R.string.bind_shizuku)) } @@ -100,7 +95,8 @@ fun ShizukuActivate() { coScope.launch { outputTextScrollState.animateScrollTo(0) } - } + }, + modifier = Modifier.align(Alignment.CenterHorizontally) ) { Text(text = stringResource(R.string.check_shizuku)) } @@ -112,7 +108,8 @@ fun ShizukuActivate() { outputTextScrollState.animateScrollTo(0) } }, - enabled = enabled + enabled = enabled, + modifier = Modifier.align(Alignment.CenterHorizontally) ) { Text(text = stringResource(R.string.list_owners)) } @@ -123,7 +120,8 @@ fun ShizukuActivate() { outputTextScrollState.animateScrollTo(0) } }, - enabled = enabled + enabled = enabled, + modifier = Modifier.align(Alignment.CenterHorizontally) ) { Text(text = stringResource(R.string.list_users)) } @@ -134,7 +132,8 @@ fun ShizukuActivate() { outputTextScrollState.animateScrollTo(0) } }, - enabled = enabled + enabled = enabled, + modifier = Modifier.align(Alignment.CenterHorizontally) ) { Text(text = stringResource(R.string.list_accounts)) } @@ -150,7 +149,8 @@ fun ShizukuActivate() { showDeviceAdminButton = !context.isDeviceAdmin } }, - enabled = enabled + enabled = enabled, + modifier = Modifier.align(Alignment.CenterHorizontally) ) { Text(text = stringResource(R.string.activate_device_admin)) } @@ -166,7 +166,8 @@ fun ShizukuActivate() { showDeviceOwnerButton = !context.isDeviceOwner } }, - enabled = enabled + enabled = enabled, + modifier = Modifier.align(Alignment.CenterHorizontally) ) { Text(text = stringResource(R.string.activate_device_owner)) } @@ -186,7 +187,8 @@ fun ShizukuActivate() { showOrgProfileOwnerButton = !dpm.isOrganizationOwnedDeviceWithManagedProfile } }, - enabled = enabled + enabled = enabled, + modifier = Modifier.align(Alignment.CenterHorizontally) ) { Text(text = stringResource(R.string.activate_org_profile)) } @@ -194,10 +196,8 @@ fun ShizukuActivate() { } SelectionContainer(modifier = Modifier.fillMaxWidth().horizontalScroll(outputTextScrollState)) { - Text(text = outputText, softWrap = false, modifier = Modifier.padding(4.dp)) + Text(text = outputText, softWrap = false, modifier = Modifier.padding(vertical = 9.dp, horizontal = 12.dp)) } - - Spacer(Modifier.padding(vertical = 30.dp)) } } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt index 4a78320..2db2cd2 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt @@ -44,23 +44,19 @@ import android.os.UserManager import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.ScrollState import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.List import androidx.compose.material.icons.filled.Add @@ -72,7 +68,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -90,7 +85,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager @@ -101,10 +95,6 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.core.app.NotificationCompat import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController import com.bintianqi.owndroid.R import com.bintianqi.owndroid.StopLockTaskModeReceiver import com.bintianqi.owndroid.exportFile @@ -115,15 +105,14 @@ import com.bintianqi.owndroid.getFile import com.bintianqi.owndroid.prepareForNotification import com.bintianqi.owndroid.selectedPackage import com.bintianqi.owndroid.toggle -import com.bintianqi.owndroid.ui.Animations import com.bintianqi.owndroid.ui.CheckBoxItem +import com.bintianqi.owndroid.ui.FunctionItem import com.bintianqi.owndroid.ui.InfoCard import com.bintianqi.owndroid.ui.Information import com.bintianqi.owndroid.ui.ListItem +import com.bintianqi.owndroid.ui.MyScaffold import com.bintianqi.owndroid.ui.RadioButtonItem -import com.bintianqi.owndroid.ui.SubPageItem import com.bintianqi.owndroid.ui.SwitchItem -import com.bintianqi.owndroid.ui.TopBar import com.bintianqi.owndroid.uriToStream import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -137,54 +126,9 @@ import java.util.TimeZone import java.util.concurrent.Executors import kotlin.math.pow -@Composable -fun SystemManage(navCtrl: NavHostController) { - val localNavCtrl = rememberNavController() - val backStackEntry by localNavCtrl.currentBackStackEntryAsState() - val scrollState = rememberScrollState() - Scaffold( - topBar = { - TopBar(backStackEntry,navCtrl,localNavCtrl) { - if(backStackEntry?.destination?.route=="Home"&&scrollState.maxValue > 100) { - Text( - text = stringResource(R.string.system), - modifier = Modifier.alpha((maxOf(scrollState.value-30,0)).toFloat()/80) - ) - } - } - } - ) { - NavHost( - navController = localNavCtrl, startDestination = "Home", - enterTransition = Animations.navHostEnterTransition, - exitTransition = Animations.navHostExitTransition, - popEnterTransition = Animations.navHostPopEnterTransition, - popExitTransition = Animations.navHostPopExitTransition, - modifier = Modifier.padding(top = it.calculateTopPadding()) - ) { - composable(route = "Home") { Home(localNavCtrl, scrollState) } - composable(route = "Switches") { Switches() } - composable(route = "Keyguard") { Keyguard() } - composable(route = "EditTime") { EditTime() } - composable(route = "EditTimeZone") { EditTimeZone() } - composable(route = "PermissionPolicy") { PermissionPolicy() } - composable(route = "MTEPolicy") { MTEPolicy() } - composable(route = "NearbyStreamingPolicy") { NearbyStreamingPolicy() } - composable(route = "LockTaskMode") { LockTaskMode(navCtrl) } - composable(route = "CaCert") { CaCert() } - composable(route = "SecurityLogs") { SecurityLogs() } - composable(route = "DisableAccountManagement") { DisableAccountManagement() } - composable(route = "SystemUpdatePolicy") { SysUpdatePolicy() } - composable(route = "InstallSystemUpdate") { InstallSystemUpdate() } - composable(route = "WipeData") { WipeData() } - composable(route = "FRP") { FactoryResetProtection() } - } - } -} - @SuppressLint("NewApi") @Composable -private fun Home(navCtrl: NavHostController, scrollState: ScrollState) { +fun SystemManage(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -194,60 +138,54 @@ private fun Home(navCtrl: NavHostController, scrollState: ScrollState) { val deviceOwner = context.isDeviceOwner val profileOwner = context.isProfileOwner var dialog by remember { mutableIntStateOf(0) } - Column(modifier = Modifier.fillMaxSize().verticalScroll(scrollState)) { - Text( - text = stringResource(R.string.system), - style = typography.headlineLarge, - modifier = Modifier.padding(top = 8.dp, bottom = 5.dp, start = 16.dp) - ) + MyScaffold(R.string.system, 0.dp, navCtrl) { if(deviceOwner || profileOwner) { - SubPageItem(R.string.options, "", R.drawable.tune_fill0) { navCtrl.navigate("Switches") } + FunctionItem(R.string.options, "", R.drawable.tune_fill0) { navCtrl.navigate("SystemOptions") } } - SubPageItem(R.string.keyguard, "", R.drawable.screen_lock_portrait_fill0) { navCtrl.navigate("Keyguard") } + FunctionItem(R.string.keyguard, "", R.drawable.screen_lock_portrait_fill0) { navCtrl.navigate("Keyguard") } if(VERSION.SDK_INT >= 24 && deviceOwner) { - SubPageItem(R.string.reboot, "", R.drawable.restart_alt_fill0) { dialog = 1 } + FunctionItem(R.string.reboot, "", R.drawable.restart_alt_fill0) { dialog = 1 } } if(deviceOwner && ((VERSION.SDK_INT >= 28 && dpm.isAffiliatedUser) || VERSION.SDK_INT >= 24)) { - SubPageItem(R.string.bug_report, "", R.drawable.bug_report_fill0) { dialog = 2 } + FunctionItem(R.string.bug_report, "", R.drawable.bug_report_fill0) { dialog = 2 } } if(VERSION.SDK_INT >= 28 && (deviceOwner || dpm.isOrgProfile(receiver))) { - SubPageItem(R.string.change_time, "", R.drawable.schedule_fill0) { navCtrl.navigate("EditTime") } - SubPageItem(R.string.change_timezone, "", R.drawable.schedule_fill0) { navCtrl.navigate("EditTimeZone") } + FunctionItem(R.string.change_time, "", R.drawable.schedule_fill0) { navCtrl.navigate("ChangeTime") } + FunctionItem(R.string.change_timezone, "", R.drawable.schedule_fill0) { navCtrl.navigate("ChangeTimeZone") } } if(VERSION.SDK_INT >= 23 && (deviceOwner || profileOwner)) { - SubPageItem(R.string.permission_policy, "", R.drawable.key_fill0) { navCtrl.navigate("PermissionPolicy") } + FunctionItem(R.string.permission_policy, "", R.drawable.key_fill0) { navCtrl.navigate("PermissionPolicy") } } if(VERSION.SDK_INT >= 34 && deviceOwner) { - SubPageItem(R.string.mte_policy, "", R.drawable.memory_fill0) { navCtrl.navigate("MTEPolicy") } + FunctionItem(R.string.mte_policy, "", R.drawable.memory_fill0) { navCtrl.navigate("MTEPolicy") } } if(VERSION.SDK_INT >= 31 && (deviceOwner || profileOwner)) { - SubPageItem(R.string.nearby_streaming_policy, "", R.drawable.share_fill0) { navCtrl.navigate("NearbyStreamingPolicy") } + FunctionItem(R.string.nearby_streaming_policy, "", R.drawable.share_fill0) { navCtrl.navigate("NearbyStreamingPolicy") } } if(VERSION.SDK_INT >= 28 && deviceOwner) { - SubPageItem(R.string.lock_task_mode, "", R.drawable.lock_fill0) { navCtrl.navigate("LockTaskMode") } + FunctionItem(R.string.lock_task_mode, "", R.drawable.lock_fill0) { navCtrl.navigate("LockTaskMode") } } if(deviceOwner || profileOwner) { - SubPageItem(R.string.ca_cert, "", R.drawable.license_fill0) { navCtrl.navigate("CaCert") } + FunctionItem(R.string.ca_cert, "", R.drawable.license_fill0) { navCtrl.navigate("CACert") } } if(VERSION.SDK_INT >= 26 && !dhizuku && (deviceOwner || dpm.isOrgProfile(receiver))) { - SubPageItem(R.string.security_logs, "", R.drawable.description_fill0) { navCtrl.navigate("SecurityLogs") } + FunctionItem(R.string.security_logging, "", R.drawable.description_fill0) { navCtrl.navigate("SecurityLogging") } } if(deviceOwner || profileOwner) { - SubPageItem(R.string.disable_account_management, "", R.drawable.account_circle_fill0) { navCtrl.navigate("DisableAccountManagement") } + FunctionItem(R.string.disable_account_management, "", R.drawable.account_circle_fill0) { navCtrl.navigate("DisableAccountManagement") } } if(VERSION.SDK_INT >= 23 && (deviceOwner || dpm.isOrgProfile(receiver))) { - SubPageItem(R.string.system_update_policy, "", R.drawable.system_update_fill0) { navCtrl.navigate("SystemUpdatePolicy") } + FunctionItem(R.string.system_update_policy, "", R.drawable.system_update_fill0) { navCtrl.navigate("SystemUpdatePolicy") } } if(VERSION.SDK_INT >= 29 && (deviceOwner || dpm.isOrgProfile(receiver))) { - SubPageItem(R.string.install_system_update, "", R.drawable.system_update_fill0) { navCtrl.navigate("InstallSystemUpdate") } + FunctionItem(R.string.install_system_update, "", R.drawable.system_update_fill0) { navCtrl.navigate("InstallSystemUpdate") } } if(VERSION.SDK_INT >= 30 && (deviceOwner || dpm.isOrgProfile(receiver))) { - SubPageItem(R.string.frp_policy, "", R.drawable.device_reset_fill0) { navCtrl.navigate("FRP") } + FunctionItem(R.string.frp_policy, "", R.drawable.device_reset_fill0) { navCtrl.navigate("FRPPolicy") } } if(dangerousFeatures && context.isDeviceAdmin && !(VERSION.SDK_INT >= 24 && profileOwner && dpm.isManagedProfile(receiver))) { - SubPageItem(R.string.wipe_data, "", R.drawable.device_reset_fill0) { navCtrl.navigate("WipeData") } + FunctionItem(R.string.wipe_data, "", R.drawable.device_reset_fill0) { navCtrl.navigate("WipeData") } } - Spacer(Modifier.padding(vertical = 30.dp)) LaunchedEffect(Unit) { fileUriFlow.value = Uri.parse("") } } if(dialog != 0) AlertDialog( @@ -279,7 +217,7 @@ private fun Home(navCtrl: NavHostController, scrollState: ScrollState) { } @Composable -private fun Switches() { +fun SystemOptions(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -287,8 +225,7 @@ private fun Switches() { val profileOwner = context.isProfileOwner val um = context.getSystemService(Context.USER_SERVICE) as UserManager var dialog by remember { mutableIntStateOf(0) } - Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.options, 0.dp, navCtrl) { if(deviceOwner || profileOwner) { SwitchItem(R.string.disable_cam,"", R.drawable.photo_camera_fill0, { dpm.getCameraDisabled(null) }, { dpm.setCameraDisabled(receiver,it) } @@ -344,7 +281,6 @@ private fun Switches() { { dpm.isUsbDataSignalingEnabled = !it }, ) } - Spacer(Modifier.padding(vertical = 30.dp)) } if(dialog != 0) AlertDialog( text = { @@ -364,17 +300,14 @@ private fun Switches() { } @Composable -private fun Keyguard() { +fun Keyguard(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val deviceOwner = context.isDeviceOwner val profileOwner = context.isProfileOwner - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) + MyScaffold(R.string.keyguard, 8.dp, navCtrl) { if(VERSION.SDK_INT >= 23) { - Text(text = stringResource(R.string.keyguard), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) Row( horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth() @@ -401,7 +334,7 @@ private fun Keyguard() { InfoCard(R.string.info_disable_keyguard) Spacer(Modifier.padding(vertical = 12.dp)) } - Text(text = stringResource(R.string.lock_now), style = typography.headlineLarge) + if(VERSION.SDK_INT >= 23) Text(text = stringResource(R.string.lock_now), style = typography.headlineLarge) Spacer(Modifier.padding(vertical = 2.dp)) var flag by remember { mutableIntStateOf(0) } if(VERSION.SDK_INT >= 26 && profileOwner && dpm.isManagedProfile(receiver)) { @@ -424,22 +357,18 @@ private fun Keyguard() { if(VERSION.SDK_INT >= 26 && profileOwner && dpm.isManagedProfile(receiver)) { InfoCard(R.string.info_evict_credential_encryption_key) } - Spacer(Modifier.padding(vertical = 30.dp)) } } @SuppressLint("NewApi") @Composable -private fun EditTime() { +fun ChangeTime(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current var inputTime by remember { mutableStateOf("") } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.change_time), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.change_time, 8.dp, navCtrl) { OutlinedTextField( value = inputTime, label = { Text(stringResource(R.string.time_unit_ms)) }, @@ -459,22 +388,17 @@ private fun EditTime() { } } } + @SuppressLint("NewApi") @Composable -private fun EditTimeZone() { +fun ChangeTimeZone(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val focusMgr = LocalFocusManager.current val receiver = context.getReceiver() var inputTimezone by remember { mutableStateOf("") } var dialog by remember { mutableStateOf(false) } - Column( - modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.change_timezone), style = typography.headlineLarge, modifier = Modifier.align(Alignment.Start)) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.change_timezone, 8.dp, navCtrl) { OutlinedTextField( value = inputTimezone, label = { Text(stringResource(R.string.timezone_id)) }, @@ -533,15 +457,12 @@ private fun EditTimeZone() { @SuppressLint("NewApi") @Composable -private fun PermissionPolicy() { +fun PermissionPolicy(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - var selectedPolicy by remember { mutableIntStateOf(dpm.getPermissionPolicy(receiver)) } - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.permission_policy), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) + var selectedPolicy by remember { mutableIntStateOf(dpm.getPermissionPolicy(receiver)) } + MyScaffold(R.string.permission_policy, 8.dp, navCtrl) { RadioButtonItem(R.string.default_stringres, selectedPolicy == PERMISSION_POLICY_PROMPT, { selectedPolicy = PERMISSION_POLICY_PROMPT }) RadioButtonItem(R.string.auto_grant, selectedPolicy == PERMISSION_POLICY_AUTO_GRANT, { selectedPolicy = PERMISSION_POLICY_AUTO_GRANT }) RadioButtonItem(R.string.auto_deny, selectedPolicy == PERMISSION_POLICY_AUTO_DENY, { selectedPolicy = PERMISSION_POLICY_AUTO_DENY }) @@ -561,14 +482,11 @@ private fun PermissionPolicy() { @SuppressLint("NewApi") @Composable -private fun MTEPolicy() { +fun MTEPolicy(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.mte_policy), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) - var selectedMtePolicy by remember { mutableIntStateOf(dpm.mtePolicy) } + var selectedMtePolicy by remember { mutableIntStateOf(dpm.mtePolicy) } + MyScaffold(R.string.mte_policy, 8.dp, navCtrl) { RadioButtonItem( R.string.decide_by_user, selectedMtePolicy == MTE_NOT_CONTROLLED_BY_POLICY, @@ -599,17 +517,16 @@ private fun MTEPolicy() { Text(stringResource(R.string.apply)) } InfoCard(R.string.info_mte_policy) - Spacer(Modifier.padding(vertical = 30.dp)) } } @SuppressLint("NewApi") @Composable -private fun NearbyStreamingPolicy() { +fun NearbyStreamingPolicy(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - var appPolicy by remember { mutableIntStateOf(dpm.nearbyAppStreamingPolicy) } + var appPolicy by remember { mutableIntStateOf(dpm.nearbyAppStreamingPolicy) } + MyScaffold(R.string.nearby_streaming_policy, 8.dp, navCtrl) { Spacer(Modifier.padding(vertical = 10.dp)) Text(text = stringResource(R.string.nearby_app_streaming), style = typography.titleLarge) Spacer(Modifier.padding(vertical = 3.dp)) @@ -679,20 +596,19 @@ private fun NearbyStreamingPolicy() { Text(stringResource(R.string.apply)) } InfoCard(R.string.info_nearby_notification_streaming_policy) - Spacer(Modifier.padding(vertical = 30.dp)) } } @SuppressLint("NewApi") @Composable -private fun LockTaskMode(navCtrl: NavHostController) { +fun LockTaskMode(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current val coroutine = rememberCoroutineScope() var appSelectorRequest by rememberSaveable { mutableIntStateOf(0) } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { + MyScaffold(R.string.lock_task_mode, 8.dp, navCtrl, false) { val lockTaskFeatures = remember { mutableStateListOf() } var custom by rememberSaveable { mutableStateOf(false) } val refreshFeature = { @@ -710,7 +626,7 @@ private fun LockTaskMode(navCtrl: NavHostController) { } if(calculate - 1 >= 0) { lockTaskFeatures += 1 } custom = true - }else{ + } else { custom = false } } @@ -803,7 +719,7 @@ private fun LockTaskMode(navCtrl: NavHostController) { keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }), trailingIcon = { - Icon(painter = painterResource(R.drawable.checklist_fill0), contentDescription = null, + Icon(painter = painterResource(R.drawable.list_fill0), contentDescription = null, modifier = Modifier .clip(RoundedCornerShape(50)) .clickable(onClick = { @@ -865,7 +781,7 @@ private fun LockTaskMode(navCtrl: NavHostController) { keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }), trailingIcon = { - Icon(painter = painterResource(R.drawable.checklist_fill0), contentDescription = null, + Icon(painter = painterResource(R.drawable.list_fill0), contentDescription = null, modifier = Modifier .clip(RoundedCornerShape(50)) .clickable(onClick = { @@ -915,12 +831,11 @@ private fun LockTaskMode(navCtrl: NavHostController) { Text(stringResource(R.string.start)) } InfoCard(R.string.info_start_lock_task_mode) - Spacer(Modifier.padding(vertical = 30.dp)) } } @Composable -private fun CaCert() { +fun CACert(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -941,10 +856,7 @@ private fun CaCert() { exist = dpm.hasCaCertInstalled(receiver, caCertByteArray) } } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.ca_cert), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.ca_cert, 8.dp, navCtrl) { AnimatedVisibility(uriPath != "") { Text(text = uriPath) } @@ -1004,7 +916,7 @@ private fun CaCert() { @SuppressLint("NewApi") @Composable -private fun SecurityLogs() { +fun SecurityLogging(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -1013,10 +925,7 @@ private fun SecurityLogs() { LaunchedEffect(Unit) { fileSize = logFile.length() } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.security_logs), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.security_logging, 8.dp, navCtrl) { SwitchItem(R.string.enable, "", null, { dpm.isSecurityLoggingEnabled(receiver) }, { dpm.setSecurityLoggingEnabled(receiver, it) }, padding = false) Text(stringResource(R.string.log_file_size_is, formatFileSize(fileSize))) Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { @@ -1087,21 +996,18 @@ private fun SecurityLogs() { } @Composable -private fun DisableAccountManagement() { +fun DisableAccountManagement(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current - Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 8.dp)) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.disable_account_management), style = typography.headlineLarge) + MyScaffold(R.string.disable_account_management, 8.dp, navCtrl) { val list = remember { mutableStateListOf() } fun refreshList() { list.clear() dpm.accountTypesWithManagementDisabled?.forEach { list += it } } LaunchedEffect(Unit) { refreshList() } - Spacer(Modifier.padding(vertical = 5.dp)) Column(modifier = Modifier.animateContentSize()) { if(list.isEmpty()) Text(stringResource(R.string.none)) for(i in list) { @@ -1139,7 +1045,7 @@ private fun DisableAccountManagement() { @SuppressLint("NewApi") @Composable -fun FactoryResetProtection() { +fun FRPPolicy(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val focusMgr = LocalFocusManager.current @@ -1165,12 +1071,7 @@ fun FactoryResetProtection() { } } } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.frp_policy), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 3.dp)) - Text(stringResource(R.string.factory_reset_protection_policy)) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.frp_policy, 8.dp, navCtrl) { Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(horizontal = 6.dp, vertical = 8.dp) @@ -1231,13 +1132,12 @@ fun FactoryResetProtection() { if(unsupported) Text(stringResource(R.string.frp_policy_not_supported)) Spacer(Modifier.padding(vertical = 6.dp)) InfoCard(R.string.info_frp_policy) - Spacer(Modifier.padding(vertical = 30.dp)) } } @SuppressLint("NewApi") @Composable -private fun WipeData() { +fun WipeData(navCtrl: NavHostController) { val context = LocalContext.current val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager val dpm = context.getDPM() @@ -1249,14 +1149,7 @@ private fun WipeData() { var euicc by remember { mutableStateOf(false) } var silent by remember { mutableStateOf(false) } var reason by remember { mutableStateOf("") } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text( - text = stringResource(R.string.wipe_data), - style = typography.headlineLarge, - modifier = Modifier.padding(6.dp),color = colorScheme.error - ) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.wipe_data, 8.dp, navCtrl) { CheckBoxItem( R.string.wipe_external_storage, externalStorage, { externalStorage = it } @@ -1302,7 +1195,6 @@ private fun WipeData() { Text("WipeDevice") } } - Spacer(Modifier.padding(vertical = 30.dp)) } if(warning) { LaunchedEffect(Unit) { silent = reason == "" } @@ -1360,18 +1252,15 @@ private fun WipeData() { } @Composable -private fun SysUpdatePolicy() { +fun SystemUpdatePolicy(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) + MyScaffold(R.string.system_update_policy, 8.dp, navCtrl) { if(VERSION.SDK_INT >= 23) { Column { var selectedPolicy by remember { mutableStateOf(dpm.systemUpdatePolicy?.policyType) } - Text(text = stringResource(R.string.system_update_policy), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) RadioButtonItem( R.string.system_update_policy_automatic, selectedPolicy == TYPE_INSTALL_AUTOMATIC, { selectedPolicy = TYPE_INSTALL_AUTOMATIC } @@ -1447,13 +1336,12 @@ private fun SysUpdatePolicy() { } } } - Spacer(Modifier.padding(vertical = 30.dp)) } } @SuppressLint("NewApi") @Composable -fun InstallSystemUpdate() { +fun InstallSystemUpdate(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -1472,10 +1360,7 @@ fun InstallSystemUpdate() { } } val uri by fileUriFlow.collectAsState() - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.install_system_update), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.install_system_update, 8.dp, navCtrl) { Button( onClick = { val intent = Intent(Intent.ACTION_GET_CONTENT) @@ -1508,10 +1393,7 @@ fun InstallSystemUpdate() { } } Spacer(Modifier.padding(vertical = 10.dp)) - Information { - Text(stringResource(R.string.auto_reboot_after_install_succeed)) - } - Spacer(Modifier.padding(vertical = 30.dp)) + InfoCard(R.string.auto_reboot_after_install_succeed) } } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt index 57a50bc..b50a866 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt @@ -6,34 +6,20 @@ import android.os.UserManager import android.widget.Toast import androidx.annotation.DrawableRes import androidx.annotation.StringRes -import androidx.compose.foundation.ScrollState import androidx.compose.foundation.layout.Box -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.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.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController import com.bintianqi.owndroid.R -import com.bintianqi.owndroid.ui.Animations -import com.bintianqi.owndroid.ui.SubPageItem +import com.bintianqi.owndroid.ui.FunctionItem +import com.bintianqi.owndroid.ui.MyScaffold import com.bintianqi.owndroid.ui.SwitchItem -import com.bintianqi.owndroid.ui.TopBar data class Restriction( val id: String, @@ -42,92 +28,29 @@ data class Restriction( ) @Composable -fun UserRestriction(navCtrl: NavHostController) { - val localNavCtrl = rememberNavController() - val backStackEntry by localNavCtrl.currentBackStackEntryAsState() - val scrollState = rememberScrollState() - /*val titleMap = mapOf( - "Internet" to R.string.network_internet, - "Connectivity" to R.string.more_connectivity, - "Users" to R.string.users, - "Media" to R.string.media, - "Applications" to R.string.applications, - "Other" to R.string.other - )*/ - Scaffold( - topBar = { - /*TopAppBar( - title = {Text(text = stringResource(titleMap[backStackEntry?.destination?.route]?:R.string.user_restrict))}, - navigationIcon = {NavIcon{if(backStackEntry?.destination?.route=="Home"){navCtrl.navigateUp()}else{localNavCtrl.navigateUp()}}}, - colors = TopAppBarDefaults.topAppBarColors(containerColor = colorScheme.surfaceVariant) - )*/ - TopBar(backStackEntry,navCtrl,localNavCtrl){ - if(backStackEntry?.destination?.route=="Home"&&scrollState.maxValue>80){ - Text( - text = stringResource(R.string.user_restrict), - modifier = Modifier.alpha((maxOf(scrollState.value-30, 0)).toFloat() / 80) - ) - } - } - } - ) { - NavHost( - navController = localNavCtrl, startDestination = "Home", - enterTransition = Animations.navHostEnterTransition, - exitTransition = Animations.navHostExitTransition, - popEnterTransition = Animations.navHostPopEnterTransition, - popExitTransition = Animations.navHostPopExitTransition, - modifier = Modifier.padding(top = it.calculateTopPadding()) - ) { - composable(route = "Home") { Home(localNavCtrl, scrollState) } - composable(route = "Internet") { UserRestrictionPage(RestrictionData.internet()) } - composable(route = "Connectivity") { UserRestrictionPage(RestrictionData.connectivity())} - composable(route = "Applications") { UserRestrictionPage(RestrictionData.connectivity()) } - composable(route = "Users") { UserRestrictionPage(RestrictionData.user()) } - composable(route = "Media") { UserRestrictionPage(RestrictionData.media()) } - composable(route = "Other") { UserRestrictionPage(RestrictionData.other()) } - } - } -} - -@Composable -private fun Home(navCtrl:NavHostController, scrollState: ScrollState) { +fun UserRestriction(navCtrl:NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() - Column(modifier = Modifier.fillMaxSize().verticalScroll(scrollState)) { - Text( - text = stringResource(R.string.user_restrict), - style = typography.headlineLarge, - modifier = Modifier.padding(top = 8.dp, bottom = 7.dp, start = 16.dp) - ) + MyScaffold(R.string.user_restriction, 0.dp, navCtrl) { Text(text = stringResource(R.string.switch_to_disable_feature), modifier = Modifier.padding(start = 16.dp)) if(context.isProfileOwner) { Text(text = stringResource(R.string.profile_owner_is_restricted), modifier = Modifier.padding(start = 16.dp)) } if(context.isProfileOwner && (VERSION.SDK_INT < 24 || (VERSION.SDK_INT >= 24 && dpm.isManagedProfile(receiver)))) { Text(text = stringResource(R.string.some_features_invalid_in_work_profile), modifier = Modifier.padding(start = 16.dp)) } Spacer(Modifier.padding(vertical = 2.dp)) - SubPageItem(R.string.network_internet, "", R.drawable.wifi_fill0) { navCtrl.navigate("Internet") } - SubPageItem(R.string.more_connectivity, "", R.drawable.devices_other_fill0) { navCtrl.navigate("Connectivity") } - SubPageItem(R.string.applications, "", R.drawable.apps_fill0) { navCtrl.navigate("Applications") } - SubPageItem(R.string.users, "", R.drawable.account_circle_fill0) { navCtrl.navigate("Users") } - SubPageItem(R.string.media, "", R.drawable.volume_up_fill0) { navCtrl.navigate("Media") } - SubPageItem(R.string.other, "", R.drawable.more_horiz_fill0) { navCtrl.navigate("Other") } - Spacer(Modifier.padding(vertical = 30.dp)) - } -} - -@Composable -private fun UserRestrictionPage(restrictions: List) { - Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())) { - restrictions.forEach { UserRestrictionItem(it) } - Spacer(Modifier.padding(vertical = 30.dp)) + FunctionItem(R.string.network_and_internet, "", R.drawable.wifi_fill0) { navCtrl.navigate("UR-Internet") } + FunctionItem(R.string.connectivity, "", R.drawable.devices_other_fill0) { navCtrl.navigate("UR-Connectivity") } + FunctionItem(R.string.applications, "", R.drawable.apps_fill0) { navCtrl.navigate("UR-Applications") } + FunctionItem(R.string.users, "", R.drawable.account_circle_fill0) { navCtrl.navigate("UR-Users") } + FunctionItem(R.string.media, "", R.drawable.volume_up_fill0) { navCtrl.navigate("UR-Media") } + FunctionItem(R.string.other, "", R.drawable.more_horiz_fill0) { navCtrl.navigate("UR-Other") } } } @SuppressLint("NewApi") @Composable -private fun UserRestrictionItem(restriction: Restriction) { +fun UserRestrictionItem(restriction: Restriction) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -153,7 +76,7 @@ private fun UserRestrictionItem(restriction: Restriction) { } object RestrictionData { - fun internet(): List { + val internet: List get() { val list = mutableListOf() list += Restriction(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS, R.string.config_mobile_network, R.drawable.signal_cellular_alt_fill0) list += Restriction(UserManager.DISALLOW_CONFIG_WIFI, R.string.config_wifi, R.drawable.wifi_fill0) @@ -179,7 +102,7 @@ object RestrictionData { list += Restriction(UserManager.DISALLOW_OUTGOING_CALLS, R.string.outgoing_calls, R.drawable.phone_forwarded_fill0) return list } - fun connectivity(): List { + val connectivity: List get() { val list = mutableListOf() if(VERSION.SDK_INT>=26) { list += Restriction(UserManager.DISALLOW_BLUETOOTH, R.string.bluetooth, R.drawable.bluetooth_fill0) @@ -193,7 +116,7 @@ object RestrictionData { if(VERSION.SDK_INT>=28) list += Restriction(UserManager.DISALLOW_PRINTING, R.string.printing, R.drawable.print_fill0) return list } - fun application(): List { + val applications: List get() { val list = mutableListOf() list += Restriction(UserManager.DISALLOW_INSTALL_APPS, R.string.install_app, R.drawable.android_fill0) if(VERSION.SDK_INT>=29) list += Restriction(UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES_GLOBALLY, R.string.install_unknown_src_globally, R.drawable.android_fill0) @@ -203,7 +126,7 @@ object RestrictionData { if(VERSION.SDK_INT>=34) list += Restriction(UserManager.DISALLOW_CONFIG_DEFAULT_APPS, R.string.config_default_apps, R.drawable.apps_fill0) return list } - fun media(): List { + val media: List get() { val list = mutableListOf() if(VERSION.SDK_INT>=28) { list += Restriction(UserManager.DISALLOW_CONFIG_BRIGHTNESS, R.string.config_brightness, R.drawable.brightness_5_fill0) @@ -218,7 +141,7 @@ object RestrictionData { } return list } - fun user(): List { + val users: List get() { val list = mutableListOf() list += Restriction(UserManager.DISALLOW_ADD_USER, R.string.add_user, R.drawable.account_circle_fill0) list += Restriction(UserManager.DISALLOW_REMOVE_USER, R.string.remove_user, R.drawable.account_circle_fill0) @@ -231,7 +154,7 @@ object RestrictionData { } return list } - fun other(): List { + val other: List get() { val list = mutableListOf() if(VERSION.SDK_INT>=26) list += Restriction(UserManager.DISALLOW_AUTOFILL, R.string.autofill, R.drawable.password_fill0) list += Restriction(UserManager.DISALLOW_CONFIG_CREDENTIALS, R.string.config_credentials, R.drawable.android_fill0) @@ -254,14 +177,7 @@ object RestrictionData { list += Restriction(UserManager.DISALLOW_DEBUGGING_FEATURES, R.string.debug_features, R.drawable.adb_fill0) return list } - fun getAllRestrictions(): List { - val result = mutableListOf() - internet().forEach { result.add(it.id) } - connectivity().forEach { result.add(it.id) } - media().forEach { result.add(it.id) } - application().forEach { result.add(it.id) } - user().forEach { result.add(it.id) } - other().forEach { result.add(it.id) } - return result - } + fun getAllRestrictions(): List = + internet + connectivity + media + applications + users + other + } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt index 6d849c3..995ad65 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt @@ -18,20 +18,16 @@ import androidx.annotation.StringRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.foundation.Image -import androidx.compose.foundation.ScrollState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material3.AlertDialog @@ -39,9 +35,7 @@ import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -55,7 +49,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.platform.LocalContext @@ -65,106 +58,56 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.currentBackStackEntryAsState -import androidx.navigation.compose.rememberNavController import com.bintianqi.owndroid.R import com.bintianqi.owndroid.fileUriFlow import com.bintianqi.owndroid.getFile import com.bintianqi.owndroid.parseTimestamp import com.bintianqi.owndroid.toggle -import com.bintianqi.owndroid.ui.Animations import com.bintianqi.owndroid.ui.CardItem import com.bintianqi.owndroid.ui.CheckBoxItem +import com.bintianqi.owndroid.ui.FunctionItem import com.bintianqi.owndroid.ui.InfoCard import com.bintianqi.owndroid.ui.ListItem -import com.bintianqi.owndroid.ui.SubPageItem +import com.bintianqi.owndroid.ui.MyScaffold import com.bintianqi.owndroid.ui.SwitchItem -import com.bintianqi.owndroid.ui.TopBar import com.bintianqi.owndroid.uriToStream import com.bintianqi.owndroid.yesOrNo @Composable -fun UserManage(navCtrl: NavHostController) { - val localNavCtrl = rememberNavController() - val backStackEntry by localNavCtrl.currentBackStackEntryAsState() - val scrollState = rememberScrollState() - Scaffold( - topBar = { - TopBar(backStackEntry, navCtrl, localNavCtrl) { - if(backStackEntry?.destination?.route == "Home" && scrollState.maxValue > 100) { - Text( - text = stringResource(R.string.users), - modifier = Modifier.alpha((maxOf(scrollState.value-30, 0)).toFloat() / 80) - ) - } - } - } - ) { - NavHost( - navController = localNavCtrl, startDestination = "Home", - enterTransition = Animations.navHostEnterTransition, - exitTransition = Animations.navHostExitTransition, - popEnterTransition = Animations.navHostPopEnterTransition, - popExitTransition = Animations.navHostPopExitTransition, - modifier = Modifier.padding(top = it.calculateTopPadding()) - ) { - composable(route = "Home") { Home(localNavCtrl, scrollState) } - composable(route = "UserInfo") { CurrentUserInfo() } - composable(route = "Options") { Options() } - composable(route = "UserOperation") { UserOperation() } - composable(route = "CreateUser") { CreateUser() } - composable(route = "EditUsername") { Username() } - composable(route = "ChangeUserIcon") { UserIcon() } - composable(route = "UserSessionMessage") { UserSessionMessage() } - composable(route = "AffiliationID") { AffiliationID() } - } - } -} - -@Composable -private fun Home(navCtrl: NavHostController,scrollState: ScrollState) { +fun Users(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val deviceOwner = context.isDeviceOwner val profileOwner = context.isProfileOwner - //var logoutDialog by remember { mutableStateOf(false) } var dialog by remember { mutableIntStateOf(0) } - Column(modifier = Modifier.fillMaxSize().verticalScroll(scrollState)) { - Text( - text = stringResource(R.string.users), - style = typography.headlineLarge, - modifier = Modifier.padding(top = 8.dp, bottom = 5.dp, start = 16.dp) - ) - SubPageItem(R.string.user_info, "", R.drawable.person_fill0) { navCtrl.navigate("UserInfo") } + MyScaffold(R.string.users, 0.dp, navCtrl) { + FunctionItem(R.string.user_info, "", R.drawable.person_fill0) { navCtrl.navigate("UserInfo") } if(deviceOwner && VERSION.SDK_INT >= 28) { - SubPageItem(R.string.secondary_users, "", R.drawable.list_fill0) { dialog = 1 } - SubPageItem(R.string.options, "", R.drawable.tune_fill0) { navCtrl.navigate("Options") } + FunctionItem(R.string.secondary_users, "", R.drawable.list_fill0) { dialog = 1 } + FunctionItem(R.string.options, "", R.drawable.tune_fill0) { navCtrl.navigate("UserOptions") } } if(deviceOwner) { - SubPageItem(R.string.user_operation, "", R.drawable.sync_alt_fill0) { navCtrl.navigate("UserOperation") } + FunctionItem(R.string.user_operation, "", R.drawable.sync_alt_fill0) { navCtrl.navigate("UserOperation") } } if(VERSION.SDK_INT >= 24 && deviceOwner) { - SubPageItem(R.string.create_user, "", R.drawable.person_add_fill0) { navCtrl.navigate("CreateUser") } + FunctionItem(R.string.create_user, "", R.drawable.person_add_fill0) { navCtrl.navigate("CreateUser") } } if(VERSION.SDK_INT >= 28 && profileOwner && dpm.isAffiliatedUser) { - SubPageItem(R.string.logout_current_user, "", R.drawable.logout_fill0) { dialog = 2 } + FunctionItem(R.string.logout_current_user, "", R.drawable.logout_fill0) { dialog = 2 } } if(deviceOwner || profileOwner) { - SubPageItem(R.string.change_username, "", R.drawable.edit_fill0) { navCtrl.navigate("EditUsername") } + FunctionItem(R.string.change_username, "", R.drawable.edit_fill0) { navCtrl.navigate("ChangeUsername") } } if(VERSION.SDK_INT >= 23 && (deviceOwner || profileOwner)) { - SubPageItem(R.string.change_user_icon, "", R.drawable.account_circle_fill0) { navCtrl.navigate("ChangeUserIcon") } + FunctionItem(R.string.change_user_icon, "", R.drawable.account_circle_fill0) { navCtrl.navigate("ChangeUserIcon") } } if(VERSION.SDK_INT >= 28 && deviceOwner) { - SubPageItem(R.string.user_session_msg, "", R.drawable.notifications_fill0) { navCtrl.navigate("UserSessionMessage") } + FunctionItem(R.string.user_session_msg, "", R.drawable.notifications_fill0) { navCtrl.navigate("UserSessionMessage") } } if(VERSION.SDK_INT >= 26 && (deviceOwner || profileOwner)) { - SubPageItem(R.string.affiliation_id, "", R.drawable.id_card_fill0) { navCtrl.navigate("AffiliationID") } + FunctionItem(R.string.affiliation_id, "", R.drawable.id_card_fill0) { navCtrl.navigate("AffiliationID") } } - Spacer(Modifier.padding(vertical = 30.dp)) LaunchedEffect(Unit) { fileUriFlow.value = Uri.parse("") } } if(dialog != 0 && VERSION.SDK_INT >= 28) AlertDialog( @@ -208,11 +151,11 @@ private fun Home(navCtrl: NavHostController,scrollState: ScrollState) { } @Composable -private fun Options() { +fun UserOptions(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() - Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())) { + MyScaffold(R.string.options, 0.dp, navCtrl) { if(VERSION.SDK_INT >= 28) { SwitchItem(R.string.enable_logout, "", null, { dpm.isLogoutEnabled }, { dpm.setLogoutEnabled(receiver, it) }) } @@ -220,17 +163,14 @@ private fun Options() { } @Composable -private fun CurrentUserInfo() { +fun CurrentUserInfo(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager val user = Process.myUserHandle() var infoDialog by remember { mutableIntStateOf(0) } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.user_info), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.user_info, 8.dp, navCtrl) { if(VERSION.SDK_INT >= 24) CardItem(R.string.support_multiuser, UserManager.supportsMultipleUsers().yesOrNo) if(VERSION.SDK_INT >= 31) CardItem(R.string.headless_system_user_mode, UserManager.isHeadlessSystemUserMode().yesOrNo) { infoDialog = 1 } Spacer(Modifier.padding(vertical = 8.dp)) @@ -247,7 +187,6 @@ private fun CurrentUserInfo() { } CardItem(R.string.user_id, (Binder.getCallingUid() / 100000).toString()) CardItem(R.string.user_serial_number, userManager.getSerialNumberForUser(Process.myUserHandle()).toString()) - Spacer(Modifier.padding(vertical = 30.dp)) } if(infoDialog != 0) AlertDialog( text = { Text(stringResource(R.string.info_headless_system_user_mode)) }, @@ -261,7 +200,7 @@ private fun CurrentUserInfo() { } @Composable -private fun UserOperation() { +fun UserOperation(navCtrl: NavHostController) { val context = LocalContext.current val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager val dpm = context.getDPM() @@ -287,10 +226,7 @@ private fun UserOperation() { } catch(_: Exception) { false } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.user_operation), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.user_operation, 8.dp, navCtrl) { OutlinedTextField( value = idInput, onValueChange = { @@ -365,13 +301,12 @@ private fun UserOperation() { Text(stringResource(R.string.delete)) } InfoCard(R.string.info_user_operation) - Spacer(Modifier.padding(vertical = 30.dp)) } } @SuppressLint("NewApi") @Composable -private fun CreateUser() { +fun CreateUser(navCtrl: NavHostController) { val context = LocalContext.current val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager val dpm = context.getDPM() @@ -379,10 +314,7 @@ private fun CreateUser() { val focusMgr = LocalFocusManager.current var userName by remember { mutableStateOf("") } val flags = remember { mutableStateListOf() } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.create_user), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.create_user, 8.dp, navCtrl) { OutlinedTextField( value = userName, onValueChange = { userName= it }, @@ -423,13 +355,12 @@ private fun CreateUser() { } Spacer(Modifier.padding(vertical = 5.dp)) if(newUserHandle != null) { Text(text = stringResource(R.string.serial_number_of_new_user_is, userManager.getSerialNumberForUser(newUserHandle))) } - Spacer(Modifier.padding(vertical = 30.dp)) } } @SuppressLint("NewApi") @Composable -private fun AffiliationID() { +fun AffiliationID(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -441,10 +372,7 @@ private fun AffiliationID() { list.addAll(dpm.getAffiliationIds(receiver)) } LaunchedEffect(Unit) { refreshIds() } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.affiliation_id), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.affiliation_id, 8.dp, navCtrl) { Column(modifier = Modifier.animateContentSize()) { if(list.isEmpty()) Text(stringResource(R.string.none)) for(i in list) { @@ -483,21 +411,17 @@ private fun AffiliationID() { Text(stringResource(R.string.apply)) } InfoCard(R.string.info_affiliated_id) - Spacer(Modifier.padding(vertical = 30.dp)) } } @Composable -private fun Username() { +fun ChangeUsername(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current var inputUsername by remember { mutableStateOf("") } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.change_username), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.change_username, 8.dp, navCtrl) { OutlinedTextField( value = inputUsername, onValueChange = { inputUsername= it }, @@ -527,7 +451,7 @@ private fun Username() { @SuppressLint("NewApi") @Composable -private fun UserSessionMessage() { +fun UserSessionMessage(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -539,10 +463,7 @@ private fun UserSessionMessage() { end = dpm.getEndUserSessionMessage(receiver)?.toString() ?: "" } LaunchedEffect(Unit) { refreshMsg() } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.user_session_msg), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.user_session_msg, 8.dp, navCtrl) { OutlinedTextField( value = start, onValueChange = { start= it }, @@ -604,13 +525,12 @@ private fun UserSessionMessage() { Text(stringResource(R.string.reset)) } } - Spacer(Modifier.padding(vertical = 30.dp)) } } @SuppressLint("NewApi") @Composable -private fun UserIcon() { +fun ChangeUserIcon(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -623,10 +543,7 @@ private fun UserIcon() { bitmap = BitmapFactory.decodeStream(stream) } } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.change_user_icon), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.change_user_icon, 8.dp, navCtrl) { CheckBoxItem(R.string.file_picker_instead_gallery, getContent, { getContent = it }) Spacer(Modifier.padding(vertical = 5.dp)) Button( diff --git a/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt b/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt index 5a7385a..c1e8582 100644 --- a/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt +++ b/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt @@ -8,8 +8,10 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Info import androidx.compose.material3.* @@ -18,11 +20,13 @@ import androidx.compose.material3.MaterialTheme.typography import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.navigation.NavBackStackEntry import androidx.navigation.NavHostController @@ -33,9 +37,9 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch @Composable -fun SubPageItem( +fun FunctionItem( @StringRes title: Int, - desc:String, + desc: String, @DrawableRes icon: Int? = null, operation: () -> Unit ) { @@ -48,8 +52,7 @@ fun SubPageItem( verticalAlignment = Alignment.CenterVertically ) { if(icon != null) Icon( - painter = painterResource(icon), - contentDescription = null, + painter = painterResource(icon), contentDescription = null, modifier = Modifier.padding(top = 1.dp, end = 20.dp).offset(x = (-2).dp) ) Column { @@ -194,25 +197,6 @@ fun SwitchItem( } } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun TopBar( - backStackEntry: NavBackStackEntry?, - navCtrl: NavHostController, - localNavCtrl: NavHostController, - title: @Composable ()->Unit = {} -) { - TopAppBar( - title = title, - navigationIcon = { - NavIcon{ - if(backStackEntry?.destination?.route == "Home") { navCtrl.navigateUp() } else { localNavCtrl.navigateUp() } - } - }, - colors = TopAppBarDefaults.topAppBarColors(containerColor = colorScheme.background) - ) -} - @Composable fun CopyTextButton(@StringRes label: Int, content: String) { val context = LocalContext.current @@ -287,6 +271,7 @@ fun ListItem(text: String, onDelete: () -> Unit) { fun InfoCard(@StringRes strID: Int) { Column( modifier = Modifier + .fillMaxWidth() .padding(vertical = 8.dp) .clip(RoundedCornerShape(10)) .background(color = colorScheme.tertiaryContainer) @@ -296,3 +281,53 @@ fun InfoCard(@StringRes strID: Int) { Text(stringResource(strID)) } } + +@Composable +fun MyScaffold( + @StringRes title: Int, + horizonPadding: Dp, + navCtrl: NavHostController, + displayTitle: Boolean = true, + content: @Composable ColumnScope.() -> Unit +) = MyScaffold(title, horizonPadding, { navCtrl.navigateUp() }, displayTitle, content) + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MyScaffold( + @StringRes title: Int, + horizonPadding: Dp, + onNavIconClicked: () -> Unit, + displayTitle: Boolean, + content: @Composable ColumnScope.() -> Unit +) { + val scrollState = rememberScrollState() + Scaffold( + topBar = { + TopAppBar( + title = { + Text( + text = stringResource(title), + modifier = if(displayTitle) Modifier.alpha((maxOf(scrollState.value-90,0)).toFloat()/50) else Modifier + ) + }, + navigationIcon = { NavIcon (onNavIconClicked) } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(horizontal = horizonPadding) + .verticalScroll(scrollState) + .padding(bottom = 80.dp) + ) { + if(displayTitle) Text( + text = stringResource(title), + style = typography.headlineLarge, + modifier = Modifier.padding(start = if(horizonPadding == 0.dp) 16.dp else 0.dp,top = 10.dp, bottom = 5.dp) + ) + content() + } + } +} diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index ed5b259..eb9b342 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -91,8 +91,8 @@ Тип аккаунта Передача прав владения Имя целевого компонента - Информация на экране блокировки - Сообщение поддержки + Информация на экране блокировки + Сообщение поддержки Краткое сообщение Подробное сообщение Передать @@ -175,7 +175,7 @@ Сертификат установлен: %1$s Выберите сертификат... Удалить все пользовательские CA-сертификаты - Журналы безопасности + Security logging Журналы безопасности перед перезагрузкой Стереть данные Стереть внешнее хранилище @@ -186,7 +186,6 @@ Ваш рабочий профиль будет УДАЛЕН Статус шифрования Политика FRP - Политика защиты от сброса к заводским настройкам Политика FRP не поддерживается на этом устройстве Включить FRP Список аккаунтов: @@ -236,12 +235,11 @@ Указать порт Неверная конфигурация Исключить хосты - Журналы сети + Network logging Размер файла журнала: %1$s Удалить журналы Экспортировать журналы Пара ключей Wi-Fi - Пара ключей Предпочтительная сетевая служба Идентификатор сети Разрешить переход на соединение по умолчанию @@ -286,7 +284,6 @@ Перенести аккаунт Имя аккаунта Сохранить аккаунт - Рабочий профиль принадлежит организации: %1$s Рабочий профиль организации Пропустить шифрование Создать @@ -316,7 +313,7 @@ Постоянный VPN Включить блокировку Очистить текущую конфигурацию - Разрешение + Разрешение Область действия: рабочий профиль Информация о приложении Не установлено @@ -366,12 +363,12 @@ - Ограничения пользователя + Ограничения пользователя Владелец профиля может использовать ограниченные функции Включите переключатель, чтобы отключить эту функцию. Функции в рабочем профиле ограничены. - Сеть - Другие подключения + Сеть + Другие подключения Медиа Другое Требуется владелец устройства @@ -478,7 +475,6 @@ Изменить значок пользователя Использовать выборщик файлов вместо галереи Выберите изображение... - Неизвестный результат (возможно, ошибка) Ошибка: управляемый профиль Ошибка: текущий пользователь Сообщение о сеансе пользователя diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index bf82952..7dd3071 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -92,8 +92,8 @@ Hesap Türleri Sahipliği Devret Target component name - Ekran Kilidi Bilgisi - Destek Mesajı + Ekran Kilidi Bilgisi + Destek Mesajı Kısa Mesaj Uzun Mesaj Transfer @@ -177,7 +177,7 @@ Yüklenen sertifika: %1$s Sertifika seç... Tüm kullanıcı sertifikalarını kaldır - Güvenlik kayıtları + Security logging Yeniden başlatmadan önce güvenlik kayıtları Verileri sil Harici depolamayı sil @@ -188,7 +188,6 @@ Your work profile will be DELETED Encryption status FRP politikası - Fabrika ayarlarına sıfırlama koruma politikası FRP politikası bu cihazda desteklenmiyor FRP\'yi etkinleştir "Hesap listesi: " @@ -237,12 +236,11 @@ Specify port Invalid config Exclude hosts - Ağ kayıtları + Network logging Log file size: %1$s Delete logs Export logs Wi-Fi anahtar çifti - Anahtar çifti Tercihli ağ hizmeti Network ID Allow fallback to default connection @@ -285,7 +283,6 @@ Migrate account Account name Keep account - Kuruluşa ait iş profili: %1$s Kuruluş iş profili Şifrelemeyi atla Oluştur @@ -314,7 +311,7 @@ Her zaman açık VPN Enable lockdown Clear current config - İzin + İzin Kapsam: iş profili Uygulama bilgisi Yüklü değil @@ -365,12 +362,12 @@ Başarısız: zaman aşımı - Kullanıcı kısıtlaması + Kullanıcı kısıtlaması Profil sahibi sınırlı işlev kullanabilir Bu işlevi devre dışı bırakmak için bir anahtar açın. İş profilindeki işlevler sınırlıdır. - - Diğer bağlantı + + Diğer bağlantı Medya Diğer Cihaz sahibi gerektirir @@ -476,7 +473,6 @@ Kullanıcı simgesini değiştir Galeri yerine dosya seçici kullan Resim seç... - Bilinmeyen sonuç (başarısız olabilir) Başarısız: yönetilen profil Başarısız: mevcut kullanıcı Kullanıcı oturum mesajı diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 380ebd6..188e96f 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -87,8 +87,8 @@ 账号类型 转移所有权 目标组件名 - 锁屏提示信息 - 提供支持的消息 + 锁屏提示信息 + 提供支持的消息 提供支持的短消息 提供支持的长消息 转移 @@ -172,7 +172,7 @@ 证书已安装:%1$s 选择证书... 卸载所有用户证书 - 安全日志 + 安全日志 重启前安全日志 清除数据 清除外部存储 @@ -183,7 +183,6 @@ 你的工作资料将会被删除 加密状态 FRP策略 - 恢复出厂设置保护策略 这个设备不支持恢复出厂设置保护策略 启用FRP 账户列表: @@ -232,12 +231,11 @@ 指定端口 无效配置 排除列表 - 收集网络日志 + 网络日志 日志文件大小:%1$s 删除日志 导出日志 Wi-Fi密钥对 - 密钥对 首选网络服务 网络ID 允许回落到默认连接 @@ -280,7 +278,6 @@ 迁移账号 账号名 保留账号 - 由组织拥有的工作资料:%1$s 组织拥有的工作资料 跳过加密 创建 @@ -309,7 +306,7 @@ VPN保持打开 启用锁定 清除当前配置 - 权限 + 权限 作用域: 工作资料 应用详情 未安装 @@ -357,12 +354,12 @@ 超时 - 用户限制 + 用户限制 Profile owner无法使用部分功能 打开开关后会禁用对应的功能 工作资料中部分功能无效 - 网络和互联网 - 更多连接 + 网络和互联网 + 更多连接 媒体 其他 需要DeviceOwner @@ -468,7 +465,6 @@ 更换用户头像 使用文件选择器而不是相册 选择图片... - 未知结果(失败) 失败:受管理的资料 失败:当前用户 用户会话消息 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1a1b94e..7f2d672 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -94,8 +94,8 @@ Account type Transfer Ownership Target component name - Lockscreen info - Support Message + Lockscreen info + Support Messages Short message Long message Transfer @@ -181,7 +181,7 @@ Certificate installed: %1$s Select certificate... Uninstall all user CA certificate - Security logs + Security logging Pre-reboot security logs Wipe data Wipe external storage @@ -192,7 +192,6 @@ Your work profile will be DELETED Encryption status FRP policy - Factory reset protection policy FRP policy is not supported on this device Enable FRP "Account list: " @@ -242,12 +241,11 @@ Specify port Invalid config Exclude hosts - Network logs + Network logging Log file size: %1$s Delete logs Export logs Wi-Fi keypair - Keypair Preferential network service Network ID Allow fallback to default connection @@ -290,7 +288,6 @@ Migrate account Account name Keep account - Organization owned work profile: %1$s Organization work profile dpm mark-profile-owner-on-organization-owned-device --user %1$s com.bintianqi.owndroid/com.bintianqi.owndroid.Receiver @@ -322,7 +319,7 @@ Always-on VPN Enable lockdown Clear current config - Permission + Permissions Scope: work profile App info Not installed @@ -371,12 +368,12 @@ Fail: timeout - User restriction + User restriction Profile owner can use limited function Turn on a switch to disable that function. Functions in work profile is limited. - Network - Other connection + Network + Other connection Media Other Require device owner @@ -482,7 +479,6 @@ Change user icon Use file picker instead of gallery Select image... - Unknown result(may failed) Failed: managed profile Failed: current user User session message @@ -631,7 +627,7 @@ If a Device owner use this function, all users should be affiliated. Not all devices support pre-reboot security logs. When account management is disabled for an account type, adding or removing an account of that type will not be possible. - FRP can protect the device after untrusted factory reset (in Recovery or Fastboot).\nTo enable this feature, the device must support persistent data block service. + Factory reset protection can protect the device after untrusted factory reset (in Recovery or Fastboot).\nTo enable this feature, the device must support persistent data block service. All data of this user will be wiped, but that user won\'t be removed. Control whether the user can change networks configured by the admin.\nWhen this lockdown is enabled, the user can still configure and connect to other Wi-Fi networks, or use other Wi-Fi capabilities such as tethering. Specify the minimum security level required for Wi-Fi networks. The device may not connect to networks that do not meet the minimum security level. If the current network does not meet the minimum security level set, it will be disconnected. diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0f32f42..61fa424 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,14 +1,14 @@ [versions] -agp = "8.7.2" +agp = "8.7.3" kotlin = "2.0.21" -navigation-compose = "2.8.3" -composeBom = "2024.10.01" +navigation-compose = "2.8.4" +composeBom = "2024.11.00" accompanist-drawablepainter = "0.35.0-alpha" shizuku = "13.1.5" biometric = "1.2.0-alpha05" -fragment = "1.8.0-beta01" -dhizuku = "2.5.2" +fragment = "1.8.5" +dhizuku = "2.5.3" hiddenApiBypass = "4.3" serialization = "1.7.3" From f7b18d1a31a2e8d319ce43f29d39597ec82eb267 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sun, 8 Dec 2024 14:19:11 +0800 Subject: [PATCH 02/12] Date and time picker in Change time --- .../main/java/com/bintianqi/owndroid/Utils.kt | 10 ++ .../java/com/bintianqi/owndroid/dpm/System.kt | 124 ++++++++++++++++-- app/src/main/res/values-ru/strings.xml | 9 +- app/src/main/res/values-tr/strings.xml | 9 +- app/src/main/res/values-zh-rCN/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + 6 files changed, 143 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/bintianqi/owndroid/Utils.kt b/app/src/main/java/com/bintianqi/owndroid/Utils.kt index 8d2cf09..6179a8b 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Utils.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Utils.kt @@ -23,9 +23,12 @@ import java.io.File import java.io.FileNotFoundException import java.io.IOException import java.io.InputStream +import java.text.SimpleDateFormat import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle +import java.util.Date import java.util.Locale lateinit var getFile: ActivityResultLauncher @@ -132,3 +135,10 @@ fun parseTimestamp(timestamp: Long): String { val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault()) return formatter.format(instant) } + +val Long.humanReadableDate: String + get() = SimpleDateFormat("yyyy/MM/dd", Locale.getDefault()).format(Date(this)) + +val Long.humanReadableTime: String + get() = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date(this)) + diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt index 2db2cd2..54e82ab 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt @@ -45,15 +45,20 @@ import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -63,14 +68,23 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.DatePicker +import androidx.compose.material3.DatePickerDialog +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.TimePicker +import androidx.compose.material3.rememberDatePickerState +import androidx.compose.material3.rememberTimePickerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -102,6 +116,7 @@ import com.bintianqi.owndroid.exportFilePath import com.bintianqi.owndroid.fileUriFlow import com.bintianqi.owndroid.formatFileSize import com.bintianqi.owndroid.getFile +import com.bintianqi.owndroid.humanReadableDate import com.bintianqi.owndroid.prepareForNotification import com.bintianqi.owndroid.selectedPackage import com.bintianqi.owndroid.toggle @@ -360,6 +375,7 @@ fun Keyguard(navCtrl: NavHostController) { } } +@OptIn(ExperimentalMaterial3Api::class) @SuppressLint("NewApi") @Composable fun ChangeTime(navCtrl: NavHostController) { @@ -367,26 +383,108 @@ fun ChangeTime(navCtrl: NavHostController) { val dpm = context.getDPM() val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current - var inputTime by remember { mutableStateOf("") } + val coroutine = rememberCoroutineScope() + val pagerState = rememberPagerState { 2 } + var manualInput by remember { mutableStateOf(false) } + var inputTime by remember { mutableStateOf("")} + var picker by remember { mutableIntStateOf(0) } //0:None, 1:DatePicker, 2:TimePicker + val datePickerState = rememberDatePickerState() + val timePickerState = rememberTimePickerState() + val dateInteractionSource = remember { MutableInteractionSource() } + val timeInteractionSource = remember { MutableInteractionSource() } + if(dateInteractionSource.collectIsPressedAsState().value) picker = 1 + if(timeInteractionSource.collectIsPressedAsState().value) picker = 2 + val isInputLegal = (manualInput && (try { inputTime.toLong() } catch(_: Exception) { -1 }) >= 0) || + (!manualInput && datePickerState.selectedDateMillis != null) MyScaffold(R.string.change_time, 8.dp, navCtrl) { - OutlinedTextField( - value = inputTime, - label = { Text(stringResource(R.string.time_unit_ms)) }, - onValueChange = { inputTime = it }, - supportingText = { Text(stringResource(R.string.info_change_time)) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = {focusMgr.clearFocus() }), - modifier = Modifier.fillMaxWidth() - ) - Spacer(Modifier.padding(vertical = 5.dp)) + SingleChoiceSegmentedButtonRow( + modifier = Modifier.fillMaxWidth().padding(top = 4.dp) + ) { + SegmentedButton( + selected = !manualInput, shape = SegmentedButtonDefaults.itemShape(0, 2), + onClick = { + manualInput = false + coroutine.launch { + pagerState.animateScrollToPage(0) + } + } + ) { + Text(stringResource(R.string.selector)) + } + SegmentedButton( + selected = manualInput, shape = SegmentedButtonDefaults.itemShape(1, 2), + onClick = { + manualInput = true + coroutine.launch { + pagerState.animateScrollToPage(1) + } + } + ) { + Text(stringResource(R.string.manually_input)) + } + } + HorizontalPager( + state = pagerState, modifier = Modifier.height(140.dp).padding(top = 4.dp), + verticalAlignment = Alignment.Top + ) { page -> + if(page == 0) Column { + OutlinedTextField( + value = datePickerState.selectedDateMillis?.humanReadableDate ?: "", + onValueChange = {}, readOnly = true, + label = { Text(stringResource(R.string.date)) }, + interactionSource = dateInteractionSource, + modifier = Modifier.fillMaxWidth() + ) + OutlinedTextField( + value = timePickerState.hour.toString() + ":" + timePickerState.minute.toString(), + onValueChange = {}, readOnly = true, + label = { Text(stringResource(R.string.time)) }, + interactionSource = timeInteractionSource, + modifier = Modifier.fillMaxWidth() + ) + } + if(page == 1) OutlinedTextField( + value = inputTime, + label = { Text(stringResource(R.string.time_unit_ms)) }, + onValueChange = { inputTime = it }, + supportingText = { Text(stringResource(R.string.info_change_time)) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = {focusMgr.clearFocus() }), + modifier = Modifier.fillMaxWidth() + ) + } Button( - onClick = { dpm.setTime(receiver, inputTime.toLong()) }, + onClick = { + val timeMillis = if(manualInput) inputTime.toLong() + else datePickerState.selectedDateMillis!! + timePickerState.hour * 3600000 + timePickerState.minute * 60000 + val result = dpm.setTime(receiver, timeMillis) + Toast.makeText(context, if(result) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show() + }, modifier = Modifier.fillMaxWidth(), - enabled = inputTime != "" + enabled = isInputLegal ) { Text(stringResource(R.string.apply)) } } + if(picker == 1) DatePickerDialog( + confirmButton = { + TextButton(onClick = { picker = 0; focusMgr.clearFocus() } ) { + Text(stringResource(R.string.confirm)) + } + }, + onDismissRequest = { picker = 0; focusMgr.clearFocus() } + ) { + DatePicker(datePickerState) + } + if(picker == 2) AlertDialog( + text = { TimePicker(timePickerState) }, + confirmButton = { + TextButton(onClick = { picker = 0; focusMgr.clearFocus() } ) { + Text(stringResource(R.string.confirm)) + } + }, + onDismissRequest = { picker = 0; focusMgr.clearFocus() } + ) } @SuppressLint("NewApi") diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index eb9b342..523ba40 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -143,8 +143,13 @@ Отчет об ошибке Запросить отчет об ошибке? Перезагрузить - Change time - Change timezone + + Change time + Selector + Manually input + Date + Time + Change timezone Идентификатор часового пояса Автоматический часовой пояс должен быть отключен перед установкой пользовательского часового пояса. Политика разрешений diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 7dd3071..797be1a 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -144,8 +144,13 @@ Hata raporu Hata raporu iste? Yeniden başlat - Change time - Change timezone + + Change time + Selector + Manually input + Date + Time + Change timezone Saat dilimi kimliği Özel bir saat dilimi ayarlamadan önce otomatik saat dilimi devre dışı bırakılmalıdır İzin politikası diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 188e96f..8422449 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -140,6 +140,10 @@ 请求错误报告? 重启 更改时间 + 选择器 + 手动输入 + 日期 + 时间 更改时区 时区ID 在设置时区前需要关闭自动时区 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7f2d672..76c392f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -150,6 +150,10 @@ Request bug report? Reboot Change time + Selector + Manually input + Date + Time Change timezone Timezone ID Auto timezone should be disabled before set a custom timezone. From 867668832e052da16b26bb31e06febcc61d103f8 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sat, 14 Dec 2024 22:03:39 +0800 Subject: [PATCH 03/12] Fix some bugs of authentication Display authentication screen in NavHost Remove "Protect storage", authenticate to clear storage instead Force enable biometrics on if using password alone is not supported --- .../main/java/com/bintianqi/owndroid/Auth.kt | 107 +++++------------- .../com/bintianqi/owndroid/MainActivity.kt | 51 ++++++--- .../bintianqi/owndroid/ManageSpaceActivity.kt | 56 ++++++--- .../java/com/bintianqi/owndroid/Settings.kt | 26 +++-- .../com/bintianqi/owndroid/ui/Animations.kt | 2 - app/src/main/res/values-ru/strings.xml | 5 - app/src/main/res/values-tr/strings.xml | 5 - app/src/main/res/values-zh-rCN/strings.xml | 5 - app/src/main/res/values/strings.xml | 5 - gradle/libs.versions.toml | 4 +- 10 files changed, 120 insertions(+), 146 deletions(-) diff --git a/app/src/main/java/com/bintianqi/owndroid/Auth.kt b/app/src/main/java/com/bintianqi/owndroid/Auth.kt index 39d1352..25a7d58 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Auth.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Auth.kt @@ -1,55 +1,34 @@ package com.bintianqi.owndroid import android.content.Context +import androidx.activity.compose.BackHandler import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt.AuthenticationCallback import androidx.biometric.BiometricPrompt.PromptInfo.Builder -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.background -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity -import com.bintianqi.owndroid.ui.Animations +import androidx.navigation.NavHostController import kotlinx.coroutines.delay -import kotlinx.coroutines.launch @Composable -fun AuthScreen(activity: FragmentActivity, showAuth: MutableState) { - val context = activity.applicationContext - val coroutineScope = rememberCoroutineScope() - var canStartAuth by remember { mutableStateOf(true) } - var fallback by remember { mutableStateOf(false) } - var startFade by remember { mutableStateOf(false) } - val alpha by animateFloatAsState( - targetValue = if(startFade) 0F else 1F, - label = "AuthScreenFade", - animationSpec = Animations.authScreenFade - ) - val onAuthSucceed = { - startFade = true - coroutineScope.launch { - delay(300) - showAuth.value = false - } - } - val promptInfo = Builder() - .setTitle(context.getText(R.string.authenticate)) - .setSubtitle(context.getText(R.string.auth_with_bio)) - .setConfirmationRequired(true) +fun Authenticate(activity: FragmentActivity, navCtrl: NavHostController) { + BackHandler { activity.moveTaskToBack(true) } + var status by rememberSaveable { mutableIntStateOf(0) } // 0:Prompt automatically, 1:Authenticating, 2:Prompt manually + val onAuthSucceed = { navCtrl.navigateUp() } val callback = object: AuthenticationCallback() { override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { super.onAuthenticationSucceeded(result) @@ -59,49 +38,33 @@ fun AuthScreen(activity: FragmentActivity, showAuth: MutableState) { super.onAuthenticationError(errorCode, errString) when(errorCode) { BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> onAuthSucceed() - BiometricPrompt.ERROR_NEGATIVE_BUTTON -> fallback = true - else -> canStartAuth = true + else -> status = 2 } } } - LaunchedEffect(fallback) { - if(fallback) { - val fallbackPromptInfo = promptInfo - .setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL) - .setSubtitle(context.getText(R.string.auth_with_password)) - .build() - val executor = ContextCompat.getMainExecutor(context) - val biometricPrompt = BiometricPrompt(activity, executor, callback) - biometricPrompt.authenticate(fallbackPromptInfo) + LaunchedEffect(Unit) { + if(status == 0) { + delay(300) + startAuth(activity, callback) + status = 1 } } - Surface( - modifier = Modifier - .fillMaxSize() - .alpha(alpha) - .background(if(isSystemInDarkTheme()) Color.Black else Color.White) - ) { + Scaffold { paddingValues -> Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background) + modifier = Modifier.fillMaxSize().padding(paddingValues) ) { - LaunchedEffect(Unit) { - delay(300) - startAuth(activity, promptInfo, callback) - canStartAuth = false - } Text( text = stringResource(R.string.authenticate), style = MaterialTheme.typography.headlineLarge, - color = MaterialTheme.colorScheme.onBackground ) Button( onClick = { - startAuth(activity, promptInfo, callback) - canStartAuth = false + startAuth(activity, callback) + status = 1 }, - enabled = canStartAuth + enabled = status != 1 ) { Text(text = stringResource(R.string.start)) } @@ -109,31 +72,15 @@ fun AuthScreen(activity: FragmentActivity, showAuth: MutableState) { } } -private fun startAuth(activity: FragmentActivity, basicPromptInfo: Builder, callback: AuthenticationCallback) { +fun startAuth(activity: FragmentActivity, callback: AuthenticationCallback) { val context = activity.applicationContext - val promptInfo = basicPromptInfo - 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 promptInfo = Builder().setTitle(context.getText(R.string.authenticate)) + if(sharedPref.getInt("biometrics_auth", 0) != 0) { + promptInfo.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL or BiometricManager.Authenticators.BIOMETRIC_WEAK) + } else { + promptInfo.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL) } val executor = ContextCompat.getMainExecutor(context) - val biometricPrompt = BiometricPrompt(activity, executor, callback) - biometricPrompt.authenticate(promptInfo.build()) + BiometricPrompt(activity, executor, callback).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 c3bee73..b3fe7fe 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -8,6 +8,9 @@ import android.widget.Toast import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures @@ -30,6 +33,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -48,6 +52,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost @@ -77,7 +84,6 @@ import com.bintianqi.owndroid.dpm.Keyguard import com.bintianqi.owndroid.dpm.LockScreenInfo import com.bintianqi.owndroid.dpm.LockTaskMode import com.bintianqi.owndroid.dpm.MTEPolicy -import com.bintianqi.owndroid.dpm.WorkProfile import com.bintianqi.owndroid.dpm.NearbyStreamingPolicy import com.bintianqi.owndroid.dpm.Network import com.bintianqi.owndroid.dpm.NetworkLogging @@ -115,6 +121,7 @@ import com.bintianqi.owndroid.dpm.WifiAuthKeypair import com.bintianqi.owndroid.dpm.WifiSecurityLevel import com.bintianqi.owndroid.dpm.WifiSsidPolicy import com.bintianqi.owndroid.dpm.WipeData +import com.bintianqi.owndroid.dpm.WorkProfile import com.bintianqi.owndroid.dpm.dhizukuErrorStatus import com.bintianqi.owndroid.dpm.dhizukuPermissionGranted import com.bintianqi.owndroid.dpm.getDPM @@ -134,20 +141,16 @@ import kotlinx.coroutines.launch import org.lsposed.hiddenapibypass.HiddenApiBypass import java.util.Locale -var backToHomeStateFlow = MutableStateFlow(false) +val backToHomeStateFlow = MutableStateFlow(false) @ExperimentalMaterial3Api class MainActivity : FragmentActivity() { - private val showAuth = mutableStateOf(false) - override fun onCreate(savedInstanceState: Bundle?) { registerActivityResult(this) enableEdgeToEdge() WindowCompat.setDecorFitsSystemWindows(window, false) super.onCreate(savedInstanceState) val context = applicationContext - val sharedPref = context.getSharedPreferences("data", MODE_PRIVATE) if (VERSION.SDK_INT >= 28) HiddenApiBypass.setHiddenApiExemptions("") - if(sharedPref.getBoolean("auth", false)) showAuth.value = true val locale = context.resources?.configuration?.locale zhCN = locale == Locale.SIMPLIFIED_CHINESE || locale == Locale.CHINESE || locale == Locale.CHINA toggleInstallAppActivity() @@ -156,10 +159,7 @@ class MainActivity : FragmentActivity() { lifecycleScope.launch { delay(5000); setDefaultAffiliationID(context) } setContent { OwnDroidTheme(vm) { - Home(vm) - if(showAuth.value) { - AuthScreen(this, showAuth) - } + Home(this, vm) } } } @@ -167,12 +167,6 @@ class MainActivity : FragmentActivity() { override fun onResume() { super.onResume() val sharedPref = applicationContext.getSharedPreferences("data", MODE_PRIVATE) - if( - sharedPref.getBoolean("auth", false) && - sharedPref.getBoolean("lock_in_background", false) - ) { - showAuth.value = true - } if (sharedPref.getBoolean("dhizuku", false)) { if (Dhizuku.init(applicationContext)) { if (!dhizukuPermissionGranted()) { dhizukuErrorStatus.value = 2 } @@ -187,7 +181,7 @@ class MainActivity : FragmentActivity() { @ExperimentalMaterial3Api @Composable -fun Home(vm: MyViewModel) { +fun Home(activity: FragmentActivity, vm: MyViewModel) { val navCtrl = rememberNavController() val context = LocalContext.current val dpm = context.getDPM() @@ -196,6 +190,7 @@ fun Home(vm: MyViewModel) { val focusMgr = LocalFocusManager.current val dialogStatus = remember { mutableIntStateOf(0) } val backToHome by backToHomeStateFlow.collectAsState() + val lifecycleOwner = LocalLifecycleOwner.current LaunchedEffect(backToHome) { if(backToHome) { navCtrl.navigateUp(); backToHomeStateFlow.value = false } } @@ -308,6 +303,28 @@ fun Home(vm: MyViewModel) { composable(route = "About") { About(navCtrl) } composable(route = "PackageSelector") { PackageSelector(navCtrl) } + + composable( + route = "Authenticate", + enterTransition = { fadeIn(animationSpec = tween(200)) }, + popExitTransition = { fadeOut(animationSpec = tween(400)) } + ) { Authenticate(activity, navCtrl) } + } + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if( + (event == Lifecycle.Event.ON_RESUME && + sharedPref.getBoolean("auth", false) && + sharedPref.getBoolean("lock_in_background", false)) || + (event == Lifecycle.Event.ON_CREATE && sharedPref.getBoolean("auth", false)) + ) { + navCtrl.navigate("Authenticate") { launchSingleTop = true } + } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } } LaunchedEffect(Unit) { val profileInitialized = sharedPref.getBoolean("ManagedProfileActivated", false) diff --git a/app/src/main/java/com/bintianqi/owndroid/ManageSpaceActivity.kt b/app/src/main/java/com/bintianqi/owndroid/ManageSpaceActivity.kt index 100bbe4..c9e9f14 100644 --- a/app/src/main/java/com/bintianqi/owndroid/ManageSpaceActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/ManageSpaceActivity.kt @@ -5,9 +5,15 @@ import android.os.Bundle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels +import androidx.biometric.BiometricPrompt +import androidx.biometric.BiometricPrompt.AuthenticationCallback import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import androidx.core.view.WindowCompat import androidx.fragment.app.FragmentActivity @@ -20,17 +26,40 @@ class ManageSpaceActivity: FragmentActivity() { WindowCompat.setDecorFitsSystemWindows(window, false) super.onCreate(savedInstanceState) val sharedPref = applicationContext.getSharedPreferences("data", MODE_PRIVATE) - val protected = sharedPref.getBoolean("protect_storage", false) + val authenticate = sharedPref.getBoolean("auth", false) val vm by viewModels() if(!vm.initialized) vm.initialize(applicationContext) + fun clearStorage() { + filesDir.deleteRecursively() + cacheDir.deleteRecursively() + codeCacheDir.deleteRecursively() + if(Build.VERSION.SDK_INT >= 24) { + dataDir.resolve("shared_prefs").deleteRecursively() + } else { + sharedPref.edit().clear().apply() + } + finish() + exitProcess(0) + } setContent { + var authenticating by remember { mutableStateOf(false) } + val callback = object: AuthenticationCallback() { + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + clearStorage() + } + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + when(errorCode) { + BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> clearStorage() + else -> authenticating = false + } + } + } OwnDroidTheme(vm) { AlertDialog( - title = { - Text(stringResource(R.string.clear_storage)) - }, text = { - if(protected) Text(stringResource(R.string.storage_is_protected)) + Text(stringResource(R.string.clear_storage)) }, onDismissRequest = { finish() }, dismissButton = { @@ -39,19 +68,16 @@ class ManageSpaceActivity: FragmentActivity() { } }, confirmButton = { - if(!protected) TextButton( + TextButton( onClick = { - filesDir.deleteRecursively() - cacheDir.deleteRecursively() - codeCacheDir.deleteRecursively() - if(Build.VERSION.SDK_INT >= 24) { - dataDir.resolve("shared_prefs").deleteRecursively() + if(authenticate) { + authenticating = true + startAuth(this, callback) } else { - sharedPref.edit().clear().apply() + clearStorage() } - finish() - exitProcess(0) - } + }, + enabled = !authenticating ) { Text(stringResource(R.string.confirm)) } diff --git a/app/src/main/java/com/bintianqi/owndroid/Settings.kt b/app/src/main/java/com/bintianqi/owndroid/Settings.kt index 44f3b18..19f1924 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Settings.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Settings.kt @@ -5,6 +5,7 @@ import android.content.Intent import android.net.Uri import android.os.Build.VERSION import android.widget.Toast +import androidx.biometric.BiometricManager import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box @@ -19,7 +20,9 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -117,7 +120,8 @@ fun Appearance(navCtrl: NavHostController, vm: MyViewModel) { @Composable fun AuthSettings(navCtrl: NavHostController) { - val sharedPref = LocalContext.current.getSharedPreferences("data", Context.MODE_PRIVATE) + val context = LocalContext.current + val sharedPref = context.getSharedPreferences("data", Context.MODE_PRIVATE) var auth by remember{ mutableStateOf(sharedPref.getBoolean("auth",false)) } MyScaffold(R.string.security, 0.dp, navCtrl) { SwitchItem( @@ -128,22 +132,24 @@ fun AuthSettings(navCtrl: NavHostController) { } ) if(auth) { + var bioAuth by remember { mutableIntStateOf(sharedPref.getInt("biometrics_auth", 0)) } // 0:Disabled, 1:Enabled 2:Force enabled + LaunchedEffect(Unit) { + val bioManager = BiometricManager.from(context) + if(bioManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL) != BiometricManager.BIOMETRIC_SUCCESS) { + bioAuth = 2 + sharedPref.edit().putInt("biometrics_auth", 2).apply() + } + } SwitchItem( - R.string.enable_bio_auth, "", null, - { sharedPref.getBoolean("bio_auth", false) }, - { sharedPref.edit().putBoolean("bio_auth", it).apply() } + R.string.enable_bio_auth, "", null, bioAuth != 0, + { bioAuth = if(it) 1 else 0; sharedPref.edit().putInt("biometrics_auth", bioAuth).apply() }, bioAuth != 2 ) SwitchItem( - R.string.lock_in_background, stringResource(R.string.developing), null, + R.string.lock_in_background, "", null, { sharedPref.getBoolean("lock_in_background", false) }, { sharedPref.edit().putBoolean("lock_in_background", it).apply() } ) } - SwitchItem( - R.string.protect_storage, "", null, - { sharedPref.getBoolean("protect_storage", false) }, - { sharedPref.edit().putBoolean("protect_storage", it).apply() } - ) } } diff --git a/app/src/main/java/com/bintianqi/owndroid/ui/Animations.kt b/app/src/main/java/com/bintianqi/owndroid/ui/Animations.kt index ba79fb1..90c364a 100644 --- a/app/src/main/java/com/bintianqi/owndroid/ui/Animations.kt +++ b/app/src/main/java/com/bintianqi/owndroid/ui/Animations.kt @@ -13,8 +13,6 @@ object Animations { private val tween: FiniteAnimationSpec = tween(durationMillis = 550, easing = bezier, delayMillis = 50) - val authScreenFade: FiniteAnimationSpec = tween(durationMillis = 200, easing = LinearEasing) - val navHostEnterTransition: AnimatedContentTransitionScope.() -> EnterTransition = { fadeIn(tween(100, easing = LinearEasing)) + slideIntoContainer( diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 523ba40..4dd6219 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -557,12 +557,7 @@ Заблокировать OwnDroid Аутентификация по биометрии Аутентифицировать - Использовать пароль - Аутентифицировать OwnDroid с помощью пароля - Аутентифицировать OwnDroid с помощью биометрии Блокировать при переключении в фоновый режим - Защитить хранилище - Хранилище защищено, вы не можете очистить хранилище OwnDroid Очистить хранилище API автоматизации diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 797be1a..ab987bb 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -552,12 +552,7 @@ OwnDroid\'u kilitle Biyometri ile doğrulama Doğrula - Şifre kullan - OwnDroid\'u şifre ile doğrula - OwnDroid\'u biyometri ile doğrula Arka plana geçince kilitle - Depolamayı koru - Depolama korunuyor Depolamayı temizle Automation API diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 8422449..bcdf09a 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -543,12 +543,7 @@ 锁定OwnDroid 使用生物识别 验证 - 使用密码 - 使用密码进行验证 - 使用生物识别进行验证 处于后台时锁定 - 保护存储空间 - 存储空间受到保护,你不能清除OwnDroid的存储空间 清除存储空间 自动化API diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 76c392f..286d2fb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -557,12 +557,7 @@ Lock OwnDroid Auth with biometrics Authenticate - Use password - Authenticate OwnDroid with password - Authenticate OwnDroid with biometrics Lock when switch to background - Protect storage - Storage is protected, you can\'t clear storage of OwnDroid Clear storage Automation API diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 61fa424..05db652 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,8 +2,8 @@ agp = "8.7.3" kotlin = "2.0.21" -navigation-compose = "2.8.4" -composeBom = "2024.11.00" +navigation-compose = "2.8.5" +composeBom = "2024.12.01" accompanist-drawablepainter = "0.35.0-alpha" shizuku = "13.1.5" biometric = "1.2.0-alpha05" From 3fb4fb078fd1fe04d6eaca1e2ea6cd2853d31fd1 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sun, 15 Dec 2024 12:01:09 +0800 Subject: [PATCH 04/12] Request Shizuku permission before enter Shizuku screen Bind Shizuku service automatically after enter Shizuku functions page --- .../com/bintianqi/owndroid/IUserService.aidl | 1 - .../com/bintianqi/owndroid/MainActivity.kt | 2 +- .../com/bintianqi/owndroid/MyViewModel.kt | 2 + .../com/bintianqi/owndroid/dpm/Permissions.kt | 27 +++- .../com/bintianqi/owndroid/dpm/Shizuku.kt | 133 +++++------------- .../bintianqi/owndroid/dpm/ShizukuService.kt | 12 +- app/src/main/res/values-ru/strings.xml | 17 +-- app/src/main/res/values-tr/strings.xml | 29 ++-- app/src/main/res/values-zh-rCN/strings.xml | 8 +- app/src/main/res/values/strings.xml | 8 +- 10 files changed, 84 insertions(+), 155 deletions(-) diff --git a/app/src/main/aidl/com/bintianqi/owndroid/IUserService.aidl b/app/src/main/aidl/com/bintianqi/owndroid/IUserService.aidl index f53e85e..2175af9 100644 --- a/app/src/main/aidl/com/bintianqi/owndroid/IUserService.aidl +++ b/app/src/main/aidl/com/bintianqi/owndroid/IUserService.aidl @@ -1,7 +1,6 @@ package com.bintianqi.owndroid; interface IUserService { - void destroy() = 16777114; String execute(String command) = 1; int getUid() = 2; } diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index b3fe7fe..80a1cda 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -210,7 +210,7 @@ fun Home(activity: FragmentActivity, vm: MyViewModel) { composable(route = "HomePage") { HomePage(navCtrl) } composable(route = "Permissions") { Permissions(navCtrl) } - composable(route = "Shizuku") { Shizuku(navCtrl) } + composable(route = "Shizuku") { Shizuku(vm, navCtrl) } composable(route = "DeviceAdmin") { DeviceAdmin(navCtrl) } composable(route = "ProfileOwner") { ProfileOwner(navCtrl) } composable(route = "DeviceOwner") { DeviceOwner(navCtrl) } diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt index 0c5dc74..b4af388 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -2,6 +2,7 @@ package com.bintianqi.owndroid import android.content.Context import android.os.Build +import android.os.IBinder import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow @@ -9,6 +10,7 @@ import kotlinx.coroutines.launch class MyViewModel: ViewModel() { val theme = MutableStateFlow(ThemeSettings()) + val shizukuBinder = MutableStateFlow(null) var initialized = false fun initialize(context: Context) { diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt index 2e4a93f..bdb7df6 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt @@ -37,6 +37,8 @@ import com.bintianqi.owndroid.yesOrNo import com.rosan.dhizuku.api.Dhizuku import com.rosan.dhizuku.api.DhizukuRequestPermissionListener import kotlinx.coroutines.launch +import rikka.shizuku.Shizuku +import rikka.sui.Sui @SuppressLint("NewApi") @Composable @@ -76,7 +78,30 @@ fun Permissions(navCtrl: NavHostController) { operation = { navCtrl.navigate("DeviceOwner") } ) } - FunctionItem(R.string.shizuku,"") { navCtrl.navigate("Shizuku") } + FunctionItem(R.string.shizuku,"") { + try { + if(Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { navCtrl.navigate("Shizuku") } + else if(Shizuku.shouldShowRequestPermissionRationale()) { + Toast.makeText(context, R.string.permission_denied, Toast.LENGTH_SHORT).show() + } else { + Sui.init(context.packageName) + val listener = object: Shizuku.OnRequestPermissionResultListener { + override fun onRequestPermissionResult(requestCode: Int, grantResult: Int) { + if(grantResult == PackageManager.PERMISSION_GRANTED) { + navCtrl.navigate("Shizuku") + } else { + Toast.makeText(context, R.string.permission_denied, Toast.LENGTH_SHORT).show() + } + Shizuku.removeRequestPermissionResultListener(this) + } + } + Shizuku.addRequestPermissionResultListener(listener) + Shizuku.requestPermission(0) + } + } catch(_: IllegalStateException) { + Toast.makeText(context, R.string.shizuku_not_started, Toast.LENGTH_SHORT).show() + } + } FunctionItem(R.string.device_info, "", R.drawable.perm_device_information_fill0) { navCtrl.navigate("DeviceInfo") } if((VERSION.SDK_INT >= 26 && deviceOwner) || (VERSION.SDK_INT>=24 && profileOwner)) { FunctionItem(R.string.org_name, "", R.drawable.corporate_fare_fill0) { dialog = 2 } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Shizuku.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Shizuku.kt index a7c11b3..42422b4 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Shizuku.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Shizuku.kt @@ -3,7 +3,6 @@ package com.bintianqi.owndroid.dpm import android.content.ComponentName import android.content.Context import android.content.ServiceConnection -import android.content.pm.PackageManager import android.os.Binder import android.os.Build.VERSION import android.os.IBinder @@ -13,14 +12,14 @@ import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -32,74 +31,53 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import com.bintianqi.owndroid.IUserService +import com.bintianqi.owndroid.MyViewModel import com.bintianqi.owndroid.R import com.bintianqi.owndroid.ui.MyScaffold import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import rikka.shizuku.Shizuku -private var waitGrantPermission = false - +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun Shizuku(navCtrl: NavHostController) { +fun Shizuku(vm: MyViewModel, navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val coScope = rememberCoroutineScope() val outputTextScrollState = rememberScrollState() - var enabled by remember { mutableStateOf(false) } - var bindShizuku by remember { mutableStateOf(false) } var outputText by rememberSaveable { mutableStateOf("") } var showDeviceAdminButton by remember { mutableStateOf(!context.isDeviceAdmin) } var showDeviceOwnerButton by remember { mutableStateOf(!context.isDeviceOwner) } var showOrgProfileOwnerButton by remember { mutableStateOf(true) } - val service by shizukuService.collectAsState() - LaunchedEffect(service) { - if(service == null) { - enabled = false - bindShizuku = checkShizukuStatus() == 1 + val binder by vm.shizukuBinder.collectAsStateWithLifecycle() + var service by remember { mutableStateOf(null) } + var loading by remember { mutableStateOf(true) } + LaunchedEffect(binder) { + if(binder != null && binder!!.pingBinder()) { + service = IUserService.Stub.asInterface(binder) + loading = false } else { - enabled = true - bindShizuku = false + service = null } } + LaunchedEffect(service) { + if(service == null && !loading) navCtrl.navigateUp() + } LaunchedEffect(Unit) { - shizukuService.value = null - userServiceControl(context, true) + if(binder == null) bindShizukuService(context, vm.shizukuBinder) } MyScaffold(R.string.shizuku, 0.dp, navCtrl, false) { - AnimatedVisibility(visible = bindShizuku, modifier = Modifier.fillMaxWidth()) { - Button( - onClick = { - userServiceControl(context, true) - outputText = "" - }, - modifier = Modifier.wrapContentWidth(Alignment.CenterHorizontally) - ) { - Text(stringResource(R.string.bind_shizuku)) + if(loading) { + Dialog(onDismissRequest = { navCtrl.navigateUp() }) { + CircularProgressIndicator() } } - - Button( - onClick = { - outputText = checkPermission(context) - if(service != null) { - enabled = true - bindShizuku = false - } else { - enabled = false - bindShizuku = checkShizukuStatus() == 1 - } - coScope.launch { - outputTextScrollState.animateScrollTo(0) - } - }, - modifier = Modifier.align(Alignment.CenterHorizontally) - ) { - Text(text = stringResource(R.string.check_shizuku)) - } Button( onClick = { @@ -108,7 +86,6 @@ fun Shizuku(navCtrl: NavHostController) { outputTextScrollState.animateScrollTo(0) } }, - enabled = enabled, modifier = Modifier.align(Alignment.CenterHorizontally) ) { Text(text = stringResource(R.string.list_owners)) @@ -120,7 +97,6 @@ fun Shizuku(navCtrl: NavHostController) { outputTextScrollState.animateScrollTo(0) } }, - enabled = enabled, modifier = Modifier.align(Alignment.CenterHorizontally) ) { Text(text = stringResource(R.string.list_users)) @@ -132,7 +108,6 @@ fun Shizuku(navCtrl: NavHostController) { outputTextScrollState.animateScrollTo(0) } }, - enabled = enabled, modifier = Modifier.align(Alignment.CenterHorizontally) ) { Text(text = stringResource(R.string.list_accounts)) @@ -149,7 +124,6 @@ fun Shizuku(navCtrl: NavHostController) { showDeviceAdminButton = !context.isDeviceAdmin } }, - enabled = enabled, modifier = Modifier.align(Alignment.CenterHorizontally) ) { Text(text = stringResource(R.string.activate_device_admin)) @@ -166,7 +140,6 @@ fun Shizuku(navCtrl: NavHostController) { showDeviceOwnerButton = !context.isDeviceOwner } }, - enabled = enabled, modifier = Modifier.align(Alignment.CenterHorizontally) ) { Text(text = stringResource(R.string.activate_device_owner)) @@ -187,7 +160,6 @@ fun Shizuku(navCtrl: NavHostController) { showOrgProfileOwnerButton = !dpm.isOrganizationOwnedDeviceWithManagedProfile } }, - enabled = enabled, modifier = Modifier.align(Alignment.CenterHorizontally) ) { Text(text = stringResource(R.string.activate_org_profile)) @@ -201,63 +173,24 @@ fun Shizuku(navCtrl: NavHostController) { } } -private fun checkPermission(context: Context): String { - if(checkShizukuStatus() == -1) { return context.getString(R.string.shizuku_not_started) } - return shizukuService.value.let { - if(it == null) { - context.getString(R.string.shizuku_not_bind) - } else { - when(it.uid) { - 2000 -> context.getString(R.string.shizuku_activated_shell) - 0 -> context.getString(R.string.shizuku_activated_root) - else -> context.getString(R.string.unknown_status) + "\nUID: ${it.uid}" - } - } - } -} - -fun checkShizukuStatus(): Int { - val status = try { - if(Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { waitGrantPermission = false; 1 } - else if(Shizuku.shouldShowRequestPermissionRationale()) { 0 } - else{ - if(!waitGrantPermission) { Shizuku.requestPermission(0) } - waitGrantPermission = true - 0 - } - } catch(_: Exception) { -1 } - return status -} - -fun userServiceControl(context:Context, status:Boolean) { - if(checkShizukuStatus() != 1) { return } +fun bindShizukuService(context: Context, shizukuBinder: MutableStateFlow) { val userServiceConnection = object : ServiceConnection { override fun onServiceConnected(componentName: ComponentName, binder: IBinder) { - if (binder.pingBinder()) { - shizukuService.value = IUserService.Stub.asInterface(binder) - } else { - Toast.makeText(context, R.string.invalid_binder, Toast.LENGTH_SHORT).show() - } + shizukuBinder.value = binder } override fun onServiceDisconnected(componentName: ComponentName) { - shizukuService.value = null + shizukuBinder.value = null Toast.makeText(context, R.string.shizuku_service_disconnected, Toast.LENGTH_SHORT).show() } } - val userServiceArgs = Shizuku.UserServiceArgs( - ComponentName( - context.packageName,ShizukuService::class.java.name - ) - ) + val userServiceArgs = Shizuku.UserServiceArgs(ComponentName(context, ShizukuService::class.java)) .daemon(false) - .processNameSuffix("service") - .debuggable(true) + .processNameSuffix("shizuku-service") + .debuggable(false) .version(26) try { - if(status) { - Shizuku.bindUserService(userServiceArgs, userServiceConnection) - }else{ - Shizuku.unbindUserService(userServiceArgs, userServiceConnection, false) - } - } catch(_: Exception) { } + Shizuku.bindUserService(userServiceArgs, userServiceConnection) + } catch(e: Exception) { + e.printStackTrace() + } } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/ShizukuService.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/ShizukuService.kt index 5eda5f0..7871433 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/ShizukuService.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/ShizukuService.kt @@ -3,23 +3,18 @@ package com.bintianqi.owndroid.dpm import android.system.Os import androidx.annotation.Keep import com.bintianqi.owndroid.IUserService -import kotlinx.coroutines.flow.MutableStateFlow import java.io.BufferedReader import java.io.InputStreamReader -val shizukuService = MutableStateFlow(null) - @Keep class ShizukuService: IUserService.Stub() { - override fun destroy() { } - override fun execute(command: String): String { var result = "" val process: Process try { process = Runtime.getRuntime().exec(command) val exitCode = process.waitFor() - if(exitCode != 0){ result += "Error: $exitCode" } + if(exitCode != 0) { result += "Error: $exitCode" } } catch(e: Exception) { e.printStackTrace() return e.toString() @@ -34,11 +29,8 @@ class ShizukuService: IUserService.Stub() { } catch(e: NullPointerException) { e.printStackTrace() } - if(result == "") { return "No result" } return result } - override fun getUid(): Int { - return Os.getuid() - } + override fun getUid(): Int = Os.getuid() } diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 4dd6219..c826eb1 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -43,7 +43,6 @@ Копировать команду Имя пакета Не существует - Неизвестный статус Копировать Файл не существует Ошибка ввода/вывода @@ -58,10 +57,12 @@ Нет Предыдущий Следующий - On - Off - Alias - Unknown error + + On + Off + Alias + Unknown error + Permission denied @@ -109,19 +110,13 @@ Разрешение Dhizuku не предоставлено Режим Dhizuku отключен - Проверить разрешение Список владельцев Список пользователей Список аккаунтов Shizuku не запущен. - Разрешение предоставлено (Shell) - Разрешение предоставлено (Root) Активировать владельца устройства Активировать рабочий профиль, принадлежащий организации Служба Shizuku отключена - Неверный binder - Подключить Shizuku - Shizuku отключен diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index ab987bb..f837cf3 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -44,7 +44,6 @@ Komutu Kopyala Paket Adı Mevcut Değil - Bilinmeyen Durum Kopyala Dosya Mevcut Değil G/Ç Hatası @@ -53,16 +52,18 @@ Tümünü İzin Ver Politika Kullan Hesap - Warning - Delete - Yes - No - Previous - Next - On - Off - Alias - Unknown error + + Warning + Delete + Yes + No + Previous + Next + On + Off + Alias + Unknown error + Permission denied Etkinleştirmek İçin Tıklayın @@ -111,19 +112,13 @@ Dhizuku mode disabled - İzni Kontrol Et Sahipleri Listele List users List accounts Shizuku Başlatılmadı. - İzin Verildi (Kabuk) - İzin Verildi (Root) Cihaz Sahibini Etkinleştir Kuruluş Profili Sahibini Etkinleştir Shizuku Hizmeti Bağlantısı Kesildi - Geçersiz Bağlayıcı - Shizuku\'ya Bağlan - Shizuku Bağlantısı Kesildi Sistem diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index bcdf09a..333c0e9 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -41,7 +41,6 @@ 功能开发中 选项 复制代码 - 未知状态 复制 文件不存在 IO异常 @@ -60,6 +59,7 @@ 关闭 别名 未知错误 + 无权限 点击以激活 @@ -106,19 +106,13 @@ Dhizuku模式已禁用 - 检查Shizuku 列出Owners 列出用户 列出账号 服务未启动 - 已授权(Shell) - 已授权(Root) 激活Device owner 激活由组织拥有的工作资料 Shizuku服务断开连接 - Shizuku未连接 - 无效Binder - 连接Shizuku 系统 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 286d2fb..c97bc09 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -44,7 +44,6 @@ Copy Command Package name Not exist - Unknown status Copy File not exist IO Exception @@ -63,6 +62,7 @@ Off Alias Unknown error + Permission denied Click to activate @@ -114,21 +114,15 @@ Dhizuku mode disabled Shizuku - Check permission List owners List users List accounts Shizuku not started. dpm set-device-owner com.bintianqi.owndroid/com.bintianqi.owndroid.Receiver dpm set-active-admin com.bintianqi.owndroid/com.bintianqi.owndroid.Receiver - Permission granted (Shell) - Permission granted (Root) Activate Device owner Activate organization-owned work profile Shizuku service disconnected - Invalid binder - Connect Shizuku - Shizuku disconnected System From 6d531f8fd5fac908071fc381f979c8228bfa4c58 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sat, 21 Dec 2024 12:44:28 +0800 Subject: [PATCH 05/12] Fix network logging Keep some files when packaging apk Improve retreiving security/network logs efficiency --- app/build.gradle.kts | 9 -- .../com/bintianqi/owndroid/MainActivity.kt | 2 +- .../main/java/com/bintianqi/owndroid/Utils.kt | 9 +- .../java/com/bintianqi/owndroid/dpm/DPM.kt | 114 +++++++----------- .../com/bintianqi/owndroid/dpm/Network.kt | 4 +- .../java/com/bintianqi/owndroid/dpm/System.kt | 7 +- 6 files changed, 62 insertions(+), 83 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9efb9d8..41a93de 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,15 +54,6 @@ android { compose = true aidl = true } - packaging { - resources { - excludes += "/META-INF/{AL2.0,LGPL2.1}" - excludes += "/META-INF/**.version" - excludes += "kotlin/**" - excludes += "**.bin" - excludes += "kotlin-tooling-metadata.json" - } - } androidResources { generateLocaleConfig = true } diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index 80a1cda..ff60f00 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -229,7 +229,7 @@ fun Home(activity: FragmentActivity, vm: MyViewModel) { composable(route = "NearbyStreamingPolicy") { NearbyStreamingPolicy(navCtrl) } composable(route = "LockTaskMode") { LockTaskMode(navCtrl) } composable(route = "CACert") { CACert(navCtrl) } - composable(route = "SecurityLogs") { SecurityLogging(navCtrl) } + composable(route = "SecurityLogging") { SecurityLogging(navCtrl) } composable(route = "DisableAccountManagement") { DisableAccountManagement(navCtrl) } composable(route = "SystemUpdatePolicy") { SystemUpdatePolicy(navCtrl) } composable(route = "InstallSystemUpdate") { InstallSystemUpdate(navCtrl) } diff --git a/app/src/main/java/com/bintianqi/owndroid/Utils.kt b/app/src/main/java/com/bintianqi/owndroid/Utils.kt index 6179a8b..bf3df3c 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Utils.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Utils.kt @@ -68,7 +68,8 @@ fun writeClipBoard(context: Context, string: String):Boolean{ lateinit var requestPermission: ActivityResultLauncher lateinit var exportFile: ActivityResultLauncher -val exportFilePath = MutableStateFlow(null) +var exportFilePath: String? = null +var isExportingSecurityOrNetworkLogs = false fun registerActivityResult(context: ComponentActivity){ getFile = context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult -> @@ -87,15 +88,19 @@ fun registerActivityResult(context: ComponentActivity){ exportFile = context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> val intentData = result.data ?: return@registerForActivityResult val uriData = intentData.data ?: return@registerForActivityResult - val path = exportFilePath.value ?: return@registerForActivityResult + val path = exportFilePath ?: return@registerForActivityResult context.contentResolver.openOutputStream(uriData).use { outStream -> if(outStream != null) { + if(isExportingSecurityOrNetworkLogs) outStream.write("[".encodeToByteArray()) File(path).inputStream().use { inStream -> inStream.copyTo(outStream) } + if(isExportingSecurityOrNetworkLogs) outStream.write("]".encodeToByteArray()) Toast.makeText(context.applicationContext, R.string.success, Toast.LENGTH_SHORT).show() } } + isExportingSecurityOrNetworkLogs = false + exportFilePath = null } } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt index 1529bd9..8640055 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt @@ -32,24 +32,16 @@ import com.rosan.dhizuku.api.Dhizuku import com.rosan.dhizuku.api.Dhizuku.binderWrapper import com.rosan.dhizuku.api.DhizukuBinderWrapper import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.addJsonObject -import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.add import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.decodeFromStream -import kotlinx.serialization.json.encodeToStream -import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.put +import kotlinx.serialization.json.putJsonArray import java.io.IOException import java.io.InputStream -import kotlin.io.path.inputStream -import kotlin.io.path.notExists -import kotlin.io.path.outputStream -import kotlin.io.path.writeText lateinit var createManagedProfile: ActivityResultLauncher lateinit var addDeviceAdmin: ActivityResultLauncher @@ -331,73 +323,59 @@ fun permissionList(): List{ @RequiresApi(26) fun handleNetworkLogs(context: Context, batchToken: Long) { val networkEvents = context.getDPM().retrieveNetworkLogs(context.getReceiver(), batchToken) ?: return - val file = context.filesDir.toPath().resolve("NetworkLogs.json") - if(file.notExists()) file.writeText("[]") + val file = context.filesDir.resolve("NetworkLogs.json") + val fileExist = file.exists() val json = Json { ignoreUnknownKeys = true; explicitNulls = false } - var events: MutableList - file.inputStream().use { - events = json.decodeFromStream(it) - } - networkEvents.forEach { event -> - try { - val dnsEvent = event as DnsEvent - val addresses = mutableListOf() - dnsEvent.inetAddresses.forEach { inetAddresses -> - addresses += inetAddresses.hostAddress + val buffer = file.bufferedWriter() + networkEvents.forEachIndexed { index, event -> + if(fileExist && index == 0) buffer.write(",") + val item = buildJsonObject { + if(VERSION.SDK_INT >= 28) put("id", event.id) + put("time", event.timestamp) + put("package", event.packageName) + if(event is DnsEvent) { + put("type", "dns") + put("host", event.hostname) + put("count", event.totalResolvedAddressCount) + putJsonArray("addresses") { + event.inetAddresses.forEach { inetAddresses -> + add(inetAddresses.hostAddress) + } + } + } + if(event is ConnectEvent) { + put("type", "connect") + put("address", event.inetAddress.hostAddress) + put("port", event.port) } - events += NetworkEventItem( - id = if(VERSION.SDK_INT >= 28) event.id else null, packageName = event.packageName - , timestamp = event.timestamp, type = "dns", hostName = dnsEvent.hostname, - hostAddresses = addresses, totalResolvedAddressCount = dnsEvent.totalResolvedAddressCount - ) - } catch(_: Exception) { - val connectEvent = event as ConnectEvent - events += NetworkEventItem( - id = if(VERSION.SDK_INT >= 28) event.id else null, packageName = event.packageName, timestamp = event.timestamp, type = "connect", - hostAddress = connectEvent.inetAddress.hostAddress, port = connectEvent.port - ) } + buffer.write(json.encodeToString(item)) + if(index < networkEvents.size - 1) buffer.write(",") } - file.outputStream().use { - json.encodeToStream(events, it) - } + buffer.close() } -@Serializable -data class NetworkEventItem( - val id: Long? = null, - @SerialName("package_name") val packageName: String, - val timestamp: Long, - val type: String, - @SerialName("address") val hostAddress: String? = null, - val port: Int? = null, - @SerialName("host_name") val hostName: String? = null, - @SerialName("count") val totalResolvedAddressCount: Int? = null, - @SerialName("addresses") val hostAddresses: List? = null -) - @RequiresApi(24) fun handleSecurityLogs(context: Context) { val file = context.filesDir.resolve("SecurityLogs.json") val json = Json { ignoreUnknownKeys = true; explicitNulls = false } - if(!file.exists()) file.writeText("[]") val securityEvents = context.getDPM().retrieveSecurityLogs(context.getReceiver()) securityEvents ?: return - val logArray = json.parseToJsonElement(file.readText()).jsonArray - val newLogs = buildJsonArray { - securityEvents.forEach { event -> - addJsonObject { - put("time_nanos", event.timeNanos) - put("tag", event.tag) - if(VERSION.SDK_INT >= 28) put("level", event.logLevel) - if(VERSION.SDK_INT >= 28) put("id", event.id) - parseSecurityEventData(event).let { if(it != null) put("data", it) } - } + val fileExist = file.exists() + val buffer = file.bufferedWriter() + securityEvents.forEachIndexed { index, event -> + if(fileExist && index == 0) buffer.write(",") + val item = buildJsonObject { + put("time_nanos", event.timeNanos) + put("tag", event.tag) + if(VERSION.SDK_INT >= 28) put("level", event.logLevel) + if(VERSION.SDK_INT >= 28) put("id", event.id) + parseSecurityEventData(event).let { if(it != null) put("data", it) } } + buffer.write(json.encodeToString(item)) + if(index < securityEvents.size - 1) buffer.write(",") } - file.outputStream().use { - json.encodeToStream(logArray + newLogs, it) - } + buffer.close() } @RequiresApi(24) @@ -409,7 +387,7 @@ fun parseSecurityEventData(event: SecurityLog.SecurityEvent): JsonElement? { val payload = event.data as Array<*> buildJsonObject { put("name", payload[0] as String) - put("start_time", payload[1] as Long) + put("time", payload[1] as Long) put("uid", payload[2] as Int) put("pid", payload[3] as Int) put("seinfo", payload[4] as String) @@ -456,7 +434,7 @@ fun parseSecurityEventData(event: SecurityLog.SecurityEvent): JsonElement? { put("admin", payload[0] as String) put("admin_user_id", payload[1] as Int) put("target_user_id", payload[2] as Int) - put("feature_mask", payload[3] as Int) + put("mask", payload[3] as Int) } } SecurityLog.TAG_KEYGUARD_DISMISSED -> null @@ -521,8 +499,8 @@ fun parseSecurityEventData(event: SecurityLog.SecurityEvent): JsonElement? { SecurityLog.TAG_PACKAGE_INSTALLED, SecurityLog.TAG_PACKAGE_UNINSTALLED, SecurityLog.TAG_PACKAGE_UPDATED -> { val payload = event.data as Array<*> buildJsonObject { - put("package_name", payload[0] as String) - put("version_code", payload[1] as Long) + put("name", payload[0] as String) + put("version", payload[1] as Long) put("user_id", payload[2] as Int) } } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt index f7cdd3e..b7c94f0 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt @@ -96,6 +96,7 @@ import com.bintianqi.owndroid.R import com.bintianqi.owndroid.exportFile import com.bintianqi.owndroid.exportFilePath import com.bintianqi.owndroid.formatFileSize +import com.bintianqi.owndroid.isExportingSecurityOrNetworkLogs import com.bintianqi.owndroid.selectedPackage import com.bintianqi.owndroid.ui.CheckBoxItem import com.bintianqi.owndroid.ui.FunctionItem @@ -585,7 +586,8 @@ fun NetworkLogging(navCtrl: NavHostController) { intent.addCategory(Intent.CATEGORY_OPENABLE) intent.setType("application/json") intent.putExtra(Intent.EXTRA_TITLE, "NetworkLogs.json") - exportFilePath.value = logFile.path + exportFilePath = logFile.path + isExportingSecurityOrNetworkLogs = true exportFile.launch(intent) }, enabled = fileSize > 0, diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt index 54e82ab..ab9dbf0 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt @@ -117,6 +117,7 @@ import com.bintianqi.owndroid.fileUriFlow import com.bintianqi.owndroid.formatFileSize import com.bintianqi.owndroid.getFile import com.bintianqi.owndroid.humanReadableDate +import com.bintianqi.owndroid.isExportingSecurityOrNetworkLogs import com.bintianqi.owndroid.prepareForNotification import com.bintianqi.owndroid.selectedPackage import com.bintianqi.owndroid.toggle @@ -1033,7 +1034,8 @@ fun SecurityLogging(navCtrl: NavHostController) { intent.addCategory(Intent.CATEGORY_OPENABLE) intent.setType("application/json") intent.putExtra(Intent.EXTRA_TITLE, "SecurityLogs.json") - exportFilePath.value = logFile.path + exportFilePath = logFile.path + isExportingSecurityOrNetworkLogs = true exportFile.launch(intent) }, enabled = fileSize > 0, @@ -1081,7 +1083,8 @@ fun SecurityLogging(navCtrl: NavHostController) { intent.addCategory(Intent.CATEGORY_OPENABLE) intent.setType("application/json") intent.putExtra(Intent.EXTRA_TITLE, "PreRebootSecurityLogs.json") - exportFilePath.value = preRebootSecurityLogs.path + exportFilePath = preRebootSecurityLogs.path + isExportingSecurityOrNetworkLogs = true exportFile.launch(intent) } }, From 873896ec10aa141c2374eb3b90daaec17c6bdda1 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sat, 21 Dec 2024 17:11:41 +0800 Subject: [PATCH 06/12] Refactor code of API Rename Automation API to API Add API section to READMEs --- Readme-en.md | 18 +++++ Readme.md | 18 +++++ app/src/main/AndroidManifest.xml | 21 ++---- .../com/bintianqi/owndroid/ApiReceiver.kt | 51 ++++++++++++++ .../bintianqi/owndroid/AutomationActivity.kt | 27 -------- .../bintianqi/owndroid/AutomationReceiver.kt | 44 ------------ .../com/bintianqi/owndroid/MainActivity.kt | 2 +- .../java/com/bintianqi/owndroid/Receiver.kt | 2 +- .../java/com/bintianqi/owndroid/Settings.kt | 69 ++++++++++--------- .../main/java/com/bintianqi/owndroid/Utils.kt | 5 -- app/src/main/res/values-ru/strings.xml | 5 +- app/src/main/res/values-tr/strings.xml | 5 +- app/src/main/res/values-zh-rCN/strings.xml | 4 +- app/src/main/res/values/strings.xml | 7 +- 14 files changed, 145 insertions(+), 133 deletions(-) create mode 100644 app/src/main/java/com/bintianqi/owndroid/ApiReceiver.kt delete mode 100644 app/src/main/java/com/bintianqi/owndroid/AutomationActivity.kt delete mode 100644 app/src/main/java/com/bintianqi/owndroid/AutomationReceiver.kt diff --git a/Readme-en.md b/Readme-en.md index 7dc6876..f36a9fb 100644 --- a/Readme-en.md +++ b/Readme-en.md @@ -43,6 +43,24 @@ Use Android Device owner privilege to manage your device. - Set screen timeout - ... +## API + +| ID | Description | Extras | Minimum Android version | +|:---------:|------------------|---------------------------------------|:-----------------------:| +| HIDE | Hide an app | `package`: package name of target app | | +| UNHIDE | Unhide an app | `package`: package name of target app | | +| SUSPEND | Suspend an app | `package`: package name of target app | 7 | +| UNSUSPEND | Unsuspend an app | `package`: package name of target app | 7 | +| LOCK | Lock screen | | | + +Use this API in adb shell +```shell +am broadcast -a com.bintianqi.owndroid.action. -n com.bintianqi.owndroid/.ApiReceiver --es key +# Example +am broadcast -a com.bintianqi.owndroid.action.HIDE -n com.bintianqi.owndroid/.ApiReceiver --es key abcdefg --es package com.example.app +``` +If the return value is 0, the operation is successful. + ## License [License.md](LICENSE.md) diff --git a/Readme.md b/Readme.md index 384a296..7d8e2db 100644 --- a/Readme.md +++ b/Readme.md @@ -43,6 +43,24 @@ - 设置屏幕超时 - ... +## API + +| ID | 描述 | Extras | 最小安卓版本 | +|:---------:|----------|--------------------|:------:| +| HIDE | 隐藏一个应用 | `package`: 目标应用的包名 | | +| UNHIDE | 取消隐藏一个应用 | `package`: 目标应用的包名 | | +| SUSPEND | 挂起一个应用 | `package`: 目标应用的包名 | 7 | +| UNSUSPEND | 取消挂起一个应用 | `package`: 目标应用的包名 | 7 | +| LOCK | 锁屏 | | | + +在adb shell中使用API +```shell +am broadcast -a com.bintianqi.owndroid.action. -n com.bintianqi.owndroid/.ApiReceiver --es key +# 示例 +am broadcast -a com.bintianqi.owndroid.action.HIDE -n com.bintianqi.owndroid/.ApiReceiver --es key abcdefg --es package com.example.app +``` +如果返回值为0,操作成功 + ## 许可证 [License.md](LICENSE.md) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9bfdde6..0ca96a2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -52,17 +52,6 @@ android:windowSoftInputMode="adjustResize|stateHidden" android:theme="@style/Theme.Transparent"> - - - - - + android:name=".ApiReceiver" + android:exported="true"> - + + + + + dpm.setApplicationHidden(receiver, app, true) + "com.bintianqi.owndroid.action.UNHIDE" -> dpm.setApplicationHidden(receiver, app, false) + "com.bintianqi.owndroid.action.SUSPEND" -> dpm.setPackagesSuspended(receiver, arrayOf(app), true).isEmpty() + "com.bintianqi.owndroid.action.UNSUSPEND" -> dpm.setPackagesSuspended(receiver, arrayOf(app), false).isEmpty() + "com.bintianqi.owndroid.action.LOCK" -> { dpm.lockNow(); true } + else -> { + Log.w(TAG, "Invalid action") + resultData = "Invalid action" + false + } + } + if(!ok) resultCode = 1 + } catch(e: Exception) { + e.printStackTrace() + val message = (e::class.qualifiedName ?: "Exception") + ": " + (e.message ?: "") + Log.w(TAG, message) + resultCode = 1 + resultData = message + } + } else { + Log.w(TAG, "Unauthorized") + resultCode = 1 + resultData = "Unauthorized" + } + } + companion object { + private const val TAG = "API" + } +} diff --git a/app/src/main/java/com/bintianqi/owndroid/AutomationActivity.kt b/app/src/main/java/com/bintianqi/owndroid/AutomationActivity.kt deleted file mode 100644 index 2245e64..0000000 --- a/app/src/main/java/com/bintianqi/owndroid/AutomationActivity.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.bintianqi.owndroid - -import android.app.AlertDialog -import android.content.Context -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.ui.platform.LocalContext - -class AutomationActivity: ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val result = handleTask(applicationContext, this.intent) - val sharedPrefs = applicationContext.getSharedPreferences("data", Context.MODE_PRIVATE) - if(sharedPrefs.getBoolean("automation_debug", false)) { - setContent { - AlertDialog.Builder(LocalContext.current) - .setMessage(result) - .setOnDismissListener { finish() } - .setPositiveButton(R.string.confirm) { _, _ -> finish() } - .show() - } - } else { - finish() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/bintianqi/owndroid/AutomationReceiver.kt b/app/src/main/java/com/bintianqi/owndroid/AutomationReceiver.kt deleted file mode 100644 index c151e09..0000000 --- a/app/src/main/java/com/bintianqi/owndroid/AutomationReceiver.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.bintianqi.owndroid - -import android.annotation.SuppressLint -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import com.bintianqi.owndroid.dpm.getDPM -import com.bintianqi.owndroid.dpm.getReceiver - -class AutomationReceiver: BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - handleTask(context, intent) - } -} - -@SuppressLint("NewApi") -fun handleTask(context: Context, intent: Intent): String { - val sharedPrefs = context.getSharedPreferences("data", Context.MODE_PRIVATE) - val key = sharedPrefs.getString("automation_key", "") - if(key == null || key != intent.getStringExtra("key")) { - return "Wrong key" - } - val operation = intent.getStringExtra("operation") - val dpm = context.getDPM() - val receiver = context.getReceiver() - val app = intent.getStringExtra("app") - val restriction = intent.getStringExtra("restriction") - try { - when(operation) { - "suspend" -> dpm.setPackagesSuspended(receiver, arrayOf(app), true) - "unsuspend" -> dpm.setPackagesSuspended(receiver, arrayOf(app), false) - "hide" -> dpm.setApplicationHidden(receiver, app, true) - "unhide" -> dpm.setApplicationHidden(receiver, app, false) - "lock" -> dpm.lockNow() - "reboot" -> dpm.reboot(receiver) - "addUserRestriction" -> dpm.addUserRestriction(receiver, restriction) - "clearUserRestriction" -> dpm.clearUserRestriction(receiver, restriction) - else -> return "Operation not defined" - } - } catch(e: Exception) { - return e.message ?: "Failed to get error message" - } - return "No error, or error is unhandled" -} diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index ff60f00..576fecb 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -299,7 +299,7 @@ fun Home(activity: FragmentActivity, vm: MyViewModel) { composable(route = "Options") { SettingsOptions(navCtrl) } composable(route = "Appearance") { Appearance(navCtrl, vm) } composable(route = "AuthSettings") { AuthSettings(navCtrl) } - composable(route = "Automation") { Automation(navCtrl) } + composable(route = "ApiSettings") { ApiSettings(navCtrl) } composable(route = "About") { About(navCtrl) } composable(route = "PackageSelector") { PackageSelector(navCtrl) } diff --git a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt index ffeb552..5823f21 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt @@ -82,7 +82,7 @@ class Receiver : DeviceAdminReceiver() { val installAppDone = MutableStateFlow(false) -class PackageInstallerReceiver:BroadcastReceiver(){ +class PackageInstallerReceiver: BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val toastText = when(intent.getIntExtra(EXTRA_STATUS, 999)){ STATUS_PENDING_USER_ACTION -> R.string.status_pending_action diff --git a/app/src/main/java/com/bintianqi/owndroid/Settings.kt b/app/src/main/java/com/bintianqi/owndroid/Settings.kt index 19f1924..ce8af0b 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Settings.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Settings.kt @@ -32,9 +32,11 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp +import androidx.core.content.edit import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import com.bintianqi.owndroid.ui.FunctionItem +import com.bintianqi.owndroid.ui.InfoCard import com.bintianqi.owndroid.ui.MyScaffold import com.bintianqi.owndroid.ui.SwitchItem import java.security.SecureRandom @@ -45,7 +47,7 @@ fun Settings(navCtrl: NavHostController) { FunctionItem(R.string.options, "", R.drawable.tune_fill0) { navCtrl.navigate("Options") } FunctionItem(R.string.appearance, "", R.drawable.format_paint_fill0) { navCtrl.navigate("Appearance") } FunctionItem(R.string.security, "", R.drawable.lock_fill0) { navCtrl.navigate("AuthSettings") } - FunctionItem(R.string.automation_api, "", R.drawable.apps_fill0) { navCtrl.navigate("Automation") } + FunctionItem(R.string.api, "", R.drawable.apps_fill0) { navCtrl.navigate("ApiSettings") } FunctionItem(R.string.about, "", R.drawable.info_fill0) { navCtrl.navigate("About") } } } @@ -154,43 +156,46 @@ fun AuthSettings(navCtrl: NavHostController) { } @Composable -fun Automation(navCtrl: NavHostController) { +fun ApiSettings(navCtrl: NavHostController) { val context = LocalContext.current val sharedPref = context.getSharedPreferences("data", Context.MODE_PRIVATE) - MyScaffold(R.string.automation_api, 8.dp, navCtrl) { - Spacer(Modifier.padding(vertical = 5.dp)) - var key by remember { mutableStateOf("") } - OutlinedTextField( - value = key, onValueChange = { key = it }, label = { Text("Key")}, - modifier = Modifier.fillMaxWidth(), - trailingIcon = { - IconButton( - onClick = { - val charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" - val sr = SecureRandom() - key = (1..20).map { charset[sr.nextInt(charset.length)] }.joinToString("") + MyScaffold(R.string.api, 8.dp, navCtrl) { + var enabled by remember { mutableStateOf(sharedPref.getBoolean("enable_api", false)) } + LaunchedEffect(enabled) { + sharedPref.edit { + putBoolean("enable_api", enabled) + if(!enabled) remove("api_key") + } + } + SwitchItem(R.string.enable, "", null, enabled, { enabled = it }, padding = false) + if(enabled) { + var key by remember { mutableStateOf("") } + OutlinedTextField( + value = key, onValueChange = { key = it }, label = { Text(stringResource(R.string.api_key)) }, + modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp), readOnly = true, + trailingIcon = { + IconButton( + onClick = { + val charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + val sr = SecureRandom() + key = (1..20).map { charset[sr.nextInt(charset.length)] }.joinToString("") + } + ) { + Icon(painter = painterResource(R.drawable.casino_fill0), contentDescription = "Random") } - ) { - Icon(painter = painterResource(R.drawable.casino_fill0), contentDescription = "Random") } + ) + Button( + modifier = Modifier.fillMaxWidth().padding(bottom = 10.dp), + onClick = { + sharedPref.edit().putString("api_key", key).apply() + Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + } + ) { + Text(stringResource(R.string.apply)) } - ) - Button( - modifier = Modifier.fillMaxWidth(), - onClick = { - sharedPref.edit().putString("automation_key", key).apply() - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() - }, - enabled = key.length >= 20 - ) { - Text(stringResource(R.string.apply)) + if(sharedPref.contains("api_key")) InfoCard(R.string.api_key_exist) } - SwitchItem( - R.string.automation_debug, "", null, - { sharedPref.getBoolean("automation_debug", false) }, - { sharedPref.edit().putBoolean("automation_debug", it).apply() }, - padding = false - ) } } diff --git a/app/src/main/java/com/bintianqi/owndroid/Utils.kt b/app/src/main/java/com/bintianqi/owndroid/Utils.kt index bf3df3c..879df73 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Utils.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Utils.kt @@ -27,7 +27,6 @@ import java.text.SimpleDateFormat import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle import java.util.Date import java.util.Locale @@ -143,7 +142,3 @@ fun parseTimestamp(timestamp: Long): String { val Long.humanReadableDate: String get() = SimpleDateFormat("yyyy/MM/dd", Locale.getDefault()).format(Date(this)) - -val Long.humanReadableTime: String - get() = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date(this)) - diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index c826eb1..4608d0d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -555,8 +555,9 @@ Блокировать при переключении в фоновый режим Очистить хранилище - API автоматизации - Режим отладки + + API key + The API key already exists, setting a new key will overwrite the current key. Отправлять уведомления diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index f837cf3..80d2991 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -550,8 +550,9 @@ Arka plana geçince kilitle Depolamayı temizle - Automation API - Debug mode + + API key + The API key already exists, setting a new key will overwrite the current key. Bildirim gönder diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 333c0e9..74350c2 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -540,8 +540,8 @@ 处于后台时锁定 清除存储空间 - 自动化API - 调试模式 + API密钥 + API密钥已存在,设置新的密钥将会覆盖当前密钥 发送通知 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c97bc09..f69941e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -63,6 +63,7 @@ Alias Unknown error Permission denied + API Click to activate @@ -553,9 +554,9 @@ Authenticate Lock when switch to background Clear storage - - Automation API - Debug mode + + API key + The API key already exists, setting a new key will overwrite the current key. Post notifications From 84c1dff9e6f3d85455adc59352ac8dc8be52ec71 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sat, 21 Dec 2024 20:07:32 +0800 Subject: [PATCH 07/12] Optimize start/stop lock task mode flow Optimize Package selector --- app/src/main/AndroidManifest.xml | 4 - .../com/bintianqi/owndroid/MainActivity.kt | 10 +- .../com/bintianqi/owndroid/MyViewModel.kt | 2 + .../bintianqi/owndroid/NotificationUtils.kt | 29 ++ .../{PkgSelector.kt => PackageSelector.kt} | 25 +- .../java/com/bintianqi/owndroid/Receiver.kt | 50 ++-- .../main/java/com/bintianqi/owndroid/Utils.kt | 20 -- .../bintianqi/owndroid/dpm/Applications.kt | 269 ++++++++---------- .../com/bintianqi/owndroid/dpm/Network.kt | 12 +- .../com/bintianqi/owndroid/dpm/Password.kt | 3 +- .../java/com/bintianqi/owndroid/dpm/System.kt | 57 +--- .../com/bintianqi/owndroid/ui/Components.kt | 16 -- app/src/main/res/values-ru/strings.xml | 1 + app/src/main/res/values-tr/strings.xml | 1 + app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 16 files changed, 216 insertions(+), 285 deletions(-) create mode 100644 app/src/main/java/com/bintianqi/owndroid/NotificationUtils.kt rename app/src/main/java/com/bintianqi/owndroid/{PkgSelector.kt => PackageSelector.kt} (94%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0ca96a2..3d7b8ee 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -91,10 +91,6 @@ android:description="@string/app_name" android:permission="android.permission.BIND_DEVICE_ADMIN"> - - diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index 576fecb..60fe69b 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -37,7 +37,6 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -188,7 +187,6 @@ fun Home(activity: FragmentActivity, vm: MyViewModel) { val receiver = context.getReceiver() val sharedPref = context.getSharedPreferences("data", Context.MODE_PRIVATE) val focusMgr = LocalFocusManager.current - val dialogStatus = remember { mutableIntStateOf(0) } val backToHome by backToHomeStateFlow.collectAsState() val lifecycleOwner = LocalLifecycleOwner.current LaunchedEffect(backToHome) { @@ -227,7 +225,7 @@ fun Home(activity: FragmentActivity, vm: MyViewModel) { composable(route = "PermissionPolicy") { PermissionPolicy(navCtrl) } composable(route = "MTEPolicy") { MTEPolicy(navCtrl) } composable(route = "NearbyStreamingPolicy") { NearbyStreamingPolicy(navCtrl) } - composable(route = "LockTaskMode") { LockTaskMode(navCtrl) } + composable(route = "LockTaskMode") { LockTaskMode(navCtrl, vm) } composable(route = "CACert") { CACert(navCtrl) } composable(route = "SecurityLogging") { SecurityLogging(navCtrl) } composable(route = "DisableAccountManagement") { DisableAccountManagement(navCtrl) } @@ -241,7 +239,7 @@ fun Home(activity: FragmentActivity, vm: MyViewModel) { composable(route = "MinWifiSecurityLevel") { WifiSecurityLevel(navCtrl) } composable(route = "WifiSsidPolicy") { WifiSsidPolicy(navCtrl) } composable(route = "PrivateDNS") { PrivateDNS(navCtrl) } - composable(route = "AlwaysOnVpn") { AlwaysOnVPNPackage(navCtrl) } + composable(route = "AlwaysOnVpn") { AlwaysOnVPNPackage(navCtrl, vm) } composable(route = "RecommendedGlobalProxy") { RecommendedGlobalProxy(navCtrl) } composable(route = "NetworkLog") { NetworkLogging(navCtrl) } composable(route = "WifiAuthKeypair") { WifiAuthKeypair(navCtrl) } @@ -255,7 +253,7 @@ fun Home(activity: FragmentActivity, vm: MyViewModel) { composable(route = "IntentFilter") { IntentFilter(navCtrl) } composable(route = "DeleteWorkProfile") { DeleteWorkProfile(navCtrl) } - composable(route = "Applications") { ApplicationManage(navCtrl, dialogStatus) } + composable(route = "Applications") { ApplicationManage(navCtrl, vm) } composable(route = "UserRestriction") { UserRestriction(navCtrl) } composable(route = "UR-Internet") { @@ -302,7 +300,7 @@ fun Home(activity: FragmentActivity, vm: MyViewModel) { composable(route = "ApiSettings") { ApiSettings(navCtrl) } composable(route = "About") { About(navCtrl) } - composable(route = "PackageSelector") { PackageSelector(navCtrl) } + composable(route = "PackageSelector") { PackageSelector(navCtrl, vm) } composable( route = "Authenticate", diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt index b4af388..932dc54 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -10,6 +10,8 @@ import kotlinx.coroutines.launch class MyViewModel: ViewModel() { val theme = MutableStateFlow(ThemeSettings()) + val installedPackages = mutableListOf() + val selectedPackage = MutableStateFlow("") val shizukuBinder = MutableStateFlow(null) var initialized = false diff --git a/app/src/main/java/com/bintianqi/owndroid/NotificationUtils.kt b/app/src/main/java/com/bintianqi/owndroid/NotificationUtils.kt new file mode 100644 index 0000000..13a2033 --- /dev/null +++ b/app/src/main/java/com/bintianqi/owndroid/NotificationUtils.kt @@ -0,0 +1,29 @@ +package com.bintianqi.owndroid + +import android.Manifest +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build + +/** + * ### Notification channels + * - LockTaskMode + * + * ### Notification IDs + * - 1: Stop lock task mode + */ +object NotificationUtils { + fun checkPermission(context: Context): Boolean { + return if(Build.VERSION.SDK_INT >= 33) + context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED + else false + } + fun registerChannels(context: Context) { + if(Build.VERSION.SDK_INT < 26) return + val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val channel = NotificationChannel("LockTaskMode", context.getString(R.string.lock_task_mode), NotificationManager.IMPORTANCE_HIGH) + nm.createNotificationChannel(channel) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bintianqi/owndroid/PkgSelector.kt b/app/src/main/java/com/bintianqi/owndroid/PackageSelector.kt similarity index 94% rename from app/src/main/java/com/bintianqi/owndroid/PkgSelector.kt rename to app/src/main/java/com/bintianqi/owndroid/PackageSelector.kt index 2f8e542..fef2530 100644 --- a/app/src/main/java/com/bintianqi/owndroid/PkgSelector.kt +++ b/app/src/main/java/com/bintianqi/owndroid/PackageSelector.kt @@ -54,24 +54,19 @@ import com.bintianqi.owndroid.ui.NavIcon import com.google.accompanist.drawablepainter.rememberDrawablePainter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -private data class PkgInfo( +data class PackageInfo( val pkgName: String, val label: String, val icon: Drawable, val system: Boolean ) -private val pkgs = mutableListOf() - -val selectedPackage = MutableStateFlow("") - @OptIn(ExperimentalMaterial3Api::class) @Composable -fun PackageSelector(navCtrl: NavHostController) { +fun PackageSelector(navCtrl: NavHostController, vm: MyViewModel) { val context = LocalContext.current val pm = context.packageManager val apps = pm.getInstalledApplications(0) @@ -88,9 +83,9 @@ fun PackageSelector(navCtrl: NavHostController) { show = false progress = 0 hideProgress = false - pkgs.clear() + vm.installedPackages.clear() for(pkg in apps) { - pkgs += PkgInfo( + vm.installedPackages += PackageInfo( pkg.packageName, pkg.loadLabel(pm).toString(), pkg.loadIcon(pm), (pkg.flags and ApplicationInfo.FLAG_SYSTEM) != 0 ) @@ -181,14 +176,14 @@ fun PackageSelector(navCtrl: NavHostController) { } } if(show) { - items(pkgs) { + items(vm.installedPackages) { if(system == it.system) { if(search != "") { if(it.pkgName.contains(search, ignoreCase = true) || it.label.contains(search, ignoreCase = true)) { - PackageItem(it, navCtrl) + PackageItem(it, navCtrl, vm) } } else { - PackageItem(it, navCtrl) + PackageItem(it, navCtrl, vm) } } } @@ -201,13 +196,13 @@ fun PackageSelector(navCtrl: NavHostController) { } } LaunchedEffect(Unit) { - if(pkgs.size == 0) { getPkgList() } + if(vm.installedPackages.isEmpty()) { getPkgList() } } } } @Composable -private fun PackageItem(pkg: PkgInfo, navCtrl: NavHostController) { +private fun PackageItem(pkg: PackageInfo, navCtrl: NavHostController, vm: MyViewModel) { val focusMgr = LocalFocusManager.current Row( verticalAlignment = Alignment.CenterVertically, @@ -215,7 +210,7 @@ private fun PackageItem(pkg: PkgInfo, navCtrl: NavHostController) { .fillMaxWidth() .clickable{ focusMgr.clearFocus() - selectedPackage.value = pkg.pkgName + vm.selectedPackage.value = pkg.pkgName navCtrl.navigateUp() } .padding(horizontal = 8.dp, vertical = 10.dp) diff --git a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt index 5823f21..bb9cb1f 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt @@ -1,9 +1,10 @@ package com.bintianqi.owndroid -import android.annotation.SuppressLint import android.app.NotificationManager +import android.app.PendingIntent import android.app.admin.DeviceAdminReceiver import android.content.BroadcastReceiver +import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.pm.PackageInstaller.EXTRA_STATUS @@ -21,8 +22,7 @@ import android.os.Build.VERSION import android.os.PersistableBundle import android.util.Log import android.widget.Toast -import com.bintianqi.owndroid.dpm.getDPM -import com.bintianqi.owndroid.dpm.getReceiver +import androidx.core.app.NotificationCompat import com.bintianqi.owndroid.dpm.handleNetworkLogs import com.bintianqi.owndroid.dpm.handleSecurityLogs import com.bintianqi.owndroid.dpm.isDeviceAdmin @@ -35,6 +35,17 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch class Receiver : DeviceAdminReceiver() { + override fun onReceive(context: Context, intent: Intent) { + super.onReceive(context, intent) + if(VERSION.SDK_INT >= 26 && intent.action == "com.bintianqi.owndroid.action.STOP_LOCK_TASK_MODE") { + val dpm = getManager(context) + val receiver = ComponentName(context, this::class.java) + val packages = dpm.getLockTaskPackages(receiver) + dpm.setLockTaskPackages(receiver, arrayOf()) + dpm.setLockTaskPackages(receiver, packages) + } + } + override fun onEnabled(context: Context, intent: Intent) { super.onEnabled(context, intent) context.toggleInstallAppActivity() @@ -78,6 +89,26 @@ class Receiver : DeviceAdminReceiver() { sp.edit().putBoolean("dhizuku", false).apply() context.toggleInstallAppActivity() } + + override fun onLockTaskModeEntering(context: Context, intent: Intent, pkg: String) { + super.onLockTaskModeEntering(context, intent, pkg) + NotificationUtils.registerChannels(context) + val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val intent = Intent(context, this::class.java).apply { action = "com.bintianqi.owndroid.action.STOP_LOCK_TASK_MODE" } + val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) + val builder = NotificationCompat.Builder(context, "LockTaskMode") + .setContentTitle(context.getText(R.string.lock_task_mode)) + .setSmallIcon(R.drawable.lock_fill0) + .addAction(NotificationCompat.Action.Builder(null, context.getString(R.string.stop), pendingIntent).build()) + .setPriority(NotificationCompat.PRIORITY_HIGH) + nm.notify(1, builder.build()) + } + + override fun onLockTaskModeExiting(context: Context, intent: Intent) { + super.onLockTaskModeExiting(context, intent) + val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + nm.cancel(1) + } } val installAppDone = MutableStateFlow(false) @@ -105,16 +136,3 @@ class PackageInstallerReceiver: BroadcastReceiver() { } } } - -class StopLockTaskModeReceiver: BroadcastReceiver() { - @SuppressLint("NewApi") - override fun onReceive(context: Context, intent: Intent) { - val dpm = context.getDPM() - val receiver = context.getReceiver() - val packages = dpm.getLockTaskPackages(receiver) - dpm.setLockTaskPackages(receiver, arrayOf()) - dpm.setLockTaskPackages(receiver, packages) - val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - nm.cancel(1) - } -} diff --git a/app/src/main/java/com/bintianqi/owndroid/Utils.kt b/app/src/main/java/com/bintianqi/owndroid/Utils.kt index 879df73..efdd902 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Utils.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Utils.kt @@ -1,15 +1,12 @@ package com.bintianqi.owndroid -import android.Manifest import android.app.admin.DevicePolicyManager import android.content.ClipData import android.content.ClipboardManager import android.content.ComponentName import android.content.Context import android.content.Intent -import android.content.pm.PackageManager import android.net.Uri -import android.os.Build.VERSION import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher @@ -65,7 +62,6 @@ fun writeClipBoard(context: Context, string: String):Boolean{ return true } -lateinit var requestPermission: ActivityResultLauncher lateinit var exportFile: ActivityResultLauncher var exportFilePath: String? = null var isExportingSecurityOrNetworkLogs = false @@ -83,7 +79,6 @@ fun registerActivityResult(context: ComponentActivity){ backToHomeStateFlow.value = true } } - requestPermission = context.registerForActivityResult(ActivityResultContracts.RequestPermission()) { permissionGranted.value = it } exportFile = context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> val intentData = result.data ?: return@registerForActivityResult val uriData = intentData.data ?: return@registerForActivityResult @@ -103,21 +98,6 @@ fun registerActivityResult(context: ComponentActivity){ } } -val permissionGranted = MutableStateFlow(null) - -suspend fun prepareForNotification(context: Context, action: ()->Unit) { - if(VERSION.SDK_INT >= 33) { - if(context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { - action() - } else { - requestPermission.launch(Manifest.permission.POST_NOTIFICATIONS) - permissionGranted.collect { if(it == true) action() } - } - } else { - action() - } -} - fun formatFileSize(bytes: Long): String { val kb = 1024 val mb = kb * 1024 diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt index 3ebb4c7..a395680 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -50,7 +50,6 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableIntState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -74,37 +73,37 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat.startActivity +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.bintianqi.owndroid.InstallAppActivity +import com.bintianqi.owndroid.MyViewModel import com.bintianqi.owndroid.PackageInstallerReceiver import com.bintianqi.owndroid.R import com.bintianqi.owndroid.fileUriFlow import com.bintianqi.owndroid.getFile -import com.bintianqi.owndroid.selectedPackage import com.bintianqi.owndroid.ui.Animations +import com.bintianqi.owndroid.ui.FunctionItem import com.bintianqi.owndroid.ui.InfoCard -import com.bintianqi.owndroid.ui.Information import com.bintianqi.owndroid.ui.ListItem import com.bintianqi.owndroid.ui.NavIcon import com.bintianqi.owndroid.ui.RadioButtonItem -import com.bintianqi.owndroid.ui.FunctionItem import com.bintianqi.owndroid.ui.SwitchItem import java.util.concurrent.Executors @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ApplicationManage(navCtrl:NavHostController, dialogStatus: MutableIntState) { +fun ApplicationManage(navCtrl:NavHostController, vm: MyViewModel) { val focusMgr = LocalFocusManager.current val localNavCtrl = rememberNavController() var pkgName by rememberSaveable { mutableStateOf("") } - val updatePackage by selectedPackage.collectAsState() + val updatePackage by vm.selectedPackage.collectAsStateWithLifecycle() LaunchedEffect(updatePackage) { if(updatePackage != "") { pkgName = updatePackage - selectedPackage.value = "" + vm.selectedPackage.value = "" } } Scaffold( @@ -145,9 +144,7 @@ fun ApplicationManage(navCtrl:NavHostController, dialogStatus: MutableIntState) popEnterTransition = Animations.navHostPopEnterTransition, popExitTransition = Animations.navHostPopExitTransition ) { - composable(route = "Home") { - Home(localNavCtrl, pkgName, dialogStatus) - } + composable(route = "Home") { Home(localNavCtrl, pkgName) } composable(route = "UserControlDisabled") { UserCtrlDisabledPkg(pkgName) } composable(route = "PermissionManage") { PermissionManage(pkgName) } composable(route = "CrossProfilePackage") { CrossProfilePkg(pkgName) } @@ -160,23 +157,11 @@ fun ApplicationManage(navCtrl:NavHostController, dialogStatus: MutableIntState) composable(route = "UninstallApp") { UninstallApp(pkgName) } } } - when(dialogStatus.intValue) { - 0 -> {} - 1 -> EnableSystemAppDialog(dialogStatus, pkgName) - 2 -> ClearAppDataDialog(dialogStatus, pkgName) - 3 -> DefaultDialerAppDialog(dialogStatus, pkgName) - } - LaunchedEffect(dialogStatus.intValue) { - focusMgr.clearFocus() - } } @Composable -private fun Home( - navCtrl:NavHostController, - pkgName: String, - dialogStatus: MutableIntState -) { +private fun Home(navCtrl:NavHostController, pkgName: String) { + var dialogStatus by remember { mutableIntStateOf(0) } val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -190,7 +175,6 @@ private fun Home( hide = dpm.isApplicationHidden(receiver, pkgName) var blockUninstall by remember { mutableStateOf(false) } blockUninstall = dpm.isUninstallBlocked(receiver,pkgName) - var appControlDialog by remember { mutableStateOf(false) } var appControlAction by remember { mutableIntStateOf(0) } val focusMgr = LocalFocusManager.current val appControl: (Boolean) -> Unit = { @@ -226,20 +210,20 @@ private fun Home( title = R.string.suspend, desc = "", icon = R.drawable.block_fill0, state = suspend, onCheckedChange = { appControlAction = 1; appControl(it) }, - onClickBlank = { appControlAction = 1; appControlDialog = true } + onClickBlank = { appControlAction = 1; dialogStatus = 4 } ) } SwitchItem( title = R.string.hide, desc = stringResource(R.string.isapphidden_desc), icon = R.drawable.visibility_off_fill0, state = hide, onCheckedChange = { appControlAction = 2; appControl(it) }, - onClickBlank = { appControlAction = 2; appControlDialog = true } + onClickBlank = { appControlAction = 2; dialogStatus = 4 } ) SwitchItem( title = R.string.block_uninstall, desc = "", icon = R.drawable.delete_forever_fill0, state = blockUninstall, onCheckedChange = { appControlAction = 3; appControl(it) }, - onClickBlank = { appControlAction = 3; appControlDialog = true } + onClickBlank = { appControlAction = 3; dialogStatus = 4 } ) if((VERSION.SDK_INT >= 33 && profileOwner) || (VERSION.SDK_INT >= 30 && deviceOwner)) { FunctionItem(R.string.ucd, "", R.drawable.do_not_touch_fill0) { navCtrl.navigate("UserControlDisabled") } @@ -259,32 +243,124 @@ private fun Home( FunctionItem(R.string.permitted_accessibility_services, "", R.drawable.settings_accessibility_fill0) { navCtrl.navigate("Accessibility") } FunctionItem(R.string.permitted_ime, "", R.drawable.keyboard_fill0) { navCtrl.navigate("IME") } FunctionItem(R.string.enable_system_app, "", R.drawable.enable_fill0) { - if(pkgName != "") dialogStatus.intValue = 1 + if(pkgName != "") dialogStatus = 1 } if(VERSION.SDK_INT >= 28 && deviceOwner) { FunctionItem(R.string.keep_uninstalled_packages, "", R.drawable.delete_fill0) { navCtrl.navigate("KeepUninstalled") } } if(VERSION.SDK_INT >= 28) { FunctionItem(R.string.clear_app_storage, "", R.drawable.mop_fill0) { - if(pkgName != "") dialogStatus.intValue = 2 + if(pkgName != "") dialogStatus = 2 } } FunctionItem(R.string.install_app, "", R.drawable.install_mobile_fill0) { navCtrl.navigate("InstallApp") } FunctionItem(R.string.uninstall_app, "", R.drawable.delete_fill0) { navCtrl.navigate("UninstallApp") } if(VERSION.SDK_INT >= 34 && (deviceOwner || dpm.isOrgProfile(receiver))) { FunctionItem(R.string.set_default_dialer, "", R.drawable.call_fill0) { - if(pkgName != "") dialogStatus.intValue = 3 + if(pkgName != "") dialogStatus = 3 } } Spacer(Modifier.padding(vertical = 30.dp)) LaunchedEffect(Unit) { fileUriFlow.value = Uri.parse("") } } - if(appControlDialog) { + if(dialogStatus == 1) AlertDialog( + title = { Text(stringResource(R.string.enable_system_app)) }, + text = { + Text(stringResource(R.string.enable_system_app_desc) + "\n" + pkgName) + }, + onDismissRequest = { dialogStatus = 0 }, + dismissButton = { + TextButton(onClick = { dialogStatus = 0 }) { + Text(stringResource(R.string.cancel)) + } + }, + confirmButton = { + TextButton( + onClick = { + try { + dpm.enableSystemApp(receiver, pkgName) + Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + } catch(_: IllegalArgumentException) { + Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show() + } + dialogStatus = 0 + } + ) { + Text(stringResource(R.string.confirm)) + } + }, + modifier = Modifier.fillMaxWidth() + ) + if(dialogStatus == 2 && VERSION.SDK_INT >= 28) AlertDialog( + title = { Text(text = stringResource(R.string.clear_app_storage)) }, + text = { + Text(stringResource(R.string.app_storage_will_be_cleared) + "\n" + pkgName) + }, + confirmButton = { + TextButton( + onClick = { + val executor = Executors.newCachedThreadPool() + val onClear = DevicePolicyManager.OnClearApplicationUserDataListener { pkg: String, succeed: Boolean -> + Looper.prepare() + val toastText = + if(pkg!="") { "$pkg\n" }else{ "" } + + context.getString(R.string.clear_data) + + context.getString(if(succeed) R.string.success else R.string.failed ) + Toast.makeText(context, toastText, Toast.LENGTH_SHORT).show() + Looper.loop() + } + dpm.clearApplicationUserData(receiver, pkgName, executor, onClear) + dialogStatus = 0 + }, + colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error) + ) { + Text(text = stringResource(R.string.clear)) + } + }, + dismissButton = { + TextButton( + onClick = { dialogStatus = 0 } + ) { + Text(text = stringResource(R.string.cancel)) + } + }, + onDismissRequest = { dialogStatus = 0 }, + modifier = Modifier.fillMaxWidth() + ) + if(dialogStatus == 3 && VERSION.SDK_INT >= 34) AlertDialog( + title = { Text(stringResource(R.string.set_default_dialer)) }, + text = { + Text(stringResource(R.string.app_will_be_default_dialer) + "\n" + pkgName) + }, + onDismissRequest = { dialogStatus = 0 }, + dismissButton = { + TextButton(onClick = { dialogStatus = 0 }) { + Text(stringResource(R.string.cancel)) + } + }, + confirmButton = { + TextButton( + onClick = { + try{ + dpm.setDefaultDialerApplication(pkgName) + Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + } catch(_: IllegalArgumentException) { + Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show() + } + dialogStatus = 0 + } + ) { + Text(stringResource(R.string.confirm)) + } + }, + modifier = Modifier.fillMaxWidth() + ) + if(dialogStatus == 4) { LaunchedEffect(Unit) { focusMgr.clearFocus() } AlertDialog( - onDismissRequest = { appControlDialog = false }, + onDismissRequest = { dialogStatus = 0 }, title = { Text( text = stringResource( @@ -316,7 +392,7 @@ private fun Home( TextButton( onClick = { appControl(true) - appControlDialog = false + dialogStatus = 0 } ) { Text(text = stringResource(R.string.enable)) @@ -326,7 +402,7 @@ private fun Home( TextButton( onClick = { appControl(false) - appControlDialog = false + dialogStatus = 0 } ) { Text(text = stringResource(R.string.disable)) @@ -334,6 +410,7 @@ private fun Home( } ) } + LaunchedEffect(dialogStatus) { focusMgr.clearFocus() } } @@ -697,9 +774,7 @@ private fun PermittedAccessibility(pkgName: String) { } } } - Information { - Text(stringResource(R.string.system_accessibility_always_allowed)) - } + InfoCard(R.string.system_accessibility_always_allowed) Spacer(Modifier.padding(vertical = 30.dp)) } } @@ -755,9 +830,7 @@ private fun PermittedIME(pkgName: String) { } } } - Information { - Text(stringResource(R.string.system_ime_always_allowed)) - } + InfoCard(R.string.system_ime_always_allowed) Spacer(Modifier.padding(vertical = 30.dp)) } } @@ -892,117 +965,3 @@ private fun InstallApp() { } } } - -@SuppressLint("NewApi") -@Composable -private fun ClearAppDataDialog(status: MutableIntState, pkgName: String) { - val context = LocalContext.current - val dpm = context.getDPM() - val receiver = context.getReceiver() - AlertDialog( - title = { Text(text = stringResource(R.string.clear_app_storage)) }, - text = { - Text(stringResource(R.string.app_storage_will_be_cleared) + "\n" + pkgName) - }, - confirmButton = { - TextButton( - onClick = { - val executor = Executors.newCachedThreadPool() - val onClear = DevicePolicyManager.OnClearApplicationUserDataListener { pkg: String, succeed: Boolean -> - Looper.prepare() - val toastText = - if(pkg!="") { "$pkg\n" }else{ "" } + - context.getString(R.string.clear_data) + - context.getString(if(succeed) R.string.success else R.string.failed ) - Toast.makeText(context, toastText, Toast.LENGTH_SHORT).show() - Looper.loop() - } - dpm.clearApplicationUserData(receiver, pkgName, executor, onClear) - status.intValue = 0 - }, - colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error) - ) { - Text(text = stringResource(R.string.clear)) - } - }, - dismissButton = { - TextButton( - onClick = { status.intValue = 0 } - ) { - Text(text = stringResource(R.string.cancel)) - } - }, - onDismissRequest = { status.intValue = 0 }, - modifier = Modifier.fillMaxWidth() - ) -} - -@SuppressLint("NewApi") -@Composable -private fun DefaultDialerAppDialog(status: MutableIntState, pkgName: String) { - val context = LocalContext.current - val dpm = context.getDPM() - AlertDialog( - title = { Text(stringResource(R.string.set_default_dialer)) }, - text = { - Text(stringResource(R.string.app_will_be_default_dialer) + "\n" + pkgName) - }, - onDismissRequest = { status.intValue = 0 }, - dismissButton = { - TextButton(onClick = { status.intValue = 0 }) { - Text(stringResource(R.string.cancel)) - } - }, - confirmButton = { - TextButton( - onClick = { - try{ - dpm.setDefaultDialerApplication(pkgName) - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() - }catch(_: IllegalArgumentException) { - Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show() - } - status.intValue = 0 - } - ) { - Text(stringResource(R.string.confirm)) - } - }, - modifier = Modifier.fillMaxWidth() - ) -} - -@Composable -private fun EnableSystemAppDialog(status: MutableIntState, pkgName: String) { - val context = LocalContext.current - val dpm = context.getDPM() - val receiver = context.getReceiver() - AlertDialog( - title = { Text(stringResource(R.string.enable_system_app)) }, - text = { - Text(stringResource(R.string.enable_system_app_desc) + "\n" + pkgName) - }, - onDismissRequest = { status.intValue = 0 }, - dismissButton = { - TextButton(onClick = { status.intValue = 0 }) { - Text(stringResource(R.string.cancel)) - } - }, - confirmButton = { - TextButton( - onClick = { - try { - dpm.enableSystemApp(receiver, pkgName) - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() - } catch(_: IllegalArgumentException) { - Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show() - } - status.intValue = 0 - } - ) { - Text(stringResource(R.string.confirm)) - } - }, - modifier = Modifier.fillMaxWidth() - ) -} diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt index b7c94f0..e87d024 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt @@ -92,12 +92,12 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.navigation.NavHostController +import com.bintianqi.owndroid.MyViewModel import com.bintianqi.owndroid.R import com.bintianqi.owndroid.exportFile import com.bintianqi.owndroid.exportFilePath import com.bintianqi.owndroid.formatFileSize import com.bintianqi.owndroid.isExportingSecurityOrNetworkLogs -import com.bintianqi.owndroid.selectedPackage import com.bintianqi.owndroid.ui.CheckBoxItem import com.bintianqi.owndroid.ui.FunctionItem import com.bintianqi.owndroid.ui.InfoCard @@ -400,7 +400,7 @@ fun PrivateDNS(navCtrl: NavHostController) { @SuppressLint("NewApi") @Composable -fun AlwaysOnVPNPackage(navCtrl: NavHostController) { +fun AlwaysOnVPNPackage(navCtrl: NavHostController, vm: MyViewModel) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() @@ -409,11 +409,11 @@ fun AlwaysOnVPNPackage(navCtrl: NavHostController) { val focusMgr = LocalFocusManager.current val refresh = { pkgName = dpm.getAlwaysOnVpnPackage(receiver) ?: "" } LaunchedEffect(Unit) { refresh() } - val updatePackage by selectedPackage.collectAsState() + val updatePackage by vm.selectedPackage.collectAsState() LaunchedEffect(updatePackage) { - if(selectedPackage.value != "") { - pkgName = selectedPackage.value - selectedPackage.value = "" + if(updatePackage != "") { + pkgName = updatePackage + vm.selectedPackage.value = "" } } val setAlwaysOnVpn: (String?, Boolean)->Boolean = { vpnPkg: String?, lockdownEnabled: Boolean -> diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt index 4e486ec..1941b00 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt @@ -75,7 +75,6 @@ import com.bintianqi.owndroid.ui.CardItem import com.bintianqi.owndroid.ui.CheckBoxItem import com.bintianqi.owndroid.ui.FunctionItem import com.bintianqi.owndroid.ui.InfoCard -import com.bintianqi.owndroid.ui.Information import com.bintianqi.owndroid.ui.MyScaffold import com.bintianqi.owndroid.ui.RadioButtonItem import com.bintianqi.owndroid.yesOrNo @@ -303,7 +302,7 @@ fun ResetPasswordToken(navCtrl: NavHostController) { } } Spacer(Modifier.padding(vertical = 5.dp)) - Information{ Text(stringResource(R.string.activate_token_not_required_when_no_password)) } + InfoCard(R.string.activate_token_not_required_when_no_password) } } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt index ab9dbf0..e622287 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt @@ -3,9 +3,6 @@ package com.bintianqi.owndroid.dpm import android.annotation.SuppressLint import android.app.ActivityOptions import android.app.AlertDialog -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent import android.app.admin.DevicePolicyManager.FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY import android.app.admin.DevicePolicyManager.InstallSystemUpdateCallback import android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_BLOCK_ACTIVITY_START_IN_TASK @@ -107,10 +104,11 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp -import androidx.core.app.NotificationCompat +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController +import com.bintianqi.owndroid.MyViewModel +import com.bintianqi.owndroid.NotificationUtils import com.bintianqi.owndroid.R -import com.bintianqi.owndroid.StopLockTaskModeReceiver import com.bintianqi.owndroid.exportFile import com.bintianqi.owndroid.exportFilePath import com.bintianqi.owndroid.fileUriFlow @@ -118,13 +116,10 @@ import com.bintianqi.owndroid.formatFileSize import com.bintianqi.owndroid.getFile import com.bintianqi.owndroid.humanReadableDate import com.bintianqi.owndroid.isExportingSecurityOrNetworkLogs -import com.bintianqi.owndroid.prepareForNotification -import com.bintianqi.owndroid.selectedPackage import com.bintianqi.owndroid.toggle import com.bintianqi.owndroid.ui.CheckBoxItem import com.bintianqi.owndroid.ui.FunctionItem import com.bintianqi.owndroid.ui.InfoCard -import com.bintianqi.owndroid.ui.Information import com.bintianqi.owndroid.ui.ListItem import com.bintianqi.owndroid.ui.MyScaffold import com.bintianqi.owndroid.ui.RadioButtonItem @@ -522,9 +517,7 @@ fun ChangeTimeZone(navCtrl: NavHostController) { Text(stringResource(R.string.apply)) } Spacer(Modifier.padding(vertical = 10.dp)) - Information { - Text(stringResource(R.string.disable_auto_time_zone_before_set)) - } + InfoCard(R.string.disable_auto_time_zone_before_set) } if(dialog) AlertDialog( text = { @@ -700,12 +693,11 @@ fun NearbyStreamingPolicy(navCtrl: NavHostController) { @SuppressLint("NewApi") @Composable -fun LockTaskMode(navCtrl: NavHostController) { +fun LockTaskMode(navCtrl: NavHostController, vm: MyViewModel) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current - val coroutine = rememberCoroutineScope() var appSelectorRequest by rememberSaveable { mutableIntStateOf(0) } MyScaffold(R.string.lock_task_mode, 8.dp, navCtrl, false) { val lockTaskFeatures = remember { mutableStateListOf() } @@ -784,11 +776,11 @@ fun LockTaskMode(navCtrl: NavHostController) { lockTaskFeatures.forEach { result += it } } try { - dpm.setLockTaskFeatures(receiver,result) + dpm.setLockTaskFeatures(receiver, result) Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() } catch (e: IllegalArgumentException) { AlertDialog.Builder(context) - .setTitle("Error") + .setTitle(R.string.error) .setMessage(e.message) .setPositiveButton(R.string.confirm) { dialog, _ -> dialog.dismiss() } .show() @@ -863,11 +855,11 @@ fun LockTaskMode(navCtrl: NavHostController) { var startLockTaskApp by rememberSaveable { mutableStateOf("") } var startLockTaskActivity by rememberSaveable { mutableStateOf("") } var specifyActivity by rememberSaveable { mutableStateOf(false) } - val updatePackage by selectedPackage.collectAsState() + val updatePackage by vm.selectedPackage.collectAsStateWithLifecycle() LaunchedEffect(updatePackage) { if(updatePackage != "") { if(appSelectorRequest == 1) inputLockTaskPkg = updatePackage else startLockTaskApp = updatePackage - selectedPackage.value = "" + vm.selectedPackage.value = "" } } Spacer(Modifier.padding(vertical = 10.dp)) @@ -906,7 +898,8 @@ fun LockTaskMode(navCtrl: NavHostController) { Button( modifier = Modifier.fillMaxWidth(), onClick = { - if(!dpm.getLockTaskPackages(receiver).contains(startLockTaskApp)) { + if(!NotificationUtils.checkPermission(context)) return@Button + if(!dpm.isLockTaskPermitted(startLockTaskApp)) { Toast.makeText(context, R.string.app_not_allowed, Toast.LENGTH_SHORT).show() return@Button } @@ -915,13 +908,7 @@ fun LockTaskMode(navCtrl: NavHostController) { val launchIntent = if(specifyActivity) Intent().setComponent(ComponentName(startLockTaskApp, startLockTaskActivity)) else packageManager.getLaunchIntentForPackage(startLockTaskApp) if (launchIntent != null) { - coroutine.launch { - prepareForNotification(context) { - sendStopLockTaskNotification(context) - context.startActivity(launchIntent, options.toBundle()) - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() - } - } + context.startActivity(launchIntent, options.toBundle()) } else { Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show() } @@ -1497,23 +1484,3 @@ fun InstallSystemUpdate(navCtrl: NavHostController) { InfoCard(R.string.auto_reboot_after_install_succeed) } } - -@SuppressLint("NewApi") -private fun sendStopLockTaskNotification(context: Context) { - val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - if (VERSION.SDK_INT >= 26) { - val channel = NotificationChannel("LockTaskMode", context.getString(R.string.lock_task_mode), NotificationManager.IMPORTANCE_HIGH).apply { - description = "Notification channel for stop lock task mode" - setShowBadge(false) - } - nm.createNotificationChannel(channel) - } - val intent = Intent(context, StopLockTaskModeReceiver::class.java) - val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) - val builder = NotificationCompat.Builder(context, "LockTaskMode") - .setContentTitle(context.getText(R.string.lock_task_mode)) - .setSmallIcon(R.drawable.lock_fill0) - .addAction(NotificationCompat.Action.Builder(R.drawable.lock_fill0, context.getText(R.string.stop), pendingIntent).build()) - .setPriority(NotificationCompat.PRIORITY_HIGH) - nm.notify(1, builder.build()) -} diff --git a/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt b/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt index c1e8582..0d8e8be 100644 --- a/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt +++ b/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt @@ -28,7 +28,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.navigation.NavBackStackEntry import androidx.navigation.NavHostController import com.bintianqi.owndroid.R import com.bintianqi.owndroid.writeClipBoard @@ -79,21 +78,6 @@ fun NavIcon(operation: () -> Unit) { ) } -@Composable -fun Information(content: @Composable ()->Unit) { - Column(modifier = Modifier.fillMaxWidth().padding(start = 5.dp, top = 20.dp)) { - Icon( - painter = painterResource(R.drawable.info_fill0), - contentDescription = "info", - tint = colorScheme.onBackground.copy(alpha = 0.8F) - ) - Spacer(Modifier.padding(vertical = 1.dp)) - Column(modifier = Modifier.padding(start = 2.dp)) { - content() - } - } -} - @Composable fun RadioButtonItem( @StringRes text: Int, diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 4608d0d..3aa2940 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -63,6 +63,7 @@ Alias Unknown error Permission denied + Error diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 80d2991..1e42e9c 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -64,6 +64,7 @@ Alias Unknown error Permission denied + Error Etkinleştirmek İçin Tıklayın diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 74350c2..a4ab158 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -60,6 +60,7 @@ 别名 未知错误 无权限 + 错误 点击以激活 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f69941e..83de418 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -64,6 +64,7 @@ Unknown error Permission denied API + Error Click to activate From 38d384d6690c02ffcd0d5c55e278a9bfa083a583 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sat, 28 Dec 2024 12:46:46 +0800 Subject: [PATCH 08/12] Add Wi-Fi --- app/build.gradle.kts | 1 + app/src/main/AndroidManifest.xml | 1 + .../com/bintianqi/owndroid/MainActivity.kt | 2 + .../com/bintianqi/owndroid/dpm/Network.kt | 276 +++++++++++++++++- .../com/bintianqi/owndroid/ui/Components.kt | 17 ++ app/src/main/res/drawable/wifi_add_fill0.xml | 9 + app/src/main/res/values-ru/strings.xml | 16 +- app/src/main/res/values-tr/strings.xml | 16 +- app/src/main/res/values-zh-rCN/strings.xml | 19 +- app/src/main/res/values/strings.xml | 16 +- 10 files changed, 366 insertions(+), 7 deletions(-) create mode 100644 app/src/main/res/drawable/wifi_add_fill0.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 41a93de..312b58b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -86,4 +86,5 @@ dependencies { implementation(libs.androidx.fragment) implementation(libs.hiddenApiBypass) implementation(libs.serialization) + implementation(kotlin("reflect")) } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3d7b8ee..99b7dcf 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,6 +20,7 @@ + = 30) { FunctionItem(R.string.options, "", R.drawable.tune_fill0) { navCtrl.navigate("NetworkOptions") } } + FunctionItem(R.string.add_wifi, "", R.drawable.wifi_add_fill0) { navCtrl.navigate("AddWifi") } if(VERSION.SDK_INT >= 33 && (deviceOwner || dpm.isOrgProfile(receiver))) { FunctionItem(R.string.min_wifi_security_level, "", R.drawable.wifi_password_fill0) { navCtrl.navigate("MinWifiSecurityLevel") } } @@ -202,6 +216,266 @@ fun NetworkOptions(navCtrl: NavHostController) { ) } +@Suppress("DEPRECATION") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AddNetwork(navCtrl: NavHostController) { + val context = LocalContext.current + var resultDialog by remember { mutableStateOf(false) } + var createdNetworkId by remember { mutableIntStateOf(-1) } + var createNetworkResult by remember {mutableIntStateOf(0)} + var dropdownMenu by remember { mutableIntStateOf(0) } // 0: None, 1:Status, 2:Security, 3:MAC randomization, 4:Static IP, 5:Proxy + var networkId by remember { mutableStateOf("") } + var status by remember { mutableIntStateOf(WifiConfiguration.Status.ENABLED) } + var ssid by remember { mutableStateOf("") } + var hiddenSsid by remember { mutableStateOf(false) } + var securityType by remember { mutableIntStateOf(WifiConfiguration.SECURITY_TYPE_OPEN) } + var password by remember { mutableStateOf("") } + var macRandomizationSetting by remember { mutableIntStateOf(WifiConfiguration.RANDOMIZATION_AUTO) } + var useStaticIp by remember { mutableStateOf(false) } + var ipAddress by remember { mutableStateOf("") } + var gatewayAddress by remember { mutableStateOf("") } + var dnsServers by remember { mutableStateOf("") } + var useHttpProxy by remember { mutableStateOf(false) } + var httpProxyHost by remember { mutableStateOf("") } + var httpProxyPort by remember { mutableStateOf("") } + var httpProxyExclList by remember { mutableStateOf("") } + MyScaffold(R.string.add_wifi, 8.dp, navCtrl) { + OutlinedTextField( + value = networkId, onValueChange = { networkId = it }, label = { Text(stringResource(R.string.network_id)) }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + isError = networkId != "" && (try { networkId.toInt(); false } catch(_: NumberFormatException) { true }), + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp) + ) + ExposedDropdownMenuBox(dropdownMenu == 1, { dropdownMenu = if(it) 1 else 0 }) { + val statusText = when(status) { + WifiConfiguration.Status.CURRENT -> R.string.current + WifiConfiguration.Status.DISABLED -> R.string.disabled + WifiConfiguration.Status.ENABLED -> R.string.enabled + else -> R.string.place_holder + } + OutlinedTextField( + value = stringResource(statusText), onValueChange = {}, readOnly = true, + label = { Text(stringResource(R.string.status)) }, + trailingIcon = { UpOrDownTextFieldTrailingIconButton(dropdownMenu == 1) {} }, + modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(bottom = 16.dp) + ) + ExposedDropdownMenu(dropdownMenu == 1, { dropdownMenu = 0 }) { + DropdownMenuItem( + text = { Text(stringResource(R.string.current)) }, + onClick = { + status = WifiConfiguration.Status.CURRENT + dropdownMenu = 0 + } + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.disabled)) }, + onClick = { + status = WifiConfiguration.Status.DISABLED + dropdownMenu = 0 + } + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.enabled)) }, + onClick = { + status = WifiConfiguration.Status.ENABLED + dropdownMenu = 0 + } + ) + } + } + OutlinedTextField( + value = ssid, onValueChange = { ssid = it }, label = { Text("SSID") }, + modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp) + ) + CheckBoxItem(R.string.hidden_ssid, hiddenSsid, { hiddenSsid = it }) + if(VERSION.SDK_INT >= 30) { + // TODO: more protocols + val securityTypeTextMap = mutableMapOf( + WifiConfiguration.SECURITY_TYPE_OPEN to stringResource(R.string.wifi_security_open), + WifiConfiguration.SECURITY_TYPE_PSK to "PSK" + ) + ExposedDropdownMenuBox(dropdownMenu == 2, { dropdownMenu = if(it) 2 else 0 }) { + OutlinedTextField( + value = securityTypeTextMap[securityType] ?: "", onValueChange = {}, label = { Text(stringResource(R.string.security)) }, + trailingIcon = { UpOrDownTextFieldTrailingIconButton(dropdownMenu == 1) {} }, readOnly = true, + modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(vertical = 4.dp) + ) + ExposedDropdownMenu(dropdownMenu == 2, { dropdownMenu = 0 }) { + securityTypeTextMap.forEach { + DropdownMenuItem(text = { Text(it.value) }, onClick = { securityType = it.key; dropdownMenu = 0 }) + } + } + } + AnimatedVisibility(securityType == WifiConfiguration.SECURITY_TYPE_PSK) { + OutlinedTextField( + value = password, onValueChange = { password = it }, label = { Text(stringResource(R.string.password)) }, + modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp) + ) + } + } + if(VERSION.SDK_INT >= 33) { + val macRandomizationSettingTextMap = mapOf( + WifiConfiguration.RANDOMIZATION_NONE to R.string.none, + WifiConfiguration.RANDOMIZATION_PERSISTENT to R.string.persistent, + WifiConfiguration.RANDOMIZATION_NON_PERSISTENT to R.string.non_persistent, + WifiConfiguration.RANDOMIZATION_AUTO to R.string.auto + ) + ExposedDropdownMenuBox(dropdownMenu == 3, { dropdownMenu = if(it) 3 else 0 }) { + OutlinedTextField( + value = stringResource(macRandomizationSettingTextMap[macRandomizationSetting] ?: R.string.place_holder), + onValueChange = {}, readOnly = true, + label = { Text(stringResource(R.string.mac_randomization)) }, + trailingIcon = { UpOrDownTextFieldTrailingIconButton(dropdownMenu == 3) {} }, + modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(bottom = 8.dp) + ) + ExposedDropdownMenu(dropdownMenu == 3, { dropdownMenu = 0 }) { + macRandomizationSettingTextMap.forEach { + DropdownMenuItem( + text = { Text(stringResource(it.value)) }, + onClick = { + macRandomizationSetting = it.key + dropdownMenu = 0 + } + ) + } + } + } + } + if(VERSION.SDK_INT >= 33) { + ExposedDropdownMenuBox(dropdownMenu == 4, { dropdownMenu = if(it) 4 else 0 }) { + OutlinedTextField( + value = if(useStaticIp) stringResource(R.string.static_str) else "DHCP", + onValueChange = {}, readOnly = true, + label = { Text(stringResource(R.string.ip_settings)) }, + trailingIcon = { UpOrDownTextFieldTrailingIconButton(dropdownMenu == 4) {} }, + modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(bottom = 4.dp) + ) + ExposedDropdownMenu(dropdownMenu == 4, { dropdownMenu = 0 }) { + DropdownMenuItem(text = { Text("DHCP") }, onClick = { useStaticIp = false; dropdownMenu = 0 }) + DropdownMenuItem(text = { Text(stringResource(R.string.static_str)) }, onClick = { useStaticIp = true; dropdownMenu = 0 }) + } + } + AnimatedVisibility(visible = useStaticIp, modifier = Modifier.padding(bottom = 8.dp)) { + Column { + OutlinedTextField( + value = ipAddress, onValueChange = { ipAddress = it }, + placeholder = { Text("192.168.1.2/24") }, label = { Text(stringResource(R.string.ip_address)) }, + modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp) + ) + OutlinedTextField( + value = gatewayAddress, onValueChange = { gatewayAddress = it }, + placeholder = { Text("192.168.1.1") }, label = { Text(stringResource(R.string.gateway_address)) }, + modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp) + ) + OutlinedTextField( + value = dnsServers, onValueChange = { dnsServers = it }, + label = { Text(stringResource(R.string.dns_servers)) }, minLines = 2, + modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp) + ) + } + } + } + if(VERSION.SDK_INT >= 26) { + ExposedDropdownMenuBox(dropdownMenu == 5, { dropdownMenu = if(it) 5 else 0 }) { + OutlinedTextField( + value = if(useHttpProxy) "HTTP" else stringResource(R.string.none), + onValueChange = {}, readOnly = true, + label = { Text(stringResource(R.string.proxy)) }, + trailingIcon = { UpOrDownTextFieldTrailingIconButton(dropdownMenu == 5) {} }, + modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(bottom = 4.dp) + ) + ExposedDropdownMenu(dropdownMenu == 5, { dropdownMenu = 0 }) { + DropdownMenuItem(text = { Text(stringResource(R.string.none)) }, onClick = { useHttpProxy = false; dropdownMenu = 0 }) + DropdownMenuItem(text = { Text("HTTP") }, onClick = { useHttpProxy = true; dropdownMenu = 0 }) + } + } + AnimatedVisibility(visible = useHttpProxy, modifier = Modifier.padding(bottom = 8.dp)) { + Column { + OutlinedTextField( + value = httpProxyHost, onValueChange = { httpProxyHost = it }, label = { Text(stringResource(R.string.host)) }, + modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp) + ) + OutlinedTextField( + value = httpProxyPort, onValueChange = { httpProxyPort = it }, label = { Text(stringResource(R.string.port)) }, + modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp) + ) + OutlinedTextField( + value = httpProxyExclList, onValueChange = { httpProxyExclList = it }, label = { Text(stringResource(R.string.excluded_hosts)) }, + minLines = 2, placeholder = { Text("example.com\n*.example.com") }, + modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp) + ) + } + } + } + Button( + onClick = { + val wm = context.getSystemService(Context.WIFI_SERVICE) as WifiManager + try { + val config = WifiConfiguration() + if(networkId != "") config.networkId = networkId.toInt() + config.status = status + config.SSID = '"' + ssid + '"' + config.hiddenSSID = hiddenSsid + if(VERSION.SDK_INT >= 30) config.setSecurityParams(securityType) + if(securityType == WifiConfiguration.SECURITY_TYPE_PSK) config.preSharedKey = '"' + password + '"' + if(VERSION.SDK_INT >= 33) config.macRandomizationSetting = macRandomizationSetting + if(VERSION.SDK_INT >= 33 && useStaticIp) { + val ipConf = IpConfiguration.Builder() + val staticIpConf = StaticIpConfiguration.Builder() + val la: LinkAddress + val con = LinkAddress::class.constructors.find { it.parameters.size == 1 && it.parameters[0].type.jvmErasure == String::class } + la = con!!.call(ipAddress) + staticIpConf.setIpAddress(la) + staticIpConf.setGateway(InetAddress.getByName(gatewayAddress)) + staticIpConf.setDnsServers(dnsServers.lines().map { InetAddress.getByName(it) }) + ipConf.setStaticIpConfiguration(staticIpConf.build()) + config.setIpConfiguration(ipConf.build()) + } + if(VERSION.SDK_INT >= 26 && useHttpProxy) { + config.httpProxy = ProxyInfo.buildDirectProxy(httpProxyHost, httpProxyPort.toInt(), httpProxyExclList.lines()) + } + if(VERSION.SDK_INT >= 31) { + val result = wm.addNetworkPrivileged(config) + createdNetworkId = result.networkId + createNetworkResult = result.statusCode + } else { + createdNetworkId = wm.addNetwork(config) + } + resultDialog = true + } catch(e: Exception) { + e.printStackTrace() + AlertDialog.Builder(context) + .setTitle(R.string.error) + .setPositiveButton(R.string.confirm) { dialog, _ -> dialog.cancel() } + .setMessage(e.message ?: "") + .show() + } + }, + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp) + ) { + Text(stringResource(R.string.add)) + } + if(resultDialog) AlertDialog( + text = { + val statusText = when(createNetworkResult) { + WifiManager.AddNetworkResult.STATUS_SUCCESS -> R.string.success + //WifiManager.AddNetworkResult.STATUS_ADD_WIFI_CONFIG_FAILURE -> R.string.failed + WifiManager.AddNetworkResult.STATUS_INVALID_CONFIGURATION -> R.string.add_network_result_invalid_configuration + else -> R.string.failed + } + Text(stringResource(statusText) + "\n" + stringResource(R.string.network_id) + ": " + createdNetworkId) + }, + confirmButton = { + TextButton(onClick = { resultDialog = false }) { + Text(stringResource(R.string.confirm)) + } + }, + onDismissRequest = { resultDialog = false } + ) + } +} + @SuppressLint("NewApi") @Composable fun WifiSecurityLevel(navCtrl: NavHostController) { @@ -513,7 +787,7 @@ fun RecommendedGlobalProxy(navCtrl: NavHostController) { OutlinedTextField( value = exclList, onValueChange = { exclList = it }, - label = { Text(stringResource(R.string.exclude_hosts)) }, + label = { Text(stringResource(R.string.excluded_hosts)) }, maxLines = 5, minLines = 2, modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp) diff --git a/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt b/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt index 0d8e8be..d83e433 100644 --- a/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt +++ b/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt @@ -4,6 +4,7 @@ import android.widget.Toast import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -13,6 +14,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.outlined.Info import androidx.compose.material3.* import androidx.compose.material3.MaterialTheme.colorScheme @@ -22,6 +24,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -315,3 +318,17 @@ fun MyScaffold( } } } + +@Composable +fun UpOrDownTextFieldTrailingIconButton(active: Boolean, onClick: () -> Unit) { + val degrees by animateFloatAsState(if(active) 180F else 0F) + IconButton( + onClick = onClick, + modifier = Modifier.clip(RoundedCornerShape(50)) + ) { + Icon( + imageVector = Icons.Default.ArrowDropDown, contentDescription = null, + modifier = Modifier.rotate(degrees) + ) + } +} diff --git a/app/src/main/res/drawable/wifi_add_fill0.xml b/app/src/main/res/drawable/wifi_add_fill0.xml new file mode 100644 index 0000000..a815f40 --- /dev/null +++ b/app/src/main/res/drawable/wifi_add_fill0.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 3aa2940..b8e39f2 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -64,6 +64,7 @@ Unknown error Permission denied Error + Status @@ -215,6 +216,19 @@ Сеть MAC-адрес Wi-Fi + + Add Wi-Fi + Current + Hidden SSID + IP settings + Static + IP address + Gateway address + DNS servers + MAC randomization + Non persistent + Host + Invalid configuration Минимальный уровень безопасности Wi-Fi Открытая Блокировка ети, настроенной администратором @@ -235,7 +249,7 @@ Прямой прокси Указать порт Неверная конфигурация - Исключить хосты + Исключить хосты Network logging Размер файла журнала: %1$s Удалить журналы diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 1e42e9c..0006a03 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -65,6 +65,7 @@ Unknown error Permission denied Error + Status Etkinleştirmek İçin Tıklayın @@ -216,6 +217,19 @@ Wi-Fi MAC adresi + + Add Wi-Fi + Current + Hidden SSID + IP settings + Static + IP address + Gateway address + DNS servers + MAC randomization + Non persistent + Host + Invalid configuration Minimum Wi-Fi güvenlik seviyesi Açık Yönetici tarafından yapılandırılmış ağı kilitle @@ -236,7 +250,7 @@ Direct proxy Specify port Invalid config - Exclude hosts + Excluded hosts Network logging Log file size: %1$s Delete logs diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index a4ab158..6f6804e 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -61,6 +61,7 @@ 未知错误 无权限 错误 + 状态 点击以激活 @@ -208,7 +209,19 @@ 网络 - Wi-Fi Mac地址 + Wi-Fi MAC地址 + 添加Wi-Fi + 当前 + 隐藏的SSID + IP设置 + 静态 + IP地址 + 网关地址 + DNS服务器 + MAC随机化 + 非持久 + 主机 + Invalid configuration 最低Wi-Fi安全等级 开放 锁定由管理员配置的网络 @@ -229,7 +242,7 @@ 直连代理 指定端口 无效配置 - 排除列表 + 排除的主机 网络日志 日志文件大小:%1$s 删除日志 @@ -534,7 +547,7 @@ 项目主页 外观 - 安全 + 安全性 锁定OwnDroid 使用生物识别 验证 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 83de418..c8e4020 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,7 @@ OwnDroid + Disabled Enabled Disable @@ -65,6 +66,7 @@ Permission denied API Error + Status Click to activate @@ -220,6 +222,18 @@ Network Wi-Fi Mac address + Add Wi-Fi + Current + Hidden SSID + IP settings + Static + IP address + Gateway address + DNS servers + MAC randomization + Non persistent + Host + Invalid configuration Minimum Wi-Fi security level Open Lockdown admin configured network @@ -240,7 +254,7 @@ Direct proxy Specify port Invalid config - Exclude hosts + Excluded hosts Network logging Log file size: %1$s Delete logs From 4250d4768335d41f985bb898705a06380cedd826 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sat, 28 Dec 2024 21:17:23 +0800 Subject: [PATCH 09/12] Some Wi-Fi operations Enable/disable Wi-Fi Disconnect/reconnect Wi-Fi View, edit or delete saved networks --- app/build.gradle.kts | 1 + app/src/main/AndroidManifest.xml | 2 + .../com/bintianqi/owndroid/MainActivity.kt | 6 +- .../main/java/com/bintianqi/owndroid/Utils.kt | 4 + .../com/bintianqi/owndroid/dpm/Network.kt | 373 +++++++++++++++--- app/src/main/res/drawable/wifi_add_fill0.xml | 9 - app/src/main/res/values-ru/strings.xml | 7 +- app/src/main/res/values-tr/strings.xml | 7 +- app/src/main/res/values-zh-rCN/strings.xml | 7 +- app/src/main/res/values/strings.xml | 12 +- gradle/libs.versions.toml | 2 + 11 files changed, 351 insertions(+), 79 deletions(-) delete mode 100644 app/src/main/res/drawable/wifi_add_fill0.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 312b58b..fed4743 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -77,6 +77,7 @@ dependencies { implementation(libs.androidx.activity.compose) implementation(platform(libs.androidx.compose.bom)) implementation(libs.accompanist.drawablepainter) + implementation(libs.accompanist.permissions) implementation(libs.androidx.material3) implementation(libs.androidx.navigation.compose) implementation(libs.shizuku.provider) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 99b7dcf..5ca5af9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -20,6 +20,8 @@ + + = 24 && (deviceOwner || dpm.isOrgProfile(receiver))) { - FunctionItem(R.string.wifi_mac_address, "", R.drawable.wifi_fill0) { wifiMacDialog = true } - } + if(!dhizuku) FunctionItem(R.string.wifi, "", R.drawable.wifi_fill0) { navCtrl.navigate("Wifi") } if(VERSION.SDK_INT >= 30) { FunctionItem(R.string.options, "", R.drawable.tune_fill0) { navCtrl.navigate("NetworkOptions") } } - FunctionItem(R.string.add_wifi, "", R.drawable.wifi_add_fill0) { navCtrl.navigate("AddWifi") } - if(VERSION.SDK_INT >= 33 && (deviceOwner || dpm.isOrgProfile(receiver))) { - FunctionItem(R.string.min_wifi_security_level, "", R.drawable.wifi_password_fill0) { navCtrl.navigate("MinWifiSecurityLevel") } - } - if(VERSION.SDK_INT >= 33 && (deviceOwner || dpm.isOrgProfile(receiver))) { - FunctionItem(R.string.wifi_ssid_policy, "", R.drawable.wifi_fill0) { navCtrl.navigate("WifiSsidPolicy") } - } if(VERSION.SDK_INT >= 29 && deviceOwner) { FunctionItem(R.string.private_dns, "", R.drawable.dns_fill0) { navCtrl.navigate("PrivateDNS") } } - if(VERSION.SDK_INT >= 24 && (deviceOwner || profileOwner)) { + if(VERSION.SDK_INT >= 24) { FunctionItem(R.string.always_on_vpn, "", R.drawable.vpn_key_fill0) { navCtrl.navigate("AlwaysOnVpn") } } if(deviceOwner) { @@ -157,39 +173,16 @@ fun Network(navCtrl:NavHostController) { if(VERSION.SDK_INT >= 26 && !dhizuku && (deviceOwner || (profileOwner && dpm.isManagedProfile(receiver)))) { FunctionItem(R.string.network_logging, "", R.drawable.description_fill0) { navCtrl.navigate("NetworkLog") } } - if(VERSION.SDK_INT >= 31 && (deviceOwner || profileOwner)) { + if(VERSION.SDK_INT >= 31) { FunctionItem(R.string.wifi_auth_keypair, "", R.drawable.key_fill0) { navCtrl.navigate("WifiAuthKeypair") } } - if(VERSION.SDK_INT >= 33 && (deviceOwner || profileOwner)) { + if(VERSION.SDK_INT >= 33) { FunctionItem(R.string.preferential_network_service, "", R.drawable.globe_fill0) { navCtrl.navigate("PreferentialNetworkService") } } if(VERSION.SDK_INT >= 28 && deviceOwner) { FunctionItem(R.string.override_apn_settings, "", R.drawable.cell_tower_fill0) { navCtrl.navigate("OverrideAPN") } } } - if(wifiMacDialog && VERSION.SDK_INT >= 24) { - val context = LocalContext.current - val dpm = context.getDPM() - val receiver = context.getReceiver() - AlertDialog( - onDismissRequest = { wifiMacDialog = false }, - confirmButton = { TextButton(onClick = { wifiMacDialog = false }) { Text(stringResource(R.string.confirm)) } }, - title = { Text(stringResource(R.string.wifi_mac_address)) }, - text = { - val mac = dpm.getWifiMacAddress(receiver) - OutlinedTextField( - value = mac ?: stringResource(R.string.none), - onValueChange = {}, readOnly = true, modifier = Modifier.fillMaxWidth(), textStyle = typography.bodyLarge, - trailingIcon = { - if(mac != null) IconButton(onClick = { writeClipBoard(context, mac) }) { - Icon(painter = painterResource(R.drawable.content_copy_fill0), contentDescription = stringResource(R.string.copy)) - } - } - ) - }, - modifier = Modifier.fillMaxWidth() - ) - } } @Composable @@ -216,16 +209,265 @@ fun NetworkOptions(navCtrl: NavHostController) { ) } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun Wifi(navCtrl: NavHostController) { + val context = LocalContext.current + val coroutine = rememberCoroutineScope() + val pagerState = rememberPagerState { 3 } + var tabIndex by rememberSaveable { mutableIntStateOf(0) } + tabIndex = pagerState.currentPage + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.wifi)) }, + navigationIcon = { NavIcon { navCtrl.navigateUp() } } + ) + } + ) { paddingValues -> + var wifiMacDialog by remember { mutableStateOf(false) } + Column( + modifier = Modifier.fillMaxSize().padding(paddingValues) + ) { + TabRow(tabIndex) { + Tab( + selected = tabIndex == 0, onClick = { tabIndex = 0; coroutine.launch { pagerState.animateScrollToPage(tabIndex) } }, + text = { Text(stringResource(R.string.overview)) } + ) + Tab( + selected = tabIndex == 1, onClick = { tabIndex = 1; coroutine.launch { pagerState.animateScrollToPage(tabIndex) } }, + text = { Text(stringResource(R.string.saved_networks)) } + ) + Tab( + selected = tabIndex == 2, onClick = { tabIndex = 2; coroutine.launch { pagerState.animateScrollToPage(tabIndex) } }, + text = { Text(stringResource(R.string.add_network)) } + ) + } + HorizontalPager(state = pagerState, verticalAlignment = Alignment.Top) { page -> + if(page == 0) { + val wm = context.getSystemService(Context.WIFI_SERVICE) as WifiManager + val deviceOwner = context.isDeviceOwner + val orgProfileOwner = context.getDPM().isOrgProfile(context.getReceiver()) + @Suppress("DEPRECATION") Column( + modifier = Modifier.fillMaxSize().padding(top = 12.dp) + ) { + Row( + horizontalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxWidth() + ) { + Button( + onClick = { context.showOperationResultToast(wm.setWifiEnabled(true)) }, + modifier = Modifier.padding(end = 8.dp) + ) { + Text(stringResource(R.string.enable)) + } + Button(onClick = { context.showOperationResultToast(wm.setWifiEnabled(false)) }) { + Text(stringResource(R.string.disable)) + } + } + Row( + horizontalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp) + ) { + Button( + onClick = { context.showOperationResultToast(wm.disconnect()) }, + modifier = Modifier.padding(end = 8.dp) + ) { + Text(stringResource(R.string.disconnect)) + } + Button(onClick = { context.showOperationResultToast(wm.reconnect()) }) { + Text(stringResource(R.string.reconnect)) + } + } + if(VERSION.SDK_INT >= 24 && (deviceOwner || orgProfileOwner)) { + FunctionItem(R.string.wifi_mac_address, "", null) { wifiMacDialog = true } + } + if(VERSION.SDK_INT >= 33 && (deviceOwner || orgProfileOwner)) { + FunctionItem(R.string.min_wifi_security_level, "", null) { navCtrl.navigate("MinWifiSecurityLevel") } + FunctionItem(R.string.wifi_ssid_policy, "", null) { navCtrl.navigate("WifiSsidPolicy") } + } + } + } else if(page == 1) { + SavedNetworks(navCtrl) + } else { + AddNetwork() + } + } + } + if(wifiMacDialog && VERSION.SDK_INT >= 24) { + val context = LocalContext.current + val dpm = context.getDPM() + val receiver = context.getReceiver() + AlertDialog( + onDismissRequest = { wifiMacDialog = false }, + confirmButton = { TextButton(onClick = { wifiMacDialog = false }) { Text(stringResource(R.string.confirm)) } }, + text = { + val mac = dpm.getWifiMacAddress(receiver) + OutlinedTextField( + value = mac ?: stringResource(R.string.none), label = { Text(stringResource(R.string.wifi_mac_address)) }, + onValueChange = {}, readOnly = true, modifier = Modifier.fillMaxWidth(), textStyle = typography.bodyLarge, + trailingIcon = { + if(mac != null) IconButton(onClick = { writeClipBoard(context, mac) }) { + Icon(painter = painterResource(R.drawable.content_copy_fill0), contentDescription = stringResource(R.string.copy)) + } + } + ) + }, + modifier = Modifier.fillMaxWidth() + ) + } + } +} + +@Suppress("DEPRECATION") +@OptIn(ExperimentalPermissionsApi::class) +@Composable +private fun SavedNetworks(navCtrl: NavHostController) { + val context = LocalContext.current + val wm = context.getSystemService(Context.WIFI_SERVICE) as WifiManager + val configuredNetworks = remember { mutableStateListOf() } + var networkDetailsDialog by remember { mutableIntStateOf(-1) } // -1:Hidden, 0+:Index of configuredNetworks + fun refresh() { + configuredNetworks.clear() + wm.configuredNetworks.forEach { network -> + if(configuredNetworks.none { it.networkId == network.networkId }) configuredNetworks += network + } + } + LaunchedEffect(Unit) { refresh() } + Column( + modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(start = 8.dp, end = 8.dp, bottom = 60.dp) + ) { + val locationPermission = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION) + val requestPermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { + if(it) refresh() + } + if(!locationPermission.status.isGranted) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + .padding(12.dp) + .clip(RoundedCornerShape(15)) + .background(MaterialTheme.colorScheme.primaryContainer) + .clickable { requestPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION) } + ) { + Icon( + imageVector = Icons.Outlined.LocationOn, contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.padding(start = 8.dp, end = 4.dp)) + Text( + text = stringResource(R.string.request_location_permission_description), + color = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.padding(8.dp) + ) + } + } + configuredNetworks.forEachIndexed { index, network -> + Row( + horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().padding(start = 8.dp, top = 8.dp) + ) { + Text(text = network.SSID.removeSurrounding("\""), style = typography.titleLarge) + IconButton(onClick = { networkDetailsDialog = index }) { + Icon(painter = painterResource(R.drawable.more_horiz_fill0), contentDescription = null) + } + } + } + } + if(networkDetailsDialog != -1) AlertDialog( + text = { + val network = configuredNetworks[networkDetailsDialog] + val statusText = when(network.status) { + WifiConfiguration.Status.CURRENT -> R.string.current + WifiConfiguration.Status.DISABLED -> R.string.disabled + WifiConfiguration.Status.ENABLED -> R.string.enabled + else -> R.string.place_holder + } + Column { + Text(stringResource(R.string.network_id) + ": " + network.networkId.toString()) + SelectionContainer { + Text("SSID: " + network.SSID) + if(network.BSSID != null) Text("BSSID: " + network.BSSID) + } + Text(stringResource(R.string.status) + ": " + stringResource(statusText)) + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth().padding(top = 12.dp) + ) { + Button( + onClick = { + val success = wm.enableNetwork(network.networkId, false) + Toast.makeText(context, if(success) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show() + networkDetailsDialog = -1 + refresh() + }, + modifier = Modifier.fillMaxWidth(0.49F) + ) { + Text(stringResource(R.string.enable)) + } + Button( + onClick = { + val success = wm.disableNetwork(network.networkId) + Toast.makeText(context, if(success) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show() + networkDetailsDialog = -1 + refresh() + }, + modifier = Modifier.fillMaxWidth(0.96F) + ) { + Text(stringResource(R.string.disable)) + } + } + Button( + onClick = { + networkDetailsDialog = -1 + val dest = navCtrl.graph.findNode("UpdateNetwork") + if(dest != null) + navCtrl.navigate(dest.id, bundleOf("wifi_configuration" to network)) + }, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(R.string.edit)) + } + TextButton( + onClick = { + val success = wm.removeNetwork(network.networkId) + Toast.makeText(context, if(success) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show() + networkDetailsDialog = -1 + refresh() + }, + colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error), + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(R.string.remove)) + } + } + }, + confirmButton = { + TextButton(onClick = { networkDetailsDialog = -1 }) { + Text(stringResource(R.string.confirm)) + } + }, + onDismissRequest = { networkDetailsDialog = -1 } + ) +} + +@Composable +fun UpdateNetwork(arguments: Bundle, navCtrl: NavHostController) { + MyScaffold(R.string.update_network, 0.dp, navCtrl, false) { + AddNetwork(arguments.getParcelable("wifi_configuration"), navCtrl) + } +} + @Suppress("DEPRECATION") @OptIn(ExperimentalMaterial3Api::class) @Composable -fun AddNetwork(navCtrl: NavHostController) { +private fun AddNetwork(wifiConfig: WifiConfiguration? = null, navCtrl: NavHostController? = null) { val context = LocalContext.current var resultDialog by remember { mutableStateOf(false) } var createdNetworkId by remember { mutableIntStateOf(-1) } - var createNetworkResult by remember {mutableIntStateOf(0)} + var createNetworkResult by remember { mutableIntStateOf(0) } var dropdownMenu by remember { mutableIntStateOf(0) } // 0: None, 1:Status, 2:Security, 3:MAC randomization, 4:Static IP, 5:Proxy - var networkId by remember { mutableStateOf("") } var status by remember { mutableIntStateOf(WifiConfiguration.Status.ENABLED) } var ssid by remember { mutableStateOf("") } var hiddenSsid by remember { mutableStateOf(false) } @@ -240,16 +482,19 @@ fun AddNetwork(navCtrl: NavHostController) { var httpProxyHost by remember { mutableStateOf("") } var httpProxyPort by remember { mutableStateOf("") } var httpProxyExclList by remember { mutableStateOf("") } - MyScaffold(R.string.add_wifi, 8.dp, navCtrl) { - OutlinedTextField( - value = networkId, onValueChange = { networkId = it }, label = { Text(stringResource(R.string.network_id)) }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - isError = networkId != "" && (try { networkId.toInt(); false } catch(_: NumberFormatException) { true }), - modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp) - ) + LaunchedEffect(Unit) { + if(wifiConfig != null) { + status = wifiConfig.status + if(wifiConfig.status == WifiConfiguration.Status.CURRENT) status = WifiConfiguration.Status.ENABLED + ssid = wifiConfig.SSID.removeSurrounding("\"") + } + } + Column( + modifier = (if(wifiConfig == null) Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(bottom = 60.dp) else Modifier) + .padding(start = 8.dp, end = 8.dp, top = 12.dp) + ) { ExposedDropdownMenuBox(dropdownMenu == 1, { dropdownMenu = if(it) 1 else 0 }) { val statusText = when(status) { - WifiConfiguration.Status.CURRENT -> R.string.current WifiConfiguration.Status.DISABLED -> R.string.disabled WifiConfiguration.Status.ENABLED -> R.string.enabled else -> R.string.place_holder @@ -261,13 +506,6 @@ fun AddNetwork(navCtrl: NavHostController) { modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable).fillMaxWidth().padding(bottom = 16.dp) ) ExposedDropdownMenu(dropdownMenu == 1, { dropdownMenu = 0 }) { - DropdownMenuItem( - text = { Text(stringResource(R.string.current)) }, - onClick = { - status = WifiConfiguration.Status.CURRENT - dropdownMenu = 0 - } - ) DropdownMenuItem( text = { Text(stringResource(R.string.disabled)) }, onClick = { @@ -413,7 +651,6 @@ fun AddNetwork(navCtrl: NavHostController) { val wm = context.getSystemService(Context.WIFI_SERVICE) as WifiManager try { val config = WifiConfiguration() - if(networkId != "") config.networkId = networkId.toInt() config.status = status config.SSID = '"' + ssid + '"' config.hiddenSSID = hiddenSsid @@ -435,12 +672,17 @@ fun AddNetwork(navCtrl: NavHostController) { if(VERSION.SDK_INT >= 26 && useHttpProxy) { config.httpProxy = ProxyInfo.buildDirectProxy(httpProxyHost, httpProxyPort.toInt(), httpProxyExclList.lines()) } - if(VERSION.SDK_INT >= 31) { - val result = wm.addNetworkPrivileged(config) - createdNetworkId = result.networkId - createNetworkResult = result.statusCode + if(wifiConfig != null) { + config.networkId = wifiConfig.networkId + createdNetworkId = wm.updateNetwork(config) } else { - createdNetworkId = wm.addNetwork(config) + if(VERSION.SDK_INT >= 31) { + val result = wm.addNetworkPrivileged(config) + createdNetworkId = result.networkId + createNetworkResult = result.statusCode + } else { + createdNetworkId = wm.addNetwork(config) + } } resultDialog = true } catch(e: Exception) { @@ -454,7 +696,7 @@ fun AddNetwork(navCtrl: NavHostController) { }, modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp) ) { - Text(stringResource(R.string.add)) + Text(stringResource(if(wifiConfig != null) R.string.update else R.string.add)) } if(resultDialog) AlertDialog( text = { @@ -467,7 +709,12 @@ fun AddNetwork(navCtrl: NavHostController) { Text(stringResource(statusText) + "\n" + stringResource(R.string.network_id) + ": " + createdNetworkId) }, confirmButton = { - TextButton(onClick = { resultDialog = false }) { + TextButton( + onClick = { + resultDialog = false + if(createdNetworkId != -1) navCtrl?.navigateUp() + } + ) { Text(stringResource(R.string.confirm)) } }, diff --git a/app/src/main/res/drawable/wifi_add_fill0.xml b/app/src/main/res/drawable/wifi_add_fill0.xml deleted file mode 100644 index a815f40..0000000 --- a/app/src/main/res/drawable/wifi_add_fill0.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b8e39f2..376bc56 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -217,7 +217,12 @@ Сеть MAC-адрес Wi-Fi - Add Wi-Fi + Disconnect + Reconnect + Saved networks + This app need location permission to get saved networks, your geographic location will not be read.\nClick to request the permission. + Add network + Update network Current Hidden SSID IP settings diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 0006a03..f376da7 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -218,7 +218,12 @@ Wi-Fi MAC adresi - Add Wi-Fi + Disconnect + Reconnect + Saved networks + This app need location permission to get saved networks, your geographic location will not be read.\nClick to request the permission. + Add network + Update network Current Hidden SSID IP settings diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 6f6804e..e2085f3 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -210,7 +210,12 @@ 网络 Wi-Fi MAC地址 - 添加Wi-Fi + 断开连接 + 重新连接 + 已保存的网络 + 此app需要定位权限以获取已保存的网络,你的地理位置将不会被读取。点击以请求权限 + 添加网络 + 更新网络 当前 隐藏的SSID IP设置 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c8e4020..1d9ab20 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -67,6 +67,8 @@ API Error Status + Edit + Overview Click to activate @@ -221,8 +223,14 @@ Network - Wi-Fi Mac address - Add Wi-Fi + Wi-Fi MAC address + Wi-Fi + Disconnect + Reconnect + Saved networks + This app need location permission to get saved networks, your geographic location will not be read.\nClick to request the permission. + Add network + Update network Current Hidden SSID IP settings diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 05db652..0728b0e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,6 +5,7 @@ kotlin = "2.0.21" navigation-compose = "2.8.5" composeBom = "2024.12.01" accompanist-drawablepainter = "0.35.0-alpha" +accompanist-permissions = "0.37.0" shizuku = "13.1.5" biometric = "1.2.0-alpha05" fragment = "1.8.5" @@ -19,6 +20,7 @@ androidx-material3 = { module = "androidx.compose.material3:material3" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" } accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "accompanist-drawablepainter" } +accompanist-permissions = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanist-permissions" } androidx-biometric = { group = "androidx.biometric", name = "biometric", version.ref = "biometric" } shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku" } From 937afe9417deae5f014eca5697ee4020057e43e2 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sun, 29 Dec 2024 14:03:28 +0800 Subject: [PATCH 10/12] Optimize code --- app/src/main/AndroidManifest.xml | 1 + .../bintianqi/owndroid/InstallAppActivity.kt | 6 +- .../java/com/bintianqi/owndroid/Receiver.kt | 10 +- .../java/com/bintianqi/owndroid/Settings.kt | 47 +- .../main/java/com/bintianqi/owndroid/Utils.kt | 57 +-- .../bintianqi/owndroid/dpm/Applications.kt | 84 ++-- .../java/com/bintianqi/owndroid/dpm/DPM.kt | 11 +- .../bintianqi/owndroid/dpm/ManagedProfile.kt | 59 ++- .../com/bintianqi/owndroid/dpm/Network.kt | 223 ++++------ .../com/bintianqi/owndroid/dpm/Password.kt | 168 +++---- .../com/bintianqi/owndroid/dpm/Permissions.kt | 39 +- .../java/com/bintianqi/owndroid/dpm/System.kt | 417 +++++++----------- .../bintianqi/owndroid/dpm/UserRestriction.kt | 12 +- .../java/com/bintianqi/owndroid/dpm/Users.kt | 85 ++-- .../com/bintianqi/owndroid/ui/Components.kt | 44 +- 15 files changed, 513 insertions(+), 750 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5ca5af9..5385ad3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,6 +21,7 @@ + diff --git a/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt b/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt index 1c94198..86ecea4 100644 --- a/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt @@ -1,6 +1,5 @@ package com.bintianqi.owndroid -import android.content.Context import android.content.Intent import android.graphics.Color import android.graphics.drawable.ColorDrawable @@ -47,7 +46,6 @@ class InstallAppActivity: FragmentActivity() { enableEdgeToEdge() WindowCompat.setDecorFitsSystemWindows(window, false) val context = applicationContext - val sharedPref = applicationContext.getSharedPreferences("data", Context.MODE_PRIVATE) window.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) val uri = this.intent.data!! var apkInfoText by mutableStateOf(context.getString(R.string.parsing_apk_info)) @@ -97,7 +95,9 @@ class InstallAppActivity: FragmentActivity() { TextButton( onClick = { status = "installing" - uriToStream(applicationContext, this.intent.data) { stream -> installPackage(applicationContext, stream) } + intent.data?.let { + uriToStream(applicationContext, it) { stream -> installPackage(applicationContext, stream) } + } }, enabled = status != "installing" ) { diff --git a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt index bb9cb1f..c0d8e4c 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt @@ -24,10 +24,10 @@ import android.util.Log import android.widget.Toast import androidx.core.app.NotificationCompat import com.bintianqi.owndroid.dpm.handleNetworkLogs -import com.bintianqi.owndroid.dpm.handleSecurityLogs import com.bintianqi.owndroid.dpm.isDeviceAdmin import com.bintianqi.owndroid.dpm.isDeviceOwner import com.bintianqi.owndroid.dpm.isProfileOwner +import com.bintianqi.owndroid.dpm.processSecurityLogs import com.bintianqi.owndroid.dpm.toggleInstallAppActivity import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -78,7 +78,13 @@ class Receiver : DeviceAdminReceiver() { super.onSecurityLogsAvailable(context, intent) if(VERSION.SDK_INT >= 24) { CoroutineScope(Dispatchers.IO).launch { - handleSecurityLogs(context) + val events = getManager(context).retrieveSecurityLogs(ComponentName(context, this@Receiver::class.java)) ?: return@launch + val file = context.filesDir.resolve("SecurityLogs.json") + val fileExists = file.exists() + file.outputStream().use { + if(fileExists) it.write(",".encodeToByteArray()) + processSecurityLogs(events, it) + } } } } diff --git a/app/src/main/java/com/bintianqi/owndroid/Settings.kt b/app/src/main/java/com/bintianqi/owndroid/Settings.kt index ce8af0b..dfc85de 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Settings.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Settings.kt @@ -4,7 +4,6 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build.VERSION -import android.widget.Toast import androidx.biometric.BiometricManager import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.isSystemInDarkTheme @@ -44,11 +43,11 @@ import java.security.SecureRandom @Composable fun Settings(navCtrl: NavHostController) { MyScaffold(R.string.settings, 0.dp, navCtrl) { - FunctionItem(R.string.options, "", R.drawable.tune_fill0) { navCtrl.navigate("Options") } - FunctionItem(R.string.appearance, "", R.drawable.format_paint_fill0) { navCtrl.navigate("Appearance") } - FunctionItem(R.string.security, "", R.drawable.lock_fill0) { navCtrl.navigate("AuthSettings") } - FunctionItem(R.string.api, "", R.drawable.apps_fill0) { navCtrl.navigate("ApiSettings") } - FunctionItem(R.string.about, "", R.drawable.info_fill0) { navCtrl.navigate("About") } + FunctionItem(title = R.string.options, icon = R.drawable.tune_fill0) { navCtrl.navigate("Options") } + FunctionItem(title = R.string.appearance, icon = R.drawable.format_paint_fill0) { navCtrl.navigate("Appearance") } + FunctionItem(title = R.string.security, icon = R.drawable.lock_fill0) { navCtrl.navigate("AuthSettings") } + FunctionItem(title = R.string.api, icon = R.drawable.apps_fill0) { navCtrl.navigate("ApiSettings") } + FunctionItem(title = R.string.about, icon = R.drawable.info_fill0) { navCtrl.navigate("About") } } } @@ -57,9 +56,9 @@ fun SettingsOptions(navCtrl: NavHostController) { val sharedPref = LocalContext.current.getSharedPreferences("data", Context.MODE_PRIVATE) MyScaffold(R.string.options, 0.dp, navCtrl) { SwitchItem( - R.string.show_dangerous_features, "", R.drawable.warning_fill0, - { sharedPref.getBoolean("dangerous_features", false) }, - { sharedPref.edit().putBoolean("dangerous_features", it).apply() } + R.string.show_dangerous_features, icon = R.drawable.warning_fill0, + getState = { sharedPref.getBoolean("dangerous_features", false) }, + onCheckedChange = { sharedPref.edit().putBoolean("dangerous_features", it).apply() } ) } } @@ -75,11 +74,7 @@ fun Appearance(navCtrl: NavHostController, vm: MyViewModel) { } MyScaffold(R.string.appearance, 0.dp, navCtrl) { if(VERSION.SDK_INT >= 31) { - SwitchItem( - R.string.material_you_color, "", null, - theme.materialYou, - { vm.theme.value = theme.copy(materialYou = it) } - ) + SwitchItem(R.string.material_you_color, state = theme.materialYou, onCheckedChange = { vm.theme.value = theme.copy(materialYou = it) }) } Box { FunctionItem(R.string.dark_theme, stringResource(darkThemeTextID)) { darkThemeMenu = true } @@ -111,11 +106,7 @@ fun Appearance(navCtrl: NavHostController, vm: MyViewModel) { } } AnimatedVisibility(theme.darkTheme == true || (theme.darkTheme == null && isSystemInDarkTheme())) { - SwitchItem( - R.string.black_theme, "", null, - theme.blackTheme, - { vm.theme.value = theme.copy(blackTheme = it) } - ) + SwitchItem(R.string.black_theme, state = theme.blackTheme, onCheckedChange = { vm.theme.value = theme.copy(blackTheme = it) }) } } } @@ -127,8 +118,8 @@ fun AuthSettings(navCtrl: NavHostController) { var auth by remember{ mutableStateOf(sharedPref.getBoolean("auth",false)) } MyScaffold(R.string.security, 0.dp, navCtrl) { SwitchItem( - R.string.lock_owndroid, "", null, auth, - { + R.string.lock_owndroid, state = auth, + onCheckedChange = { sharedPref.edit().putBoolean("auth", it).apply() auth = sharedPref.getBoolean("auth", false) } @@ -143,13 +134,13 @@ fun AuthSettings(navCtrl: NavHostController) { } } SwitchItem( - R.string.enable_bio_auth, "", null, bioAuth != 0, - { bioAuth = if(it) 1 else 0; sharedPref.edit().putInt("biometrics_auth", bioAuth).apply() }, bioAuth != 2 + R.string.enable_bio_auth, state = bioAuth != 0, + onCheckedChange = { bioAuth = if(it) 1 else 0; sharedPref.edit().putInt("biometrics_auth", bioAuth).apply() }, enabled = bioAuth != 2 ) SwitchItem( - R.string.lock_in_background, "", null, - { sharedPref.getBoolean("lock_in_background", false) }, - { sharedPref.edit().putBoolean("lock_in_background", it).apply() } + R.string.lock_in_background, + getState = { sharedPref.getBoolean("lock_in_background", false) }, + onCheckedChange = { sharedPref.edit().putBoolean("lock_in_background", it).apply() } ) } } @@ -167,7 +158,7 @@ fun ApiSettings(navCtrl: NavHostController) { if(!enabled) remove("api_key") } } - SwitchItem(R.string.enable, "", null, enabled, { enabled = it }, padding = false) + SwitchItem(R.string.enable, state = enabled, onCheckedChange = { enabled = it }, padding = false) if(enabled) { var key by remember { mutableStateOf("") } OutlinedTextField( @@ -189,7 +180,7 @@ fun ApiSettings(navCtrl: NavHostController) { modifier = Modifier.fillMaxWidth().padding(bottom = 10.dp), onClick = { sharedPref.edit().putString("api_key", key).apply() - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) } ) { Text(stringResource(R.string.apply)) diff --git a/app/src/main/java/com/bintianqi/owndroid/Utils.kt b/app/src/main/java/com/bintianqi/owndroid/Utils.kt index 1d758f5..5aeb61c 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Utils.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Utils.kt @@ -5,18 +5,13 @@ import android.content.ClipData import android.content.ClipboardManager import android.content.ComponentName import android.content.Context -import android.content.Intent import android.net.Uri import android.widget.Toast import androidx.activity.ComponentActivity -import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi import androidx.annotation.StringRes import com.bintianqi.owndroid.dpm.addDeviceAdmin -import com.bintianqi.owndroid.dpm.createManagedProfile -import kotlinx.coroutines.flow.MutableStateFlow -import java.io.File import java.io.FileNotFoundException import java.io.IOException import java.io.InputStream @@ -27,29 +22,20 @@ import java.time.format.DateTimeFormatter import java.util.Date import java.util.Locale -lateinit var getFile: ActivityResultLauncher -val fileUriFlow = MutableStateFlow(Uri.parse("")) - var zhCN = true fun uriToStream( context: Context, - uri: Uri?, + uri: Uri, operation: (stream: InputStream)->Unit ){ - if(uri!=null){ - try { - val stream = context.contentResolver.openInputStream(uri) - if(stream != null) { operation(stream) } - stream?.close() - } - catch(_: FileNotFoundException) { Toast.makeText(context, R.string.file_not_exist, Toast.LENGTH_SHORT).show() } - catch(_: IOException) { Toast.makeText(context, R.string.io_exception, Toast.LENGTH_SHORT).show() } + try { + val stream = context.contentResolver.openInputStream(uri) + if(stream != null) { operation(stream) } + stream?.close() } -} - -fun MutableList.toggle(status: Boolean, element: Int) { - if(status) add(element) else remove(element) + catch(_: FileNotFoundException) { Toast.makeText(context, R.string.file_not_exist, Toast.LENGTH_SHORT).show() } + catch(_: IOException) { Toast.makeText(context, R.string.io_exception, Toast.LENGTH_SHORT).show() } } fun writeClipBoard(context: Context, string: String):Boolean{ @@ -62,40 +48,13 @@ fun writeClipBoard(context: Context, string: String):Boolean{ return true } -lateinit var exportFile: ActivityResultLauncher -var exportFilePath: String? = null -var isExportingSecurityOrNetworkLogs = false - -fun registerActivityResult(context: ComponentActivity){ - getFile = context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult -> - activityResult.data.let { - if(it != null) fileUriFlow.value = it.data - } - } - createManagedProfile = context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {} +fun registerActivityResult(context: ComponentActivity) { addDeviceAdmin = context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { val dpm = context.applicationContext.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager if(dpm.isAdminActive(ComponentName(context.applicationContext, Receiver::class.java))) { backToHomeStateFlow.value = true } } - exportFile = context.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - val intentData = result.data ?: return@registerForActivityResult - val uriData = intentData.data ?: return@registerForActivityResult - val path = exportFilePath ?: return@registerForActivityResult - context.contentResolver.openOutputStream(uriData).use { outStream -> - if(outStream != null) { - if(isExportingSecurityOrNetworkLogs) outStream.write("[".encodeToByteArray()) - File(path).inputStream().use { inStream -> - inStream.copyTo(outStream) - } - if(isExportingSecurityOrNetworkLogs) outStream.write("]".encodeToByteArray()) - Toast.makeText(context.applicationContext, R.string.success, Toast.LENGTH_SHORT).show() - } - } - isExportingSecurityOrNetworkLogs = false - exportFilePath = null - } } fun formatFileSize(bytes: Long): String { diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt index a395680..0817898 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -18,6 +18,8 @@ import android.os.Build.VERSION import android.os.Looper import android.provider.Settings import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background @@ -50,7 +52,6 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf @@ -82,8 +83,7 @@ import com.bintianqi.owndroid.InstallAppActivity import com.bintianqi.owndroid.MyViewModel import com.bintianqi.owndroid.PackageInstallerReceiver import com.bintianqi.owndroid.R -import com.bintianqi.owndroid.fileUriFlow -import com.bintianqi.owndroid.getFile +import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.ui.Animations import com.bintianqi.owndroid.ui.FunctionItem import com.bintianqi.owndroid.ui.InfoCard @@ -200,14 +200,14 @@ private fun Home(navCtrl:NavHostController, pkgName: String) { if(VERSION.SDK_INT >= 24 && profileOwner && dpm.isManagedProfile(receiver)) { Text(text = stringResource(R.string.scope_is_work_profile), textAlign = TextAlign.Center,modifier = Modifier.fillMaxWidth()) } - FunctionItem(R.string.app_info,"", R.drawable.open_in_new) { + FunctionItem(title = R.string.app_info, icon = R.drawable.open_in_new) { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) intent.setData(Uri.parse("package:$pkgName")) startActivity(context, intent, null) } if(VERSION.SDK_INT >= 24) { SwitchItem( - title = R.string.suspend, desc = "", icon = R.drawable.block_fill0, + title = R.string.suspend, icon = R.drawable.block_fill0, state = suspend, onCheckedChange = { appControlAction = 1; appControl(it) }, onClickBlank = { appControlAction = 1; dialogStatus = 4 } @@ -220,48 +220,47 @@ private fun Home(navCtrl:NavHostController, pkgName: String) { onClickBlank = { appControlAction = 2; dialogStatus = 4 } ) SwitchItem( - title = R.string.block_uninstall, desc = "", icon = R.drawable.delete_forever_fill0, + title = R.string.block_uninstall, icon = R.drawable.delete_forever_fill0, state = blockUninstall, onCheckedChange = { appControlAction = 3; appControl(it) }, onClickBlank = { appControlAction = 3; dialogStatus = 4 } ) if((VERSION.SDK_INT >= 33 && profileOwner) || (VERSION.SDK_INT >= 30 && deviceOwner)) { - FunctionItem(R.string.ucd, "", R.drawable.do_not_touch_fill0) { navCtrl.navigate("UserControlDisabled") } + FunctionItem(title = R.string.ucd, icon = R.drawable.do_not_touch_fill0) { navCtrl.navigate("UserControlDisabled") } } if(VERSION.SDK_INT>=23) { - FunctionItem(R.string.permission_manage, "", R.drawable.key_fill0) { navCtrl.navigate("PermissionManage") } + FunctionItem(title = R.string.permission_manage, icon = R.drawable.key_fill0) { navCtrl.navigate("PermissionManage") } } if(VERSION.SDK_INT >= 30 && profileOwner && dpm.isManagedProfile(receiver)) { - FunctionItem(R.string.cross_profile_package, "", R.drawable.work_fill0) { navCtrl.navigate("CrossProfilePackage") } + FunctionItem(title = R.string.cross_profile_package, icon = R.drawable.work_fill0) { navCtrl.navigate("CrossProfilePackage") } } if(profileOwner) { - FunctionItem(R.string.cross_profile_widget, "", R.drawable.widgets_fill0) { navCtrl.navigate("CrossProfileWidget") } + FunctionItem(title = R.string.cross_profile_widget, icon = R.drawable.widgets_fill0) { navCtrl.navigate("CrossProfileWidget") } } if(VERSION.SDK_INT >= 34 && deviceOwner) { - FunctionItem(R.string.credential_manage_policy, "", R.drawable.license_fill0) { navCtrl.navigate("CredentialManagePolicy") } + FunctionItem(title = R.string.credential_manage_policy, icon = R.drawable.license_fill0) { navCtrl.navigate("CredentialManagePolicy") } } - FunctionItem(R.string.permitted_accessibility_services, "", R.drawable.settings_accessibility_fill0) { navCtrl.navigate("Accessibility") } - FunctionItem(R.string.permitted_ime, "", R.drawable.keyboard_fill0) { navCtrl.navigate("IME") } - FunctionItem(R.string.enable_system_app, "", R.drawable.enable_fill0) { + FunctionItem(title = R.string.permitted_accessibility_services, icon = R.drawable.settings_accessibility_fill0) { navCtrl.navigate("Accessibility") } + FunctionItem(title = R.string.permitted_ime, icon = R.drawable.keyboard_fill0) { navCtrl.navigate("IME") } + FunctionItem(title = R.string.enable_system_app, icon = R.drawable.enable_fill0) { if(pkgName != "") dialogStatus = 1 } if(VERSION.SDK_INT >= 28 && deviceOwner) { - FunctionItem(R.string.keep_uninstalled_packages, "", R.drawable.delete_fill0) { navCtrl.navigate("KeepUninstalled") } + FunctionItem(title = R.string.keep_uninstalled_packages, icon = R.drawable.delete_fill0) { navCtrl.navigate("KeepUninstalled") } } if(VERSION.SDK_INT >= 28) { - FunctionItem(R.string.clear_app_storage, "", R.drawable.mop_fill0) { + FunctionItem(title = R.string.clear_app_storage, icon = R.drawable.mop_fill0) { if(pkgName != "") dialogStatus = 2 } } - FunctionItem(R.string.install_app, "", R.drawable.install_mobile_fill0) { navCtrl.navigate("InstallApp") } - FunctionItem(R.string.uninstall_app, "", R.drawable.delete_fill0) { navCtrl.navigate("UninstallApp") } + FunctionItem(title = R.string.install_app, icon = R.drawable.install_mobile_fill0) { navCtrl.navigate("InstallApp") } + FunctionItem(title = R.string.uninstall_app, icon = R.drawable.delete_fill0) { navCtrl.navigate("UninstallApp") } if(VERSION.SDK_INT >= 34 && (deviceOwner || dpm.isOrgProfile(receiver))) { - FunctionItem(R.string.set_default_dialer, "", R.drawable.call_fill0) { + FunctionItem(title = R.string.set_default_dialer, icon = R.drawable.call_fill0) { if(pkgName != "") dialogStatus = 3 } } Spacer(Modifier.padding(vertical = 30.dp)) - LaunchedEffect(Unit) { fileUriFlow.value = Uri.parse("") } } if(dialogStatus == 1) AlertDialog( title = { Text(stringResource(R.string.enable_system_app)) }, @@ -279,7 +278,7 @@ private fun Home(navCtrl:NavHostController, pkgName: String) { onClick = { try { dpm.enableSystemApp(receiver, pkgName) - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) } catch(_: IllegalArgumentException) { Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show() } @@ -343,7 +342,7 @@ private fun Home(navCtrl:NavHostController, pkgName: String) { onClick = { try{ dpm.setDefaultDialerApplication(pkgName) - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) } catch(_: IllegalArgumentException) { Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show() } @@ -656,25 +655,13 @@ private fun CredentialManagePolicy(pkgName: String) { Spacer(Modifier.padding(vertical = 10.dp)) Text(text = stringResource(R.string.credential_manage_policy), style = typography.headlineLarge) Spacer(Modifier.padding(vertical = 5.dp)) - RadioButtonItem( - R.string.none, - policyType == -1, { policyType = -1 } - ) - RadioButtonItem( - R.string.blacklist, - policyType == PACKAGE_POLICY_BLOCKLIST, - { policyType = PACKAGE_POLICY_BLOCKLIST } - ) - RadioButtonItem( - R.string.whitelist, - policyType == PACKAGE_POLICY_ALLOWLIST, - { policyType = PACKAGE_POLICY_ALLOWLIST } - ) + RadioButtonItem(R.string.none, policyType == -1) { policyType = -1 } + RadioButtonItem(R.string.blacklist, policyType == PACKAGE_POLICY_BLOCKLIST) { policyType = PACKAGE_POLICY_BLOCKLIST } + RadioButtonItem(R.string.whitelist, policyType == PACKAGE_POLICY_ALLOWLIST){ policyType = PACKAGE_POLICY_ALLOWLIST } RadioButtonItem( R.string.whitelist_and_system_app, - policyType == PACKAGE_POLICY_ALLOWLIST_AND_SYSTEM, - { policyType = PACKAGE_POLICY_ALLOWLIST_AND_SYSTEM } - ) + policyType == PACKAGE_POLICY_ALLOWLIST_AND_SYSTEM + ) { policyType = PACKAGE_POLICY_ALLOWLIST_AND_SYSTEM } Spacer(Modifier.padding(vertical = 5.dp)) AnimatedVisibility(policyType != -1) { Column { @@ -699,7 +686,7 @@ private fun CredentialManagePolicy(pkgName: String) { } else { dpm.credentialManagerPolicy = null } - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) } catch(_: IllegalArgumentException) { Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show() } finally { @@ -798,8 +785,8 @@ private fun PermittedIME(pkgName: String) { Text(text = stringResource(R.string.permitted_ime), style = typography.headlineLarge) Spacer(Modifier.padding(vertical = 5.dp)) SwitchItem( - R.string.allow_all, "", null, allowAll, - { + R.string.allow_all, state = allowAll, + onCheckedChange = { dpm.setPermittedInputMethods(receiver, if(it) null else listOf()) refresh() }, padding = false @@ -918,8 +905,11 @@ private fun UninstallApp(pkgName: String) { private fun InstallApp() { val context = LocalContext.current val focusMgr = LocalFocusManager.current - val selected = fileUriFlow.collectAsState().value != Uri.parse("") val sharedPrefs = context.getSharedPreferences("data", Context.MODE_PRIVATE) + var apkFileUri by remember { mutableStateOf(null) } + val getFileLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + result.data.also { if(it != null) apkFileUri = it.data } + } Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { Spacer(Modifier.padding(vertical = 10.dp)) Text(text = stringResource(R.string.install_app), style = typography.headlineLarge) @@ -930,19 +920,19 @@ private fun InstallApp() { val installApkIntent = Intent(Intent.ACTION_GET_CONTENT) installApkIntent.setType("application/vnd.android.package-archive") installApkIntent.addCategory(Intent.CATEGORY_OPENABLE) - getFile.launch(installApkIntent) + getFileLauncher.launch(installApkIntent) }, modifier = Modifier.fillMaxWidth() ) { Text(stringResource(R.string.select_apk)) } - AnimatedVisibility(selected) { + AnimatedVisibility(apkFileUri != null) { Spacer(Modifier.padding(vertical = 3.dp)) Column(modifier = Modifier.fillMaxWidth()) { Button( onClick = { val intent = Intent(context, InstallAppActivity::class.java) - intent.data = fileUriFlow.value + intent.data = apkFileUri context.startActivity(intent) }, enabled = !sharedPrefs.getBoolean("dhizuku", false) && context.isDeviceOwner, @@ -953,7 +943,7 @@ private fun InstallApp() { Button( onClick = { val intent = Intent(Intent.ACTION_INSTALL_PACKAGE) - intent.setData(fileUriFlow.value) + intent.setData(apkFileUri) intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) context.startActivity(intent) }, diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt index 8640055..0e210a0 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt @@ -42,8 +42,8 @@ import kotlinx.serialization.json.put import kotlinx.serialization.json.putJsonArray import java.io.IOException import java.io.InputStream +import java.io.OutputStream -lateinit var createManagedProfile: ActivityResultLauncher lateinit var addDeviceAdmin: ActivityResultLauncher val Context.isDeviceOwner: Boolean @@ -356,15 +356,10 @@ fun handleNetworkLogs(context: Context, batchToken: Long) { } @RequiresApi(24) -fun handleSecurityLogs(context: Context) { - val file = context.filesDir.resolve("SecurityLogs.json") +fun processSecurityLogs(securityEvents: List, outputStream: OutputStream) { val json = Json { ignoreUnknownKeys = true; explicitNulls = false } - val securityEvents = context.getDPM().retrieveSecurityLogs(context.getReceiver()) - securityEvents ?: return - val fileExist = file.exists() - val buffer = file.bufferedWriter() + val buffer = outputStream.bufferedWriter() securityEvents.forEachIndexed { index, event -> - if(fileExist && index == 0) buffer.write(",") val item = buildJsonObject { put("time_nanos", event.timeNanos) put("tag", event.tag) diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/ManagedProfile.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/ManagedProfile.kt index 937c1ca..da69100 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/ManagedProfile.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/ManagedProfile.kt @@ -19,6 +19,8 @@ import android.content.* import android.os.Binder import android.os.Build.VERSION import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -38,6 +40,7 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -52,6 +55,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import com.bintianqi.owndroid.R +import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.ui.CardItem import com.bintianqi.owndroid.ui.CheckBoxItem import com.bintianqi.owndroid.ui.CopyTextButton @@ -69,19 +73,19 @@ fun WorkProfile(navCtrl: NavHostController) { val profileOwner = context.isProfileOwner MyScaffold(R.string.work_profile, 0.dp, navCtrl) { if(VERSION.SDK_INT >= 30 && profileOwner && dpm.isManagedProfile(receiver)) { - FunctionItem(R.string.org_owned_work_profile, "", R.drawable.corporate_fare_fill0) { navCtrl.navigate("OrgOwnedWorkProfile") } + FunctionItem(R.string.org_owned_work_profile, icon = R.drawable.corporate_fare_fill0) { navCtrl.navigate("OrgOwnedWorkProfile") } } if(VERSION.SDK_INT<24 || (VERSION.SDK_INT>=24 && dpm.isProvisioningAllowed(ACTION_PROVISION_MANAGED_PROFILE))) { - FunctionItem(R.string.create_work_profile, "", R.drawable.work_fill0) { navCtrl.navigate("CreateWorkProfile") } + FunctionItem(R.string.create_work_profile, icon = R.drawable.work_fill0) { navCtrl.navigate("CreateWorkProfile") } } if(dpm.isOrgProfile(receiver)) { - FunctionItem(R.string.suspend_personal_app, "", R.drawable.block_fill0) { navCtrl.navigate("SuspendPersonalApp") } + FunctionItem(R.string.suspend_personal_app, icon = R.drawable.block_fill0) { navCtrl.navigate("SuspendPersonalApp") } } if(profileOwner && (VERSION.SDK_INT < 24 || (VERSION.SDK_INT >= 24 && dpm.isManagedProfile(receiver)))) { - FunctionItem(R.string.intent_filter, "", R.drawable.filter_alt_fill0) { navCtrl.navigate("IntentFilter") } + FunctionItem(R.string.intent_filter, icon = R.drawable.filter_alt_fill0) { navCtrl.navigate("IntentFilter") } } if(profileOwner && (VERSION.SDK_INT < 24 || (VERSION.SDK_INT >= 24 && dpm.isManagedProfile(receiver)))) { - FunctionItem(R.string.delete_work_profile, "", R.drawable.delete_forever_fill0) { navCtrl.navigate("DeleteWorkProfile") } + FunctionItem(R.string.delete_work_profile, icon = R.drawable.delete_forever_fill0) { navCtrl.navigate("DeleteWorkProfile") } } } } @@ -91,6 +95,7 @@ fun CreateWorkProfile(navCtrl: NavHostController) { val context = LocalContext.current val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current + val launcher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { } MyScaffold(R.string.create_work_profile, 8.dp, navCtrl) { var skipEncrypt by remember { mutableStateOf(false) } var offlineProvisioning by remember { mutableStateOf(true) } @@ -99,7 +104,7 @@ fun CreateWorkProfile(navCtrl: NavHostController) { var migrateAccountType by remember { mutableStateOf("") } var keepAccount by remember { mutableStateOf(true) } if(VERSION.SDK_INT >= 22) { - CheckBoxItem(R.string.migrate_account, migrateAccount, { migrateAccount = it }) + CheckBoxItem(R.string.migrate_account, migrateAccount) { migrateAccount = it } AnimatedVisibility(migrateAccount) { val fr = FocusRequester() Column(modifier = Modifier.padding(start = 10.dp)) { @@ -118,23 +123,19 @@ fun CreateWorkProfile(navCtrl: NavHostController) { modifier = Modifier.fillMaxWidth().focusRequester(fr) ) if(VERSION.SDK_INT >= 26) { - CheckBoxItem(R.string.keep_account, keepAccount, { keepAccount = it }) + CheckBoxItem(R.string.keep_account, keepAccount) { keepAccount = it } } } } } - if(VERSION.SDK_INT >= 24) { - CheckBoxItem(R.string.skip_encryption, skipEncrypt, { skipEncrypt = it }) - } - if(VERSION.SDK_INT >= 33) { - CheckBoxItem(R.string.offline_provisioning, offlineProvisioning, { offlineProvisioning = it }) - } + if(VERSION.SDK_INT >= 24) CheckBoxItem(R.string.skip_encryption, skipEncrypt) { skipEncrypt = it } + if(VERSION.SDK_INT >= 33) CheckBoxItem(R.string.offline_provisioning, offlineProvisioning) { offlineProvisioning = it } Spacer(Modifier.padding(vertical = 5.dp)) Button( onClick = { try { val intent = Intent(ACTION_PROVISION_MANAGED_PROFILE) - if(VERSION.SDK_INT>=23) { + if(VERSION.SDK_INT >= 23) { intent.putExtra(EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME,receiver) } else { intent.putExtra(EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME, context.packageName) @@ -147,8 +148,8 @@ fun CreateWorkProfile(navCtrl: NavHostController) { } if(VERSION.SDK_INT >= 24) { intent.putExtra(EXTRA_PROVISIONING_SKIP_ENCRYPTION, skipEncrypt) } if(VERSION.SDK_INT >= 33) { intent.putExtra(EXTRA_PROVISIONING_ALLOW_OFFLINE, offlineProvisioning) } - createManagedProfile.launch(intent) - } catch(_:ActivityNotFoundException) { + launcher.launch(intent) + } catch(_: ActivityNotFoundException) { Toast.makeText(context, R.string.unsupported, Toast.LENGTH_SHORT).show() } }, @@ -188,10 +189,8 @@ fun SuspendPersonalApp(navCtrl: NavHostController) { val focusMgr = LocalFocusManager.current var suspend by remember { mutableStateOf(dpm.getPersonalAppsSuspendedReasons(receiver) != PERSONAL_APPS_NOT_SUSPENDED) } MyScaffold(R.string.suspend_personal_app, 8.dp, navCtrl) { - SwitchItem( - R.string.suspend_personal_app, "", null, - suspend, - { + SwitchItem(R.string.suspend_personal_app, state = suspend, + onCheckedChange = { dpm.setPersonalAppsSuspended(receiver,it) suspend = dpm.getPersonalAppsSuspendedReasons(receiver) != PERSONAL_APPS_NOT_SUSPENDED }, padding = false @@ -217,7 +216,7 @@ fun SuspendPersonalApp(navCtrl: NavHostController) { Button( onClick = { dpm.setManagedProfileMaximumTimeOff(receiver,time.toLong()) - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth() ) { @@ -246,7 +245,7 @@ fun IntentFilter(navCtrl: NavHostController) { Button( onClick = { dpm.addCrossProfileIntentFilter(receiver, IntentFilter(action), FLAG_PARENT_CAN_ACCESS_MANAGED) - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth() ) { @@ -255,7 +254,7 @@ fun IntentFilter(navCtrl: NavHostController) { Button( onClick = { dpm.addCrossProfileIntentFilter(receiver, IntentFilter(action), FLAG_MANAGED_CAN_ACCESS_PARENT) - Toast.makeText(context, R.string.success,Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth() ) { @@ -265,7 +264,7 @@ fun IntentFilter(navCtrl: NavHostController) { Button( onClick = { dpm.clearCrossProfileIntentFilters(receiver) - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth() ) { @@ -280,15 +279,14 @@ fun DeleteWorkProfile(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val focusMgr = LocalFocusManager.current + var flag by remember { mutableIntStateOf(0) } var warning by remember { mutableStateOf(false) } - var externalStorage by remember { mutableStateOf(false) } - var euicc by remember { mutableStateOf(false) } var silent by remember { mutableStateOf(false) } var reason by remember { mutableStateOf("") } MyScaffold(R.string.delete_work_profile, 8.dp, navCtrl) { - CheckBoxItem(R.string.wipe_external_storage, externalStorage, { externalStorage = it }) - if(VERSION.SDK_INT >= 28) { CheckBoxItem(R.string.wipe_euicc, euicc, { euicc = it }) } - CheckBoxItem(R.string.wipe_silently, silent, { silent = it }) + CheckBoxItem(R.string.wipe_external_storage, flag and WIPE_EXTERNAL_STORAGE != 0) { flag = flag xor WIPE_EXTERNAL_STORAGE } + if(VERSION.SDK_INT >= 28) CheckBoxItem(R.string.wipe_euicc, flag and WIPE_EUICC != 0) { flag = flag xor WIPE_EUICC } + CheckBoxItem(R.string.wipe_silently, silent) { silent = it } AnimatedVisibility(!silent && VERSION.SDK_INT >= 28) { OutlinedTextField( value = reason, onValueChange = { reason = it }, @@ -322,9 +320,6 @@ fun DeleteWorkProfile(navCtrl: NavHostController) { confirmButton = { TextButton( onClick = { - var flag = 0 - if(externalStorage) { flag += WIPE_EXTERNAL_STORAGE } - if(euicc && VERSION.SDK_INT >= 28) { flag += WIPE_EUICC } if(VERSION.SDK_INT >= 28 && !silent) { dpm.wipeData(flag, reason) } else { diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt index 7e6439b..a54d8ca 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt @@ -124,10 +124,7 @@ import androidx.core.os.bundleOf import androidx.navigation.NavHostController import com.bintianqi.owndroid.MyViewModel import com.bintianqi.owndroid.R -import com.bintianqi.owndroid.exportFile -import com.bintianqi.owndroid.exportFilePath import com.bintianqi.owndroid.formatFileSize -import com.bintianqi.owndroid.isExportingSecurityOrNetworkLogs import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.ui.CheckBoxItem import com.bintianqi.owndroid.ui.FunctionItem @@ -157,30 +154,30 @@ fun Network(navCtrl:NavHostController) { val sharedPref = context.getSharedPreferences("data", Context.MODE_PRIVATE) val dhizuku = sharedPref.getBoolean("dhizuku", false) MyScaffold(R.string.network, 0.dp, navCtrl) { - if(!dhizuku) FunctionItem(R.string.wifi, "", R.drawable.wifi_fill0) { navCtrl.navigate("Wifi") } + if(!dhizuku) FunctionItem(R.string.wifi, icon = R.drawable.wifi_fill0) { navCtrl.navigate("Wifi") } if(VERSION.SDK_INT >= 30) { - FunctionItem(R.string.options, "", R.drawable.tune_fill0) { navCtrl.navigate("NetworkOptions") } + FunctionItem(R.string.options, icon = R.drawable.tune_fill0) { navCtrl.navigate("NetworkOptions") } } if(VERSION.SDK_INT >= 29 && deviceOwner) { - FunctionItem(R.string.private_dns, "", R.drawable.dns_fill0) { navCtrl.navigate("PrivateDNS") } + FunctionItem(R.string.private_dns, icon = R.drawable.dns_fill0) { navCtrl.navigate("PrivateDNS") } } if(VERSION.SDK_INT >= 24) { - FunctionItem(R.string.always_on_vpn, "", R.drawable.vpn_key_fill0) { navCtrl.navigate("AlwaysOnVpn") } + FunctionItem(R.string.always_on_vpn, icon = R.drawable.vpn_key_fill0) { navCtrl.navigate("AlwaysOnVpn") } } if(deviceOwner) { - FunctionItem(R.string.recommended_global_proxy, "", R.drawable.vpn_key_fill0) { navCtrl.navigate("RecommendedGlobalProxy") } + FunctionItem(R.string.recommended_global_proxy, icon = R.drawable.vpn_key_fill0) { navCtrl.navigate("RecommendedGlobalProxy") } } if(VERSION.SDK_INT >= 26 && !dhizuku && (deviceOwner || (profileOwner && dpm.isManagedProfile(receiver)))) { - FunctionItem(R.string.network_logging, "", R.drawable.description_fill0) { navCtrl.navigate("NetworkLog") } + FunctionItem(R.string.network_logging, icon = R.drawable.description_fill0) { navCtrl.navigate("NetworkLog") } } if(VERSION.SDK_INT >= 31) { - FunctionItem(R.string.wifi_auth_keypair, "", R.drawable.key_fill0) { navCtrl.navigate("WifiAuthKeypair") } + FunctionItem(R.string.wifi_auth_keypair, icon = R.drawable.key_fill0) { navCtrl.navigate("WifiAuthKeypair") } } if(VERSION.SDK_INT >= 33) { - FunctionItem(R.string.preferential_network_service, "", R.drawable.globe_fill0) { navCtrl.navigate("PreferentialNetworkService") } + FunctionItem(R.string.preferential_network_service, icon = R.drawable.globe_fill0) { navCtrl.navigate("PreferentialNetworkService") } } if(VERSION.SDK_INT >= 28 && deviceOwner) { - FunctionItem(R.string.override_apn_settings, "", R.drawable.cell_tower_fill0) { navCtrl.navigate("OverrideAPN") } + FunctionItem(R.string.override_apn_settings, icon = R.drawable.cell_tower_fill0) { navCtrl.navigate("OverrideAPN") } } } } @@ -194,8 +191,8 @@ fun NetworkOptions(navCtrl: NavHostController) { var dialog by remember { mutableIntStateOf(0) } MyScaffold(R.string.options, 0.dp, navCtrl) { if(VERSION.SDK_INT>=30 && (deviceOwner || dpm.isOrgProfile(receiver))) { - SwitchItem(R.string.lockdown_admin_configured_network, "", R.drawable.wifi_password_fill0, - { dpm.hasLockdownAdminConfiguredNetworks(receiver) }, { dpm.setConfiguredNetworksLockdownState(receiver,it) }, + SwitchItem(R.string.lockdown_admin_configured_network, icon = R.drawable.wifi_password_fill0, + getState = { dpm.hasLockdownAdminConfiguredNetworks(receiver) }, onCheckedChange = { dpm.setConfiguredNetworksLockdownState(receiver,it) }, onClickBlank = { dialog = 1 } ) } @@ -280,11 +277,11 @@ fun Wifi(navCtrl: NavHostController) { } } if(VERSION.SDK_INT >= 24 && (deviceOwner || orgProfileOwner)) { - FunctionItem(R.string.wifi_mac_address, "", null) { wifiMacDialog = true } + FunctionItem(R.string.wifi_mac_address) { wifiMacDialog = true } } if(VERSION.SDK_INT >= 33 && (deviceOwner || orgProfileOwner)) { - FunctionItem(R.string.min_wifi_security_level, "", null) { navCtrl.navigate("MinWifiSecurityLevel") } - FunctionItem(R.string.wifi_ssid_policy, "", null) { navCtrl.navigate("WifiSsidPolicy") } + FunctionItem(R.string.min_wifi_security_level) { navCtrl.navigate("MinWifiSecurityLevel") } + FunctionItem(R.string.wifi_ssid_policy) { navCtrl.navigate("WifiSsidPolicy") } } } } else if(page == 1) { @@ -397,8 +394,7 @@ private fun SavedNetworks(navCtrl: NavHostController) { ) { Button( onClick = { - val success = wm.enableNetwork(network.networkId, false) - Toast.makeText(context, if(success) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(wm.enableNetwork(network.networkId, false)) networkDetailsDialog = -1 refresh() }, @@ -408,8 +404,7 @@ private fun SavedNetworks(navCtrl: NavHostController) { } Button( onClick = { - val success = wm.disableNetwork(network.networkId) - Toast.makeText(context, if(success) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(wm.disableNetwork(network.networkId)) networkDetailsDialog = -1 refresh() }, @@ -431,8 +426,7 @@ private fun SavedNetworks(navCtrl: NavHostController) { } TextButton( onClick = { - val success = wm.removeNetwork(network.networkId) - Toast.makeText(context, if(success) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(wm.removeNetwork(network.networkId)) networkDetailsDialog = -1 refresh() }, @@ -526,7 +520,7 @@ private fun AddNetwork(wifiConfig: WifiConfiguration? = null, navCtrl: NavHostCo value = ssid, onValueChange = { ssid = it }, label = { Text("SSID") }, modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp) ) - CheckBoxItem(R.string.hidden_ssid, hiddenSsid, { hiddenSsid = it }) + CheckBoxItem(R.string.hidden_ssid, hiddenSsid) { hiddenSsid = it } if(VERSION.SDK_INT >= 30) { // TODO: more protocols val securityTypeTextMap = mutableMapOf( @@ -731,31 +725,15 @@ fun WifiSecurityLevel(navCtrl: NavHostController) { var selectedWifiSecLevel by remember { mutableIntStateOf(0) } LaunchedEffect(Unit) { selectedWifiSecLevel = dpm.minimumRequiredWifiSecurityLevel } MyScaffold(R.string.min_wifi_security_level, 8.dp, navCtrl) { - RadioButtonItem( - R.string.wifi_security_open, - selectedWifiSecLevel == WIFI_SECURITY_OPEN, - { selectedWifiSecLevel = WIFI_SECURITY_OPEN } - ) - RadioButtonItem( - "WEP, WPA(2)-PSK", - selectedWifiSecLevel == WIFI_SECURITY_PERSONAL, - { selectedWifiSecLevel = WIFI_SECURITY_PERSONAL } - ) - RadioButtonItem( - "WPA-EAP", - selectedWifiSecLevel == WIFI_SECURITY_ENTERPRISE_EAP, - { selectedWifiSecLevel = WIFI_SECURITY_ENTERPRISE_EAP } - ) - RadioButtonItem( - "WPA3-192bit", - selectedWifiSecLevel == WIFI_SECURITY_ENTERPRISE_192, - { selectedWifiSecLevel = WIFI_SECURITY_ENTERPRISE_192 } - ) + RadioButtonItem(R.string.wifi_security_open, selectedWifiSecLevel == WIFI_SECURITY_OPEN) { selectedWifiSecLevel = WIFI_SECURITY_OPEN } + RadioButtonItem("WEP, WPA(2)-PSK", selectedWifiSecLevel == WIFI_SECURITY_PERSONAL) { selectedWifiSecLevel = WIFI_SECURITY_PERSONAL } + RadioButtonItem("WPA-EAP", selectedWifiSecLevel == WIFI_SECURITY_ENTERPRISE_EAP) { selectedWifiSecLevel = WIFI_SECURITY_ENTERPRISE_EAP } + RadioButtonItem("WPA3-192bit", selectedWifiSecLevel == WIFI_SECURITY_ENTERPRISE_192) { selectedWifiSecLevel = WIFI_SECURITY_ENTERPRISE_192 } Spacer(Modifier.padding(vertical = 5.dp)) Button( onClick = { dpm.minimumRequiredWifiSecurityLevel = selectedWifiSecLevel - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth() ) { @@ -781,21 +759,13 @@ fun WifiSsidPolicy(navCtrl: NavHostController) { ssidList.addAll(policy?.ssids ?: mutableSetOf()) } LaunchedEffect(Unit) { refreshPolicy() } - RadioButtonItem( - R.string.none, - selectedPolicyType == -1, - { selectedPolicyType = -1 } - ) - RadioButtonItem( - R.string.whitelist, - selectedPolicyType == WIFI_SSID_POLICY_TYPE_ALLOWLIST, - { selectedPolicyType = WIFI_SSID_POLICY_TYPE_ALLOWLIST } - ) - RadioButtonItem( - R.string.blacklist, - selectedPolicyType == WIFI_SSID_POLICY_TYPE_DENYLIST, - { selectedPolicyType = WIFI_SSID_POLICY_TYPE_DENYLIST } - ) + RadioButtonItem(R.string.none, selectedPolicyType == -1) { selectedPolicyType = -1 } + RadioButtonItem(R.string.whitelist, selectedPolicyType == WIFI_SSID_POLICY_TYPE_ALLOWLIST) { + selectedPolicyType = WIFI_SSID_POLICY_TYPE_ALLOWLIST + } + RadioButtonItem(R.string.blacklist, selectedPolicyType == WIFI_SSID_POLICY_TYPE_DENYLIST) { + selectedPolicyType = WIFI_SSID_POLICY_TYPE_DENYLIST + } AnimatedVisibility(selectedPolicyType != -1) { var inputSsid by remember { mutableStateOf("") } Column { @@ -838,7 +808,7 @@ fun WifiSsidPolicy(navCtrl: NavHostController) { WifiSsidPolicy(selectedPolicyType, ssidList.toSet()) } refreshPolicy() - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth() ) { @@ -940,7 +910,7 @@ fun AlwaysOnVPNPackage(navCtrl: NavHostController, vm: MyViewModel) { val setAlwaysOnVpn: (String?, Boolean)->Boolean = { vpnPkg: String?, lockdownEnabled: Boolean -> try { dpm.setAlwaysOnVpnPackage(receiver, vpnPkg, lockdownEnabled) - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) true } catch(e: UnsupportedOperationException) { e.printStackTrace() @@ -971,7 +941,7 @@ fun AlwaysOnVPNPackage(navCtrl: NavHostController, vm: MyViewModel) { }, modifier = Modifier.fillMaxWidth().padding(vertical = 3.dp) ) - SwitchItem(R.string.enable_lockdown, "", null, lockdown, { lockdown = it }, padding = false) + SwitchItem(R.string.enable_lockdown, state = lockdown, onCheckedChange = { lockdown = it }, padding = false) Spacer(Modifier.padding(vertical = 5.dp)) Button( onClick = { if(setAlwaysOnVpn(pkgName, lockdown)) refresh() }, @@ -1002,9 +972,9 @@ fun RecommendedGlobalProxy(navCtrl: NavHostController) { var proxyPort by remember { mutableStateOf("") } var exclList by remember { mutableStateOf("") } MyScaffold(R.string.recommended_global_proxy, 8.dp, navCtrl) { - RadioButtonItem(R.string.proxy_type_off, proxyType == 0, { proxyType = 0 }) - RadioButtonItem(R.string.proxy_type_pac, proxyType == 1, { proxyType = 1 }) - RadioButtonItem(R.string.proxy_type_direct, proxyType == 2, { proxyType = 2 }) + RadioButtonItem(R.string.proxy_type_off, proxyType == 0) { proxyType = 0 } + RadioButtonItem(R.string.proxy_type_pac, proxyType == 1) { proxyType = 1 } + RadioButtonItem(R.string.proxy_type_direct, proxyType == 2) { proxyType = 2 } AnimatedVisibility(proxyType != 0) { OutlinedTextField( value = proxyUri, @@ -1017,7 +987,7 @@ fun RecommendedGlobalProxy(navCtrl: NavHostController) { } AnimatedVisibility(proxyType == 1 && VERSION.SDK_INT >= 30) { Box(modifier = Modifier.padding(top = 2.dp)) { - CheckBoxItem(R.string.specify_port, specifyPort, { specifyPort = it }) + CheckBoxItem(R.string.specify_port, specifyPort) { specifyPort = it } } } AnimatedVisibility((proxyType == 1 && specifyPort && VERSION.SDK_INT >= 30) || proxyType == 2) { @@ -1045,7 +1015,7 @@ fun RecommendedGlobalProxy(navCtrl: NavHostController) { onClick = { if(proxyType == 0) { dpm.setRecommendedGlobalProxy(receiver, null) - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) return@Button } if(proxyUri == "") { @@ -1076,7 +1046,7 @@ fun RecommendedGlobalProxy(navCtrl: NavHostController) { return@Button } dpm.setRecommendedGlobalProxy(receiver, proxyInfo) - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth() ) { @@ -1094,11 +1064,24 @@ fun NetworkLogging(navCtrl: NavHostController) { val receiver = context.getReceiver() val logFile = context.filesDir.resolve("NetworkLogs.json") var fileSize by remember { mutableLongStateOf(0) } - LaunchedEffect(Unit) { - fileSize = logFile.length() + LaunchedEffect(Unit) { fileSize = logFile.length() } + val exportNetworkLogsLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + result.data?.data?.let { uri -> + context.contentResolver.openOutputStream(uri)?.use { outStream -> + outStream.write("[".encodeToByteArray()) + logFile.inputStream().use { it.copyTo(outStream) } + outStream.write("]".encodeToByteArray()) + context.showOperationResultToast(true) + } + } } MyScaffold(R.string.network_logging, 8.dp, navCtrl) { - SwitchItem(R.string.enable, "", null, { dpm.isNetworkLoggingEnabled(receiver) }, { dpm.setNetworkLoggingEnabled(receiver,it) }, padding = false) + SwitchItem( + R.string.enable, + getState = { dpm.isNetworkLoggingEnabled(receiver) }, + onCheckedChange = { dpm.setNetworkLoggingEnabled(receiver,it) }, + padding = false + ) Text(stringResource(R.string.log_file_size_is, formatFileSize(fileSize))) Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { Button( @@ -1107,9 +1090,7 @@ fun NetworkLogging(navCtrl: NavHostController) { intent.addCategory(Intent.CATEGORY_OPENABLE) intent.setType("application/json") intent.putExtra(Intent.EXTRA_TITLE, "NetworkLogs.json") - exportFilePath = logFile.path - isExportingSecurityOrNetworkLogs = true - exportFile.launch(intent) + exportNetworkLogsLauncher.launch(intent) }, enabled = fileSize > 0, modifier = Modifier.fillMaxWidth(0.49F) @@ -1158,19 +1139,13 @@ fun WifiAuthKeypair(navCtrl: NavHostController) { Spacer(Modifier.padding(vertical = 5.dp)) Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Button( - onClick = { - val result = dpm.grantKeyPairToWifiAuth(keyPair) - Toast.makeText(context, if(result) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show() - }, + onClick = { context.showOperationResultToast(dpm.grantKeyPairToWifiAuth(keyPair)) }, modifier = Modifier.fillMaxWidth(0.49F) ) { Text(stringResource(R.string.grant)) } Button( - onClick = { - val result = dpm.revokeKeyPairFromWifiAuth(keyPair) - Toast.makeText(context, if(result) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show() - }, + onClick = { context.showOperationResultToast(dpm.revokeKeyPairFromWifiAuth(keyPair)) }, modifier = Modifier.fillMaxWidth(0.96F) ) { Text(stringResource(R.string.revoke)) @@ -1221,10 +1196,7 @@ fun PreferentialNetworkService(navCtrl: NavHostController) { } LaunchedEffect(Unit) { initialize() } MyScaffold(R.string.preferential_network_service, 8.dp, navCtrl) { - SwitchItem( - title = R.string.enabled, desc = "", icon = null, - state = masterEnabled, onCheckedChange = { masterEnabled = it }, padding = false - ) + SwitchItem(R.string.enabled, state = masterEnabled, onCheckedChange = { masterEnabled = it }, padding = false) Row( horizontalArrangement = Arrangement.SpaceAround, verticalAlignment = Alignment.CenterVertically, @@ -1272,7 +1244,7 @@ fun PreferentialNetworkService(navCtrl: NavHostController) { onClick = { try { saveCurrentConfig() - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) } catch(e: Exception) { e.printStackTrace() Toast.makeText(context, R.string.failed_to_save_current_config, Toast.LENGTH_SHORT).show() @@ -1292,10 +1264,7 @@ fun PreferentialNetworkService(navCtrl: NavHostController) { Icon(imageVector = Icons.Default.Delete, contentDescription = stringResource(R.string.delete_current_config)) } } - SwitchItem( - title = R.string.enabled, desc = "", icon = null, - state = enabled, onCheckedChange = { enabled = it }, padding = false - ) + SwitchItem(title = R.string.enabled, state = enabled, onCheckedChange = { enabled = it }, padding = false) OutlinedTextField( value = networkId, onValueChange = { networkId = it }, label = { Text(stringResource(R.string.network_id)) }, @@ -1304,11 +1273,11 @@ fun PreferentialNetworkService(navCtrl: NavHostController) { modifier = Modifier.fillMaxWidth().padding(bottom = 6.dp) ) SwitchItem( - title = R.string.allow_fallback_to_default_connection, desc = "", icon = null, + title = R.string.allow_fallback_to_default_connection, state = allowFallback, onCheckedChange = { allowFallback = it }, padding = false ) if(VERSION.SDK_INT >= 34) SwitchItem( - title = R.string.block_non_matching_networks, desc = "", icon = null, + title = R.string.block_non_matching_networks, state = blockNonMatching, onCheckedChange = { blockNonMatching = it }, padding = false ) OutlinedTextField( @@ -1328,7 +1297,7 @@ fun PreferentialNetworkService(navCtrl: NavHostController) { dpm.isPreferentialNetworkServiceEnabled = masterEnabled dpm.preferentialNetworkServiceConfigs = configs initialize() - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth().padding(top = 12.dp) ) { @@ -1351,7 +1320,11 @@ fun OverrideAPN(navCtrl: NavHostController) { MyScaffold(R.string.override_apn_settings, 8.dp, navCtrl) { Text(text = stringResource(id = R.string.developing)) Spacer(Modifier.padding(vertical = 5.dp)) - SwitchItem(R.string.enable, "", null, { dpm.isOverrideApnEnabled(receiver) }, { dpm.setOverrideApnsEnabled(receiver,it) }, padding = false) + SwitchItem( + R.string.enable, + getState = { dpm.isOverrideApnEnabled(receiver) }, onCheckedChange = { dpm.setOverrideApnsEnabled(receiver,it) }, + padding = false + ) Text(text = stringResource(R.string.total_apn_amount, setting.size)) if(setting.isNotEmpty()) { Text(text = stringResource(R.string.select_a_apn_or_create, setting.size)) @@ -1468,11 +1441,11 @@ fun OverrideAPN(navCtrl: NavHostController) { } Text(text = stringResource(R.string.auth_type), style = typography.titleLarge) - RadioButtonItem(R.string.none, selectedAuthType==AUTH_TYPE_NONE , { selectedAuthType=AUTH_TYPE_NONE }) - RadioButtonItem("CHAP", selectedAuthType == AUTH_TYPE_CHAP , { selectedAuthType = AUTH_TYPE_CHAP }) - RadioButtonItem("PAP", selectedAuthType == AUTH_TYPE_PAP, { selectedAuthType = AUTH_TYPE_PAP }) - RadioButtonItem("PAP/CHAP", selectedAuthType == AUTH_TYPE_PAP_OR_CHAP, { selectedAuthType = AUTH_TYPE_PAP_OR_CHAP }) - + RadioButtonItem(R.string.none, selectedAuthType==AUTH_TYPE_NONE) { selectedAuthType = AUTH_TYPE_NONE } + RadioButtonItem("CHAP", selectedAuthType == AUTH_TYPE_CHAP) { selectedAuthType = AUTH_TYPE_CHAP } + RadioButtonItem("PAP", selectedAuthType == AUTH_TYPE_PAP) { selectedAuthType = AUTH_TYPE_PAP } + RadioButtonItem("PAP/CHAP", selectedAuthType == AUTH_TYPE_PAP_OR_CHAP) { selectedAuthType = AUTH_TYPE_PAP_OR_CHAP } + if(VERSION.SDK_INT>=29) { val ts = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager carrierId = ts.simCarrierId.toString() @@ -1578,11 +1551,11 @@ fun OverrideAPN(navCtrl: NavHostController) { } Text(text = "MVNO", style = typography.titleLarge) - RadioButtonItem("SPN", mvnoType == MVNO_TYPE_SPN, { mvnoType = MVNO_TYPE_SPN }) - RadioButtonItem("IMSI", mvnoType == MVNO_TYPE_IMSI, { mvnoType = MVNO_TYPE_IMSI }) - RadioButtonItem("GID", mvnoType == MVNO_TYPE_GID, { mvnoType = MVNO_TYPE_GID }) - RadioButtonItem("ICCID", mvnoType == MVNO_TYPE_ICCID, { mvnoType = MVNO_TYPE_ICCID }) - + RadioButtonItem("SPN", mvnoType == MVNO_TYPE_SPN) { mvnoType = MVNO_TYPE_SPN } + RadioButtonItem("IMSI", mvnoType == MVNO_TYPE_IMSI) { mvnoType = MVNO_TYPE_IMSI } + RadioButtonItem("GID", mvnoType == MVNO_TYPE_GID) { mvnoType = MVNO_TYPE_GID } + RadioButtonItem("ICCID", mvnoType == MVNO_TYPE_ICCID) { mvnoType = MVNO_TYPE_ICCID } + Text(text = stringResource(R.string.network_type), style = typography.titleLarge) TextField( value = networkTypeBitmask, @@ -1625,23 +1598,23 @@ fun OverrideAPN(navCtrl: NavHostController) { } Text(text = stringResource(R.string.protocol), style = typography.titleLarge) - RadioButtonItem("IPV4", protocol == PROTOCOL_IP, { protocol = PROTOCOL_IP }) - RadioButtonItem("IPV6", protocol == PROTOCOL_IPV6, { protocol = PROTOCOL_IPV6 }) - RadioButtonItem("IPV4/IPV6", protocol == PROTOCOL_IPV4V6, { protocol = PROTOCOL_IPV4V6 }) - RadioButtonItem("PPP", protocol == PROTOCOL_PPP, { protocol = PROTOCOL_PPP }) + RadioButtonItem("IPV4", protocol == PROTOCOL_IP) { protocol = PROTOCOL_IP } + RadioButtonItem("IPV6", protocol == PROTOCOL_IPV6) { protocol = PROTOCOL_IPV6 } + RadioButtonItem("IPV4/IPV6", protocol == PROTOCOL_IPV4V6) { protocol = PROTOCOL_IPV4V6 } + RadioButtonItem("PPP", protocol == PROTOCOL_PPP) { protocol = PROTOCOL_PPP } if(VERSION.SDK_INT>=29) { - RadioButtonItem("non-IP", protocol == PROTOCOL_NON_IP, { protocol = PROTOCOL_NON_IP }) - RadioButtonItem("Unstructured", protocol == PROTOCOL_UNSTRUCTURED, { protocol = PROTOCOL_UNSTRUCTURED }) + RadioButtonItem("non-IP", protocol == PROTOCOL_NON_IP) { protocol = PROTOCOL_NON_IP } + RadioButtonItem("Unstructured", protocol == PROTOCOL_UNSTRUCTURED) { protocol = PROTOCOL_UNSTRUCTURED } } Text(text = stringResource(R.string.roaming_protocol), style = typography.titleLarge) - RadioButtonItem("IPV4", roamingProtocol == PROTOCOL_IP, { roamingProtocol = PROTOCOL_IP }) - RadioButtonItem("IPV6", roamingProtocol == PROTOCOL_IPV6, { roamingProtocol = PROTOCOL_IPV6 }) - RadioButtonItem("IPV4/IPV6", roamingProtocol == PROTOCOL_IPV4V6, { roamingProtocol = PROTOCOL_IPV4V6 }) - RadioButtonItem("PPP", roamingProtocol == PROTOCOL_PPP, { roamingProtocol = PROTOCOL_PPP}) + RadioButtonItem("IPV4", roamingProtocol == PROTOCOL_IP) { roamingProtocol = PROTOCOL_IP } + RadioButtonItem("IPV6", roamingProtocol == PROTOCOL_IPV6) { roamingProtocol = PROTOCOL_IPV6 } + RadioButtonItem("IPV4/IPV6", roamingProtocol == PROTOCOL_IPV4V6) { roamingProtocol = PROTOCOL_IPV4V6 } + RadioButtonItem("PPP", roamingProtocol == PROTOCOL_PPP) { roamingProtocol = PROTOCOL_PPP } if(VERSION.SDK_INT>=29) { - RadioButtonItem("non-IP", roamingProtocol == PROTOCOL_NON_IP, { roamingProtocol = PROTOCOL_NON_IP }) - RadioButtonItem("Unstructured", roamingProtocol == PROTOCOL_UNSTRUCTURED, { roamingProtocol = PROTOCOL_UNSTRUCTURED }) + RadioButtonItem("non-IP", roamingProtocol == PROTOCOL_NON_IP) { roamingProtocol = PROTOCOL_NON_IP } + RadioButtonItem("Unstructured", roamingProtocol == PROTOCOL_UNSTRUCTURED) { roamingProtocol = PROTOCOL_UNSTRUCTURED } } var finalStep by remember { mutableStateOf(false) } @@ -1688,20 +1661,14 @@ fun OverrideAPN(navCtrl: NavHostController) { }else{ Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Button( - onClick = { - val success = dpm.updateOverrideApn(receiver,id,result) - Toast.makeText(context, if(success) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show() - }, - Modifier.fillMaxWidth(0.49F) + onClick = { context.showOperationResultToast(dpm.updateOverrideApn(receiver, id, result)) }, + modifier = Modifier.fillMaxWidth(0.49F) ) { Text(stringResource(R.string.update)) } Button( - onClick = { - val success = dpm.removeOverrideApn(receiver,id) - Toast.makeText(context, if(success) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show() - }, - Modifier.fillMaxWidth(0.96F) + onClick = { context.showOperationResultToast(dpm.removeOverrideApn(receiver,id)) }, + modifier = Modifier.fillMaxWidth(0.96F) ) { Text(stringResource(R.string.remove)) } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt index 1941b00..1681d18 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt @@ -9,7 +9,6 @@ import android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_ALL import android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NONE import android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT import android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_IRIS -import android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_REMOTE_INPUT import android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_SECURE_CAMERA import android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_SECURE_NOTIFICATIONS import android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_SHORTCUTS_ALL @@ -55,7 +54,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -70,7 +68,7 @@ import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat.startActivity import androidx.navigation.NavHostController import com.bintianqi.owndroid.R -import com.bintianqi.owndroid.toggle +import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.ui.CardItem import com.bintianqi.owndroid.ui.CheckBoxItem import com.bintianqi.owndroid.ui.FunctionItem @@ -89,34 +87,34 @@ fun Password(navCtrl: NavHostController) { val profileOwner = context.isProfileOwner var dialog by remember { mutableIntStateOf(0) } MyScaffold(R.string.password_and_keyguard, 0.dp, navCtrl) { - FunctionItem(R.string.password_info, "", R.drawable.info_fill0) { navCtrl.navigate("PasswordInfo") } + FunctionItem(R.string.password_info, icon = R.drawable.info_fill0) { navCtrl.navigate("PasswordInfo") } if(sharedPrefs.getBoolean("dangerous_features", false)) { if(VERSION.SDK_INT >= 26 && (deviceOwner || profileOwner)) { - FunctionItem(R.string.reset_password_token, "", R.drawable.key_vertical_fill0) { navCtrl.navigate("ResetPasswordToken") } + FunctionItem(R.string.reset_password_token, icon = R.drawable.key_vertical_fill0) { navCtrl.navigate("ResetPasswordToken") } } if(deviceAdmin || deviceOwner || profileOwner) { - FunctionItem(R.string.reset_password, "", R.drawable.lock_reset_fill0) { navCtrl.navigate("ResetPassword") } + FunctionItem(R.string.reset_password, icon = R.drawable.lock_reset_fill0) { navCtrl.navigate("ResetPassword") } } } if(VERSION.SDK_INT >= 31 && (deviceOwner || profileOwner)) { - FunctionItem(R.string.required_password_complexity, "", R.drawable.password_fill0) { navCtrl.navigate("RequirePasswordComplexity") } + FunctionItem(R.string.required_password_complexity, icon = R.drawable.password_fill0) { navCtrl.navigate("RequirePasswordComplexity") } } if(deviceAdmin) { - FunctionItem(R.string.disable_keyguard_features, "", R.drawable.screen_lock_portrait_fill0) { navCtrl.navigate("DisableKeyguardFeatures") } + FunctionItem(R.string.disable_keyguard_features, icon = R.drawable.screen_lock_portrait_fill0) { navCtrl.navigate("DisableKeyguardFeatures") } } if(deviceOwner) { - FunctionItem(R.string.max_time_to_lock, "", R.drawable.schedule_fill0) { dialog = 1 } - FunctionItem(R.string.pwd_expiration_timeout, "", R.drawable.lock_clock_fill0) { dialog = 3 } - FunctionItem(R.string.max_pwd_fail, "", R.drawable.no_encryption_fill0) { dialog = 4 } + FunctionItem(R.string.max_time_to_lock, icon = R.drawable.schedule_fill0) { dialog = 1 } + FunctionItem(R.string.pwd_expiration_timeout, icon = R.drawable.lock_clock_fill0) { dialog = 3 } + FunctionItem(R.string.max_pwd_fail, icon = R.drawable.no_encryption_fill0) { dialog = 4 } } if(VERSION.SDK_INT >= 26 && (deviceOwner || profileOwner)) { - FunctionItem(R.string.required_strong_auth_timeout, "", R.drawable.fingerprint_off_fill0) { dialog = 2 } + FunctionItem(R.string.required_strong_auth_timeout, icon = R.drawable.fingerprint_off_fill0) { dialog = 2 } } if(deviceAdmin){ - FunctionItem(R.string.pwd_history, "", R.drawable.history_fill0) { dialog = 5 } + FunctionItem(R.string.pwd_history, icon = R.drawable.history_fill0) { dialog = 5 } } if(VERSION.SDK_INT < 31 && (deviceOwner || profileOwner)) { - FunctionItem(R.string.required_password_quality, "", R.drawable.password_fill0) { navCtrl.navigate("RequirePasswordQuality") } + FunctionItem(R.string.required_password_quality, icon = R.drawable.password_fill0) { navCtrl.navigate("RequirePasswordQuality") } } } if(dialog != 0) { @@ -259,12 +257,8 @@ fun ResetPasswordToken(navCtrl: NavHostController) { Button( onClick = { try { - Toast.makeText( - context, - if(dpm.setResetPasswordToken(receiver, tokenByteArray)) R.string.success else R.string.failed, - Toast.LENGTH_SHORT - ).show() - }catch(_:SecurityException) { + context.showOperationResultToast(dpm.setResetPasswordToken(receiver, tokenByteArray)) + } catch(_:SecurityException) { Toast.makeText(context, R.string.security_exception, Toast.LENGTH_SHORT).show() } }, @@ -289,13 +283,7 @@ fun ResetPasswordToken(navCtrl: NavHostController) { Text(stringResource(R.string.activate)) } Button( - onClick = { - Toast.makeText( - context, - if(dpm.clearResetPasswordToken(receiver)) R.string.success else R.string.failed, - Toast.LENGTH_SHORT - ).show() - }, + onClick = { context.showOperationResultToast(dpm.clearResetPasswordToken(receiver)) }, modifier = Modifier.fillMaxWidth(0.96F) ) { Text(stringResource(R.string.clear)) @@ -316,7 +304,7 @@ fun ResetPassword(navCtrl: NavHostController) { var useToken by remember { mutableStateOf(false) } var token by remember { mutableStateOf("") } val tokenByteArray = token.toByteArray() - val flags = remember { mutableStateListOf() } + var flag by remember { mutableIntStateOf(0) } var confirmDialog by remember { mutableStateOf(false) } MyScaffold(R.string.reset_password, 8.dp, navCtrl) { if(VERSION.SDK_INT >= 26) { @@ -342,15 +330,13 @@ fun ResetPassword(navCtrl: NavHostController) { if(VERSION.SDK_INT >= 23) { CheckBoxItem( R.string.do_not_ask_credentials_on_boot, - RESET_PASSWORD_DO_NOT_ASK_CREDENTIALS_ON_BOOT in flags, - { flags.toggle(it, RESET_PASSWORD_DO_NOT_ASK_CREDENTIALS_ON_BOOT) } - ) + flag and RESET_PASSWORD_DO_NOT_ASK_CREDENTIALS_ON_BOOT != 0 + ) { flag = flag xor RESET_PASSWORD_DO_NOT_ASK_CREDENTIALS_ON_BOOT } } CheckBoxItem( R.string.reset_password_require_entry, - RESET_PASSWORD_REQUIRE_ENTRY in flags, - { flags.toggle(it, RESET_PASSWORD_REQUIRE_ENTRY) } - ) + flag and RESET_PASSWORD_REQUIRE_ENTRY != 0 + ) { flag = flag xor RESET_PASSWORD_REQUIRE_ENTRY } Spacer(Modifier.padding(vertical = 5.dp)) if(VERSION.SDK_INT >= 26) { Button( @@ -402,14 +388,12 @@ fun ResetPassword(navCtrl: NavHostController) { confirmButton = { TextButton( onClick = { - var resetFlag = 0 - flags.forEach { resetFlag += it } val success = if(VERSION.SDK_INT >= 26 && useToken) { - dpm.resetPasswordWithToken(receiver, password, tokenByteArray, resetFlag) + dpm.resetPasswordWithToken(receiver, password, tokenByteArray, flag) } else { - dpm.resetPassword(password, resetFlag) + dpm.resetPassword(password, flag) } - Toast.makeText(context, if(success) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(success) password = "" confirmDialog = false }, @@ -443,13 +427,13 @@ fun PasswordComplexity(navCtrl: NavHostController) { LaunchedEffect(Unit) { selectedItem = dpm.requiredPasswordComplexity } MyScaffold(R.string.required_password_complexity, 8.dp, navCtrl) { passwordComplexity.forEach { - RadioButtonItem(it.value, selectedItem == it.key, { selectedItem = it.key }) + RadioButtonItem(it.value, selectedItem == it.key) { selectedItem = it.key } } Spacer(Modifier.padding(vertical = 5.dp)) Button( onClick = { dpm.requiredPasswordComplexity = selectedItem - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth() ) { @@ -470,86 +454,52 @@ fun DisableKeyguardFeatures(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() - var state by remember { mutableIntStateOf(-1) } - var shortcuts by remember { mutableStateOf(false) } - var biometrics by remember { mutableStateOf(false) } - var iris by remember { mutableStateOf(false) } - var face by remember { mutableStateOf(false) } - var remote by remember { mutableStateOf(false) } - var fingerprint by remember { mutableStateOf(false) } - var agents by remember { mutableStateOf(false) } - var unredacted by remember { mutableStateOf(false) } - var notification by remember { mutableStateOf(false) } - var camera by remember { mutableStateOf(false) } - var widgets by remember { mutableStateOf(false) } - val calculateCustomFeature = { - var calculate = dpm.getKeyguardDisabledFeatures(receiver) - if(calculate==0) {state=0} - else{ - if(calculate-KEYGUARD_DISABLE_SHORTCUTS_ALL >= 0 && VERSION.SDK_INT >= 34) { shortcuts=true; calculate-= KEYGUARD_DISABLE_SHORTCUTS_ALL } - if(calculate-KEYGUARD_DISABLE_BIOMETRICS >= 0 && VERSION.SDK_INT >= 28) { biometrics=true; calculate -= KEYGUARD_DISABLE_BIOMETRICS } - if(calculate-KEYGUARD_DISABLE_IRIS >= 0 && VERSION.SDK_INT >= 28) { iris=true; calculate -= KEYGUARD_DISABLE_IRIS } - if(calculate-KEYGUARD_DISABLE_FACE >= 0 && VERSION.SDK_INT >= 28) { face=true; calculate -= KEYGUARD_DISABLE_FACE } - if(calculate-KEYGUARD_DISABLE_REMOTE_INPUT >= 0 && VERSION.SDK_INT >= 24) { remote=true; calculate -= KEYGUARD_DISABLE_REMOTE_INPUT } - if(calculate-KEYGUARD_DISABLE_FINGERPRINT >= 0) { fingerprint=true; calculate -= KEYGUARD_DISABLE_FINGERPRINT } - if(calculate-KEYGUARD_DISABLE_TRUST_AGENTS >= 0) { agents=true; calculate -= KEYGUARD_DISABLE_TRUST_AGENTS } - if(calculate-KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS >= 0) { unredacted=true; calculate -= KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS } - if(calculate-KEYGUARD_DISABLE_SECURE_NOTIFICATIONS >= 0) { notification=true; calculate -= KEYGUARD_DISABLE_SECURE_NOTIFICATIONS } - if(calculate-KEYGUARD_DISABLE_SECURE_CAMERA >= 0) { camera=true; calculate -= KEYGUARD_DISABLE_SECURE_CAMERA } - if(calculate-KEYGUARD_DISABLE_WIDGETS_ALL >= 0) { widgets=true; calculate -= KEYGUARD_DISABLE_WIDGETS_ALL } - } + var flag by remember { mutableIntStateOf(0) } + var mode by remember { mutableIntStateOf(0) } // 0:Enable all, 1:Disable all, 2:Custom + val flagsLiat = mutableListOf( + R.string.disable_keyguard_features_widgets to KEYGUARD_DISABLE_WIDGETS_ALL, + R.string.disable_keyguard_features_camera to KEYGUARD_DISABLE_SECURE_CAMERA, + R.string.disable_keyguard_features_notification to KEYGUARD_DISABLE_SECURE_NOTIFICATIONS, + R.string.disable_keyguard_features_unredacted_notification to KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS, + R.string.disable_keyguard_features_trust_agents to KEYGUARD_DISABLE_TRUST_AGENTS, + R.string.disable_keyguard_features_fingerprint to KEYGUARD_DISABLE_FINGERPRINT + ) + if(VERSION.SDK_INT >= 28) { + flagsLiat +=R.string.disable_keyguard_features_face to KEYGUARD_DISABLE_FACE + flagsLiat += R.string.disable_keyguard_features_iris to KEYGUARD_DISABLE_IRIS + flagsLiat += R.string.disable_keyguard_features_biometrics to KEYGUARD_DISABLE_BIOMETRICS } - if(state==-1) { - state = when(dpm.getKeyguardDisabledFeatures(receiver)) { + if(VERSION.SDK_INT >= 34) flagsLiat += R.string.disable_keyguard_features_shortcuts to KEYGUARD_DISABLE_SHORTCUTS_ALL + fun refresh() { + flag = dpm.getKeyguardDisabledFeatures(receiver) + mode = when(flag) { KEYGUARD_DISABLE_FEATURES_NONE -> 0 KEYGUARD_DISABLE_FEATURES_ALL -> 1 else -> 2 } - calculateCustomFeature() } + LaunchedEffect(mode) { if(mode != 2) flag = dpm.getKeyguardDisabledFeatures(receiver) } + LaunchedEffect(Unit) { refresh() } MyScaffold(R.string.disable_keyguard_features, 8.dp, navCtrl) { - RadioButtonItem(R.string.enable_all, state == 0, { state = 0 }) - RadioButtonItem(R.string.disable_all, state == 1, { state = 1 }) - RadioButtonItem(R.string.custom, state == 2 , { state = 2 }) - AnimatedVisibility(state==2) { + RadioButtonItem(R.string.enable_all, mode == 0) { mode = 0 } + RadioButtonItem(R.string.disable_all, mode == 1) { mode = 1 } + RadioButtonItem(R.string.custom, mode == 2) { mode = 2 } + AnimatedVisibility(mode == 2) { Column { - CheckBoxItem(R.string.disable_keyguard_features_widgets, widgets, { widgets = it }) - CheckBoxItem(R.string.disable_keyguard_features_camera, camera, { camera = it }) - CheckBoxItem(R.string.disable_keyguard_features_notification, notification, { notification = it }) - CheckBoxItem(R.string.disable_keyguard_features_unredacted_notification, unredacted, { unredacted = it }) - CheckBoxItem(R.string.disable_keyguard_features_trust_agents, agents, { agents = it }) - CheckBoxItem(R.string.disable_keyguard_features_fingerprint, fingerprint, { fingerprint = it }) - if(VERSION.SDK_INT >= 24) { CheckBoxItem(R.string.disable_keyguard_features_remote_input, remote , { remote = it }) } - if(VERSION.SDK_INT >= 28) { - CheckBoxItem(R.string.disable_keyguard_features_face, face, { face = it }) - CheckBoxItem(R.string.disable_keyguard_features_iris, iris, { iris = it }) - CheckBoxItem(R.string.disable_keyguard_features_biometrics, biometrics, { biometrics = it }) + flagsLiat.forEach { + CheckBoxItem(it.first, flag and it.second == it.second) { checked -> + flag = if(checked) flag or it.second else flag and (flag xor it.second) + } } - if(VERSION.SDK_INT >= 34) { CheckBoxItem(R.string.disable_keyguard_features_shortcuts, shortcuts, { shortcuts = it }) } } } Spacer(Modifier.padding(vertical = 5.dp)) Button( onClick = { - var result = 0 - if(state==0) { result = 0 } - else if(state==1) { result = KEYGUARD_DISABLE_FEATURES_ALL } - else{ - if(shortcuts && VERSION.SDK_INT >= 34) { result+=KEYGUARD_DISABLE_SHORTCUTS_ALL } - if(biometrics && VERSION.SDK_INT >= 28) { result+=KEYGUARD_DISABLE_BIOMETRICS } - if(iris && VERSION.SDK_INT >= 28) { result+=KEYGUARD_DISABLE_IRIS } - if(face && VERSION.SDK_INT >= 28) { result+=KEYGUARD_DISABLE_FACE } - if(remote && VERSION.SDK_INT >= 24) { result+=KEYGUARD_DISABLE_REMOTE_INPUT } - if(fingerprint) { result+=KEYGUARD_DISABLE_FINGERPRINT } - if(agents) { result+=KEYGUARD_DISABLE_TRUST_AGENTS } - if(unredacted) { result+=KEYGUARD_DISABLE_UNREDACTED_NOTIFICATIONS } - if(notification) { result+=KEYGUARD_DISABLE_SECURE_NOTIFICATIONS } - if(camera) { result+=KEYGUARD_DISABLE_SECURE_CAMERA } - if(widgets) { result+=KEYGUARD_DISABLE_WIDGETS_ALL } - } - dpm.setKeyguardDisabledFeatures(receiver,result) - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() - calculateCustomFeature() + val disabledFeatures = if(mode == 0) KEYGUARD_DISABLE_FEATURES_NONE else if(mode == 1) KEYGUARD_DISABLE_FEATURES_ALL else flag + dpm.setKeyguardDisabledFeatures(receiver, disabledFeatures) + refresh() + context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth() ) { @@ -576,13 +526,13 @@ fun PasswordQuality(navCtrl: NavHostController) { LaunchedEffect(Unit) { selectedItem=dpm.getPasswordQuality(receiver) } MyScaffold(R.string.required_password_quality, 8.dp, navCtrl) { passwordQuality.forEach { - RadioButtonItem(it.value, selectedItem == it.key, { selectedItem = it.key }) + RadioButtonItem(it.value, selectedItem == it.key) { selectedItem = it.key } } Spacer(Modifier.padding(vertical = 5.dp)) Button( onClick = { dpm.setPasswordQuality(receiver,selectedItem) - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth() ) { diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt index bdb7df6..314c7ec 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import com.bintianqi.owndroid.R import com.bintianqi.owndroid.backToHomeStateFlow +import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.ui.* import com.bintianqi.owndroid.writeClipBoard import com.bintianqi.owndroid.yesOrNo @@ -56,9 +57,9 @@ fun Permissions(navCtrl: NavHostController) { MyScaffold(R.string.permissions, 0.dp, navCtrl) { if(!dpm.isDeviceOwnerApp(context.packageName)) { SwitchItem( - R.string.dhizuku, "", null, - { sharedPref.getBoolean("dhizuku", false) }, - { toggleDhizukuMode(it, context) }, + R.string.dhizuku, + getState = { sharedPref.getBoolean("dhizuku", false) }, + onCheckedChange = { toggleDhizukuMode(it, context) }, onClickBlank = { dialog = 4 } ) } @@ -78,7 +79,7 @@ fun Permissions(navCtrl: NavHostController) { operation = { navCtrl.navigate("DeviceOwner") } ) } - FunctionItem(R.string.shizuku,"") { + FunctionItem(R.string.shizuku) { try { if(Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) { navCtrl.navigate("Shizuku") } else if(Shizuku.shouldShowRequestPermissionRationale()) { @@ -102,24 +103,24 @@ fun Permissions(navCtrl: NavHostController) { Toast.makeText(context, R.string.shizuku_not_started, Toast.LENGTH_SHORT).show() } } - FunctionItem(R.string.device_info, "", R.drawable.perm_device_information_fill0) { navCtrl.navigate("DeviceInfo") } + FunctionItem(R.string.device_info, icon = R.drawable.perm_device_information_fill0) { navCtrl.navigate("DeviceInfo") } if((VERSION.SDK_INT >= 26 && deviceOwner) || (VERSION.SDK_INT>=24 && profileOwner)) { - FunctionItem(R.string.org_name, "", R.drawable.corporate_fare_fill0) { dialog = 2 } + FunctionItem(R.string.org_name, icon = R.drawable.corporate_fare_fill0) { dialog = 2 } } if(VERSION.SDK_INT >= 31 && (profileOwner || deviceOwner)) { - FunctionItem(R.string.org_id, "", R.drawable.corporate_fare_fill0) { dialog = 3 } + FunctionItem(R.string.org_id, icon = R.drawable.corporate_fare_fill0) { dialog = 3 } } if(enrollmentSpecificId != "") { - FunctionItem(R.string.enrollment_specific_id, "", R.drawable.id_card_fill0) { dialog = 1 } + FunctionItem(R.string.enrollment_specific_id, icon = R.drawable.id_card_fill0) { dialog = 1 } } if(VERSION.SDK_INT >= 24 && (deviceOwner || dpm.isOrgProfile(receiver))) { - FunctionItem(R.string.lock_screen_info, "", R.drawable.screen_lock_portrait_fill0) { navCtrl.navigate("LockScreenInfo") } + FunctionItem(R.string.lock_screen_info, icon = R.drawable.screen_lock_portrait_fill0) { navCtrl.navigate("LockScreenInfo") } } if(VERSION.SDK_INT >= 24 && deviceAdmin) { - FunctionItem(R.string.support_messages, "", R.drawable.chat_fill0) { navCtrl.navigate("SupportMessages") } + FunctionItem(R.string.support_messages, icon = R.drawable.chat_fill0) { navCtrl.navigate("SupportMessages") } } if(VERSION.SDK_INT >= 28 && (deviceOwner || profileOwner)) { - FunctionItem(R.string.transfer_ownership, "", R.drawable.admin_panel_settings_fill0) { navCtrl.navigate("TransferOwnership") } + FunctionItem(R.string.transfer_ownership, icon = R.drawable.admin_panel_settings_fill0) { navCtrl.navigate("TransferOwnership") } } } if(dialog != 0) { @@ -254,7 +255,7 @@ fun LockScreenInfo(navCtrl: NavHostController) { onClick = { focusMgr.clearFocus() dpm.setDeviceOwnerLockScreenInfo(receiver,infoText) - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth() ) { @@ -263,8 +264,8 @@ fun LockScreenInfo(navCtrl: NavHostController) { Button( onClick = { focusMgr.clearFocus() - dpm.setDeviceOwnerLockScreenInfo(receiver,null) - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + dpm.setDeviceOwnerLockScreenInfo(receiver, null) + context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth() ) { @@ -521,7 +522,7 @@ fun SupportMessages(navCtrl: NavHostController) { onClick = { dpm.setShortSupportMessage(receiver, shortMsg) refreshMsg() - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth(0.49F) ) { @@ -531,7 +532,7 @@ fun SupportMessages(navCtrl: NavHostController) { onClick = { dpm.setShortSupportMessage(receiver, null) refreshMsg() - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth(0.96F) ) { @@ -552,7 +553,7 @@ fun SupportMessages(navCtrl: NavHostController) { onClick = { dpm.setLongSupportMessage(receiver, longMsg) refreshMsg() - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth(0.49F) ) { @@ -562,7 +563,7 @@ fun SupportMessages(navCtrl: NavHostController) { onClick = { dpm.setLongSupportMessage(receiver, null) refreshMsg() - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth(0.96F) ) { @@ -615,7 +616,7 @@ fun TransferOwnership(navCtrl: NavHostController) { val receiver = context.getReceiver() try { dpm.transferOwnership(receiver, componentName!!, null) - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) dialog = false backToHomeStateFlow.value = true } catch(e: Exception) { diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt index e622287..680f5f9 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt @@ -39,6 +39,8 @@ import android.net.Uri import android.os.Build.VERSION import android.os.UserManager import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.foundation.clickable @@ -84,7 +86,6 @@ import androidx.compose.material3.rememberDatePickerState import androidx.compose.material3.rememberTimePickerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableLongStateOf @@ -109,14 +110,9 @@ import androidx.navigation.NavHostController import com.bintianqi.owndroid.MyViewModel import com.bintianqi.owndroid.NotificationUtils import com.bintianqi.owndroid.R -import com.bintianqi.owndroid.exportFile -import com.bintianqi.owndroid.exportFilePath -import com.bintianqi.owndroid.fileUriFlow import com.bintianqi.owndroid.formatFileSize -import com.bintianqi.owndroid.getFile import com.bintianqi.owndroid.humanReadableDate -import com.bintianqi.owndroid.isExportingSecurityOrNetworkLogs -import com.bintianqi.owndroid.toggle +import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.ui.CheckBoxItem import com.bintianqi.owndroid.ui.FunctionItem import com.bintianqi.owndroid.ui.InfoCard @@ -125,17 +121,13 @@ import com.bintianqi.owndroid.ui.MyScaffold import com.bintianqi.owndroid.ui.RadioButtonItem import com.bintianqi.owndroid.ui.SwitchItem import com.bintianqi.owndroid.uriToStream +import com.bintianqi.owndroid.yesOrNo import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.addJsonObject -import kotlinx.serialization.json.buildJsonArray -import kotlinx.serialization.json.encodeToStream -import kotlinx.serialization.json.put +import java.io.ByteArrayOutputStream import java.util.Date import java.util.TimeZone import java.util.concurrent.Executors -import kotlin.math.pow @SuppressLint("NewApi") @Composable @@ -151,53 +143,52 @@ fun SystemManage(navCtrl: NavHostController) { var dialog by remember { mutableIntStateOf(0) } MyScaffold(R.string.system, 0.dp, navCtrl) { if(deviceOwner || profileOwner) { - FunctionItem(R.string.options, "", R.drawable.tune_fill0) { navCtrl.navigate("SystemOptions") } + FunctionItem(R.string.options, icon = R.drawable.tune_fill0) { navCtrl.navigate("SystemOptions") } } - FunctionItem(R.string.keyguard, "", R.drawable.screen_lock_portrait_fill0) { navCtrl.navigate("Keyguard") } + FunctionItem(R.string.keyguard, icon = R.drawable.screen_lock_portrait_fill0) { navCtrl.navigate("Keyguard") } if(VERSION.SDK_INT >= 24 && deviceOwner) { - FunctionItem(R.string.reboot, "", R.drawable.restart_alt_fill0) { dialog = 1 } + FunctionItem(R.string.reboot, icon = R.drawable.restart_alt_fill0) { dialog = 1 } } if(deviceOwner && ((VERSION.SDK_INT >= 28 && dpm.isAffiliatedUser) || VERSION.SDK_INT >= 24)) { - FunctionItem(R.string.bug_report, "", R.drawable.bug_report_fill0) { dialog = 2 } + FunctionItem(R.string.bug_report, icon = R.drawable.bug_report_fill0) { dialog = 2 } } if(VERSION.SDK_INT >= 28 && (deviceOwner || dpm.isOrgProfile(receiver))) { - FunctionItem(R.string.change_time, "", R.drawable.schedule_fill0) { navCtrl.navigate("ChangeTime") } - FunctionItem(R.string.change_timezone, "", R.drawable.schedule_fill0) { navCtrl.navigate("ChangeTimeZone") } + FunctionItem(R.string.change_time, icon = R.drawable.schedule_fill0) { navCtrl.navigate("ChangeTime") } + FunctionItem(R.string.change_timezone, icon = R.drawable.schedule_fill0) { navCtrl.navigate("ChangeTimeZone") } } if(VERSION.SDK_INT >= 23 && (deviceOwner || profileOwner)) { - FunctionItem(R.string.permission_policy, "", R.drawable.key_fill0) { navCtrl.navigate("PermissionPolicy") } + FunctionItem(R.string.permission_policy, icon = R.drawable.key_fill0) { navCtrl.navigate("PermissionPolicy") } } if(VERSION.SDK_INT >= 34 && deviceOwner) { - FunctionItem(R.string.mte_policy, "", R.drawable.memory_fill0) { navCtrl.navigate("MTEPolicy") } + FunctionItem(R.string.mte_policy, icon = R.drawable.memory_fill0) { navCtrl.navigate("MTEPolicy") } } if(VERSION.SDK_INT >= 31 && (deviceOwner || profileOwner)) { - FunctionItem(R.string.nearby_streaming_policy, "", R.drawable.share_fill0) { navCtrl.navigate("NearbyStreamingPolicy") } + FunctionItem(R.string.nearby_streaming_policy, icon = R.drawable.share_fill0) { navCtrl.navigate("NearbyStreamingPolicy") } } if(VERSION.SDK_INT >= 28 && deviceOwner) { - FunctionItem(R.string.lock_task_mode, "", R.drawable.lock_fill0) { navCtrl.navigate("LockTaskMode") } + FunctionItem(R.string.lock_task_mode, icon = R.drawable.lock_fill0) { navCtrl.navigate("LockTaskMode") } } if(deviceOwner || profileOwner) { - FunctionItem(R.string.ca_cert, "", R.drawable.license_fill0) { navCtrl.navigate("CACert") } + FunctionItem(R.string.ca_cert, icon = R.drawable.license_fill0) { navCtrl.navigate("CACert") } } if(VERSION.SDK_INT >= 26 && !dhizuku && (deviceOwner || dpm.isOrgProfile(receiver))) { - FunctionItem(R.string.security_logging, "", R.drawable.description_fill0) { navCtrl.navigate("SecurityLogging") } + FunctionItem(R.string.security_logging, icon = R.drawable.description_fill0) { navCtrl.navigate("SecurityLogging") } } if(deviceOwner || profileOwner) { - FunctionItem(R.string.disable_account_management, "", R.drawable.account_circle_fill0) { navCtrl.navigate("DisableAccountManagement") } + FunctionItem(R.string.disable_account_management, icon = R.drawable.account_circle_fill0) { navCtrl.navigate("DisableAccountManagement") } } if(VERSION.SDK_INT >= 23 && (deviceOwner || dpm.isOrgProfile(receiver))) { - FunctionItem(R.string.system_update_policy, "", R.drawable.system_update_fill0) { navCtrl.navigate("SystemUpdatePolicy") } + FunctionItem(R.string.system_update_policy, icon = R.drawable.system_update_fill0) { navCtrl.navigate("SystemUpdatePolicy") } } if(VERSION.SDK_INT >= 29 && (deviceOwner || dpm.isOrgProfile(receiver))) { - FunctionItem(R.string.install_system_update, "", R.drawable.system_update_fill0) { navCtrl.navigate("InstallSystemUpdate") } + FunctionItem(R.string.install_system_update, icon = R.drawable.system_update_fill0) { navCtrl.navigate("InstallSystemUpdate") } } if(VERSION.SDK_INT >= 30 && (deviceOwner || dpm.isOrgProfile(receiver))) { - FunctionItem(R.string.frp_policy, "", R.drawable.device_reset_fill0) { navCtrl.navigate("FRPPolicy") } + FunctionItem(R.string.frp_policy, icon = R.drawable.device_reset_fill0) { navCtrl.navigate("FRPPolicy") } } if(dangerousFeatures && context.isDeviceAdmin && !(VERSION.SDK_INT >= 24 && profileOwner && dpm.isManagedProfile(receiver))) { - FunctionItem(R.string.wipe_data, "", R.drawable.device_reset_fill0) { navCtrl.navigate("WipeData") } + FunctionItem(R.string.wipe_data, icon = R.drawable.device_reset_fill0) { navCtrl.navigate("WipeData") } } - LaunchedEffect(Unit) { fileUriFlow.value = Uri.parse("") } } if(dialog != 0) AlertDialog( onDismissRequest = { dialog = 0 }, @@ -214,8 +205,7 @@ fun SystemManage(navCtrl: NavHostController) { if(dialog == 1) { dpm.reboot(receiver) } else { - val result = dpm.requestBugreport(receiver) - Toast.makeText(context, if(result) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(dpm.requestBugreport(receiver)) } dialog = 0 } @@ -238,58 +228,60 @@ fun SystemOptions(navCtrl: NavHostController) { var dialog by remember { mutableIntStateOf(0) } MyScaffold(R.string.options, 0.dp, navCtrl) { if(deviceOwner || profileOwner) { - SwitchItem(R.string.disable_cam,"", R.drawable.photo_camera_fill0, - { dpm.getCameraDisabled(null) }, { dpm.setCameraDisabled(receiver,it) } + SwitchItem(R.string.disable_cam, icon = R.drawable.photo_camera_fill0, + getState = { dpm.getCameraDisabled(null) }, onCheckedChange = { dpm.setCameraDisabled(receiver,it) } ) } if(deviceOwner || profileOwner) { - SwitchItem(R.string.disable_screen_capture, "", R.drawable.screenshot_fill0, - { dpm.getScreenCaptureDisabled(null) }, { dpm.setScreenCaptureDisabled(receiver,it) } + SwitchItem(R.string.disable_screen_capture, icon = R.drawable.screenshot_fill0, + getState = { dpm.getScreenCaptureDisabled(null) }, onCheckedChange = { dpm.setScreenCaptureDisabled(receiver,it) } ) } if(VERSION.SDK_INT >= 34 && (deviceOwner || (profileOwner && dpm.isAffiliatedUser))) { - SwitchItem(R.string.disable_status_bar, "", R.drawable.notifications_fill0, - { dpm.isStatusBarDisabled}, { dpm.setStatusBarDisabled(receiver,it) } + SwitchItem(R.string.disable_status_bar, icon = R.drawable.notifications_fill0, + getState = { dpm.isStatusBarDisabled}, onCheckedChange = { dpm.setStatusBarDisabled(receiver,it) } ) } if(deviceOwner || (VERSION.SDK_INT >= 23 && profileOwner && um.isSystemUser) || dpm.isOrgProfile(receiver)) { if(VERSION.SDK_INT >= 30) { - SwitchItem(R.string.auto_time, "", R.drawable.schedule_fill0, - { dpm.getAutoTimeEnabled(receiver) }, { dpm.setAutoTimeEnabled(receiver,it) } + SwitchItem(R.string.auto_time, icon = R.drawable.schedule_fill0, + getState = { dpm.getAutoTimeEnabled(receiver) }, onCheckedChange = { dpm.setAutoTimeEnabled(receiver,it) } ) - SwitchItem(R.string.auto_timezone, "", R.drawable.globe_fill0, - { dpm.getAutoTimeZoneEnabled(receiver) }, { dpm.setAutoTimeZoneEnabled(receiver,it) } + SwitchItem(R.string.auto_timezone, icon = R.drawable.globe_fill0, + getState = { dpm.getAutoTimeZoneEnabled(receiver) }, onCheckedChange = { dpm.setAutoTimeZoneEnabled(receiver,it) } ) - }else{ - SwitchItem(R.string.require_auto_time, "", R.drawable.schedule_fill0, { dpm.autoTimeRequired}, { dpm.setAutoTimeRequired(receiver,it) }, padding = false) + } else { + SwitchItem(R.string.require_auto_time, icon = R.drawable.schedule_fill0, + getState = { dpm.autoTimeRequired}, onCheckedChange = { dpm.setAutoTimeRequired(receiver,it) }, padding = false) } } if(deviceOwner || (profileOwner && (VERSION.SDK_INT < 24 || (VERSION.SDK_INT >= 24 && !dpm.isManagedProfile(receiver))))) { - SwitchItem(R.string.master_mute, "", R.drawable.volume_up_fill0, - { dpm.isMasterVolumeMuted(receiver) }, { dpm.setMasterVolumeMuted(receiver,it) } + SwitchItem(R.string.master_mute, icon = R.drawable.volume_up_fill0, + getState = { dpm.isMasterVolumeMuted(receiver) }, onCheckedChange = { dpm.setMasterVolumeMuted(receiver,it) } ) } if(VERSION.SDK_INT >= 26 && (deviceOwner || profileOwner)) { - SwitchItem(R.string.backup_service, "", R.drawable.backup_fill0, - { dpm.isBackupServiceEnabled(receiver) }, { dpm.setBackupServiceEnabled(receiver,it) }, + SwitchItem(R.string.backup_service, icon = R.drawable.backup_fill0, + getState = { dpm.isBackupServiceEnabled(receiver) }, onCheckedChange = { dpm.setBackupServiceEnabled(receiver,it) }, onClickBlank = { dialog = 1 } ) } if(VERSION.SDK_INT >= 24 && profileOwner && dpm.isManagedProfile(receiver)) { - SwitchItem(R.string.disable_bt_contact_share, "", R.drawable.account_circle_fill0, - { dpm.getBluetoothContactSharingDisabled(receiver) }, { dpm.setBluetoothContactSharingDisabled(receiver,it) }, + SwitchItem(R.string.disable_bt_contact_share, icon = R.drawable.account_circle_fill0, + getState = { dpm.getBluetoothContactSharingDisabled(receiver) }, + onCheckedChange = { dpm.setBluetoothContactSharingDisabled(receiver,it) } ) } if(VERSION.SDK_INT >= 30 && deviceOwner) { - SwitchItem(R.string.common_criteria_mode , "",R.drawable.security_fill0, - { dpm.isCommonCriteriaModeEnabled(receiver) }, { dpm.setCommonCriteriaModeEnabled(receiver,it) }, + SwitchItem(R.string.common_criteria_mode , icon =R.drawable.security_fill0, + getState = { dpm.isCommonCriteriaModeEnabled(receiver) }, onCheckedChange = { dpm.setCommonCriteriaModeEnabled(receiver,it) }, onClickBlank = { dialog = 2 } ) } if(VERSION.SDK_INT >= 31 && (deviceOwner || dpm.isOrgProfile(receiver)) && dpm.canUsbDataSignalingBeDisabled()) { SwitchItem( - R.string.disable_usb_signal, "", R.drawable.usb_fill0, { !dpm.isUsbDataSignalingEnabled }, - { dpm.isUsbDataSignalingEnabled = !it }, + R.string.disable_usb_signal, icon = R.drawable.usb_fill0, getState = { !dpm.isUsbDataSignalingEnabled }, + onCheckedChange = { dpm.isUsbDataSignalingEnabled = !it }, ) } } @@ -324,18 +316,14 @@ fun Keyguard(navCtrl: NavHostController) { modifier = Modifier.fillMaxWidth() ) { Button( - onClick = { - Toast.makeText(context, if(dpm.setKeyguardDisabled(receiver,true)) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show() - }, + onClick = { context.showOperationResultToast(dpm.setKeyguardDisabled(receiver, true)) }, enabled = deviceOwner || (VERSION.SDK_INT >= 28 && profileOwner && dpm.isAffiliatedUser), modifier = Modifier.fillMaxWidth(0.49F) ) { Text(stringResource(R.string.disable)) } Button( - onClick = { - Toast.makeText(context, if(dpm.setKeyguardDisabled(receiver,false)) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show() - }, + onClick = { context.showOperationResultToast(dpm.setKeyguardDisabled(receiver, false)) }, enabled = deviceOwner || (VERSION.SDK_INT >= 28 && profileOwner && dpm.isAffiliatedUser), modifier = Modifier.fillMaxWidth(0.96F) ) { @@ -351,9 +339,8 @@ fun Keyguard(navCtrl: NavHostController) { if(VERSION.SDK_INT >= 26 && profileOwner && dpm.isManagedProfile(receiver)) { CheckBoxItem( R.string.evict_credential_encryptoon_key, - flag == FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY, - { flag = if(flag==0) {1}else{0} } - ) + flag and FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY != 0 + ) { flag = flag xor FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY } Spacer(Modifier.padding(vertical = 2.dp)) } Button( @@ -453,8 +440,7 @@ fun ChangeTime(navCtrl: NavHostController) { onClick = { val timeMillis = if(manualInput) inputTime.toLong() else datePickerState.selectedDateMillis!! + timePickerState.hour * 3600000 + timePickerState.minute * 60000 - val result = dpm.setTime(receiver, timeMillis) - Toast.makeText(context, if(result) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(dpm.setTime(receiver, timeMillis)) }, modifier = Modifier.fillMaxWidth(), enabled = isInputLegal @@ -509,8 +495,7 @@ fun ChangeTimeZone(navCtrl: NavHostController) { Spacer(Modifier.padding(vertical = 5.dp)) Button( onClick = { - val result = dpm.setTimeZone(receiver, inputTimezone) - Toast.makeText(context, if(result) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(dpm.setTimeZone(receiver, inputTimezone)) }, modifier = Modifier.fillMaxWidth() ) { @@ -555,14 +540,14 @@ fun PermissionPolicy(navCtrl: NavHostController) { val receiver = context.getReceiver() var selectedPolicy by remember { mutableIntStateOf(dpm.getPermissionPolicy(receiver)) } MyScaffold(R.string.permission_policy, 8.dp, navCtrl) { - RadioButtonItem(R.string.default_stringres, selectedPolicy == PERMISSION_POLICY_PROMPT, { selectedPolicy = PERMISSION_POLICY_PROMPT }) - RadioButtonItem(R.string.auto_grant, selectedPolicy == PERMISSION_POLICY_AUTO_GRANT, { selectedPolicy = PERMISSION_POLICY_AUTO_GRANT }) - RadioButtonItem(R.string.auto_deny, selectedPolicy == PERMISSION_POLICY_AUTO_DENY, { selectedPolicy = PERMISSION_POLICY_AUTO_DENY }) + RadioButtonItem(R.string.default_stringres, selectedPolicy == PERMISSION_POLICY_PROMPT) { selectedPolicy = PERMISSION_POLICY_PROMPT } + RadioButtonItem(R.string.auto_grant, selectedPolicy == PERMISSION_POLICY_AUTO_GRANT) { selectedPolicy = PERMISSION_POLICY_AUTO_GRANT } + RadioButtonItem(R.string.auto_deny, selectedPolicy == PERMISSION_POLICY_AUTO_DENY) { selectedPolicy = PERMISSION_POLICY_AUTO_DENY } Spacer(Modifier.padding(vertical = 5.dp)) Button( onClick = { dpm.setPermissionPolicy(receiver,selectedPolicy) - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth() ) { @@ -579,26 +564,14 @@ fun MTEPolicy(navCtrl: NavHostController) { val dpm = context.getDPM() var selectedMtePolicy by remember { mutableIntStateOf(dpm.mtePolicy) } MyScaffold(R.string.mte_policy, 8.dp, navCtrl) { - RadioButtonItem( - R.string.decide_by_user, - selectedMtePolicy == MTE_NOT_CONTROLLED_BY_POLICY, - { selectedMtePolicy = MTE_NOT_CONTROLLED_BY_POLICY } - ) - RadioButtonItem( - R.string.enabled, - selectedMtePolicy == MTE_ENABLED, - { selectedMtePolicy = MTE_ENABLED } - ) - RadioButtonItem( - R.string.disabled, - selectedMtePolicy == MTE_DISABLED, - { selectedMtePolicy = MTE_DISABLED } - ) + RadioButtonItem(R.string.decide_by_user, selectedMtePolicy == MTE_NOT_CONTROLLED_BY_POLICY) { selectedMtePolicy = MTE_NOT_CONTROLLED_BY_POLICY } + RadioButtonItem(R.string.enabled, selectedMtePolicy == MTE_ENABLED) { selectedMtePolicy = MTE_ENABLED } + RadioButtonItem(R.string.disabled, selectedMtePolicy == MTE_DISABLED) { selectedMtePolicy = MTE_DISABLED } Button( onClick = { try { dpm.mtePolicy = selectedMtePolicy - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) } catch(_: java.lang.UnsupportedOperationException) { Toast.makeText(context, R.string.unsupported, Toast.LENGTH_SHORT).show() } @@ -624,29 +597,19 @@ fun NearbyStreamingPolicy(navCtrl: NavHostController) { Spacer(Modifier.padding(vertical = 3.dp)) RadioButtonItem( R.string.decide_by_user, - appPolicy == NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY, - { appPolicy = NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY } - ) - RadioButtonItem( - R.string.enabled, - appPolicy == NEARBY_STREAMING_ENABLED, - { appPolicy = NEARBY_STREAMING_ENABLED } - ) - RadioButtonItem( - R.string.disabled, - appPolicy == NEARBY_STREAMING_DISABLED, - { appPolicy = NEARBY_STREAMING_DISABLED } - ) + appPolicy == NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY + ) { appPolicy = NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY } + RadioButtonItem(R.string.enabled, appPolicy == NEARBY_STREAMING_ENABLED) { appPolicy = NEARBY_STREAMING_ENABLED } + RadioButtonItem(R.string.disabled, appPolicy == NEARBY_STREAMING_DISABLED) { appPolicy = NEARBY_STREAMING_DISABLED } RadioButtonItem( R.string.enable_if_secure_enough, - appPolicy == NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY, - { appPolicy = NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY } - ) + appPolicy == NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY + ) { appPolicy = NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY } Spacer(Modifier.padding(vertical = 3.dp)) Button( onClick = { dpm.nearbyAppStreamingPolicy = appPolicy - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth() ) { @@ -659,29 +622,25 @@ fun NearbyStreamingPolicy(navCtrl: NavHostController) { Spacer(Modifier.padding(vertical = 3.dp)) RadioButtonItem( R.string.decide_by_user, - notificationPolicy == NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY, - { notificationPolicy = NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY } - ) + notificationPolicy == NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY + ) { notificationPolicy = NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY } RadioButtonItem( R.string.enabled, - notificationPolicy == NEARBY_STREAMING_ENABLED, - { notificationPolicy = NEARBY_STREAMING_ENABLED } - ) + notificationPolicy == NEARBY_STREAMING_ENABLED + ) { notificationPolicy = NEARBY_STREAMING_ENABLED } RadioButtonItem( R.string.disabled, - notificationPolicy == NEARBY_STREAMING_DISABLED, - { notificationPolicy = NEARBY_STREAMING_DISABLED } - ) + notificationPolicy == NEARBY_STREAMING_DISABLED + ) { notificationPolicy = NEARBY_STREAMING_DISABLED } RadioButtonItem( R.string.enable_if_secure_enough, - notificationPolicy == NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY, - { notificationPolicy = NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY } - ) + notificationPolicy == NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY + ) { notificationPolicy = NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY } Spacer(Modifier.padding(vertical = 3.dp)) Button( onClick = { dpm.nearbyNotificationStreamingPolicy = notificationPolicy - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth() ) { @@ -700,84 +659,58 @@ fun LockTaskMode(navCtrl: NavHostController, vm: MyViewModel) { val focusMgr = LocalFocusManager.current var appSelectorRequest by rememberSaveable { mutableIntStateOf(0) } MyScaffold(R.string.lock_task_mode, 8.dp, navCtrl, false) { - val lockTaskFeatures = remember { mutableStateListOf() } + var lockTaskFeatures by remember { mutableIntStateOf(0) } var custom by rememberSaveable { mutableStateOf(false) } - val refreshFeature = { - var calculate = dpm.getLockTaskFeatures(receiver) - lockTaskFeatures.clear() - if(calculate != 0) { - var sq = 10 - while(sq >= 1) { - val current = (2).toDouble().pow(sq.toDouble()).toInt() - if(calculate - current >= 0) { - lockTaskFeatures += current - calculate -= current - } - sq-- - } - if(calculate - 1 >= 0) { lockTaskFeatures += 1 } - custom = true - } else { - custom = false - } + fun refreshFeature() { + lockTaskFeatures = dpm.getLockTaskFeatures(receiver) + custom = lockTaskFeatures != 0 } Spacer(Modifier.padding(vertical = 10.dp)) Text(text = stringResource(R.string.lock_task_feature), style = typography.headlineLarge) Spacer(Modifier.padding(vertical = 5.dp)) LaunchedEffect(Unit) { refreshFeature() } - RadioButtonItem(R.string.disable_all, !custom, { custom = false }) - RadioButtonItem(R.string.custom, custom, { custom = true }) + RadioButtonItem(R.string.disable_all, !custom) { custom = false } + RadioButtonItem(R.string.custom, custom) { custom = true } AnimatedVisibility(custom) { Column { CheckBoxItem( R.string.ltf_sys_info, - LOCK_TASK_FEATURE_SYSTEM_INFO in lockTaskFeatures, - { lockTaskFeatures.toggle(it, LOCK_TASK_FEATURE_SYSTEM_INFO) } - ) + lockTaskFeatures and LOCK_TASK_FEATURE_SYSTEM_INFO != 0 + ) { lockTaskFeatures = lockTaskFeatures xor LOCK_TASK_FEATURE_SYSTEM_INFO } CheckBoxItem( R.string.ltf_notifications, - LOCK_TASK_FEATURE_NOTIFICATIONS in lockTaskFeatures, - { lockTaskFeatures.toggle(it, LOCK_TASK_FEATURE_NOTIFICATIONS) } - ) + lockTaskFeatures and LOCK_TASK_FEATURE_NOTIFICATIONS != 0 + ) { lockTaskFeatures = lockTaskFeatures xor LOCK_TASK_FEATURE_NOTIFICATIONS } CheckBoxItem( R.string.ltf_home, - LOCK_TASK_FEATURE_HOME in lockTaskFeatures, - { lockTaskFeatures.toggle(it, LOCK_TASK_FEATURE_HOME) } - ) + lockTaskFeatures and LOCK_TASK_FEATURE_HOME != 0 + ) { lockTaskFeatures = lockTaskFeatures xor LOCK_TASK_FEATURE_HOME } CheckBoxItem( R.string.ltf_overview, - LOCK_TASK_FEATURE_OVERVIEW in lockTaskFeatures, - { lockTaskFeatures.toggle(it, LOCK_TASK_FEATURE_OVERVIEW) } - ) + lockTaskFeatures and LOCK_TASK_FEATURE_OVERVIEW != 0 + ) { lockTaskFeatures = lockTaskFeatures xor LOCK_TASK_FEATURE_OVERVIEW } CheckBoxItem( R.string.ltf_global_actions, - LOCK_TASK_FEATURE_GLOBAL_ACTIONS in lockTaskFeatures, - { lockTaskFeatures.toggle(it, LOCK_TASK_FEATURE_GLOBAL_ACTIONS) } - ) + lockTaskFeatures and LOCK_TASK_FEATURE_GLOBAL_ACTIONS != 0 + ) { lockTaskFeatures = lockTaskFeatures xor LOCK_TASK_FEATURE_GLOBAL_ACTIONS } CheckBoxItem( R.string.ltf_keyguard, - LOCK_TASK_FEATURE_KEYGUARD in lockTaskFeatures, - { lockTaskFeatures.toggle(it, LOCK_TASK_FEATURE_KEYGUARD) } - ) + lockTaskFeatures and LOCK_TASK_FEATURE_KEYGUARD != 0 + ) { lockTaskFeatures = lockTaskFeatures xor LOCK_TASK_FEATURE_KEYGUARD } if(VERSION.SDK_INT >= 30) { CheckBoxItem( R.string.ltf_block_activity_start_in_task, - LOCK_TASK_FEATURE_BLOCK_ACTIVITY_START_IN_TASK in lockTaskFeatures, - { lockTaskFeatures.toggle(it, LOCK_TASK_FEATURE_BLOCK_ACTIVITY_START_IN_TASK) } - ) + lockTaskFeatures and LOCK_TASK_FEATURE_BLOCK_ACTIVITY_START_IN_TASK != 0 + ) { lockTaskFeatures = lockTaskFeatures xor LOCK_TASK_FEATURE_BLOCK_ACTIVITY_START_IN_TASK } } } } Button( modifier = Modifier.fillMaxWidth(), onClick = { - var result = 0 - if(custom) { - lockTaskFeatures.forEach { result += it } - } try { - dpm.setLockTaskFeatures(receiver, result) - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + dpm.setLockTaskFeatures(receiver, lockTaskFeatures) + context.showOperationResultToast(true) } catch (e: IllegalArgumentException) { AlertDialog.Builder(context) .setTitle(R.string.error) @@ -846,7 +779,7 @@ fun LockTaskMode(navCtrl: NavHostController, vm: MyViewModel) { modifier = Modifier.fillMaxWidth(), onClick = { dpm.setLockTaskPackages(receiver, lockTaskPackages.toTypedArray()) - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) } ) { Text(stringResource(R.string.apply)) @@ -884,7 +817,7 @@ fun LockTaskMode(navCtrl: NavHostController, vm: MyViewModel) { }, modifier = Modifier.fillMaxWidth().padding(vertical = 3.dp) ) - CheckBoxItem(R.string.specify_activity, specifyActivity, { specifyActivity = it }) + CheckBoxItem(R.string.specify_activity, specifyActivity) { specifyActivity = it } AnimatedVisibility(specifyActivity) { OutlinedTextField( value = startLockTaskActivity, @@ -925,29 +858,25 @@ fun CACert(navCtrl: NavHostController) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() - val uri by fileUriFlow.collectAsState() var exist by remember { mutableStateOf(false) } - val uriPath = uri.path ?: "" - var caCertByteArray by remember { mutableStateOf(byteArrayOf()) } - LaunchedEffect(uri) { - if(uri != Uri.parse("")) { + var fileUri by remember { mutableStateOf(null) } + var caCertByteArray by remember { mutableStateOf(ByteArray(100000)) } + val getFileLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + result.data?.data?.let { uri -> uriToStream(context, uri) { val array = it.readBytes() caCertByteArray = if(array.size < 10000) { array - }else{ + } else { byteArrayOf() } } - exist = dpm.hasCaCertInstalled(receiver, caCertByteArray) } } MyScaffold(R.string.ca_cert, 8.dp, navCtrl) { - AnimatedVisibility(uriPath != "") { - Text(text = uriPath) - } Text( - text = if(uriPath == "") { stringResource(R.string.please_select_ca_cert) } else { stringResource(R.string.cert_installed, exist) }, + text = if(fileUri == null) { stringResource(R.string.please_select_ca_cert) } + else { stringResource(R.string.cert_installed, stringResource(exist.yesOrNo)) }, modifier = Modifier.animateContentSize() ) Spacer(Modifier.padding(vertical = 5.dp)) @@ -956,18 +885,17 @@ fun CACert(navCtrl: NavHostController) { val caCertIntent = Intent(Intent.ACTION_GET_CONTENT) caCertIntent.setType("*/*") caCertIntent.addCategory(Intent.CATEGORY_OPENABLE) - getFile.launch(caCertIntent) + getFileLauncher.launch(caCertIntent) }, modifier = Modifier.fillMaxWidth() ) { Text(stringResource(R.string.select_ca_cert)) } - AnimatedVisibility(uriPath != "") { + AnimatedVisibility(fileUri != null) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Button( onClick = { - val result = dpm.installCaCert(receiver, caCertByteArray) - Toast.makeText(context, if(result) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(dpm.installCaCert(receiver, caCertByteArray)) exist = dpm.hasCaCertInstalled(receiver, caCertByteArray) }, modifier = Modifier.fillMaxWidth(0.49F) @@ -976,12 +904,11 @@ fun CACert(navCtrl: NavHostController) { } Button( onClick = { - if(exist) { - dpm.uninstallCaCert(receiver, caCertByteArray) - exist = dpm.hasCaCertInstalled(receiver, caCertByteArray) - Toast.makeText(context, if(exist) R.string.failed else R.string.success, Toast.LENGTH_SHORT).show() - } else { Toast.makeText(context, R.string.not_exist, Toast.LENGTH_SHORT).show() } + dpm.uninstallCaCert(receiver, caCertByteArray) + exist = dpm.hasCaCertInstalled(receiver, caCertByteArray) + context.showOperationResultToast(true) }, + enabled = exist, modifier = Modifier.fillMaxWidth(0.96F) ) { Text(stringResource(R.string.uninstall)) @@ -991,7 +918,7 @@ fun CACert(navCtrl: NavHostController) { Button( onClick = { dpm.uninstallAllUserCaCerts(receiver) - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth() ) { @@ -1008,11 +935,31 @@ fun SecurityLogging(navCtrl: NavHostController) { val receiver = context.getReceiver() val logFile = context.filesDir.resolve("SecurityLogs.json") var fileSize by remember { mutableLongStateOf(0) } - LaunchedEffect(Unit) { - fileSize = logFile.length() + LaunchedEffect(Unit) { fileSize = logFile.length() } + var preRebootSecurityLogs by remember { mutableStateOf(byteArrayOf()) } + val exportPreRebootSecurityLogs = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + result.data?.data?.let { uri -> + context.contentResolver.openOutputStream(uri)?.use { outStream -> + preRebootSecurityLogs.inputStream().copyTo(outStream) + } + } + } + val exportSecurityLogs = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + result.data?.data?.let { uri -> + context.contentResolver.openOutputStream(uri)?.use { outStream -> + outStream.write("[".toByteArray()) + logFile.inputStream().use { it.copyTo(outStream) } + outStream.write("]".toByteArray()) + context.showOperationResultToast(true) + } + } } MyScaffold(R.string.security_logging, 8.dp, navCtrl) { - SwitchItem(R.string.enable, "", null, { dpm.isSecurityLoggingEnabled(receiver) }, { dpm.setSecurityLoggingEnabled(receiver, it) }, padding = false) + SwitchItem( + R.string.enable, + getState = { dpm.isSecurityLoggingEnabled(receiver) }, onCheckedChange = { dpm.setSecurityLoggingEnabled(receiver, it) }, + padding = false + ) Text(stringResource(R.string.log_file_size_is, formatFileSize(fileSize))) Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { Button( @@ -1021,9 +968,7 @@ fun SecurityLogging(navCtrl: NavHostController) { intent.addCategory(Intent.CATEGORY_OPENABLE) intent.setType("application/json") intent.putExtra(Intent.EXTRA_TITLE, "SecurityLogs.json") - exportFilePath = logFile.path - isExportingSecurityOrNetworkLogs = true - exportFile.launch(intent) + exportSecurityLogs.launch(intent) }, enabled = fileSize > 0, modifier = Modifier.fillMaxWidth(0.49F) @@ -1050,29 +995,16 @@ fun SecurityLogging(navCtrl: NavHostController) { Toast.makeText(context, R.string.no_logs, Toast.LENGTH_SHORT).show() return@Button } else { - val securityEvents = buildJsonArray { - logs.forEach { event -> - addJsonObject { - put("time_nanos", event.timeNanos) - put("tag", event.tag) - if(VERSION.SDK_INT >= 28) put("level", event.logLevel) - if(VERSION.SDK_INT >= 28) put("id", event.id) - parseSecurityEventData(event).let { if(it != null) put("data", it) } - } - } - } - val preRebootSecurityLogs = context.filesDir.resolve("PreRebootSecurityLogs") - preRebootSecurityLogs.outputStream().use { - val json = Json { ignoreUnknownKeys = true; explicitNulls = false } - json.encodeToStream(securityEvents, it) - } + val outputStream = ByteArrayOutputStream() + outputStream.write("[".encodeToByteArray()) + processSecurityLogs(logs, outputStream) + outputStream.write("]".encodeToByteArray()) + preRebootSecurityLogs = outputStream.toByteArray() val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) intent.addCategory(Intent.CATEGORY_OPENABLE) intent.setType("application/json") intent.putExtra(Intent.EXTRA_TITLE, "PreRebootSecurityLogs.json") - exportFilePath = preRebootSecurityLogs.path - isExportingSecurityOrNetworkLogs = true - exportFile.launch(intent) + exportPreRebootSecurityLogs.launch(intent) } }, modifier = Modifier.fillMaxWidth() @@ -1169,7 +1101,7 @@ fun FRPPolicy(navCtrl: NavHostController) { } AnimatedVisibility(usePolicy) { Column { - CheckBoxItem(R.string.enable_frp, enabled, { enabled = it }) + CheckBoxItem(R.string.enable_frp, enabled) { enabled = it } Text(stringResource(R.string.account_list_is)) Column(modifier = Modifier.animateContentSize()) { if(accountList.isEmpty()) Text(stringResource(R.string.none)) @@ -1230,25 +1162,17 @@ fun WipeData(navCtrl: NavHostController) { val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager val dpm = context.getDPM() val focusMgr = LocalFocusManager.current + var flag by remember { mutableIntStateOf(0) } var warning by remember { mutableStateOf(false) } var wipeDevice by remember { mutableStateOf(false) } - var externalStorage by remember { mutableStateOf(false) } - var protectionData by remember { mutableStateOf(false) } - var euicc by remember { mutableStateOf(false) } var silent by remember { mutableStateOf(false) } var reason by remember { mutableStateOf("") } MyScaffold(R.string.wipe_data, 8.dp, navCtrl) { - CheckBoxItem( - R.string.wipe_external_storage, - externalStorage, { externalStorage = it } - ) - if(VERSION.SDK_INT >= 22 && context.isDeviceOwner) { - CheckBoxItem(R.string.wipe_reset_protection_data, - protectionData, { protectionData = it } - ) - } - if(VERSION.SDK_INT >= 28) { CheckBoxItem(R.string.wipe_euicc, euicc, { euicc = it }) } - if(VERSION.SDK_INT >= 29) { CheckBoxItem(R.string.wipe_silently, silent, { silent = it }) } + CheckBoxItem(R.string.wipe_external_storage, flag and WIPE_EXTERNAL_STORAGE != 0) { flag = flag xor WIPE_EXTERNAL_STORAGE } + if(VERSION.SDK_INT >= 22 && context.isDeviceOwner) CheckBoxItem( + R.string.wipe_reset_protection_data, flag and WIPE_RESET_PROTECTION_DATA != 0) { flag = flag xor WIPE_RESET_PROTECTION_DATA } + if(VERSION.SDK_INT >= 28) CheckBoxItem(R.string.wipe_euicc, flag and WIPE_EUICC != 0) { flag = flag xor WIPE_EUICC } + if(VERSION.SDK_INT >= 29) CheckBoxItem(R.string.wipe_silently, silent) { silent = it } AnimatedVisibility(!silent && VERSION.SDK_INT >= 28) { OutlinedTextField( value = reason, onValueChange = { reason = it }, @@ -1308,11 +1232,7 @@ fun WipeData(navCtrl: NavHostController) { val timerText = if(timer > 0) "(${timer}s)" else "" TextButton( onClick = { - var flag = 0 - if(externalStorage) { flag += WIPE_EXTERNAL_STORAGE } - if(protectionData && VERSION.SDK_INT >= 22) { flag += WIPE_RESET_PROTECTION_DATA } - if(euicc && VERSION.SDK_INT >= 28) { flag += WIPE_EUICC } - if(silent && VERSION.SDK_INT >= 29) { flag += WIPE_SILENTLY } + if(silent && VERSION.SDK_INT >= 29) { flag = flag or WIPE_SILENTLY } if(wipeDevice) { dpm.wipeDevice(flag) } else { @@ -1351,17 +1271,17 @@ fun SystemUpdatePolicy(navCtrl: NavHostController) { var selectedPolicy by remember { mutableStateOf(dpm.systemUpdatePolicy?.policyType) } RadioButtonItem( R.string.system_update_policy_automatic, - selectedPolicy == TYPE_INSTALL_AUTOMATIC, { selectedPolicy = TYPE_INSTALL_AUTOMATIC } - ) + selectedPolicy == TYPE_INSTALL_AUTOMATIC + ) { selectedPolicy = TYPE_INSTALL_AUTOMATIC } RadioButtonItem( R.string.system_update_policy_install_windowed, - selectedPolicy == TYPE_INSTALL_WINDOWED, { selectedPolicy = TYPE_INSTALL_WINDOWED } - ) + selectedPolicy == TYPE_INSTALL_WINDOWED + ) { selectedPolicy = TYPE_INSTALL_WINDOWED } RadioButtonItem( R.string.system_update_policy_postpone, - selectedPolicy == TYPE_POSTPONE, { selectedPolicy = TYPE_POSTPONE } - ) - RadioButtonItem(R.string.none, selectedPolicy == null, { selectedPolicy = null }) + selectedPolicy == TYPE_POSTPONE + ) { selectedPolicy = TYPE_POSTPONE } + RadioButtonItem(R.string.none, selectedPolicy == null) { selectedPolicy = null } var windowedPolicyStart by remember { mutableStateOf("") } var windowedPolicyEnd by remember { mutableStateOf("") } AnimatedVisibility(selectedPolicy == 2) { @@ -1399,7 +1319,7 @@ fun SystemUpdatePolicy(navCtrl: NavHostController) { else -> null } dpm.setSystemUpdatePolicy(receiver,policy) - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth().padding(top = 8.dp) ) { @@ -1447,27 +1367,30 @@ fun InstallSystemUpdate(navCtrl: NavHostController) { Toast.makeText(context, errMsg, Toast.LENGTH_SHORT).show() } } - val uri by fileUriFlow.collectAsState() + var uri by remember { mutableStateOf(null) } + val getFileLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { + uri = it.data?.data + } MyScaffold(R.string.install_system_update, 8.dp, navCtrl) { Button( onClick = { val intent = Intent(Intent.ACTION_GET_CONTENT) intent.setType("application/zip") intent.addCategory(Intent.CATEGORY_OPENABLE) - getFile.launch(intent) + getFileLauncher.launch(intent) }, modifier = Modifier.fillMaxWidth() ) { Text(stringResource(R.string.select_ota_package)) } - AnimatedVisibility(uri != Uri.parse("")) { + AnimatedVisibility(uri != null) { Button( onClick = { val executor = Executors.newCachedThreadPool() try { - dpm.installSystemUpdate(receiver, uri, executor, callback) + dpm.installSystemUpdate(receiver, uri!!, executor, callback) Toast.makeText(context, R.string.start_install_system_update, Toast.LENGTH_SHORT).show() - }catch(e: Exception) { + } catch(e: Exception) { Toast.makeText( context, context.getString(R.string.install_system_update_failed) + e.cause.toString(), diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt index b50a866..80b0036 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt @@ -39,12 +39,12 @@ fun UserRestriction(navCtrl:NavHostController) { Text(text = stringResource(R.string.some_features_invalid_in_work_profile), modifier = Modifier.padding(start = 16.dp)) } Spacer(Modifier.padding(vertical = 2.dp)) - FunctionItem(R.string.network_and_internet, "", R.drawable.wifi_fill0) { navCtrl.navigate("UR-Internet") } - FunctionItem(R.string.connectivity, "", R.drawable.devices_other_fill0) { navCtrl.navigate("UR-Connectivity") } - FunctionItem(R.string.applications, "", R.drawable.apps_fill0) { navCtrl.navigate("UR-Applications") } - FunctionItem(R.string.users, "", R.drawable.account_circle_fill0) { navCtrl.navigate("UR-Users") } - FunctionItem(R.string.media, "", R.drawable.volume_up_fill0) { navCtrl.navigate("UR-Media") } - FunctionItem(R.string.other, "", R.drawable.more_horiz_fill0) { navCtrl.navigate("UR-Other") } + FunctionItem(R.string.network_and_internet, icon = R.drawable.wifi_fill0) { navCtrl.navigate("UR-Internet") } + FunctionItem(R.string.connectivity, icon = R.drawable.devices_other_fill0) { navCtrl.navigate("UR-Connectivity") } + FunctionItem(R.string.applications, icon = R.drawable.apps_fill0) { navCtrl.navigate("UR-Applications") } + FunctionItem(R.string.users, icon = R.drawable.account_circle_fill0) { navCtrl.navigate("UR-Users") } + FunctionItem(R.string.media, icon = R.drawable.volume_up_fill0) { navCtrl.navigate("UR-Media") } + FunctionItem(R.string.other, icon = R.drawable.more_horiz_fill0) { navCtrl.navigate("UR-Other") } } } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt index 995ad65..7ce62c0 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt @@ -6,7 +6,6 @@ import android.content.Context import android.content.Intent import android.graphics.Bitmap import android.graphics.BitmapFactory -import android.net.Uri import android.os.Binder import android.os.Build.VERSION import android.os.Process @@ -14,6 +13,8 @@ import android.os.UserHandle import android.os.UserManager import android.provider.MediaStore import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.StringRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize @@ -40,7 +41,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf @@ -59,10 +59,8 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import com.bintianqi.owndroid.R -import com.bintianqi.owndroid.fileUriFlow -import com.bintianqi.owndroid.getFile import com.bintianqi.owndroid.parseTimestamp -import com.bintianqi.owndroid.toggle +import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.ui.CardItem import com.bintianqi.owndroid.ui.CheckBoxItem import com.bintianqi.owndroid.ui.FunctionItem @@ -82,33 +80,32 @@ fun Users(navCtrl: NavHostController) { val profileOwner = context.isProfileOwner var dialog by remember { mutableIntStateOf(0) } MyScaffold(R.string.users, 0.dp, navCtrl) { - FunctionItem(R.string.user_info, "", R.drawable.person_fill0) { navCtrl.navigate("UserInfo") } + FunctionItem(R.string.user_info, icon = R.drawable.person_fill0) { navCtrl.navigate("UserInfo") } if(deviceOwner && VERSION.SDK_INT >= 28) { - FunctionItem(R.string.secondary_users, "", R.drawable.list_fill0) { dialog = 1 } - FunctionItem(R.string.options, "", R.drawable.tune_fill0) { navCtrl.navigate("UserOptions") } + FunctionItem(R.string.secondary_users, icon = R.drawable.list_fill0) { dialog = 1 } + FunctionItem(R.string.options, icon = R.drawable.tune_fill0) { navCtrl.navigate("UserOptions") } } if(deviceOwner) { - FunctionItem(R.string.user_operation, "", R.drawable.sync_alt_fill0) { navCtrl.navigate("UserOperation") } + FunctionItem(R.string.user_operation, icon = R.drawable.sync_alt_fill0) { navCtrl.navigate("UserOperation") } } if(VERSION.SDK_INT >= 24 && deviceOwner) { - FunctionItem(R.string.create_user, "", R.drawable.person_add_fill0) { navCtrl.navigate("CreateUser") } + FunctionItem(R.string.create_user, icon = R.drawable.person_add_fill0) { navCtrl.navigate("CreateUser") } } if(VERSION.SDK_INT >= 28 && profileOwner && dpm.isAffiliatedUser) { - FunctionItem(R.string.logout_current_user, "", R.drawable.logout_fill0) { dialog = 2 } + FunctionItem(R.string.logout_current_user, icon = R.drawable.logout_fill0) { dialog = 2 } } if(deviceOwner || profileOwner) { - FunctionItem(R.string.change_username, "", R.drawable.edit_fill0) { navCtrl.navigate("ChangeUsername") } + FunctionItem(R.string.change_username, icon = R.drawable.edit_fill0) { navCtrl.navigate("ChangeUsername") } } if(VERSION.SDK_INT >= 23 && (deviceOwner || profileOwner)) { - FunctionItem(R.string.change_user_icon, "", R.drawable.account_circle_fill0) { navCtrl.navigate("ChangeUserIcon") } + FunctionItem(R.string.change_user_icon, icon = R.drawable.account_circle_fill0) { navCtrl.navigate("ChangeUserIcon") } } if(VERSION.SDK_INT >= 28 && deviceOwner) { - FunctionItem(R.string.user_session_msg, "", R.drawable.notifications_fill0) { navCtrl.navigate("UserSessionMessage") } + FunctionItem(R.string.user_session_msg, icon = R.drawable.notifications_fill0) { navCtrl.navigate("UserSessionMessage") } } if(VERSION.SDK_INT >= 26 && (deviceOwner || profileOwner)) { - FunctionItem(R.string.affiliation_id, "", R.drawable.id_card_fill0) { navCtrl.navigate("AffiliationID") } + FunctionItem(R.string.affiliation_id, icon = R.drawable.id_card_fill0) { navCtrl.navigate("AffiliationID") } } - LaunchedEffect(Unit) { fileUriFlow.value = Uri.parse("") } } if(dialog != 0 && VERSION.SDK_INT >= 28) AlertDialog( title = { Text(stringResource(if(dialog == 1) R.string.secondary_users else R.string.logout_current_user)) }, @@ -157,7 +154,7 @@ fun UserOptions(navCtrl: NavHostController) { val receiver = context.getReceiver() MyScaffold(R.string.options, 0.dp, navCtrl) { if(VERSION.SDK_INT >= 28) { - SwitchItem(R.string.enable_logout, "", null, { dpm.isLogoutEnabled }, { dpm.setLogoutEnabled(receiver, it) }) + SwitchItem(R.string.enable_logout, getState = { dpm.isLogoutEnabled }, onCheckedChange = { dpm.setLogoutEnabled(receiver, it) }) } } } @@ -259,9 +256,7 @@ fun UserOperation(navCtrl: NavHostController) { Button( onClick = { focusMgr.clearFocus() - withUserHandle { - Toast.makeText(context, if(dpm.switchUser(receiver, it)) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show() - } + withUserHandle { context.showOperationResultToast(dpm.switchUser(receiver, it)) } }, enabled = legalInput, modifier = Modifier.fillMaxWidth() @@ -288,7 +283,7 @@ fun UserOperation(navCtrl: NavHostController) { focusMgr.clearFocus() withUserHandle { if(dpm.removeUser(receiver, it)) { - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) idInput = "" } else { Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show() @@ -313,7 +308,7 @@ fun CreateUser(navCtrl: NavHostController) { val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current var userName by remember { mutableStateOf("") } - val flags = remember { mutableStateListOf() } + var flag by remember { mutableIntStateOf(0) } MyScaffold(R.string.create_user, 8.dp, navCtrl) { OutlinedTextField( value = userName, @@ -326,28 +321,25 @@ fun CreateUser(navCtrl: NavHostController) { Spacer(Modifier.padding(vertical = 5.dp)) CheckBoxItem( R.string.create_user_skip_wizard, - DevicePolicyManager.SKIP_SETUP_WIZARD in flags, - { flags.toggle(it, DevicePolicyManager.SKIP_SETUP_WIZARD) } - ) + flag and DevicePolicyManager.SKIP_SETUP_WIZARD != 0 + ) { flag = flag xor DevicePolicyManager.SKIP_SETUP_WIZARD } if(VERSION.SDK_INT >= 28) { CheckBoxItem( R.string.create_user_ephemeral_user, - DevicePolicyManager.MAKE_USER_EPHEMERAL in flags, - { flags.toggle(it, DevicePolicyManager.MAKE_USER_EPHEMERAL) } - ) + flag and DevicePolicyManager.MAKE_USER_EPHEMERAL != 0 + ) { flag = flag xor DevicePolicyManager.MAKE_USER_EPHEMERAL } CheckBoxItem( R.string.create_user_enable_all_system_app, - DevicePolicyManager.LEAVE_ALL_SYSTEM_APPS_ENABLED in flags, - { flags.toggle(it, DevicePolicyManager.LEAVE_ALL_SYSTEM_APPS_ENABLED) } - ) + flag and DevicePolicyManager.LEAVE_ALL_SYSTEM_APPS_ENABLED != 0 + ) { flag = flag xor DevicePolicyManager.LEAVE_ALL_SYSTEM_APPS_ENABLED } } var newUserHandle: UserHandle? by remember { mutableStateOf(null) } Spacer(Modifier.padding(vertical = 5.dp)) Button( onClick = { focusMgr.clearFocus() - newUserHandle = dpm.createAndManageUser(receiver, userName, receiver, null, flags.sum()) - Toast.makeText(context, if(newUserHandle!=null) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show() + newUserHandle = dpm.createAndManageUser(receiver, userName, receiver, null, flag) + context.showOperationResultToast(newUserHandle != null) }, modifier = Modifier.fillMaxWidth() ) { @@ -403,7 +395,7 @@ fun AffiliationID(navCtrl: NavHostController) { onClick = { list.removeAll(listOf("")) dpm.setAffiliationIds(receiver, list.toSet()) - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) refreshIds() }, modifier = Modifier.fillMaxWidth() @@ -434,7 +426,7 @@ fun ChangeUsername(navCtrl: NavHostController) { Button( onClick = { dpm.setProfileName(receiver, inputUsername) - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth() ) { @@ -477,7 +469,6 @@ fun UserSessionMessage(navCtrl: NavHostController) { onClick = { dpm.setStartUserSessionMessage(receiver,start) refreshMsg() - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() }, modifier = Modifier.fillMaxWidth(0.49F) ) { @@ -487,7 +478,7 @@ fun UserSessionMessage(navCtrl: NavHostController) { onClick = { dpm.setStartUserSessionMessage(receiver,null) refreshMsg() - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth(0.96F) ) { @@ -508,7 +499,7 @@ fun UserSessionMessage(navCtrl: NavHostController) { onClick = { dpm.setEndUserSessionMessage(receiver,end) refreshMsg() - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth(0.49F) ) { @@ -518,7 +509,7 @@ fun UserSessionMessage(navCtrl: NavHostController) { onClick = { dpm.setEndUserSessionMessage(receiver,null) refreshMsg() - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth(0.96F) ) { @@ -536,22 +527,22 @@ fun ChangeUserIcon(navCtrl: NavHostController) { val receiver = context.getReceiver() var getContent by remember { mutableStateOf(false) } var bitmap by remember { mutableStateOf(null) } - val uriState by fileUriFlow.collectAsState() - LaunchedEffect(uriState) { - if(uriState == Uri.parse("")) return@LaunchedEffect - uriToStream(context, fileUriFlow.value) { stream -> - bitmap = BitmapFactory.decodeStream(stream) + val getFileLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { + it.data?.data?.let { + uriToStream(context, it) { stream -> + bitmap = BitmapFactory.decodeStream(stream) + } } } MyScaffold(R.string.change_user_icon, 8.dp, navCtrl) { - CheckBoxItem(R.string.file_picker_instead_gallery, getContent, { getContent = it }) + CheckBoxItem(R.string.file_picker_instead_gallery, getContent) { getContent = it } Spacer(Modifier.padding(vertical = 5.dp)) Button( onClick = { val intent = Intent(if(getContent) Intent.ACTION_GET_CONTENT else Intent.ACTION_PICK) if(getContent) intent.addCategory(Intent.CATEGORY_OPENABLE) intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*") - getFile.launch(intent) + getFileLauncher.launch(intent) }, modifier = Modifier.fillMaxWidth() ) { @@ -567,7 +558,7 @@ fun ChangeUserIcon(navCtrl: NavHostController) { Button( onClick = { dpm.setUserIcon(receiver, bitmap) - Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(true) } ) { Text(stringResource(R.string.apply)) diff --git a/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt b/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt index d83e433..f8175ca 100644 --- a/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt +++ b/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -41,7 +40,7 @@ import kotlinx.coroutines.launch @Composable fun FunctionItem( @StringRes title: Int, - desc: String, + desc: String? = null, @DrawableRes icon: Int? = null, operation: () -> Unit ) { @@ -63,7 +62,7 @@ fun FunctionItem( style = typography.titleLarge, modifier = Modifier.padding(bottom = if(zhCN) 2.dp else 0.dp) ) - if(desc != "") { Text(text = desc, color = colorScheme.onBackground.copy(alpha = 0.8F)) } + if(desc != null) { Text(text = desc, color = colorScheme.onBackground.copy(alpha = 0.8F)) } } } } @@ -85,18 +84,16 @@ fun NavIcon(operation: () -> Unit) { fun RadioButtonItem( @StringRes text: Int, selected: Boolean, - operation: () -> Unit, - textColor: Color = colorScheme.onBackground + operation: () -> Unit ) { - RadioButtonItem(stringResource(text), selected, operation, textColor) + RadioButtonItem(stringResource(text), selected, operation) } @Composable fun RadioButtonItem( text: String, selected: Boolean, - operation: () -> Unit, - textColor: Color = colorScheme.onBackground + operation: () -> Unit ) { Row(verticalAlignment = Alignment.CenterVertically,modifier = Modifier .fillMaxWidth() @@ -104,7 +101,7 @@ fun RadioButtonItem( .clickable(onClick = operation) ) { RadioButton(selected = selected, onClick = operation) - Text(text = text, color = textColor, modifier = Modifier.padding(bottom = if(zhCN) { 2 } else { 0 }.dp)) + Text(text = text, modifier = Modifier.padding(bottom = if(zhCN) { 2 } else { 0 }.dp)) } } @@ -112,8 +109,7 @@ fun RadioButtonItem( fun CheckBoxItem( @StringRes text: Int, checked: Boolean, - operation: (Boolean) -> Unit, - textColor: Color = colorScheme.onBackground + operation: (Boolean) -> Unit ) { Row(verticalAlignment = Alignment.CenterVertically,modifier = Modifier .fillMaxWidth() @@ -124,7 +120,7 @@ fun CheckBoxItem( checked = checked, onCheckedChange = operation ) - Text(text = stringResource(text), color = textColor, modifier = Modifier.padding(bottom = if(zhCN) { 2 } else { 0 }.dp)) + Text(text = stringResource(text), modifier = Modifier.padding(bottom = if(zhCN) { 2 } else { 0 }.dp)) } } @@ -132,26 +128,26 @@ fun CheckBoxItem( @Composable fun SwitchItem( @StringRes title: Int, - desc: String, - @DrawableRes icon: Int?, - getState: ()->Boolean, + desc: String? = null, + @DrawableRes icon: Int? = null, + getState: () -> Boolean, onCheckedChange: (Boolean)->Unit, - enable: Boolean = true, + enabled: Boolean = true, onClickBlank: (() -> Unit)? = null, padding: Boolean = true ) { var state by remember { mutableStateOf(getState()) } - SwitchItem(title, desc, icon, state, { onCheckedChange(it); state = getState() }, enable, onClickBlank, padding) + SwitchItem(title, desc, icon, state, { onCheckedChange(it); state = getState() }, enabled, onClickBlank, padding) } @Composable fun SwitchItem( @StringRes title: Int, - desc: String, - @DrawableRes icon: Int?, + desc: String? = null, + @DrawableRes icon: Int? = null, state: Boolean, - onCheckedChange: (Boolean)->Unit, - enable: Boolean = true, + onCheckedChange: (Boolean) -> Unit, + enabled: Boolean = true, onClickBlank: (() -> Unit)? = null, padding: Boolean = true ) { @@ -172,14 +168,12 @@ fun SwitchItem( ) Column(modifier = Modifier.padding(end = 60.dp, bottom = if(zhCN) 2.dp else 0.dp)) { Text(text = stringResource(title), style = typography.titleLarge) - if(desc != "") { - Text(text = desc, color = colorScheme.onBackground.copy(alpha = 0.8F)) - } + if(desc != null) Text(text = desc, color = colorScheme.onBackground.copy(alpha = 0.8F)) } } Switch( checked = state, onCheckedChange = { onCheckedChange(it) }, - modifier = Modifier.align(Alignment.CenterEnd), enabled = enable + modifier = Modifier.align(Alignment.CenterEnd), enabled = enabled ) } } From ec6bccc0b5652c679e4152cbfdacb2cc77974e6b Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Tue, 31 Dec 2024 20:32:40 +0800 Subject: [PATCH 11/12] Hardware monitor --- .../com/bintianqi/owndroid/MainActivity.kt | 2 + .../java/com/bintianqi/owndroid/dpm/System.kt | 91 ++++++++++++++++++- app/src/main/res/values-ru/strings.xml | 14 ++- app/src/main/res/values-tr/strings.xml | 14 ++- app/src/main/res/values-zh-rCN/strings.xml | 13 ++- app/src/main/res/values/strings.xml | 13 ++- 6 files changed, 142 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index df100ae..c0b9a60 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -77,6 +77,7 @@ import com.bintianqi.owndroid.dpm.DeviceOwner import com.bintianqi.owndroid.dpm.DisableAccountManagement import com.bintianqi.owndroid.dpm.DisableKeyguardFeatures import com.bintianqi.owndroid.dpm.FRPPolicy +import com.bintianqi.owndroid.dpm.HardwareMonitor import com.bintianqi.owndroid.dpm.InstallSystemUpdate import com.bintianqi.owndroid.dpm.IntentFilter import com.bintianqi.owndroid.dpm.Keyguard @@ -222,6 +223,7 @@ fun Home(activity: FragmentActivity, vm: MyViewModel) { composable(route = "System") { SystemManage(navCtrl) } composable(route = "SystemOptions") { SystemOptions(navCtrl) } composable(route = "Keyguard") { Keyguard(navCtrl) } + composable(route = "HardwareMonitor") { HardwareMonitor(navCtrl) } composable(route = "ChangeTime") { ChangeTime(navCtrl) } composable(route = "ChangeTimeZone") { ChangeTimeZone(navCtrl) } composable(route = "PermissionPolicy") { PermissionPolicy(navCtrl) } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt index 680f5f9..8957821 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt @@ -37,10 +37,12 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build.VERSION +import android.os.HardwarePropertiesManager import android.os.UserManager import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresApi import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.foundation.clickable @@ -78,6 +80,7 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Slider import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -87,9 +90,11 @@ import androidx.compose.material3.rememberTimePickerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -128,6 +133,7 @@ import java.io.ByteArrayOutputStream import java.util.Date import java.util.TimeZone import java.util.concurrent.Executors +import kotlin.math.roundToLong @SuppressLint("NewApi") @Composable @@ -146,6 +152,8 @@ fun SystemManage(navCtrl: NavHostController) { FunctionItem(R.string.options, icon = R.drawable.tune_fill0) { navCtrl.navigate("SystemOptions") } } FunctionItem(R.string.keyguard, icon = R.drawable.screen_lock_portrait_fill0) { navCtrl.navigate("Keyguard") } + if(VERSION.SDK_INT >= 24 && deviceOwner && !dhizuku) + FunctionItem(R.string.hardware_monitor, icon = R.drawable.memory_fill0) { navCtrl.navigate("HardwareMonitor") } if(VERSION.SDK_INT >= 24 && deviceOwner) { FunctionItem(R.string.reboot, icon = R.drawable.restart_alt_fill0) { dialog = 1 } } @@ -338,7 +346,7 @@ fun Keyguard(navCtrl: NavHostController) { var flag by remember { mutableIntStateOf(0) } if(VERSION.SDK_INT >= 26 && profileOwner && dpm.isManagedProfile(receiver)) { CheckBoxItem( - R.string.evict_credential_encryptoon_key, + R.string.evict_credential_encryption_key, flag and FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY != 0 ) { flag = flag xor FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY } Spacer(Modifier.padding(vertical = 2.dp)) @@ -358,6 +366,87 @@ fun Keyguard(navCtrl: NavHostController) { } } +@OptIn(ExperimentalMaterial3Api::class) +@RequiresApi(24) +@Composable +fun HardwareMonitor(navCtrl: NavHostController) { + val context = LocalContext.current + val hpm = context.getSystemService(HardwarePropertiesManager::class.java) + var refreshInterval by remember { mutableFloatStateOf(1F) } + val refreshIntervalMs = (refreshInterval * 1000).roundToLong() + val temperatures = remember { mutableStateMapOf>() } + val tempTypeMap = mapOf( + HardwarePropertiesManager.DEVICE_TEMPERATURE_CPU to R.string.cpu_temp, + HardwarePropertiesManager.DEVICE_TEMPERATURE_GPU to R.string.gpu_temp, + HardwarePropertiesManager.DEVICE_TEMPERATURE_BATTERY to R.string.battery_temp, + HardwarePropertiesManager.DEVICE_TEMPERATURE_SKIN to R.string.skin_temp + ) + val cpuUsages = remember { mutableStateListOf>() } + val fanSpeeds = remember { mutableStateListOf() } + fun refresh() { + cpuUsages.clear() + cpuUsages.addAll(hpm.cpuUsages.map { it.active to it.total }) + temperatures.clear() + tempTypeMap.forEach { + temperatures += it.key to hpm.getDeviceTemperatures(it.key, HardwarePropertiesManager.TEMPERATURE_CURRENT).toList() + } + fanSpeeds.clear() + fanSpeeds.addAll(hpm.fanSpeeds.toList()) + } + LaunchedEffect(Unit) { + while(true) { + refresh() + delay(refreshIntervalMs) + } + } + MyScaffold(R.string.hardware_monitor, 8.dp, navCtrl, false) { + Text(stringResource(R.string.refresh_interval), style = typography.titleLarge, modifier = Modifier.padding(vertical = 4.dp)) + Slider(refreshInterval, { refreshInterval = it }, valueRange = 0.5F..2F, steps = 14) + Text("${refreshIntervalMs}ms") + Spacer(Modifier.padding(vertical = 10.dp)) + temperatures.forEach { tempMapItem -> + Text(stringResource(tempTypeMap[tempMapItem.key]!!), style = typography.titleLarge, modifier = Modifier.padding(vertical = 4.dp)) + if(tempMapItem.value.isEmpty()) { + Text(stringResource(R.string.unsupported)) + } else { + tempMapItem.value.forEachIndexed { index, temp -> + Row(modifier = Modifier.padding(vertical = 4.dp)) { + Text(index.toString(), style = typography.titleMedium, modifier = Modifier.padding(start = 8.dp, end = 12.dp)) + Text(if(temp == HardwarePropertiesManager.UNDEFINED_TEMPERATURE) stringResource(R.string.undefined) else temp.toString()) + } + } + } + Spacer(Modifier.padding(vertical = 10.dp)) + } + Text(stringResource(R.string.cpu_usages), style = typography.titleLarge, modifier = Modifier.padding(vertical = 4.dp)) + if(cpuUsages.isEmpty()) { + Text(stringResource(R.string.unsupported)) + } else { + cpuUsages.forEachIndexed { index, usage -> + Row(modifier = Modifier.padding(vertical = 4.dp)) { + Text(index.toString(), style = typography.titleMedium, modifier = Modifier.padding(start = 8.dp, end = 12.dp)) + Column { + Text(stringResource(R.string.active) + ": " + usage.first + "ms") + Text(stringResource(R.string.total) + ": " + usage.second + "ms") + } + } + } + } + Spacer(Modifier.padding(vertical = 10.dp)) + Text(stringResource(R.string.fan_speeds), style = typography.titleLarge, modifier = Modifier.padding(vertical = 4.dp)) + if(fanSpeeds.isEmpty()) { + Text(stringResource(R.string.unsupported)) + } else { + fanSpeeds.forEachIndexed { index, speed -> + Row(modifier = Modifier.padding(vertical = 4.dp)) { + Text(index.toString(), style = typography.titleMedium, modifier = Modifier.padding(start = 8.dp, end = 12.dp)) + Text("$speed RPM") + } + } + } + } +} + @OptIn(ExperimentalMaterial3Api::class) @SuppressLint("NewApi") @Composable diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 376bc56..5cde12b 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -136,7 +136,19 @@ Отключить USB-сигнал Блокировка экрана Заблокировать экран сейчас - Удалить ключ шифрования учетных данных + Удалить ключ шифрования учетных данных + + Hardware monitor + Refresh interval + CPU temperatures + GPU temperatures + Battery temperatures + Skin temperatures + Undefined + CPU usages + Active + Total + Fan speeds Отчет об ошибке Запросить отчет об ошибке? Перезагрузить diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index f376da7..96bfcc3 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -137,7 +137,19 @@ USB sinyali Ekran kilidi Ekranı şimdi kilitle - Kimlik doğrulama şifreleme anahtarını çıkar + Kimlik doğrulama şifreleme anahtarını çıkar + + Hardware monitor + Refresh interval + CPU temperatures + GPU temperatures + Battery temperatures + Skin temperatures + Undefined + CPU usages + Active + Total + Fan speeds Hata raporu Hata raporu iste? Yeniden başlat diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index e2085f3..bce5a97 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -131,7 +131,18 @@ 禁用USB信号 锁屏 立即锁屏 - 移除凭证加密密钥 + 移除凭证加密密钥 + 硬件监视器 + 刷新间隔 + CPU温度 + GPU温度 + 电池温度 + 表面温度 + 未定义 + CPU使用情况 + 活动 + 总计 + Fan speeds 错误报告 请求错误报告? 重启 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1d9ab20..f54c1a5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -145,7 +145,18 @@ Disable USB signal Keyguard Lock screen now - Evict credential encryption key + Evict credential encryption key + Hardware monitor + Refresh interval + CPU temperatures + GPU temperatures + Battery temperatures + Skin temperatures + Undefined + CPU usages + Active + Total + Fan speeds Bug report Request bug report? Reboot From 65bf0f75d8bb304bac0a36762c1c58fd229f0bd8 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Tue, 31 Dec 2024 22:28:40 +0800 Subject: [PATCH 12/12] Use pager to split Lock task mode into 3 page Change version name to v6.3 Update workflow file Fix a typo in Readme.md --- .github/workflows/build.yml | 4 +- Readme.md | 2 +- app/build.gradle.kts | 4 +- .../bintianqi/owndroid/dpm/Applications.kt | 12 +- .../bintianqi/owndroid/dpm/ManagedProfile.kt | 8 +- .../com/bintianqi/owndroid/dpm/Network.kt | 18 +- .../com/bintianqi/owndroid/dpm/Password.kt | 5 +- .../com/bintianqi/owndroid/dpm/Permissions.kt | 7 +- .../java/com/bintianqi/owndroid/dpm/System.kt | 442 ++++++++++-------- .../bintianqi/owndroid/dpm/UserRestriction.kt | 4 +- .../java/com/bintianqi/owndroid/dpm/Users.kt | 10 +- app/src/main/res/values-ru/strings.xml | 7 +- app/src/main/res/values-tr/strings.xml | 7 +- app/src/main/res/values-zh-rCN/strings.xml | 7 +- app/src/main/res/values/strings.xml | 5 +- 15 files changed, 301 insertions(+), 241 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c9b8903..6d92713 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,8 +6,6 @@ on: - '**.md' tags-ignore: - '**' - branches-ignore: - - 'master' jobs: build: @@ -63,7 +61,7 @@ jobs: upload-telegram: name: Upload Builds - if: ${{ success() }} + if: ${{ success() && github.ref_name == 'dev' }} runs-on: ubuntu-latest needs: - build diff --git a/Readme.md b/Readme.md index 7d8e2db..38a8dc6 100644 --- a/Readme.md +++ b/Readme.md @@ -31,7 +31,7 @@ - 应用:禁止安装/卸载应用... - 用户:禁止添加/删除/切换用户... - 媒体:禁止调整亮度、禁止调整音量... - - 其他:禁止修改账号、禁止修改语言、禁止恢复出场设置、禁用调试功能... + - 其他:禁止修改账号、禁止修改语言、禁止恢复出厂设置、禁用调试功能... - 用户管理 - 用户信息 - 启动/切换/停止/删除用户 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fed4743..c77bc25 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -24,8 +24,8 @@ android { applicationId = "com.bintianqi.owndroid" minSdk = 21 targetSdk = 34 - versionCode = 34 - versionName = "6.2" + versionCode = 35 + versionName = "6.3" multiDexEnabled = false } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt index 0817898..58be73a 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -1,6 +1,5 @@ package com.bintianqi.owndroid.dpm -import android.annotation.SuppressLint import android.app.PendingIntent import android.app.admin.DevicePolicyManager import android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT @@ -20,6 +19,7 @@ import android.provider.Settings import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresApi import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background @@ -413,7 +413,7 @@ private fun Home(navCtrl:NavHostController, pkgName: String) { } -@SuppressLint("NewApi") +@RequiresApi(30) @Composable private fun UserCtrlDisabledPkg(pkgName:String) { val context = LocalContext.current @@ -456,7 +456,7 @@ private fun UserCtrlDisabledPkg(pkgName:String) { } } -@SuppressLint("NewApi") +@RequiresApi(23) @Composable private fun PermissionManage(pkgName: String) { val context = LocalContext.current @@ -556,7 +556,7 @@ private fun PermissionManage(pkgName: String) { } } -@SuppressLint("NewApi") +@RequiresApi(30) @Composable private fun CrossProfilePkg(pkgName: String) { val context = LocalContext.current @@ -636,7 +636,7 @@ private fun CrossProfileWidget(pkgName: String) { } } -@SuppressLint("NewApi") +@RequiresApi(34) @Composable private fun CredentialManagePolicy(pkgName: String) { val context = LocalContext.current @@ -822,7 +822,7 @@ private fun PermittedIME(pkgName: String) { } } -@SuppressLint("NewApi") +@RequiresApi(28) @Composable private fun KeepUninstalledApp(pkgName: String) { val context = LocalContext.current diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/ManagedProfile.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/ManagedProfile.kt index da69100..03c06b8 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/ManagedProfile.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/ManagedProfile.kt @@ -1,7 +1,6 @@ package com.bintianqi.owndroid.dpm import android.accounts.Account -import android.annotation.SuppressLint import android.app.admin.DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE import android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE import android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_ALLOW_OFFLINE @@ -21,6 +20,7 @@ import android.os.Build.VERSION import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresApi import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -59,8 +59,8 @@ import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.ui.CardItem import com.bintianqi.owndroid.ui.CheckBoxItem import com.bintianqi.owndroid.ui.CopyTextButton -import com.bintianqi.owndroid.ui.InfoCard import com.bintianqi.owndroid.ui.FunctionItem +import com.bintianqi.owndroid.ui.InfoCard import com.bintianqi.owndroid.ui.MyScaffold import com.bintianqi.owndroid.ui.SwitchItem import com.bintianqi.owndroid.yesOrNo @@ -160,7 +160,7 @@ fun CreateWorkProfile(navCtrl: NavHostController) { } } -@SuppressLint("NewApi") +@RequiresApi(30) @Composable fun OrgOwnedProfile(navCtrl: NavHostController) { val context = LocalContext.current @@ -180,7 +180,7 @@ fun OrgOwnedProfile(navCtrl: NavHostController) { } } -@SuppressLint("NewApi") +@RequiresApi(30) @Composable fun SuspendPersonalApp(navCtrl: NavHostController) { val context = LocalContext.current diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt index a54d8ca..baabd51 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt @@ -1,7 +1,6 @@ package com.bintianqi.owndroid.dpm import android.Manifest -import android.annotation.SuppressLint import android.app.AlertDialog import android.app.admin.DevicePolicyManager.PRIVATE_DNS_MODE_OFF import android.app.admin.DevicePolicyManager.PRIVATE_DNS_MODE_OPPORTUNISTIC @@ -51,6 +50,7 @@ import android.telephony.data.ApnSetting.PROTOCOL_UNSTRUCTURED import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresApi import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background @@ -717,7 +717,7 @@ private fun AddNetwork(wifiConfig: WifiConfiguration? = null, navCtrl: NavHostCo } } -@SuppressLint("NewApi") +@RequiresApi(33) @Composable fun WifiSecurityLevel(navCtrl: NavHostController) { val context = LocalContext.current @@ -743,7 +743,7 @@ fun WifiSecurityLevel(navCtrl: NavHostController) { } } -@SuppressLint("NewApi") +@RequiresApi(33) @Composable fun WifiSsidPolicy(navCtrl: NavHostController) { val context = LocalContext.current @@ -817,7 +817,7 @@ fun WifiSsidPolicy(navCtrl: NavHostController) { } } -@SuppressLint("NewApi") +@RequiresApi(29) @Composable fun PrivateDNS(navCtrl: NavHostController) { val context = LocalContext.current @@ -889,7 +889,7 @@ fun PrivateDNS(navCtrl: NavHostController) { } } -@SuppressLint("NewApi") +@RequiresApi(24) @Composable fun AlwaysOnVPNPackage(navCtrl: NavHostController, vm: MyViewModel) { val context = LocalContext.current @@ -1056,7 +1056,7 @@ fun RecommendedGlobalProxy(navCtrl: NavHostController) { } } -@SuppressLint("NewApi") +@RequiresApi(26) @Composable fun NetworkLogging(navCtrl: NavHostController) { val context = LocalContext.current @@ -1112,7 +1112,7 @@ fun NetworkLogging(navCtrl: NavHostController) { } } -@SuppressLint("NewApi") +@RequiresApi(31) @Composable fun WifiAuthKeypair(navCtrl: NavHostController) { val context = LocalContext.current @@ -1154,7 +1154,7 @@ fun WifiAuthKeypair(navCtrl: NavHostController) { } } -@SuppressLint("NewApi") +@RequiresApi(33) @Composable fun PreferentialNetworkService(navCtrl: NavHostController) { val focusMgr = LocalFocusManager.current @@ -1306,7 +1306,7 @@ fun PreferentialNetworkService(navCtrl: NavHostController) { } } -@SuppressLint("NewApi") +@RequiresApi(28) @Composable fun OverrideAPN(navCtrl: NavHostController) { val context = LocalContext.current diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt index 1681d18..10af2b1 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Password.kt @@ -33,6 +33,7 @@ import android.content.Intent import android.os.Build.VERSION import android.os.UserManager import android.widget.Toast +import androidx.annotation.RequiresApi import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -232,7 +233,7 @@ fun PasswordInfo(navCtrl: NavHostController) { } } -@SuppressLint("NewApi") +@RequiresApi(26) @Composable fun ResetPasswordToken(navCtrl: NavHostController) { val context = LocalContext.current @@ -412,7 +413,7 @@ fun ResetPassword(navCtrl: NavHostController) { } } -@SuppressLint("NewApi") +@RequiresApi(31) @Composable fun PasswordComplexity(navCtrl: NavHostController) { val context = LocalContext.current diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt index 314c7ec..29ac872 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt @@ -12,6 +12,7 @@ import android.os.Build.VERSION import android.os.RemoteException import android.os.UserManager import android.widget.Toast +import androidx.annotation.RequiresApi import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.KeyboardActions @@ -234,7 +235,7 @@ private fun toggleDhizukuMode(status: Boolean, context: Context) { } } -@SuppressLint("NewApi") +@RequiresApi(24) @Composable fun LockScreenInfo(navCtrl: NavHostController) { val context = LocalContext.current @@ -496,7 +497,7 @@ fun DeviceInfo(navCtrl: NavHostController) { ) } -@SuppressLint("NewApi") +@RequiresApi(24) @Composable fun SupportMessages(navCtrl: NavHostController) { val context = LocalContext.current @@ -574,7 +575,7 @@ fun SupportMessages(navCtrl: NavHostController) { } } -@SuppressLint("NewApi") +@RequiresApi(28) @Composable fun TransferOwnership(navCtrl: NavHostController) { val context = LocalContext.current diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt index 8957821..6440e9f 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt @@ -50,8 +50,10 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -60,9 +62,11 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.List import androidx.compose.material.icons.filled.Add @@ -77,14 +81,18 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Slider import androidx.compose.material3.Switch +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TimePicker +import androidx.compose.material3.TopAppBar import androidx.compose.material3.rememberDatePickerState import androidx.compose.material3.rememberTimePickerState import androidx.compose.runtime.Composable @@ -123,6 +131,7 @@ import com.bintianqi.owndroid.ui.FunctionItem import com.bintianqi.owndroid.ui.InfoCard import com.bintianqi.owndroid.ui.ListItem import com.bintianqi.owndroid.ui.MyScaffold +import com.bintianqi.owndroid.ui.NavIcon import com.bintianqi.owndroid.ui.RadioButtonItem import com.bintianqi.owndroid.ui.SwitchItem import com.bintianqi.owndroid.uriToStream @@ -133,9 +142,9 @@ import java.io.ByteArrayOutputStream import java.util.Date import java.util.TimeZone import java.util.concurrent.Executors +import kotlin.collections.addAll import kotlin.math.roundToLong -@SuppressLint("NewApi") @Composable fun SystemManage(navCtrl: NavHostController) { val context = LocalContext.current @@ -198,7 +207,7 @@ fun SystemManage(navCtrl: NavHostController) { FunctionItem(R.string.wipe_data, icon = R.drawable.device_reset_fill0) { navCtrl.navigate("WipeData") } } } - if(dialog != 0) AlertDialog( + if(dialog != 0 &&VERSION.SDK_INT >= 24) AlertDialog( onDismissRequest = { dialog = 0 }, title = { Text(stringResource(if(dialog == 1) R.string.reboot else R.string.bug_report)) }, text = { Text(stringResource(if(dialog == 1) R.string.info_reboot else R.string.confirm_bug_report)) }, @@ -448,7 +457,7 @@ fun HardwareMonitor(navCtrl: NavHostController) { } @OptIn(ExperimentalMaterial3Api::class) -@SuppressLint("NewApi") +@RequiresApi(28) @Composable fun ChangeTime(navCtrl: NavHostController) { val context = LocalContext.current @@ -558,7 +567,7 @@ fun ChangeTime(navCtrl: NavHostController) { ) } -@SuppressLint("NewApi") +@RequiresApi(28) @Composable fun ChangeTimeZone(navCtrl: NavHostController) { val context = LocalContext.current @@ -621,7 +630,7 @@ fun ChangeTimeZone(navCtrl: NavHostController) { ) } -@SuppressLint("NewApi") +@RequiresApi(23) @Composable fun PermissionPolicy(navCtrl: NavHostController) { val context = LocalContext.current @@ -646,7 +655,7 @@ fun PermissionPolicy(navCtrl: NavHostController) { } } -@SuppressLint("NewApi") +@RequiresApi(34) @Composable fun MTEPolicy(navCtrl: NavHostController) { val context = LocalContext.current @@ -674,7 +683,7 @@ fun MTEPolicy(navCtrl: NavHostController) { } } -@SuppressLint("NewApi") +@RequiresApi(31) @Composable fun NearbyStreamingPolicy(navCtrl: NavHostController) { val context = LocalContext.current @@ -739,206 +748,261 @@ fun NearbyStreamingPolicy(navCtrl: NavHostController) { } } -@SuppressLint("NewApi") +@OptIn(ExperimentalMaterial3Api::class) +@RequiresApi(28) @Composable fun LockTaskMode(navCtrl: NavHostController, vm: MyViewModel) { - val context = LocalContext.current - val dpm = context.getDPM() - val receiver = context.getReceiver() - val focusMgr = LocalFocusManager.current - var appSelectorRequest by rememberSaveable { mutableIntStateOf(0) } - MyScaffold(R.string.lock_task_mode, 8.dp, navCtrl, false) { - var lockTaskFeatures by remember { mutableIntStateOf(0) } - var custom by rememberSaveable { mutableStateOf(false) } - fun refreshFeature() { - lockTaskFeatures = dpm.getLockTaskFeatures(receiver) - custom = lockTaskFeatures != 0 + val coroutine = rememberCoroutineScope() + val pagerState = rememberPagerState { 3 } + var tabIndex by remember { mutableIntStateOf(0) } + tabIndex = pagerState.targetPage + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.lock_task_mode)) }, + navigationIcon = { NavIcon { navCtrl.navigateUp() } } + ) } - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.lock_task_feature), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) - LaunchedEffect(Unit) { refreshFeature() } - RadioButtonItem(R.string.disable_all, !custom) { custom = false } - RadioButtonItem(R.string.custom, custom) { custom = true } - AnimatedVisibility(custom) { - Column { - CheckBoxItem( - R.string.ltf_sys_info, - lockTaskFeatures and LOCK_TASK_FEATURE_SYSTEM_INFO != 0 - ) { lockTaskFeatures = lockTaskFeatures xor LOCK_TASK_FEATURE_SYSTEM_INFO } - CheckBoxItem( - R.string.ltf_notifications, - lockTaskFeatures and LOCK_TASK_FEATURE_NOTIFICATIONS != 0 - ) { lockTaskFeatures = lockTaskFeatures xor LOCK_TASK_FEATURE_NOTIFICATIONS } - CheckBoxItem( - R.string.ltf_home, - lockTaskFeatures and LOCK_TASK_FEATURE_HOME != 0 - ) { lockTaskFeatures = lockTaskFeatures xor LOCK_TASK_FEATURE_HOME } - CheckBoxItem( - R.string.ltf_overview, - lockTaskFeatures and LOCK_TASK_FEATURE_OVERVIEW != 0 - ) { lockTaskFeatures = lockTaskFeatures xor LOCK_TASK_FEATURE_OVERVIEW } - CheckBoxItem( - R.string.ltf_global_actions, - lockTaskFeatures and LOCK_TASK_FEATURE_GLOBAL_ACTIONS != 0 - ) { lockTaskFeatures = lockTaskFeatures xor LOCK_TASK_FEATURE_GLOBAL_ACTIONS } - CheckBoxItem( - R.string.ltf_keyguard, - lockTaskFeatures and LOCK_TASK_FEATURE_KEYGUARD != 0 - ) { lockTaskFeatures = lockTaskFeatures xor LOCK_TASK_FEATURE_KEYGUARD } - if(VERSION.SDK_INT >= 30) { - CheckBoxItem( - R.string.ltf_block_activity_start_in_task, - lockTaskFeatures and LOCK_TASK_FEATURE_BLOCK_ACTIVITY_START_IN_TASK != 0 - ) { lockTaskFeatures = lockTaskFeatures xor LOCK_TASK_FEATURE_BLOCK_ACTIVITY_START_IN_TASK } - } + ) { paddingValues -> + Column( + modifier = Modifier.fillMaxSize().padding(paddingValues) + ) { + TabRow(tabIndex) { + Tab( + tabIndex == 0, onClick = { coroutine.launch { pagerState.animateScrollToPage(0) } }, + text = { Text(stringResource(R.string.start)) } + ) + Tab( + tabIndex == 1, onClick = { coroutine.launch { pagerState.animateScrollToPage(1) } }, + text = { Text(stringResource(R.string.applications)) } + ) + Tab( + tabIndex == 2, onClick = { coroutine.launch { pagerState.animateScrollToPage(2) } }, + text = { Text(stringResource(R.string.features)) } + ) } - } - Button( - modifier = Modifier.fillMaxWidth(), - onClick = { - try { - dpm.setLockTaskFeatures(receiver, lockTaskFeatures) - context.showOperationResultToast(true) - } catch (e: IllegalArgumentException) { - AlertDialog.Builder(context) - .setTitle(R.string.error) - .setMessage(e.message) - .setPositiveButton(R.string.confirm) { dialog, _ -> dialog.dismiss() } - .show() + HorizontalPager(pagerState, verticalAlignment = Alignment.Top) { page -> + Column( + modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(start = 8.dp, end = 8.dp, bottom = 80.dp) + ) { + if(page == 0) StartLockTaskMode(navCtrl, vm) + else if(page == 1) LockTaskPackages(navCtrl, vm) + else LockTaskFeatures() } - refreshFeature() } - ) { - Text(stringResource(R.string.apply)) } + } +} - val lockTaskPackages = remember { mutableStateListOf() } - var inputLockTaskPkg by rememberSaveable { mutableStateOf("") } - LaunchedEffect(Unit) { lockTaskPackages.addAll(dpm.getLockTaskPackages(receiver)) } - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.lock_task_packages), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) - Column(modifier = Modifier.animateContentSize()) { - if(lockTaskPackages.isEmpty()) Text(text = stringResource(R.string.none)) - for(i in lockTaskPackages) { - ListItem(i) { lockTaskPackages -= i } - } +@RequiresApi(28) +@Composable +private fun ColumnScope.StartLockTaskMode(navCtrl: NavHostController, vm: MyViewModel) { + val context = LocalContext.current + val dpm = context.getDPM() + val focusMgr = LocalFocusManager.current + var startLockTaskApp by rememberSaveable { mutableStateOf("") } + var startLockTaskActivity by rememberSaveable { mutableStateOf("") } + var specifyActivity by rememberSaveable { mutableStateOf(false) } + val updatePackage by vm.selectedPackage.collectAsStateWithLifecycle() + LaunchedEffect(updatePackage) { + if(updatePackage != "") { + startLockTaskApp = updatePackage + vm.selectedPackage.value = "" } + } + Spacer(Modifier.padding(vertical = 5.dp)) + OutlinedTextField( + value = startLockTaskApp, + onValueChange = { startLockTaskApp = it }, + label = { Text(stringResource(R.string.package_name)) }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }), + trailingIcon = { + Icon(painter = painterResource(R.drawable.list_fill0), contentDescription = null, + modifier = Modifier + .clip(RoundedCornerShape(50)) + .clickable(onClick = { + focusMgr.clearFocus() + navCtrl.navigate("PackageSelector") + }) + .padding(3.dp)) + }, + modifier = Modifier.fillMaxWidth().padding(vertical = 3.dp) + ) + CheckBoxItem(R.string.specify_activity, specifyActivity) { specifyActivity = it } + AnimatedVisibility(specifyActivity) { OutlinedTextField( - value = inputLockTaskPkg, - onValueChange = { inputLockTaskPkg = it }, - label = { Text(stringResource(R.string.package_name)) }, + value = startLockTaskActivity, + onValueChange = { startLockTaskActivity = it }, + label = { Text("Activity") }, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }), - trailingIcon = { - Icon(painter = painterResource(R.drawable.list_fill0), contentDescription = null, - modifier = Modifier - .clip(RoundedCornerShape(50)) - .clickable(onClick = { - focusMgr.clearFocus() - appSelectorRequest = 1 - navCtrl.navigate("PackageSelector") - }) - .padding(3.dp)) - }, - modifier = Modifier.fillMaxWidth().padding(vertical = 3.dp) + modifier = Modifier.fillMaxWidth().padding(bottom = 5.dp) ) - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Button( - onClick = { - lockTaskPackages.add(inputLockTaskPkg) - inputLockTaskPkg = "" - }, - modifier = Modifier.fillMaxWidth(0.49F) - ) { - Text(stringResource(R.string.add)) + } + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { + if(!NotificationUtils.checkPermission(context)) return@Button + if(!dpm.isLockTaskPermitted(startLockTaskApp)) { + Toast.makeText(context, R.string.app_not_allowed, Toast.LENGTH_SHORT).show() + return@Button } - Button( - onClick = { - lockTaskPackages.remove(inputLockTaskPkg) - inputLockTaskPkg = "" - }, - modifier = Modifier.fillMaxWidth(0.96F) - ) { - Text(stringResource(R.string.remove)) + val options = ActivityOptions.makeBasic().setLockTaskEnabled(true) + val packageManager = context.packageManager + val launchIntent = if(specifyActivity) Intent().setComponent(ComponentName(startLockTaskApp, startLockTaskActivity)) + else packageManager.getLaunchIntentForPackage(startLockTaskApp) + if (launchIntent != null) { + context.startActivity(launchIntent, options.toBundle()) + } else { + Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show() } } + ) { + Text(stringResource(R.string.start)) + } + InfoCard(R.string.info_start_lock_task_mode) +} + +@RequiresApi(26) +@Composable +private fun ColumnScope.LockTaskPackages(navCtrl: NavHostController, vm: MyViewModel) { + val context = LocalContext.current + val dpm = context.getDPM() + val receiver = context.getReceiver() + val focusMgr = LocalFocusManager.current + val lockTaskPackages = remember { mutableStateListOf() } + var input by rememberSaveable { mutableStateOf("") } + val updatePackage by vm.selectedPackage.collectAsStateWithLifecycle() + LaunchedEffect(updatePackage) { + if(updatePackage != "") { + input = updatePackage + vm.selectedPackage.value = "" + } + } + LaunchedEffect(Unit) { lockTaskPackages.addAll(dpm.getLockTaskPackages(receiver)) } + Spacer(Modifier.padding(vertical = 5.dp)) + if(lockTaskPackages.isEmpty()) Text(text = stringResource(R.string.none)) + for(i in lockTaskPackages) { + ListItem(i) { lockTaskPackages -= i } + } + OutlinedTextField( + value = input, + onValueChange = { input = it }, + label = { Text(stringResource(R.string.package_name)) }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }), + trailingIcon = { + Icon(painter = painterResource(R.drawable.list_fill0), contentDescription = null, + modifier = Modifier + .clip(RoundedCornerShape(50)) + .clickable(onClick = { + focusMgr.clearFocus() + navCtrl.navigate("PackageSelector") + }) + .padding(3.dp)) + }, + modifier = Modifier.fillMaxWidth().padding(vertical = 3.dp) + ) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Button( - modifier = Modifier.fillMaxWidth(), onClick = { - dpm.setLockTaskPackages(receiver, lockTaskPackages.toTypedArray()) - context.showOperationResultToast(true) - } - ) { - Text(stringResource(R.string.apply)) - } - InfoCard(R.string.info_lock_task_packages) - var startLockTaskApp by rememberSaveable { mutableStateOf("") } - var startLockTaskActivity by rememberSaveable { mutableStateOf("") } - var specifyActivity by rememberSaveable { mutableStateOf(false) } - val updatePackage by vm.selectedPackage.collectAsStateWithLifecycle() - LaunchedEffect(updatePackage) { - if(updatePackage != "") { - if(appSelectorRequest == 1) inputLockTaskPkg = updatePackage else startLockTaskApp = updatePackage - vm.selectedPackage.value = "" - } - } - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.start_lock_task_mode), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) - OutlinedTextField( - value = startLockTaskApp, - onValueChange = { startLockTaskApp = it }, - label = { Text(stringResource(R.string.package_name)) }, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }), - trailingIcon = { - Icon(painter = painterResource(R.drawable.list_fill0), contentDescription = null, - modifier = Modifier - .clip(RoundedCornerShape(50)) - .clickable(onClick = { - focusMgr.clearFocus() - appSelectorRequest = 2 - navCtrl.navigate("PackageSelector") - }) - .padding(3.dp)) + lockTaskPackages.add(input) + input = "" }, - modifier = Modifier.fillMaxWidth().padding(vertical = 3.dp) - ) - CheckBoxItem(R.string.specify_activity, specifyActivity) { specifyActivity = it } - AnimatedVisibility(specifyActivity) { - OutlinedTextField( - value = startLockTaskActivity, - onValueChange = { startLockTaskActivity = it }, - label = { Text("Activity") }, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { focusMgr.clearFocus() }), - modifier = Modifier.fillMaxWidth().padding(bottom = 5.dp) - ) + modifier = Modifier.fillMaxWidth(0.49F) + ) { + Text(stringResource(R.string.add)) } Button( - modifier = Modifier.fillMaxWidth(), onClick = { - if(!NotificationUtils.checkPermission(context)) return@Button - if(!dpm.isLockTaskPermitted(startLockTaskApp)) { - Toast.makeText(context, R.string.app_not_allowed, Toast.LENGTH_SHORT).show() - return@Button - } - val options = ActivityOptions.makeBasic().setLockTaskEnabled(true) - val packageManager = context.packageManager - val launchIntent = if(specifyActivity) Intent().setComponent(ComponentName(startLockTaskApp, startLockTaskActivity)) - else packageManager.getLaunchIntentForPackage(startLockTaskApp) - if (launchIntent != null) { - context.startActivity(launchIntent, options.toBundle()) - } else { - Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show() - } - } + lockTaskPackages.remove(input) + input = "" + }, + modifier = Modifier.fillMaxWidth(0.96F) ) { - Text(stringResource(R.string.start)) + Text(stringResource(R.string.remove)) + } + } + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { + dpm.setLockTaskPackages(receiver, lockTaskPackages.toTypedArray()) + context.showOperationResultToast(true) + } + ) { + Text(stringResource(R.string.apply)) + } + InfoCard(R.string.info_lock_task_packages) +} + +@RequiresApi(28) +@Composable +private fun ColumnScope.LockTaskFeatures() { + val context = LocalContext.current + val dpm = context.getDPM() + val receiver = context.getReceiver() + var flags by remember { mutableIntStateOf(0) } + var custom by rememberSaveable { mutableStateOf(false) } + fun refresh() { + flags = dpm.getLockTaskFeatures(receiver) + custom = flags != 0 + } + LaunchedEffect(Unit) { refresh() } + Spacer(Modifier.padding(vertical = 5.dp)) + RadioButtonItem(R.string.disable_all, !custom) { custom = false } + RadioButtonItem(R.string.custom, custom) { custom = true } + AnimatedVisibility(custom) { + Column { + CheckBoxItem( + R.string.ltf_sys_info, + flags and LOCK_TASK_FEATURE_SYSTEM_INFO != 0 + ) { flags = flags xor LOCK_TASK_FEATURE_SYSTEM_INFO } + CheckBoxItem( + R.string.ltf_notifications, + flags and LOCK_TASK_FEATURE_NOTIFICATIONS != 0 + ) { flags = flags xor LOCK_TASK_FEATURE_NOTIFICATIONS } + CheckBoxItem( + R.string.ltf_home, + flags and LOCK_TASK_FEATURE_HOME != 0 + ) { flags = flags xor LOCK_TASK_FEATURE_HOME } + CheckBoxItem( + R.string.ltf_overview, + flags and LOCK_TASK_FEATURE_OVERVIEW != 0 + ) { flags = flags xor LOCK_TASK_FEATURE_OVERVIEW } + CheckBoxItem( + R.string.ltf_global_actions, + flags and LOCK_TASK_FEATURE_GLOBAL_ACTIONS != 0 + ) { flags = flags xor LOCK_TASK_FEATURE_GLOBAL_ACTIONS } + CheckBoxItem( + R.string.ltf_keyguard, + flags and LOCK_TASK_FEATURE_KEYGUARD != 0 + ) { flags = flags xor LOCK_TASK_FEATURE_KEYGUARD } + if(VERSION.SDK_INT >= 30) { + CheckBoxItem( + R.string.ltf_block_activity_start_in_task, + flags and LOCK_TASK_FEATURE_BLOCK_ACTIVITY_START_IN_TASK != 0 + ) { flags = flags xor LOCK_TASK_FEATURE_BLOCK_ACTIVITY_START_IN_TASK } + } } - InfoCard(R.string.info_start_lock_task_mode) + } + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { + try { + dpm.setLockTaskFeatures(receiver, flags) + context.showOperationResultToast(true) + } catch (e: IllegalArgumentException) { + AlertDialog.Builder(context) + .setTitle(R.string.error) + .setMessage(e.message) + .setPositiveButton(R.string.confirm) { dialog, _ -> dialog.dismiss() } + .show() + } + refresh() + } + ) { + Text(stringResource(R.string.apply)) } } @@ -1016,7 +1080,7 @@ fun CACert(navCtrl: NavHostController) { } } -@SuppressLint("NewApi") +@RequiresApi(24) @Composable fun SecurityLogging(navCtrl: NavHostController) { val context = LocalContext.current @@ -1152,7 +1216,7 @@ fun DisableAccountManagement(navCtrl: NavHostController) { } } -@SuppressLint("NewApi") +@RequiresApi(30) @Composable fun FRPPolicy(navCtrl: NavHostController) { val context = LocalContext.current @@ -1244,7 +1308,6 @@ fun FRPPolicy(navCtrl: NavHostController) { } } -@SuppressLint("NewApi") @Composable fun WipeData(navCtrl: NavHostController) { val context = LocalContext.current @@ -1305,7 +1368,10 @@ fun WipeData(navCtrl: NavHostController) { }, text = { Text( - text = stringResource(if(userManager.isSystemUser) R.string.wipe_data_warning else R.string.info_wipe_data_in_managed_user), + text = stringResource( + if(VERSION.SDK_INT >= 23 && userManager.isSystemUser) R.string.wipe_data_warning + else R.string.info_wipe_data_in_managed_user + ), color = colorScheme.error ) }, @@ -1322,7 +1388,7 @@ fun WipeData(navCtrl: NavHostController) { TextButton( onClick = { if(silent && VERSION.SDK_INT >= 29) { flag = flag or WIPE_SILENTLY } - if(wipeDevice) { + if(wipeDevice && VERSION.SDK_INT >= 34) { dpm.wipeDevice(flag) } else { if(VERSION.SDK_INT >= 28 && reason != "") { diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt index 80b0036..a693c78 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt @@ -1,10 +1,10 @@ package com.bintianqi.owndroid.dpm -import android.annotation.SuppressLint import android.os.Build.VERSION import android.os.UserManager import android.widget.Toast import androidx.annotation.DrawableRes +import androidx.annotation.RequiresApi import androidx.annotation.StringRes import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer @@ -48,7 +48,7 @@ fun UserRestriction(navCtrl:NavHostController) { } } -@SuppressLint("NewApi") +@RequiresApi(24) @Composable fun UserRestrictionItem(restriction: Restriction) { val context = LocalContext.current diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt index 7ce62c0..3de39c4 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Users.kt @@ -1,6 +1,5 @@ package com.bintianqi.owndroid.dpm -import android.annotation.SuppressLint import android.app.admin.DevicePolicyManager import android.content.Context import android.content.Intent @@ -15,6 +14,7 @@ import android.provider.MediaStore import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.RequiresApi import androidx.annotation.StringRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize @@ -299,7 +299,7 @@ fun UserOperation(navCtrl: NavHostController) { } } -@SuppressLint("NewApi") +@RequiresApi(24) @Composable fun CreateUser(navCtrl: NavHostController) { val context = LocalContext.current @@ -350,7 +350,7 @@ fun CreateUser(navCtrl: NavHostController) { } } -@SuppressLint("NewApi") +@RequiresApi(26) @Composable fun AffiliationID(navCtrl: NavHostController) { val context = LocalContext.current @@ -441,7 +441,7 @@ fun ChangeUsername(navCtrl: NavHostController) { } } -@SuppressLint("NewApi") +@RequiresApi(28) @Composable fun UserSessionMessage(navCtrl: NavHostController) { val context = LocalContext.current @@ -519,7 +519,7 @@ fun UserSessionMessage(navCtrl: NavHostController) { } } -@SuppressLint("NewApi") +@RequiresApi(23) @Composable fun ChangeUserIcon(navCtrl: NavHostController) { val context = LocalContext.current diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 5cde12b..1038d6f 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -65,6 +65,9 @@ Permission denied Error Status + Edit + Overview + Features @@ -170,10 +173,7 @@ Политика потоковой передачи уведомлений Nearby Только для одной управляемой учетной записи Режим закрепления задачи - Функция закрепления задачи - Закрепленные пакеты Указать Acitvity - Запустить режим закрепления задачи Приложение не разрешено Отключить все @@ -553,7 +553,6 @@ Отключить неотредактированные уведомления Отключить доверенные агенты Отключить отпечаток пальца - Отключить удаленный ввод Отключить распознавание лица Отключить сканер радужной оболочки Отключить биометрию diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 96bfcc3..8a9dca7 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -66,6 +66,9 @@ Permission denied Error Status + Edit + Overview + Features Etkinleştirmek İçin Tıklayın @@ -171,10 +174,7 @@ Yakındaki bildirim akış politikası Yalnızca aynı yönetilen hesap Lock task mode - Görev kilitleme özelliği - Lock task packages Specify Activity - Start lock task mode App is not allowed Hepsini devre dışı bırak @@ -549,7 +549,6 @@ Sansürsüz bildirimleri devre dışı bırak Güvenilir ajanları devre dışı bırak Parmak izini devre dışı bırak - Uzaktan girişleri devre dışı bırak Yüz tanımayı devre dışı bırak İris tarayıcısını devre dışı bırak Biyometrikleri devre dışı bırak diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index bce5a97..44e260b 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -62,6 +62,9 @@ 无权限 错误 状态 + 编辑 + 概览 + 功能 点击以激活 @@ -163,10 +166,7 @@ 附近通知传输 在足够安全时启用 锁定任务模式 - 锁定任务功能 - 锁定任务应用 指定Activity - 启动锁定任务模式 应用未被允许 禁用全部 允许状态栏信息 @@ -537,7 +537,6 @@ 禁用未经编辑的通知 禁用可信代理 禁用指纹解锁 - 禁止远程输入(弃用) 禁用人脸解锁 禁用虹膜解锁(?) 禁用生物识别 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f54c1a5..92a9c79 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -69,6 +69,7 @@ Status Edit Overview + Features Click to activate @@ -177,10 +178,7 @@ Nearby notification streaming policy Same managed account only Lock task mode - Lock task feature - Lock task packages Specify Activity - Start lock task mode App is not allowed Disable all @@ -556,7 +554,6 @@ Disable unredacted notification Disable trust agents Disable fingerprint - Disable remote input Disable face Disable iris Disable biometrics