mirror of
https://github.com/awfixers-stuff/OwnDroid.git
synced 2026-03-23 11:05:59 +00:00
Add 2 new user restriction items, close #124 Add User restriction editor Add Exit in settings
371 lines
16 KiB
Kotlin
371 lines
16 KiB
Kotlin
package com.bintianqi.owndroid
|
|
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.os.Build.VERSION
|
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
import androidx.activity.result.contract.ActivityResultContracts
|
|
import androidx.compose.animation.AnimatedVisibility
|
|
import androidx.compose.foundation.isSystemInDarkTheme
|
|
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.layout.widthIn
|
|
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.material.icons.Icons
|
|
import androidx.compose.material.icons.filled.Close
|
|
import androidx.compose.material.icons.filled.MoreVert
|
|
import androidx.compose.material3.Button
|
|
import androidx.compose.material3.DropdownMenu
|
|
import androidx.compose.material3.DropdownMenuItem
|
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
import androidx.compose.material3.FilledTonalButton
|
|
import androidx.compose.material3.Icon
|
|
import androidx.compose.material3.IconButton
|
|
import androidx.compose.material3.LargeTopAppBar
|
|
import androidx.compose.material3.OutlinedTextField
|
|
import androidx.compose.material3.Scaffold
|
|
import androidx.compose.material3.Switch
|
|
import androidx.compose.material3.Text
|
|
import androidx.compose.material3.TopAppBarDefaults
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.mutableStateOf
|
|
import androidx.compose.runtime.remember
|
|
import androidx.compose.runtime.setValue
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.focus.FocusRequester
|
|
import androidx.compose.ui.focus.focusRequester
|
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
import androidx.compose.ui.platform.LocalContext
|
|
import androidx.compose.ui.platform.LocalFocusManager
|
|
import androidx.compose.ui.res.painterResource
|
|
import androidx.compose.ui.res.stringResource
|
|
import androidx.compose.ui.text.input.ImeAction
|
|
import androidx.compose.ui.text.input.KeyboardType
|
|
import androidx.compose.ui.unit.DpOffset
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.core.content.edit
|
|
import androidx.core.content.pm.ShortcutManagerCompat
|
|
import androidx.core.net.toUri
|
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
import com.bintianqi.owndroid.ui.FunctionItem
|
|
import com.bintianqi.owndroid.ui.MyScaffold
|
|
import com.bintianqi.owndroid.ui.NavIcon
|
|
import com.bintianqi.owndroid.ui.Notes
|
|
import com.bintianqi.owndroid.ui.SwitchItem
|
|
import kotlinx.serialization.Serializable
|
|
import java.security.SecureRandom
|
|
import java.text.SimpleDateFormat
|
|
import java.util.Date
|
|
import java.util.Locale
|
|
import kotlin.system.exitProcess
|
|
|
|
@Serializable object Settings
|
|
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
@Composable
|
|
fun SettingsScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) {
|
|
val context = LocalContext.current
|
|
val privilege by myPrivilege.collectAsStateWithLifecycle()
|
|
val exportLogsLauncher = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) {
|
|
if(it != null) exportLogs(context, it)
|
|
}
|
|
val sb = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
|
var dropdown by remember { mutableStateOf(false) }
|
|
Scaffold(
|
|
Modifier.nestedScroll(sb.nestedScrollConnection),
|
|
topBar = {
|
|
LargeTopAppBar(
|
|
{ Text(stringResource(R.string.settings)) },
|
|
navigationIcon = { NavIcon(onNavigateUp) },
|
|
scrollBehavior = sb,
|
|
actions = {
|
|
Box {
|
|
IconButton({ dropdown = true }) {
|
|
Icon(Icons.Default.MoreVert, null)
|
|
}
|
|
DropdownMenu(dropdown, { dropdown = false }) {
|
|
DropdownMenuItem(
|
|
{ Text(stringResource(R.string.export_logs)) },
|
|
{
|
|
dropdown = false
|
|
val time = SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault())
|
|
.format(Date(System.currentTimeMillis()))
|
|
exportLogsLauncher.launch("owndroid_log_$time")
|
|
},
|
|
leadingIcon = {
|
|
Icon(painterResource(R.drawable.description_fill0), null)
|
|
}
|
|
)
|
|
DropdownMenuItem(
|
|
{ Text(stringResource(R.string.exit)) },
|
|
{ exitProcess(0) },
|
|
leadingIcon = { Icon(Icons.Default.Close, null) }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
)
|
|
}
|
|
) { paddingValues ->
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.padding(paddingValues)
|
|
.verticalScroll(rememberScrollState())
|
|
.padding(bottom = 80.dp)
|
|
) {
|
|
FunctionItem(title = R.string.options, icon = R.drawable.tune_fill0) { onNavigate(SettingsOptions) }
|
|
FunctionItem(title = R.string.appearance, icon = R.drawable.format_paint_fill0) { onNavigate(Appearance) }
|
|
FunctionItem(R.string.app_lock, icon = R.drawable.lock_fill0) { onNavigate(AppLockSettings) }
|
|
if (privilege.device || privilege.profile)
|
|
FunctionItem(title = R.string.api, icon = R.drawable.code_fill0) { onNavigate(ApiSettings) }
|
|
if (privilege.device && !privilege.dhizuku)
|
|
FunctionItem(R.string.notifications, icon = R.drawable.notifications_fill0) { onNavigate(Notifications) }
|
|
FunctionItem(title = R.string.about, icon = R.drawable.info_fill0) { onNavigate(About) }
|
|
}
|
|
}
|
|
}
|
|
|
|
@Serializable object SettingsOptions
|
|
|
|
@Composable
|
|
fun SettingsOptionsScreen(onNavigateUp: () -> Unit) {
|
|
val context = LocalContext.current
|
|
val sp = SharedPrefs(context)
|
|
MyScaffold(R.string.options, onNavigateUp, 0.dp) {
|
|
SwitchItem(
|
|
R.string.show_dangerous_features, icon = R.drawable.warning_fill0,
|
|
getState = { sp.displayDangerousFeatures },
|
|
onCheckedChange = { sp.displayDangerousFeatures = it }
|
|
)
|
|
SwitchItem(
|
|
R.string.shortcuts, icon = R.drawable.open_in_new,
|
|
getState = { sp.shortcuts }, onCheckedChange = {
|
|
sp.shortcuts = it
|
|
ShortcutManagerCompat.removeAllDynamicShortcuts(context)
|
|
createShortcuts(context)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
@Serializable object Appearance
|
|
|
|
@Composable
|
|
fun AppearanceScreen(onNavigateUp: () -> Unit, currentTheme: ThemeSettings, onThemeChange: (ThemeSettings) -> Unit) {
|
|
var darkThemeMenu by remember { mutableStateOf(false) }
|
|
var theme by remember { mutableStateOf(currentTheme) }
|
|
fun update(it: ThemeSettings) {
|
|
theme = it
|
|
onThemeChange(it)
|
|
}
|
|
val darkThemeTextID = when(theme.darkTheme) {
|
|
1 -> R.string.on
|
|
0 -> R.string.off
|
|
else -> R.string.follow_system
|
|
}
|
|
MyScaffold(R.string.appearance, onNavigateUp, 0.dp) {
|
|
if(VERSION.SDK_INT >= 31) {
|
|
SwitchItem(
|
|
R.string.material_you_color,
|
|
state = theme.materialYou,
|
|
onCheckedChange = { update(theme.copy(materialYou = it)) }
|
|
)
|
|
}
|
|
Box {
|
|
FunctionItem(R.string.dark_theme, stringResource(darkThemeTextID)) { darkThemeMenu = true }
|
|
DropdownMenu(
|
|
expanded = darkThemeMenu, onDismissRequest = { darkThemeMenu = false },
|
|
offset = DpOffset(x = 25.dp, y = 0.dp)
|
|
) {
|
|
DropdownMenuItem(
|
|
text = { Text(stringResource(R.string.follow_system)) },
|
|
onClick = {
|
|
update(theme.copy(darkTheme = -1))
|
|
darkThemeMenu = false
|
|
}
|
|
)
|
|
DropdownMenuItem(
|
|
text = { Text(stringResource(R.string.on)) },
|
|
onClick = {
|
|
update(theme.copy(darkTheme = 1))
|
|
theme = theme.copy(darkTheme = 1)
|
|
darkThemeMenu = false
|
|
}
|
|
)
|
|
DropdownMenuItem(
|
|
text = { Text(stringResource(R.string.off)) },
|
|
onClick = {
|
|
update(theme.copy(darkTheme = 0))
|
|
darkThemeMenu = false
|
|
}
|
|
)
|
|
}
|
|
}
|
|
AnimatedVisibility(theme.darkTheme == 1 || (theme.darkTheme == -1 && isSystemInDarkTheme())) {
|
|
SwitchItem(
|
|
R.string.black_theme, state = theme.blackTheme,
|
|
onCheckedChange = { update(theme.copy(blackTheme = it)) }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Serializable object AppLockSettings
|
|
|
|
@Composable
|
|
fun AppLockSettingsScreen(onNavigateUp: () -> Unit) = MyScaffold(R.string.app_lock, onNavigateUp, 0.dp) {
|
|
val fm = LocalFocusManager.current
|
|
val sp = SharedPrefs(LocalContext.current)
|
|
var password by remember { mutableStateOf("") }
|
|
var confirmPassword by remember { mutableStateOf("") }
|
|
var allowBiometrics by remember { mutableStateOf(sp.biometricsUnlock) }
|
|
var lockWhenLeaving by remember { mutableStateOf(sp.lockWhenLeaving) }
|
|
val fr = FocusRequester()
|
|
val alreadySet = !sp.lockPasswordHash.isNullOrEmpty()
|
|
val isInputLegal = password.length !in 1..3 && (alreadySet || (password.isNotEmpty() && password.isNotBlank()))
|
|
Column(Modifier.widthIn(max = 300.dp).align(Alignment.CenterHorizontally)) {
|
|
OutlinedTextField(
|
|
password, { password = it }, Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
|
label = { Text(stringResource(R.string.password)) },
|
|
supportingText = { Text(stringResource(if(alreadySet) R.string.leave_empty_to_remain_unchanged else R.string.minimum_length_4)) },
|
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Next),
|
|
keyboardActions = KeyboardActions { fr.requestFocus() }
|
|
)
|
|
OutlinedTextField(
|
|
confirmPassword, { confirmPassword = it }, Modifier.fillMaxWidth().focusRequester(fr),
|
|
label = { Text(stringResource(R.string.confirm_password)) },
|
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done),
|
|
keyboardActions = KeyboardActions { fm.clearFocus() }
|
|
)
|
|
if(VERSION.SDK_INT >= 28) Row(Modifier.fillMaxWidth().padding(vertical = 6.dp), Arrangement.SpaceBetween, Alignment.CenterVertically) {
|
|
Text(stringResource(R.string.allow_biometrics))
|
|
Switch(allowBiometrics, { allowBiometrics = it })
|
|
}
|
|
Row(Modifier.fillMaxWidth().padding(bottom = 6.dp), Arrangement.SpaceBetween, Alignment.CenterVertically) {
|
|
Text(stringResource(R.string.lock_when_leaving))
|
|
Switch(lockWhenLeaving, { lockWhenLeaving = it })
|
|
}
|
|
Button(
|
|
onClick = {
|
|
fm.clearFocus()
|
|
if(password.isNotEmpty()) sp.lockPasswordHash = password.hash()
|
|
sp.biometricsUnlock = allowBiometrics
|
|
sp.lockWhenLeaving = lockWhenLeaving
|
|
onNavigateUp()
|
|
},
|
|
modifier = Modifier.fillMaxWidth(),
|
|
enabled = isInputLegal && confirmPassword == password
|
|
) {
|
|
Text(stringResource(if(alreadySet) R.string.update else R.string.set))
|
|
}
|
|
if(alreadySet) FilledTonalButton(
|
|
onClick = {
|
|
fm.clearFocus()
|
|
sp.lockPasswordHash = ""
|
|
sp.biometricsUnlock = false
|
|
sp.lockWhenLeaving = false
|
|
onNavigateUp()
|
|
},
|
|
modifier = Modifier.fillMaxWidth()
|
|
) {
|
|
Text(stringResource(R.string.disable))
|
|
}
|
|
}
|
|
}
|
|
|
|
@Serializable object ApiSettings
|
|
|
|
@Composable
|
|
fun ApiSettings(onNavigateUp: () -> Unit) {
|
|
val context = LocalContext.current
|
|
val sp = SharedPrefs(context)
|
|
MyScaffold(R.string.api, onNavigateUp) {
|
|
var enabled by remember { mutableStateOf(sp.isApiEnabled) }
|
|
SwitchItem(R.string.enable, state = enabled, onCheckedChange = {
|
|
enabled = it
|
|
sp.isApiEnabled = it
|
|
if(!it) sp.sharedPrefs.edit { remove("api.key") }
|
|
}, 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")
|
|
}
|
|
}
|
|
)
|
|
Button(
|
|
modifier = Modifier.fillMaxWidth().padding(bottom = 10.dp),
|
|
onClick = {
|
|
sp.apiKey = key
|
|
context.showOperationResultToast(true)
|
|
}
|
|
) {
|
|
Text(stringResource(R.string.apply))
|
|
}
|
|
if(sp.apiKey != null) Notes(R.string.api_key_exist)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Serializable object Notifications
|
|
|
|
@Composable
|
|
fun NotificationsScreen(onNavigateUp: () -> Unit) = MyScaffold(R.string.notifications, onNavigateUp, 0.dp) {
|
|
val sp = LocalContext.current.getSharedPreferences("data", Context.MODE_PRIVATE)
|
|
val map = mapOf(
|
|
NotificationUtils.ID.PASSWORD_CHANGED to R.string.password_changed, NotificationUtils.ID.USER_ADDED to R.string.user_added,
|
|
NotificationUtils.ID.USER_STARTED to R.string.user_started, NotificationUtils.ID.USER_SWITCHED to R.string.user_switched,
|
|
NotificationUtils.ID.USER_STOPPED to R.string.user_stopped, NotificationUtils.ID.USER_REMOVED to R.string.user_removed,
|
|
NotificationUtils.ID.BUG_REPORT_SHARED to R.string.bug_report_shared,
|
|
NotificationUtils.ID.BUG_REPORT_SHARING_DECLINED to R.string.bug_report_sharing_declined,
|
|
NotificationUtils.ID.BUG_REPORT_FAILED to R.string.bug_report_failed,
|
|
NotificationUtils.ID.SYSTEM_UPDATE_PENDING to R.string.system_update_pending
|
|
)
|
|
map.forEach { (k, v) ->
|
|
SwitchItem(v, getState = { sp.getBoolean("n_$k", true) }, onCheckedChange = { sp.edit { putBoolean("n_$k", it) } })
|
|
}
|
|
}
|
|
|
|
@Serializable object About
|
|
|
|
@Composable
|
|
fun AboutScreen(onNavigateUp: () -> Unit) {
|
|
val context = LocalContext.current
|
|
val pkgInfo = context.packageManager.getPackageInfo(context.packageName,0)
|
|
val verCode = pkgInfo.versionCode
|
|
val verName = pkgInfo.versionName
|
|
MyScaffold(R.string.about, onNavigateUp, 0.dp) {
|
|
Text(text = stringResource(R.string.app_name)+" v$verName ($verCode)", modifier = Modifier.padding(start = 16.dp))
|
|
Spacer(Modifier.padding(vertical = 5.dp))
|
|
FunctionItem(R.string.project_homepage, "GitHub", R.drawable.open_in_new) { shareLink(context, "https://github.com/BinTianqi/OwnDroid") }
|
|
}
|
|
}
|
|
|
|
fun shareLink(inputContext: Context, link: String) {
|
|
val uri = link.toUri()
|
|
val intent = Intent(Intent.ACTION_VIEW, uri)
|
|
inputContext.startActivity(Intent.createChooser(intent, "Open in browser"), null)
|
|
}
|