mirror of
https://github.com/awfixers-stuff/OwnDroid.git
synced 2026-03-23 19:15:58 +00:00
374 lines
15 KiB
Kotlin
374 lines
15 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.WindowInsets
|
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
import androidx.compose.foundation.layout.ime
|
|
import androidx.compose.foundation.layout.padding
|
|
import androidx.compose.foundation.rememberScrollState
|
|
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.LaunchedEffect
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.mutableStateListOf
|
|
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.input.nestedscroll.nestedScroll
|
|
import androidx.compose.ui.platform.LocalContext
|
|
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.text.input.PasswordVisualTransformation
|
|
import androidx.compose.ui.unit.DpOffset
|
|
import androidx.compose.ui.unit.dp
|
|
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.coroutines.flow.StateFlow
|
|
import kotlinx.serialization.Serializable
|
|
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 Privilege.status.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) }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
)
|
|
},
|
|
contentWindowInsets = WindowInsets.ime
|
|
) { 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(
|
|
getDisplayDangerousFeatures: () -> Boolean, getShortcutsEnabled: () -> Boolean,
|
|
setDisplayDangerousFeatures: (Boolean) -> Unit, setShortcutsEnabled: (Boolean) -> Unit,
|
|
onNavigateUp: () -> Unit
|
|
) {
|
|
var dangerousFeatures by remember { mutableStateOf(getDisplayDangerousFeatures()) }
|
|
var shortcuts by remember { mutableStateOf(getShortcutsEnabled()) }
|
|
MyScaffold(R.string.options, onNavigateUp, 0.dp) {
|
|
SwitchItem(
|
|
R.string.show_dangerous_features, dangerousFeatures, {
|
|
setDisplayDangerousFeatures(it)
|
|
dangerousFeatures = it
|
|
}, R.drawable.warning_fill0
|
|
)
|
|
SwitchItem(
|
|
R.string.shortcuts, shortcuts, {
|
|
setShortcutsEnabled(it)
|
|
shortcuts = it
|
|
}, R.drawable.open_in_new
|
|
)
|
|
}
|
|
}
|
|
|
|
@Serializable object Appearance
|
|
|
|
@Composable
|
|
fun AppearanceScreen(
|
|
onNavigateUp: () -> Unit, currentTheme: StateFlow<ThemeSettings>,
|
|
setTheme: (ThemeSettings) -> Unit
|
|
) {
|
|
var darkThemeMenu by remember { mutableStateOf(false) }
|
|
val theme by currentTheme.collectAsStateWithLifecycle()
|
|
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 = { setTheme(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 = {
|
|
setTheme(theme.copy(darkTheme = -1))
|
|
darkThemeMenu = false
|
|
}
|
|
)
|
|
DropdownMenuItem(
|
|
text = { Text(stringResource(R.string.on)) },
|
|
onClick = {
|
|
setTheme(theme.copy(darkTheme = 1))
|
|
darkThemeMenu = false
|
|
}
|
|
)
|
|
DropdownMenuItem(
|
|
text = { Text(stringResource(R.string.off)) },
|
|
onClick = {
|
|
setTheme(theme.copy(darkTheme = 0))
|
|
darkThemeMenu = false
|
|
}
|
|
)
|
|
}
|
|
}
|
|
AnimatedVisibility(theme.darkTheme == 1 || (theme.darkTheme == -1 && isSystemInDarkTheme())) {
|
|
SwitchItem(
|
|
R.string.black_theme, state = theme.blackTheme,
|
|
onCheckedChange = { setTheme(theme.copy(blackTheme = it)) }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
data class AppLockConfig(
|
|
/** null means no password, empty means password already set */
|
|
val password: String?, val biometrics: Boolean, val whenLeaving: Boolean
|
|
)
|
|
|
|
@Serializable object AppLockSettings
|
|
|
|
@Composable
|
|
fun AppLockSettingsScreen(
|
|
getConfig: () -> AppLockConfig, setConfig: (AppLockConfig) -> Unit,
|
|
onNavigateUp: () -> Unit
|
|
) = MyScaffold(R.string.app_lock, onNavigateUp) {
|
|
var password by remember { mutableStateOf("") }
|
|
var confirmPassword by remember { mutableStateOf("") }
|
|
var allowBiometrics by remember { mutableStateOf(false) }
|
|
var lockWhenLeaving by remember { mutableStateOf(false) }
|
|
var alreadySet by remember { mutableStateOf(false) }
|
|
val isInputLegal = password.length !in 1..3 && (alreadySet || password.isNotBlank())
|
|
LaunchedEffect(Unit) {
|
|
val config = getConfig()
|
|
password = config.password ?: ""
|
|
allowBiometrics = config.biometrics
|
|
lockWhenLeaving = config.whenLeaving
|
|
alreadySet = config.password != null
|
|
}
|
|
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)) },
|
|
visualTransformation = PasswordVisualTransformation(),
|
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Next)
|
|
)
|
|
OutlinedTextField(
|
|
confirmPassword, { confirmPassword = it }, Modifier.fillMaxWidth(),
|
|
label = { Text(stringResource(R.string.confirm_password)) },
|
|
visualTransformation = PasswordVisualTransformation(),
|
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done)
|
|
)
|
|
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 = {
|
|
setConfig(AppLockConfig(password, allowBiometrics, lockWhenLeaving))
|
|
onNavigateUp()
|
|
},
|
|
modifier = Modifier.fillMaxWidth(),
|
|
enabled = isInputLegal && confirmPassword == password
|
|
) {
|
|
Text(stringResource(if(alreadySet) R.string.update else R.string.set))
|
|
}
|
|
if (alreadySet) FilledTonalButton(
|
|
onClick = {
|
|
setConfig(AppLockConfig(null, false, false))
|
|
onNavigateUp()
|
|
},
|
|
modifier = Modifier.fillMaxWidth()
|
|
) {
|
|
Text(stringResource(R.string.disable))
|
|
}
|
|
}
|
|
|
|
@Serializable object ApiSettings
|
|
|
|
@Composable
|
|
fun ApiSettings(
|
|
getEnabled: () -> Boolean, setKey: (String) -> Unit, onNavigateUp: () -> Unit
|
|
) {
|
|
val context = LocalContext.current
|
|
var alreadyEnabled by remember { mutableStateOf(getEnabled()) }
|
|
MyScaffold(R.string.api, onNavigateUp) {
|
|
var enabled by remember { mutableStateOf(alreadyEnabled) }
|
|
var key by remember { mutableStateOf("") }
|
|
SwitchItem(R.string.enable, state = enabled, onCheckedChange = {
|
|
enabled = it
|
|
}, padding = false)
|
|
if (enabled) {
|
|
OutlinedTextField(
|
|
key, { key = it }, Modifier.fillMaxWidth().padding(bottom = 4.dp),
|
|
label = { Text(stringResource(R.string.api_key)) },
|
|
trailingIcon = {
|
|
IconButton({ key = generateBase64Key(10) }) {
|
|
Icon(painterResource(R.drawable.casino_fill0), null)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
Button(
|
|
onClick = {
|
|
setKey(if (enabled) key else "")
|
|
alreadyEnabled = enabled
|
|
context.showOperationResultToast(true)
|
|
},
|
|
modifier = Modifier.fillMaxWidth().padding(bottom = 10.dp),
|
|
enabled = !enabled || key.length !in 0..7
|
|
) {
|
|
Text(stringResource(R.string.apply))
|
|
}
|
|
if (enabled && alreadyEnabled) Notes(R.string.api_key_exist)
|
|
}
|
|
}
|
|
|
|
@Serializable object Notifications
|
|
|
|
@Composable
|
|
fun NotificationsScreen(
|
|
getState: () -> List<NotificationType>, setNotification: (NotificationType, Boolean) -> Unit,
|
|
onNavigateUp: () -> Unit
|
|
) = MyScaffold(R.string.notifications, onNavigateUp, 0.dp) {
|
|
val enabledNotifications = remember { mutableStateListOf(*getState().toTypedArray()) }
|
|
NotificationType.entries.forEach { type ->
|
|
SwitchItem(type.text, type in enabledNotifications, {
|
|
setNotification(type, it)
|
|
enabledNotifications.run { if (it) plusAssign(type) else minusAssign(type) }
|
|
})
|
|
}
|
|
}
|
|
|
|
@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)
|
|
}
|