diff --git a/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt b/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt index d206121..e7824e5 100644 --- a/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt @@ -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) } }, diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index d9882c3..0d8d45f 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -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 { RecommendedGlobalProxyScreen(::navigateUp) } composable { NetworkLoggingScreen(::navigateUp) } composable { WifiAuthKeypairScreen(::navigateUp) } - composable { PreferentialNetworkServiceScreen(::navigateUp) } + composable { PreferentialNetworkServiceScreen(::navigateUp, ::navigate) } + composable { AddPreferentialNetworkServiceConfigScreen(it.toRoute(), ::navigateUp) } composable { OverrideApnScreen(::navigateUp) } composable { 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)) } diff --git a/app/src/main/java/com/bintianqi/owndroid/Settings.kt b/app/src/main/java/com/bintianqi/owndroid/Settings.kt index 195a992..9970d55 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Settings.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Settings.kt @@ -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) } } } diff --git a/app/src/main/java/com/bintianqi/owndroid/Utils.kt b/app/src/main/java/com/bintianqi/owndroid/Utils.kt index 9d4e044..b8ff9df 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Utils.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Utils.kt @@ -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() { 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) + } +} 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 e330718..45d0a32 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt @@ -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() } - 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) - Row( - horizontalArrangement = Arrangement.SpaceAround, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth().padding(top = 8.dp) - ) { - 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() - } - }, - enabled = index > 0 + 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( + Modifier.fillMaxWidth().padding(start = 16.dp, end = 8.dp, top = 4.dp, bottom = 4.dp), + Arrangement.SpaceBetween, 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() - } + Column { + Text(index.toString()) + } + 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)) } - ) { - Icon( - imageVector = if(index + 1 >= configs.size) Icons.Default.Add else Icons.AutoMirrored.Default.KeyboardArrowRight, - contentDescription = stringResource(R.string.previous) - ) } } Row( - horizontalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxWidth() + Modifier.fillMaxWidth() + .padding(top = 4.dp) + .clickable { onNavigate(AddPreferentialNetworkServiceConfig()) } + .padding(horizontal = 8.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically ) { - 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)) - } + Icon(Icons.Default.Add, null, Modifier.padding(horizontal = 8.dp)) + Text(stringResource(R.string.add_config)) } + } +} + +@Serializable data class AddPreferentialNetworkServiceConfig( + val enabled: Boolean = true, + val id: Int = -1, + val allowFallback: Boolean = false, + val blockNonMatching: Boolean = false, + val excludedUids: List = emptyList(), + val includedUids: List = emptyList(), + val index: Int = -1 +) + +@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 - dpm.preferentialNetworkServiceConfigs = configs - initialize() - context.showOperationResultToast(true) + 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 + 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)) } } } diff --git a/app/src/main/res/drawable/arrow_back_fill0.xml b/app/src/main/res/drawable/arrow_back_fill0.xml deleted file mode 100644 index 1d4548b..0000000 --- a/app/src/main/res/drawable/arrow_back_fill0.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/checklist_fill0.xml b/app/src/main/res/drawable/checklist_fill0.xml deleted file mode 100644 index 811a0e2..0000000 --- a/app/src/main/res/drawable/checklist_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 dde8a46..dc6a5e6 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -311,15 +311,13 @@ Экспортировать журналы Пара ключей Wi-Fi Предпочтительная сетевая служба + Add config Идентификатор сети Разрешить переход на соединение по умолчанию Блокировать несоответствующие сети Включенные UID Исключенные UID Один UID на строку - Не удалось сохранить текущую конфигурацию - Сохранить текущую конфигурацию - Удалить текущую конфигурацию Настройки APN Количество настроек APN: %1$s Выберите настройку APN для редактирования (1~%1$s) или введите 0, чтобы создать новую настройку APN. diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index d718694..f3b34aa 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -318,15 +318,13 @@ Export logs Wi-Fi anahtar çifti Tercihli ağ hizmeti + Add config Network ID Allow fallback to default connection Block non matching networks Included UIDs Excluded UIDs One UID per line - Failed to save current config - Save current config - Delete current config APN ayarlarını geçersiz kıl APN ayarlarının toplamı: %1$s Düzenlemek istediğiniz APN ayarını seçin (1~%1$s) veya yeni bir APN ayarı oluşturmak için 0 girin. diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 30f019c..564a0d6 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -306,15 +306,13 @@ 导出日志 Wi-Fi密钥对 首选网络服务 + 添加配置 网络ID 允许回落到默认连接 阻止不匹配的网络 包含的UID 排除的UIDs 一行一个UID - 保存当前配置失败 - 保存当前配置 - 删除当前配置 APN设置 一共有%1$s个APN设置 选择一个你要修改的APN设置(1~%1$s)或者输入0以新建APN设置 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 900512d..961dddf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -339,15 +339,13 @@ Export logs Wi-Fi keypair Preferential network service + Add config Network ID Allow fallback to default connection Block non matching networks Included UIDs Excluded UIDs One UID per line - Failed to save current config - Save current config - Delete current config APN settings APN settings amount: %1$s Select an APN setting you want to edit (1~%1$s) or enter 0 to create a new APN setting.