Export logs in settings

Set Preferential network config in a new screen
Hide Password entry from home if OwnDroid is not activated, fix #105
This commit is contained in:
BinTianqi
2025-02-22 11:54:21 +08:00
parent 3d6e12581b
commit 8a8d11a89b
11 changed files with 156 additions and 142 deletions

View File

@@ -146,12 +146,12 @@ private fun AppInstaller(
var tab by remember { mutableIntStateOf(0) }
val pagerState = rememberPagerState { 2 }
val scrollState = rememberScrollState()
tab = pagerState.targetPage
Column(modifier = Modifier.padding(paddingValues)) {
TabRow(tab) {
Tab(
tab == 0,
onClick = {
tab = 0
coroutine.launch { scrollState.animateScrollTo(0) }
coroutine.launch { pagerState.animateScrollToPage(0) }
},
@@ -160,7 +160,6 @@ private fun AppInstaller(
Tab(
tab == 1,
onClick = {
tab = 1
coroutine.launch { scrollState.animateScrollTo(0) }
coroutine.launch { pagerState.animateScrollToPage(1) }
},

View File

@@ -66,6 +66,8 @@ import com.bintianqi.owndroid.dpm.AddDelegatedAdmin
import com.bintianqi.owndroid.dpm.AddDelegatedAdminScreen
import com.bintianqi.owndroid.dpm.AddNetwork
import com.bintianqi.owndroid.dpm.AddNetworkScreen
import com.bintianqi.owndroid.dpm.AddPreferentialNetworkServiceConfig
import com.bintianqi.owndroid.dpm.AddPreferentialNetworkServiceConfigScreen
import com.bintianqi.owndroid.dpm.AffiliationId
import com.bintianqi.owndroid.dpm.AffiliationIdScreen
import com.bintianqi.owndroid.dpm.AlwaysOnVpnPackage
@@ -340,7 +342,8 @@ fun Home(activity: FragmentActivity, vm: MyViewModel) {
composable<RecommendedGlobalProxy> { RecommendedGlobalProxyScreen(::navigateUp) }
composable<NetworkLogging> { NetworkLoggingScreen(::navigateUp) }
composable<WifiAuthKeypair> { WifiAuthKeypairScreen(::navigateUp) }
composable<PreferentialNetworkService> { PreferentialNetworkServiceScreen(::navigateUp) }
composable<PreferentialNetworkService> { PreferentialNetworkServiceScreen(::navigateUp, ::navigate) }
composable<AddPreferentialNetworkServiceConfig> { AddPreferentialNetworkServiceConfigScreen(it.toRoute(), ::navigateUp) }
composable<OverrideApn> { OverrideApnScreen(::navigateUp) }
composable<WorkProfile> { WorkProfileScreen(::navigateUp, ::navigate) }
@@ -507,7 +510,7 @@ private fun HomeScreen(onNavigate: (Any) -> Unit) {
HomePageItem(R.string.user_restriction, R.drawable.person_off) { onNavigate(UserRestriction) }
}
HomePageItem(R.string.users,R.drawable.manage_accounts_fill0) { onNavigate(Users) }
HomePageItem(R.string.password_and_keyguard, R.drawable.password_fill0) { onNavigate(Password) }
if(deviceOwner || profileOwner) HomePageItem(R.string.password_and_keyguard, R.drawable.password_fill0) { onNavigate(Password) }
HomePageItem(R.string.settings, R.drawable.settings_fill0) { onNavigate(Settings) }
Spacer(Modifier.padding(vertical = 20.dp))
}

View File

@@ -4,6 +4,8 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build.VERSION
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.biometric.BiometricManager
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.isSystemInDarkTheme
@@ -38,16 +40,27 @@ import com.bintianqi.owndroid.ui.MyScaffold
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
@Serializable object Settings
@Composable
fun SettingsScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) {
val context = LocalContext.current
val exportLogsLauncher = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) {
if(it != null) exportLogs(context, it)
}
MyScaffold(R.string.settings, 0.dp, onNavigateUp) {
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(title = R.string.security, icon = R.drawable.lock_fill0) { onNavigate(AuthSettings) }
FunctionItem(title = R.string.api, icon = R.drawable.apps_fill0) { onNavigate(ApiSettings) }
FunctionItem(title = R.string.export_logs, icon = R.drawable.description_fill0) {
val time = SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()).format(Date(System.currentTimeMillis()))
exportLogsLauncher.launch("owndroid_log_$time")
}
FunctionItem(title = R.string.about, icon = R.drawable.info_fill0) { onNavigate(About) }
}
}

View File

@@ -8,7 +8,9 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Process
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContract
@@ -28,6 +30,7 @@ import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Date
import java.util.Locale
import java.util.concurrent.TimeUnit
import kotlin.reflect.typeOf
var zhCN = true
@@ -122,3 +125,13 @@ class ChoosePackageContract: ActivityResultContract<Nothing?, String?>() {
override fun parseResult(resultCode: Int, intent: Intent?): String? =
intent?.getStringExtra("package")
}
fun exportLogs(context: Context, uri: Uri) {
context.contentResolver.openOutputStream(uri)?.use { output ->
val proc = Runtime.getRuntime().exec("logcat -d")
proc.inputStream.copyTo(output)
if(Build.VERSION.SDK_INT >= 26) proc.waitFor(2L, TimeUnit.SECONDS)
else proc.waitFor()
context.showOperationResultToast(proc.exitValue() == 0)
}
}

View File

@@ -80,7 +80,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.LocationOn
@@ -99,6 +98,9 @@ import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.MenuAnchorType
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.Switch
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
@@ -156,7 +158,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import java.net.InetAddress
import kotlin.math.max
import kotlin.reflect.jvm.jvmErasure
@Serializable object Network
@@ -1714,122 +1715,95 @@ fun WifiAuthKeypairScreen(onNavigateUp: () -> Unit) {
@RequiresApi(33)
@Composable
fun PreferentialNetworkServiceScreen(onNavigateUp: () -> Unit) {
val focusMgr = LocalFocusManager.current
fun PreferentialNetworkServiceScreen(onNavigateUp: () -> Unit, onNavigate: (AddPreferentialNetworkServiceConfig) -> Unit) {
val context = LocalContext.current
val dpm = context.getDPM()
var masterEnabled by remember { mutableStateOf(false) }
val configs = remember { mutableStateListOf<PreferentialNetworkServiceConfig>() }
var index by remember { mutableIntStateOf(-1) }
var enabled by remember { mutableStateOf(false) }
var networkId by remember { mutableStateOf("") }
var allowFallback by remember { mutableStateOf(false) }
var blockNonMatching by remember { mutableStateOf(false) }
var excludedUids by remember { mutableStateOf("") }
var includedUids by remember { mutableStateOf("") }
fun refresh() {
val config = configs.getOrNull(index)
enabled = config?.isEnabled == true
networkId = config?.networkId?.toString() ?: ""
allowFallback = config?.isFallbackToDefaultConnectionAllowed == true
if(VERSION.SDK_INT >= 34) blockNonMatching = config?.shouldBlockNonMatchingNetworks() == true
includedUids = config?.includedUids?.joinToString("\n") ?: ""
excludedUids = config?.excludedUids?.joinToString("\n") ?: ""
}
fun saveCurrentConfig() {
val builder = PreferentialNetworkServiceConfig.Builder()
builder.setEnabled(enabled)
builder.setNetworkId(networkId.toInt())
builder.setFallbackToDefaultConnectionAllowed(allowFallback)
if(VERSION.SDK_INT >= 34) builder.setShouldBlockNonMatchingNetworks(blockNonMatching)
builder.setIncludedUids(includedUids.lines().dropWhile { it == "" }.map { it.toInt() }.toIntArray())
builder.setExcludedUids(excludedUids.lines().dropWhile { it == "" }.map { it.toInt() }.toIntArray())
if(index < configs.size) configs[index] = builder.build() else configs += builder.build()
}
fun initialize() {
masterEnabled = dpm.isPreferentialNetworkServiceEnabled
configs.clear()
configs.addAll(dpm.preferentialNetworkServiceConfigs)
index = max(0, configs.size - 1)
refresh()
}
LaunchedEffect(Unit) { initialize() }
MyScaffold(R.string.preferential_network_service, 8.dp, onNavigateUp) {
SwitchItem(R.string.enabled, state = masterEnabled, onCheckedChange = { masterEnabled = it }, padding = false)
LaunchedEffect(Unit) { refresh() }
MyScaffold(R.string.preferential_network_service, 0.dp, onNavigateUp, false) {
SwitchItem(R.string.enabled, state = masterEnabled, onCheckedChange = {
dpm.isPreferentialNetworkServiceEnabled = it
refresh()
})
Spacer(Modifier.padding(vertical = 4.dp))
configs.forEachIndexed { index, config ->
Row(
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(top = 8.dp)
Modifier.fillMaxWidth().padding(start = 16.dp, end = 8.dp, top = 4.dp, bottom = 4.dp),
Arrangement.SpaceBetween, Alignment.CenterVertically
) {
IconButton(
onClick = {
try {
saveCurrentConfig()
index -= 1
refresh()
} catch(e: Exception) {
e.printStackTrace()
Toast.makeText(context, R.string.failed_to_save_current_config, Toast.LENGTH_SHORT).show()
Column {
Text(index.toString())
}
},
enabled = index > 0
IconButton({
onNavigate(AddPreferentialNetworkServiceConfig(
enabled = config.isEnabled,
id = config.networkId,
allowFallback = config.isFallbackToDefaultConnectionAllowed,
blockNonMatching = if(VERSION.SDK_INT >= 34) config.shouldBlockNonMatchingNetworks() else false,
excludedUids = config.excludedUids.toList(),
includedUids = config.includedUids.toList(),
index = index
))
}) {
Icon(Icons.Default.Edit, stringResource(R.string.edit))
}
}
}
Row(
Modifier.fillMaxWidth()
.padding(top = 4.dp)
.clickable { onNavigate(AddPreferentialNetworkServiceConfig()) }
.padding(horizontal = 8.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(imageVector = Icons.AutoMirrored.Default.KeyboardArrowLeft, contentDescription = stringResource(R.string.previous))
}
Text("${index + 1} / ${configs.size}")
IconButton(
onClick = {
try {
saveCurrentConfig()
index += 1
refresh()
} catch(e: Exception) {
e.printStackTrace()
Toast.makeText(context, R.string.failed_to_save_current_config, Toast.LENGTH_SHORT).show()
Icon(Icons.Default.Add, null, Modifier.padding(horizontal = 8.dp))
Text(stringResource(R.string.add_config))
}
}
) {
Icon(
imageVector = if(index + 1 >= configs.size) Icons.Default.Add else Icons.AutoMirrored.Default.KeyboardArrowRight,
contentDescription = stringResource(R.string.previous)
}
@Serializable data class AddPreferentialNetworkServiceConfig(
val enabled: Boolean = true,
val id: Int = -1,
val allowFallback: Boolean = false,
val blockNonMatching: Boolean = false,
val excludedUids: List<Int> = emptyList(),
val includedUids: List<Int> = emptyList(),
val index: Int = -1
)
}
}
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
IconButton(
onClick = {
try {
saveCurrentConfig()
context.showOperationResultToast(true)
} catch(e: Exception) {
e.printStackTrace()
Toast.makeText(context, R.string.failed_to_save_current_config, Toast.LENGTH_SHORT).show()
}
},
modifier = Modifier.padding(end = 10.dp)
) {
Icon(painter = painterResource(R.drawable.save_fill0), contentDescription = stringResource(R.string.save_current_config))
}
IconButton(
onClick = {
if(index < configs.size) configs.removeAt(index)
if(index > 0) index -= 1
refresh()
}
) {
Icon(imageVector = Icons.Default.Delete, contentDescription = stringResource(R.string.delete_current_config))
}
}
@RequiresApi(33)
@Composable
fun AddPreferentialNetworkServiceConfigScreen(route: AddPreferentialNetworkServiceConfig,onNavigateUp: () -> Unit) {
val updateMode = route.index != -1
val context = LocalContext.current
val dpm = context.getDPM()
var enabled by remember { mutableStateOf(route.enabled) }
var id by remember { mutableIntStateOf(route.id) }
var allowFallback by remember { mutableStateOf(route.allowFallback) }
var blockNonMatching by remember { mutableStateOf(route.blockNonMatching) }
var excludedUids by remember { mutableStateOf(route.excludedUids.joinToString("\n")) }
var includedUids by remember { mutableStateOf(route.includedUids.joinToString("\n")) }
MyScaffold(R.string.preferential_network_service, 8.dp, onNavigateUp, 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)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions { focusMgr.clearFocus() },
modifier = Modifier.fillMaxWidth().padding(bottom = 6.dp)
)
AnimatedVisibility(enabled) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text("ID", Modifier.padding(end = 8.dp), style = typography.titleLarge)
SingleChoiceSegmentedButtonRow(Modifier.fillMaxWidth()) {
for(i in 1..5) {
SegmentedButton(id == i, { id = i }, SegmentedButtonDefaults.itemShape(i - 1, 5)) {
Text(i.toString())
}
}
}
}
}
SwitchItem(
title = R.string.allow_fallback_to_default_connection,
state = allowFallback, onCheckedChange = { allowFallback = it }, padding = false
@@ -1838,28 +1812,66 @@ fun PreferentialNetworkServiceScreen(onNavigateUp: () -> Unit) {
title = R.string.block_non_matching_networks,
state = blockNonMatching, onCheckedChange = { blockNonMatching = it }, padding = false
)
val includedUidsLegal = includedUids.lines().filter { it.isNotBlank() }.let {
it.isEmpty() || (it.all { it.toIntOrNull() != null } && excludedUids.isBlank())
}
OutlinedTextField(
value = includedUids, onValueChange = { includedUids = it }, minLines = 2,
label = { Text(stringResource(R.string.included_uids)) },
supportingText = { Text(stringResource(R.string.one_uid_per_line)) },
isError = !includedUidsLegal,
modifier = Modifier.fillMaxWidth().padding(bottom = 6.dp)
)
val excludedUidsLegal = excludedUids.lines().filter { it.isNotBlank() }.let {
it.isEmpty() || (it.all { it.toIntOrNull() != null } && includedUids.isBlank())
}
OutlinedTextField(
value = excludedUids, onValueChange = { excludedUids = it }, minLines = 2,
label = { Text(stringResource(R.string.excluded_uids)) },
supportingText = { Text(stringResource(R.string.one_uid_per_line)) },
isError = !excludedUidsLegal,
modifier = Modifier.fillMaxWidth().padding(bottom = 6.dp)
)
Button(
onClick = {
dpm.isPreferentialNetworkServiceEnabled = masterEnabled
try {
val config = PreferentialNetworkServiceConfig.Builder().apply {
setEnabled(enabled)
if(enabled) setNetworkId(id.toInt())
setFallbackToDefaultConnectionAllowed(allowFallback)
setExcludedUids(excludedUids.lines().filter { it.isNotBlank() }.map { it.toInt() }.toIntArray())
setIncludedUids(includedUids.lines().filter { it.isNotBlank() }.map { it.toInt() }.toIntArray())
if(VERSION.SDK_INT >= 34) setShouldBlockNonMatchingNetworks(blockNonMatching)
}.build()
val configs = dpm.preferentialNetworkServiceConfigs
if(updateMode) configs[route.index] = config
else configs += config
dpm.preferentialNetworkServiceConfigs = configs
initialize()
context.showOperationResultToast(true)
onNavigateUp()
} catch(e: Exception) {
context.showOperationResultToast(false)
e.printStackTrace()
}
},
modifier = Modifier.fillMaxWidth().padding(top = 12.dp)
enabled = includedUidsLegal && excludedUidsLegal,
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)
) {
Text(stringResource(R.string.apply))
Text(stringResource(if(updateMode) R.string.update else R.string.add))
}
if(updateMode) Button(
onClick = {
try {
dpm.preferentialNetworkServiceConfigs = dpm.preferentialNetworkServiceConfigs.drop(route.index)
onNavigateUp()
} catch(e: Exception) {
context.showOperationResultToast(false)
e.printStackTrace()
}
},
colors = ButtonDefaults.buttonColors(MaterialTheme.colorScheme.error, MaterialTheme.colorScheme.onError),
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.delete))
}
}
}

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FF000000"
android:pathData="m313,520 l224,224 -57,56 -320,-320 320,-320 57,56 -224,224h487v80L313,520Z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FF000000"
android:pathData="M222,760 L80,618l56,-56 85,85 170,-170 56,57 -225,226ZM222,440L80,298l56,-56 85,85 170,-170 56,57 -225,226ZM520,680v-80h360v80L520,680ZM520,360v-80h360v80L520,360Z"/>
</vector>

View File

@@ -311,15 +311,13 @@
<string name="export_logs">Экспортировать журналы</string>
<string name="wifi_auth_keypair">Пара ключей Wi-Fi</string>
<string name="preferential_network_service">Предпочтительная сетевая служба</string>
<string name="add_config">Add config</string> <!--TODO-->
<string name="network_id">Идентификатор сети</string>
<string name="allow_fallback_to_default_connection">Разрешить переход на соединение по умолчанию</string>
<string name="block_non_matching_networks">Блокировать несоответствующие сети</string>
<string name="included_uids">Включенные UID</string>
<string name="excluded_uids">Исключенные UID</string>
<string name="one_uid_per_line">Один UID на строку</string>
<string name="failed_to_save_current_config">Не удалось сохранить текущую конфигурацию</string>
<string name="save_current_config">Сохранить текущую конфигурацию</string>
<string name="delete_current_config">Удалить текущую конфигурацию</string>
<string name="override_apn_settings">Настройки APN</string>
<string name="total_apn_amount">Количество настроек APN: %1$s</string>
<string name="select_a_apn_or_create">Выберите настройку APN для редактирования (1~%1$s) или введите 0, чтобы создать новую настройку APN.</string>

View File

@@ -318,15 +318,13 @@
<string name="export_logs">Export logs</string> <!--TODO-->
<string name="wifi_auth_keypair">Wi-Fi anahtar çifti</string>
<string name="preferential_network_service">Tercihli ağ hizmeti</string>
<string name="add_config">Add config</string> <!--TODO-->
<string name="network_id">Network ID</string> <!--TODO-->
<string name="allow_fallback_to_default_connection">Allow fallback to default connection</string> <!--TODO-->
<string name="block_non_matching_networks">Block non matching networks</string> <!--TODO-->
<string name="included_uids">Included UIDs</string> <!--TODO-->
<string name="excluded_uids">Excluded UIDs</string> <!--TODO-->
<string name="one_uid_per_line">One UID per line</string> <!--TODO-->
<string name="failed_to_save_current_config">Failed to save current config</string> <!--TODO-->
<string name="save_current_config">Save current config</string> <!--TODO-->
<string name="delete_current_config">Delete current config</string> <!--TODO-->
<string name="override_apn_settings">APN ayarlarını geçersiz kıl</string>
<string name="total_apn_amount">APN ayarlarının toplamı: %1$s</string>
<string name="select_a_apn_or_create">Düzenlemek istediğiniz APN ayarını seçin (1~%1$s) veya yeni bir APN ayarı oluşturmak için 0 girin.</string>

View File

@@ -306,15 +306,13 @@
<string name="export_logs">导出日志</string>
<string name="wifi_auth_keypair">Wi-Fi密钥对</string>
<string name="preferential_network_service">首选网络服务</string>
<string name="add_config">添加配置</string>
<string name="network_id">网络ID</string>
<string name="allow_fallback_to_default_connection">允许回落到默认连接</string>
<string name="block_non_matching_networks">阻止不匹配的网络</string>
<string name="included_uids">包含的UID</string>
<string name="excluded_uids">排除的UIDs</string>
<string name="one_uid_per_line">一行一个UID</string>
<string name="failed_to_save_current_config">保存当前配置失败</string>
<string name="save_current_config">保存当前配置</string>
<string name="delete_current_config">删除当前配置</string>
<string name="override_apn_settings">APN设置</string>
<string name="total_apn_amount">一共有%1$s个APN设置</string>
<string name="select_a_apn_or_create">选择一个你要修改的APN设置(1~%1$s)或者输入0以新建APN设置</string>

View File

@@ -339,15 +339,13 @@
<string name="export_logs">Export logs</string>
<string name="wifi_auth_keypair">Wi-Fi keypair</string>
<string name="preferential_network_service">Preferential network service</string>
<string name="add_config">Add config</string>
<string name="network_id">Network ID</string>
<string name="allow_fallback_to_default_connection">Allow fallback to default connection</string>
<string name="block_non_matching_networks">Block non matching networks</string>
<string name="included_uids">Included UIDs</string>
<string name="excluded_uids">Excluded UIDs</string>
<string name="one_uid_per_line">One UID per line</string>
<string name="failed_to_save_current_config">Failed to save current config</string>
<string name="save_current_config">Save current config</string>
<string name="delete_current_config">Delete current config</string>
<string name="override_apn_settings">APN settings</string>
<string name="total_apn_amount">APN settings amount: %1$s</string>
<string name="select_a_apn_or_create">Select an APN setting you want to edit (1~%1$s) or enter 0 to create a new APN setting. </string>